proxibuy_bussiness/lib/presentation/pages/store_info.dart

724 lines
26 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:io';
import 'package:business_panel/core/config/app_colors.dart';
import 'package:business_panel/domain/entities/category_entity.dart';
import 'package:business_panel/gen/assets.gen.dart';
import 'package:business_panel/presentation/pages/store_info_display_page.dart';
import 'package:business_panel/presentation/pages/working_hours_dialog.dart';
import 'package:business_panel/presentation/store_info/bloc/store_info_bloc.dart';
import 'package:business_panel/presentation/store_info/bloc/store_info_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:image_picker/image_picker.dart';
import 'package:business_panel/presentation/pages/osm_map_picker_page.dart';
import 'package:latlong2/latlong.dart';
class StoreInfoPage extends StatefulWidget {
const StoreInfoPage({super.key});
@override
State<StoreInfoPage> createState() => _StoreInfoPageState();
}
class _StoreInfoPageState extends State<StoreInfoPage> {
final _nameController = TextEditingController();
final _provinceController = TextEditingController();
final _cityController = TextEditingController();
final _addressController = TextEditingController();
final _plaqueController = TextEditingController();
final _postalCodeController = TextEditingController();
final _phoneController = TextEditingController();
final _licenseController = TextEditingController();
@override
void initState() {
super.initState();
final bloc = context.read<StoreInfoBloc>();
_nameController.text = bloc.state.storeName;
_provinceController.text = bloc.state.province;
_cityController.text = bloc.state.city;
_addressController.text = bloc.state.address;
_plaqueController.text = bloc.state.plaque;
_postalCodeController.text = bloc.state.postalCode;
_phoneController.text = bloc.state.contactPhone ?? '';
_licenseController.text = bloc.state.licenseNumber ?? '';
}
@override
void dispose() {
_nameController.dispose();
_provinceController.dispose();
_cityController.dispose();
_addressController.dispose();
_plaqueController.dispose();
_postalCodeController.dispose();
_phoneController.dispose();
_licenseController.dispose();
super.dispose();
}
Future<void> _pickImage(BuildContext context) async {
try {
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(
source: ImageSource.gallery,
imageQuality: 80,
);
if (image != null && context.mounted) {
context.read<StoreInfoBloc>().add(StoreLogoChanged(image.path));
}
} on PlatformException catch (e) {
print("خطای دسترسی یا پلتفرم: ${e.message}");
} catch (e) {
print("یک خطای ناشناخته رخ داد: $e");
}
}
Future<void> _showWorkingHoursDialog(BuildContext context) async {
final result = await showDialog<Map<String, dynamic>>(
context: context,
builder: (context) => const WorkingHoursDialog(),
);
if (result != null && context.mounted) {
context.read<StoreInfoBloc>().add(
WorkingScheduleChanged(
days: result['days'] as List<String>,
startTime: result['startTime'] as String,
endTime: result['endTime'] as String,
),
);
}
}
final List<CategoryEntity> activityTypes = [
CategoryEntity(id: "6803b940-3e19-48cd-9190-28d9f25421ff", name: "فست فود", emoji: "🍔🍕"),
CategoryEntity(id: "71e371f8-a47a-4a58-aee6-4ed0f26bf29b", name: "پوشاک", emoji: "👚👔"),
CategoryEntity(id: "42acff41-1165-4e62-89b9-58db7329ec3a", name: "تریا", emoji: "🍨🍹"),
CategoryEntity(id: "e33dd7f9-5b20-4273-8eea-59da6ca5f206", name: "لوازم دیجیتال", emoji: "📱📷"),
CategoryEntity(id: "b5881239-bfd5-4c27-967a-187316a7e0b7", name: "رستوران", emoji: "🍣🍢"),
CategoryEntity(id: "b73a868a-a2d2-4d96-8fd4-615327ed9629", name: "کافی شاپ", emoji: "☕🍰"),
CategoryEntity(id: "2f38918c-5566-4aec-a0a9-2c7c48b1e878", name: "کیف و کفش", emoji: "👜👞"),
CategoryEntity(id: "52c51010-3a63-4264-a350-e011c889f3dd", name: "سینما", emoji: "🎭🎟️"),
CategoryEntity(id: "34185954-f79f-4b9e-8eb2-1702679c40a0", name: "لوازم آرایشی", emoji: "💄💅️"),
CategoryEntity(id: "e4517b0c-aacf-4758-94bd-85f45062980f", name: "طلا و زیورآلات", emoji: "💍💎"),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _buildCustomAppBar(context),
body: BlocListener<StoreInfoBloc, StoreInfoState>(
listener: (context, state) {
if (state.isSuccess) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => BlocProvider.value(
value: BlocProvider.of<StoreInfoBloc>(context),
child: const StoreInfoDisplayPage(),
),
),
);
}
if (state.errorMessage != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.errorMessage!), backgroundColor: Colors.red),
);
}
},
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
const Row(
children: [
Text(
"لوگوی فروشگاه",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
),
SizedBox(width: 5),
Text("(اختیاری)", style: TextStyle(color: Colors.black54)),
],
),
const SizedBox(height: 24),
Center(
child: GestureDetector(
onTap: () => _pickImage(context),
child: CircleAvatar(
radius: 65,
backgroundColor: AppColors.uploadElevated,
child: BlocBuilder<StoreInfoBloc, StoreInfoState>(
builder: (context, state) {
return Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: const Color.fromARGB(
255,
224,
224,
224,
).withOpacity(0.5),
spreadRadius: 1,
blurRadius: 40,
offset: const Offset(0, 10),
),
],
image:
state.logoPath != null
? DecorationImage(
image: FileImage(File(state.logoPath!)),
fit: BoxFit.cover,
)
: null,
),
child: Stack(
alignment: Alignment.center,
children: [
Align(
alignment: Alignment.bottomRight,
child: CircleAvatar(
radius: 22,
backgroundColor: Colors.white,
child: CircleAvatar(
radius: 20,
backgroundColor: Colors.white,
child: SvgPicture.asset(Assets.icons.edit02),
),
),
),
],
),
);
},
),
),
),
),
const SizedBox(height: 32),
_buildSectionTitle(),
const SizedBox(height: 30),
_buildTextField(
label: "نام فروشگاه",
isRequired: true,
hint: "مثلاً کافه ایرونی",
controller: _nameController,
onChanged:
(value) => context.read<StoreInfoBloc>().add(
StoreNameChanged(value),
),
),
const SizedBox(height: 30),
_buildActivityTypeDropdown(context),
const SizedBox(height: 30),
Row(
children: [
Expanded(
child: _buildTextField(
label: "استان",
hint: "اصفهان",
controller: _provinceController,
onChanged:
(value) => context.read<StoreInfoBloc>().add(
ProvinceChanged(value),
),
),
),
const SizedBox(width: 16),
Expanded(
child: _buildTextField(
controller: _cityController,
label: "شهر",
hint: "اصفهان",
onChanged:
(value) => context.read<StoreInfoBloc>().add(
CityChanged(value),
),
),
),
],
),
const SizedBox(height: 30),
_buildTextField(
controller: _addressController,
label: "جزئیات آدرس",
maxLines: 3,
hint: "خیابان، محله، ساختمان و ....",
onChanged:
(value) =>
context.read<StoreInfoBloc>().add(AddressChanged(value)),
),
const SizedBox(height: 30),
_buildFeaturesSection(),
const SizedBox(height: 50),
Row(
children: [
Expanded(
child: _buildTextField(
controller: _plaqueController,
label: "پلاک",
onChanged:
(value) => context.read<StoreInfoBloc>().add(
PlaqueChanged(value),
),
),
),
const SizedBox(width: 16),
Expanded(
child: _buildTextField(
controller: _postalCodeController,
label: "کد پستی",
onChanged: (value) => context
.read<StoreInfoBloc>()
.add(PostalCodeChanged(value)),
),
),
],
),
const SizedBox(height: 30),
Center(
child: TextButton.icon(
onPressed: () async {
final result = await Navigator.of(context).push<LatLng?>(
MaterialPageRoute(
builder: (context) => const OsmMapPickerPage(),
),
);
if (result != null) {
context.read<StoreInfoBloc>().add(
StoreLocationChanged(
latitude: result.latitude,
longitude: result.longitude,
),
);
}
},
icon: SvgPicture.asset(
Assets.icons.map,
color: AppColors.button,
height: 23,
),
label: const Text(
"انتخاب آدرس فروشگاه روی نقشه",
style: TextStyle(color: AppColors.button),
),
),
),
Center(
child: BlocBuilder<StoreInfoBloc, StoreInfoState>(
builder: (context, state) {
if (state.latitude != null && state.longitude != null) {
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
"آدرس با موفقیت ثبت شد ✓",
style: TextStyle(
color: Colors.green.shade700,
fontWeight: FontWeight.bold,
),
),
);
}
return const SizedBox.shrink();
},
),
),
const SizedBox(height: 30),
_buildTextField(
controller: _phoneController,
label: "تلفن تماس",
keyboardType: TextInputType.phone,
hint: "شماره تماس ثابت یا موبایل فروشگاه",
onChanged: (value) =>
context.read<StoreInfoBloc>().add(ContactPhoneChanged(value)),
),
const SizedBox(height: 30),
_buildWorkingHoursPicker(context),
const SizedBox(height: 30),
_buildTextField(
controller: _licenseController,
label: "شماره جواز کسب",
hint: "شناسه صنفی 12 رقمی یکتا",
onChanged: (value) =>
context.read<StoreInfoBloc>().add(LicenseNumberChanged(value)),
),
const SizedBox(height: 44),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => BlocProvider.value(
value: BlocProvider.of<StoreInfoBloc>(context),
child: const StoreInfoDisplayPage(),
),
),
);
},
child: const Text("تایید و ادامه"),
),
),
const SizedBox(height: 34),
],
),
),
));
}
Widget _buildSectionTitle() {
return const Text(
"ثبت مشخصات",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
);
}
Widget _buildTextField({
required String label,
bool isRequired = false,
String? hint,
int maxLines = 1,
int? maxLength,
TextInputType? keyboardType,
TextEditingController? controller,
ValueChanged<String>? onChanged,
}) {
return TextFormField(
controller: controller,
onChanged: onChanged,
maxLines: maxLines,
maxLength: maxLength,
keyboardType: keyboardType,
decoration: InputDecoration(
counterText: "",
hintText: hint,
hintStyle: const TextStyle(fontSize: 15, color: Colors.grey),
label: RichText(
text: TextSpan(
text: label,
style: const TextStyle(
color: Colors.black,
fontFamily: 'Dana',
fontSize: 19,
fontWeight: FontWeight.bold,
),
children: [
if (isRequired)
const TextSpan(
text: ' *',
style: TextStyle(color: Colors.red, fontSize: 16),
),
],
),
),
),
);
}
Widget _buildFeaturesSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"ویژگی‌های فروشگاه",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
"حداکثر ۳ ویژگی برای معرفی بهتر فروشگاه خود اضافه کنید.",
style: TextStyle(color: Colors.grey, fontSize: 14),
),
const SizedBox(height: 16),
BlocBuilder<StoreInfoBloc, StoreInfoState>(
buildWhen: (previous, current) => previous.features != current.features,
builder: (context, state) {
return Column(
children: [
...state.features.asMap().entries.map((entry) {
final index = entry.key;
final controller = TextEditingController(text: entry.value)
..selection = TextSelection.fromPosition(
TextPosition(offset: entry.value.length),
);
return Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: Row(
children: [
Expanded(
child: _buildTextField(
controller: controller,
label: "ویژگی ${index + 1}",
hint: "مثلا: دارای ویوی شهر",
maxLength: 60,
onChanged: (value) {
context
.read<StoreInfoBloc>()
.add(StoreFeatureUpdated(index, value));
},
),
),
IconButton(
icon: const Icon(Icons.remove_circle_outline, color: Colors.red),
onPressed: () {
context
.read<StoreInfoBloc>()
.add(StoreFeatureRemoved(index));
},
),
],
),
);
}).toList(),
if (state.features.length < 3)
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
icon: const Icon(Icons.add),
label: const Text("افزودن ویژگی جدید"),
onPressed: () {
context.read<StoreInfoBloc>().add(StoreFeatureAdded());
},
),
),
],
);
},
),
],
);
}
Widget _buildWorkingHoursPicker(BuildContext context) {
return InkWell(
onTap: () => _showWorkingHoursDialog(context),
child: InputDecorator(
decoration: InputDecoration(
label: RichText(
text: const TextSpan(
text: "ساعت کار فروشگاه",
style: TextStyle(
color: Colors.black,
fontFamily: 'Dana',
fontSize: 19,
fontWeight: FontWeight.bold,
),
children: [
TextSpan(
text: ' *',
style: TextStyle(color: Colors.red, fontSize: 16),
),
],
),
),
contentPadding: const EdgeInsets.symmetric(
vertical: 18,
horizontal: 12,
),
),
child: BlocBuilder<StoreInfoBloc, StoreInfoState>(
buildWhen: (p, c) => p.workingDays != c.workingDays,
builder: (context, state) {
final hasData =
state.workingDays.isNotEmpty && state.startTime != null;
const Map<String, String> dayTranslations = {
'Saturday': 'شنبه',
'Sunday': 'یکشنبه',
'Monday': 'دوشنبه',
'Tuesday': 'سه‌شنبه',
'Wednesday': 'چهارشنبه',
'Thursday': 'پنج‌شنبه',
'Friday': 'جمعه',
};
final displayDays = state.workingDays
.map((day) => dayTranslations[day] ?? '')
.join('، ');
String displayText =
hasData
? "$displayDays\nاز ساعت ${state.startTime} تا ${state.endTime}"
: "انتخاب روز و ساعت کاری";
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
displayText,
style: TextStyle(
fontSize: 16,
color: hasData ? Colors.black : Colors.grey.shade600,
height: 1.5,
),
textAlign: TextAlign.right,
),
),
],
);
},
),
),
);
}
Widget _buildActivityTypeDropdown(BuildContext context) {
return Theme(
data: Theme.of(context).copyWith(canvasColor: const Color(0xFFF6F6F6)),
child: DropdownButtonFormField<String>(
value: context.watch<StoreInfoBloc>().state.activityTypeId,
icon: SvgPicture.asset(
Assets.icons.arrowDown,
width: 24,
color: Colors.black,
),
decoration: InputDecoration(
label: RichText(
text: const TextSpan(
text: "نوع فعالیت",
style: TextStyle(
color: Colors.black,
fontFamily: 'Dana',
fontSize: 19,
fontWeight: FontWeight.bold,
),
children: [
TextSpan(
text: ' *',
style: TextStyle(color: Colors.red, fontSize: 16),
),
],
),
),
),
borderRadius: BorderRadius.circular(12.0),
isExpanded: true,
items: activityTypes.map((CategoryEntity category) {
return DropdownMenuItem<String>(
value: category.id,
child: Text("${category.emoji} ${category.name}"),
);
}).toList(),
onChanged: (value) {
if (value != null) {
context.read<StoreInfoBloc>().add(ActivityTypeChanged(value));
}
},
),
);
}
}
PreferredSizeWidget _buildCustomAppBar(BuildContext context) {
return PreferredSize(
preferredSize: const Size.fromHeight(70.0),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(15)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Column(
children: [
const SizedBox(height: 15),
Row(
children: [
Padding(
padding: const EdgeInsets.only(right: 8),
child: SvgPicture.asset(Assets.icons.logoWithName),
),
const Spacer(),
Row(
children: [
IconButton(
onPressed: () {},
icon: SvgPicture.asset(
Assets.icons.discountShape,
color: Colors.black,
),
),
IconButton(
onPressed: () {},
icon: SvgPicture.asset(Assets.icons.scanBarcode),
),
],
),
],
),
],
),
),
),
),
);
}
// Future<void> _pickWorkingHours(BuildContext context) async {
// // ۱. انتخاب تاریخ شروع
// Jalali? startDate = await showPersianDatePicker(
// context: context,
// initialDate: Jalali.now(),
// firstDate: Jalali(1400),
// lastDate: Jalali(1405),
// );
// if (startDate == null || !context.mounted) return;
// // ۲. انتخاب ساعت شروع
// TimeOfDay? startTime = await showTimePicker(
// context: context,
// initialTime: TimeOfDay.now(),
// );
// if (startTime == null || !context.mounted) return;
// // ۳. انتخاب تاریخ پایان
// Jalali? endDate = await showPersianDatePicker(
// context: context,
// initialDate: startDate, // شروع از تاریخ انتخابی قبلی
// firstDate: startDate, // تاریخ پایان نمی‌تواند قبل از شروع باشد
// lastDate: Jalali(1405),
// );
// if (endDate == null || !context.mounted) return;
// // ۴. انتخاب ساعت پایان
// TimeOfDay? endTime = await showTimePicker(
// context: context,
// initialTime: startTime,
// );
// if (endTime == null || !context.mounted) return;
// // ۵. تبدیل به آبجکت DateTime و ارسال به BLoC
// final DateTime startDateTime = startDate.toDateTime().add(
// Duration(hours: startTime.hour, minutes: startTime.minute),
// );
// final DateTime endDateTime = endDate.toDateTime().add(
// Duration(hours: endTime.hour, minutes: endTime.minute),
// );
// context.read<StoreInfoBloc>().add(
// WorkingHoursChanged(
// startDateTime: startDateTime,
// endDateTime: endDateTime,
// ),
// );
// }