628 lines
23 KiB
Dart
628 lines
23 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/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: _buildCustomAppBar(context),
|
|
body: BlocConsumer<DiscountBloc, DiscountState>(
|
|
listener: (context, state) {
|
|
if (state.isSuccess) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
"تخفیف با موفقیت ${this._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)),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
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: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
SvgPicture.asset(Assets.icons.logoWithName),
|
|
Row(
|
|
children: [
|
|
IconButton(
|
|
onPressed: () {},
|
|
icon: SvgPicture.asset(Assets.icons.discountShape, color: Colors.black),
|
|
),
|
|
IconButton(
|
|
onPressed: () {},
|
|
icon: SvgPicture.asset(Assets.icons.scanBarcode),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
} |