proxibuy_bussiness/lib/presentation/pages/add_discount_page.dart

587 lines
22 KiB
Dart

import 'dart:io';
import 'package:business_panel/core/config/app_colors.dart';
import 'package:business_panel/domain/entities/discount_type_entity.dart';
import 'package:business_panel/gen/assets.gen.dart';
import 'package:business_panel/presentation/discount/bloc/discount_bloc.dart';
import 'package:business_panel/presentation/discount/bloc/discount_event.dart';
import 'package:business_panel/presentation/discount/bloc/discount_state.dart';
import 'package:business_panel/presentation/widgets/custom_app_bar.dart';
import 'package:business_panel/presentation/widgets/info_popup.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:image_picker/image_picker.dart';
import 'package:persian_datetime_picker/persian_datetime_picker.dart';
class AddDiscountPage extends StatelessWidget {
final String? discountId;
const AddDiscountPage({super.key, this.discountId});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) {
final bloc = DiscountBloc();
if (discountId != null) {
bloc.add(FetchDiscountDetails(discountId!));
}
return bloc;
},
child: _AddDiscountView(discountId: discountId),
);
}
}
class _AddDiscountView extends StatefulWidget {
final String? discountId;
const _AddDiscountView({this.discountId});
@override
State<_AddDiscountView> createState() => _AddDiscountViewState();
}
class _AddDiscountViewState extends State<_AddDiscountView> {
final _nameController = TextEditingController();
final _descController = TextEditingController();
final _priceController = TextEditingController();
final _discountPriceController = TextEditingController();
bool get _isEditMode => widget.discountId != null;
@override
void dispose() {
_nameController.dispose();
_descController.dispose();
_priceController.dispose();
_discountPriceController.dispose();
super.dispose();
}
Future<void> _pickValidityDates(BuildContext context) async {
Jalali? startDate = await showPersianDatePicker(
context: context,
initialDate: Jalali.now(),
firstDate: Jalali.now(),
lastDate: Jalali(1500),
);
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(1500),
);
if (endDate == null || !context.mounted) return;
TimeOfDay? endTime = await showTimePicker(
context: context,
initialTime: startTime,
);
if (endTime == null || !context.mounted) return;
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<DiscountBloc>().add(
ValidityDateChanged(startDate: startDateTime, endDate: endDateTime),
);
}
final List<DiscountTypeEntity> discountTypes = [
DiscountTypeEntity(id: "dda9aef3-367e-48b3-ba35-8e7744d99659", name: "ساعت خوش"),
DiscountTypeEntity(id: "57577fac-7d06-4b2e-a577-7d2ce98fee58", name: "رفیق بازی"),
DiscountTypeEntity(id: "06156635-048b-4ed9-b5d5-2f89824435e1", name: "محصول جانبی رایگان"),
DiscountTypeEntity(id: "16e7d1e9-29c4-4320-9869-3c986cc20734", name: "کالای مکمل"),
DiscountTypeEntity(id: "fb600fbb-bab4-4e63-a25b-d8ffdacb7c09", name: "پلکانی"),
DiscountTypeEntity(id: "488ef29e-415d-4362-b984-509faabac058", name: "دعوت نامه طلایی"),
DiscountTypeEntity(id: "e03e5823-27d8-4f45-bd6c-f7c11822ec7a", name: "بازگشت وجه"),
DiscountTypeEntity(id: "bb0eea57-b630-4373-baff-72df72921e67", name: "سایر"),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar(),
body: BlocConsumer<DiscountBloc, DiscountState>(
listener: (context, state) {
if (state.isSuccess) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
"تخفیف با موفقیت ${_isEditMode ? 'ویرایش' : 'ثبت'} شد!"),
backgroundColor: Colors.green),
);
Navigator.of(context).pop(true);
}
if (state.errorMessage != null && !state.isLoadingDetails) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.errorMessage!), backgroundColor: Colors.red),
);
}
if (state.productName.isNotEmpty && _nameController.text.isEmpty) {
_nameController.text = state.productName;
_descController.text = state.description;
_priceController.text = state.price;
_discountPriceController.text = state.discountedPrice;
}
},
builder: (context, state) {
if (state.isLoadingDetails) {
return const Center(child: CircularProgressIndicator());
}
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_isEditMode ? "ویرایش تخفیف" : "تعریف تخفیف جدید",
style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 20),
),
const SizedBox(height: 24),
_buildSectionTitle(
title: "بارگذاری عکس از محصول",
popupTitle: "یه عکس خوب، یه فروش خوب‌تر!",
isMandatory: !_isEditMode,
infoText:
"عکس واضح، باکیفیت و واقعی از محصولت بذار. ترجیحا از عکس‌های اینترنتی یا تبلیغاتی استفاده نکن.",
iconPath: Assets.icons.camera,
),
const SizedBox(height: 16),
_buildImagePickers(),
const SizedBox(height: 30),
_buildTextField(
controller: _nameController,
label: "نام محصول",
isRequired: true,
hint: "وافل شکلات فندقی",
onChanged: (value) =>
context.read<DiscountBloc>().add(ProductNameChanged(value)),
),
const SizedBox(height: 30),
_buildDiscountTypeDropdown(state), // Pass state here
const SizedBox(height: 30),
_buildTextField(
controller: _descController,
label: "توضیح برای تخفیف",
hint: "مثلاً عصرونه، با ۵٪ تخفیف مهمون ما باش! ",
isRequired: true,
maxLines: 4,
maxLength: 200,
onChanged: (value) =>
context.read<DiscountBloc>().add(DescriptionChanged(value)),
),
const SizedBox(height: 30),
_buildDateTimePicker(),
const SizedBox(height: 30),
_buildTimeRangePicker(context),
const SizedBox(height: 30),
_buildTextField(
controller: _priceController,
label: "قیمت بدون تخفیف",
isRequired: true,
hint: "مثلاً 240000 تومان",
keyboardType: TextInputType.number,
onChanged: (value) =>
context.read<DiscountBloc>().add(PriceChanged(value)),
),
const SizedBox(height: 30),
_buildTextField(
controller: _discountPriceController,
label: "قیمت با تخفیف",
hint: "مثلاً 200000 تومان",
isRequired: true,
keyboardType: TextInputType.number,
onChanged: (value) => context
.read<DiscountBloc>()
.add(DiscountedPriceChanged(value)),
),
const SizedBox(height: 30),
_buildNotificationRadiusSlider(),
const SizedBox(height: 30),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: state.isSubmitting
? null
: () {
if (_isEditMode) {
context
.read<DiscountBloc>()
.add(UpdateDiscount(widget.discountId!));
} else {
context.read<DiscountBloc>().add(SubmitDiscount());
}
},
child: state.isSubmitting
? const CircularProgressIndicator(color: Colors.white)
: Text(_isEditMode ? "ثبت تغییرات" : "ثبت تخفیف"),
),
),
const SizedBox(height: 30),
],
),
);
},
),
);
}
Widget _buildSectionTitle({
required String title,
String? popupTitle,
bool isMandatory = false,
String? infoText,
String? iconPath,
}) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (infoText != null && iconPath != null)
IconButton(
onPressed: () => showInfoDialog(
context,
title: popupTitle ?? title,
content: infoText,
iconPath: iconPath,
),
icon: SvgPicture.asset(Assets.icons.infoCircle, width: 17),
),
Text(
title,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
),
if (isMandatory)
const Text(' *', style: TextStyle(color: Colors.red, fontSize: 17)),
],
);
}
Widget _buildImagePickers() {
return BlocBuilder<DiscountBloc, DiscountState>(
buildWhen: (p, c) => p.productImages != c.productImages,
builder: (context, state) {
// We ensure the list has at least 2 elements for the UI, filling with null
final displayImages = List<Map<String, String?>?>.from(state.productImages);
while (displayImages.length < 2) {
displayImages.add(null);
}
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(2, (index) {
// *** CHANGE IS HERE: Read from the map structure ***
final imageMap = displayImages[index];
final imageUrl = imageMap?['url'];
final isUrl = imageUrl?.startsWith('http') ?? false;
return GestureDetector(
onTap: () async {
final ImagePicker picker = ImagePicker();
final XFile? image =
await picker.pickImage(source: ImageSource.gallery, imageQuality: 80);
if (image != null && context.mounted) {
context
.read<DiscountBloc>()
.add(ProductImageAdded(image.path, index));
}
},
child: Container(
width: 125,
height: 125,
decoration: BoxDecoration(
color: AppColors.uploadElevated,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.uploadElevated),
image: imageUrl != null
? DecorationImage(
image: isUrl
? NetworkImage(imageUrl)
: FileImage(File(imageUrl)) as ImageProvider,
fit: BoxFit.cover,
)
: null,
),
child: imageUrl == null
? Center(
child: SvgPicture.asset(
Assets.icons.addPic,
width: 60,
),
)
: null,
),
);
}),
);
},
);
}
Widget _buildDiscountTypeDropdown(DiscountState state) {
// Create a set of available IDs for quick lookup.
final availableTypeIds = discountTypes.map((type) => type.id).toSet();
// Check if the current discount's type ID is in our list. If not, use null.
final String? selectedValue = availableTypeIds.contains(state.discountTypeId)
? state.discountTypeId
: null;
return DropdownButtonFormField<String>(
value: selectedValue, // Use the safe value here.
icon: SvgPicture.asset(
Assets.icons.arrowDown,
width: 24,
color: Colors.black,
),
menuMaxHeight: 400,
hint: const Text("نوع تخفیف را انتخاب کنید"),
decoration: _inputDecoration("نوع تخفیف", isRequired: true).copyWith(
contentPadding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 20,
),
),
borderRadius: BorderRadius.circular(12.0),
items: discountTypes.map((type) {
return DropdownMenuItem(value: type.id, child: Text(type.name));
}).toList(),
onChanged: (value) {
if (value != null) {
context.read<DiscountBloc>().add(DiscountTypeChanged(value));
}
},
);
}
Widget _buildDateTimePicker() {
return BlocBuilder<DiscountBloc, DiscountState>(
buildWhen: (previous, current) =>
previous.startDate != current.startDate ||
previous.endDate != current.endDate,
builder: (context, state) {
String displayText = "انتخاب تاریخ";
if (state.startDate != null && state.endDate != null) {
final jalaliStart = DateTimeExtensions(state.startDate!).toJalali();
final jalaliEnd = DateTimeExtensions(state.endDate!).toJalali();
final startFormatted =
'${jalaliStart.year}/${jalaliStart.month.toString().padLeft(2, '0')}/${jalaliStart.day.toString().padLeft(2, '0')} - ${state.startDate!.hour.toString().padLeft(2, '0')}:${state.startDate!.minute.toString().padLeft(2, '0')}';
final endFormatted =
'${jalaliEnd.year}/${jalaliEnd.month.toString().padLeft(2, '0')}/${jalaliEnd.day.toString().padLeft(2, '0')} - ${state.endDate!.hour.toString().padLeft(2, '0')}:${state.endDate!.minute.toString().padLeft(2, '0')}';
displayText = 'از $startFormatted\nتا $endFormatted';
}
return InkWell(
onTap: () => _pickValidityDates(context),
child: InputDecorator(
decoration: _inputDecoration(
"تاریخ اعتبار تخفیف",
isRequired: true,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
displayText,
textDirection: TextDirection.rtl,
style: const TextStyle(fontSize: 15),
),
),
SvgPicture.asset(Assets.icons.calendarSearch),
],
),
),
);
},
);
}
Widget _buildTimeRangePicker(BuildContext context) {
return BlocBuilder<DiscountBloc, DiscountState>(
buildWhen: (previous, current) =>
previous.startTime != current.startTime ||
previous.endTime != current.endTime,
builder: (context, state) {
String displayText = "انتخاب بازه زمانی";
if (state.startTime != null && state.endTime != null) {
displayText = 'از ساعت ${state.startTime} تا ${state.endTime}';
}
return InkWell(
onTap: () async {
final TimeOfDay? startTime =
await showTimePicker(context: context, initialTime: TimeOfDay.now());
if (startTime == null) return;
final TimeOfDay? endTime =
await showTimePicker(context: context, initialTime: startTime);
if (endTime == null) return;
final formattedStartTime =
'${startTime.hour.toString().padLeft(2, '0')}:${startTime.minute.toString().padLeft(2, '0')}';
final formattedEndTime =
'${endTime.hour.toString().padLeft(2, '0')}:${endTime.minute.toString().padLeft(2, '0')}';
context.read<DiscountBloc>().add(
TimeRangeChanged(
startTime: formattedStartTime, endTime: formattedEndTime),
);
},
child: InputDecorator(
decoration: _inputDecoration("بازه زمانی معتبر", isRequired: true),
child: Text(displayText),
),
);
},
);
}
Widget _buildNotificationRadiusSlider() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
IconButton(
onPressed: () => showInfoDialog(
context,
title: "انتخاب محدوده نمایش تخفیف",
content:
"محدوده‌ای رو مشخص کن که تخفیف‌هات فقط به کاربرانی که تو اون شعاع هستن نشون داده بشه.",
iconPath: Assets.icons.radar2,
),
icon: SvgPicture.asset(Assets.icons.infoCircle, width: 17),
),
const Text(
"شعاع ارسال اعلان تخفیف به مشتری‌ها",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
),
],
),
],
),
BlocBuilder<DiscountBloc, DiscountState>(
builder: (context, state) {
return Column(
children: [
SliderTheme(
data: SliderTheme.of(context).copyWith(
activeTrackColor: AppColors.active,
inactiveTrackColor: Colors.grey.shade300,
trackShape: const RoundedRectSliderTrackShape(),
trackHeight: 4.0,
thumbColor: AppColors.active,
thumbShape:
const RoundSliderThumbShape(enabledThumbRadius: 12.0),
overlayColor: AppColors.active.withAlpha(32),
overlayShape:
const RoundSliderOverlayShape(overlayRadius: 28.0),
),
child: Slider(
value: state.notificationRadius,
min: 0,
max: 1000,
divisions: 100,
label: '${state.notificationRadius.toInt()} متر',
onChanged: (value) {
context
.read<DiscountBloc>()
.add(NotificationRadiusChanged(value));
},
),
),
const SizedBox(height: 7),
Text(
'${state.notificationRadius.toInt()} متر',
style: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 14,
color: Colors.black,
),
),
],
);
},
),
],
);
}
Widget _buildTextField({
required String label,
String? hint,
bool isRequired = false,
int? maxLines,
int? maxLength,
TextInputType? keyboardType,
required TextEditingController controller,
ValueChanged<String>? onChanged,
}) {
return ValueListenableBuilder<TextEditingValue>(
valueListenable: controller,
builder: (context, value, child) {
return TextFormField(
controller: controller,
onChanged: onChanged,
maxLines: maxLines,
maxLength: maxLength,
keyboardType: keyboardType,
decoration: _inputDecoration(label, hint: hint, isRequired: isRequired)
.copyWith(
counterText: '',
counter: maxLength != null
? Text(
'${value.text.length}/$maxLength',
style: Theme.of(context).textTheme.bodySmall,
)
: null,
),
);
},
);
}
InputDecoration _inputDecoration(String label,
{String? hint, bool isRequired = false}) {
return InputDecoration(
hintText: hint,
hintStyle: const TextStyle(color: Color.fromARGB(255, 95, 95, 95), fontSize: 14),
label: RichText(
text: TextSpan(
text: label,
style: const TextStyle(
color: Colors.black,
fontFamily: 'Dana',
fontSize: 18,
fontWeight: FontWeight.bold,
),
children: [
if (isRequired)
const TextSpan(
text: ' *', style: TextStyle(color: Colors.red)),
],
),
),
);
}
}