607 lines
20 KiB
Dart
607 lines
20 KiB
Dart
import 'dart:io';
|
|
import 'package:business_panel/core/config/app_colors.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 {
|
|
const AddDiscountPage({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return BlocProvider(
|
|
create: (_) => DiscountBloc(),
|
|
child: const _AddDiscountView(),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _AddDiscountView extends StatefulWidget {
|
|
const _AddDiscountView();
|
|
|
|
@override
|
|
State<_AddDiscountView> createState() => _AddDiscountViewState();
|
|
}
|
|
|
|
class _AddDiscountViewState extends State<_AddDiscountView> {
|
|
final _nameController = TextEditingController();
|
|
final _descController = TextEditingController();
|
|
final _priceController = TextEditingController();
|
|
final _discountPriceController = TextEditingController();
|
|
|
|
@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),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: _buildCustomAppBar(context),
|
|
body: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
"تعریف تخفیف جدید",
|
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
|
|
),
|
|
const SizedBox(height: 24),
|
|
_buildSectionTitle(
|
|
title: "بارگذاری عکس از محصول",
|
|
popupTitle: "یه عکس خوب، یه فروش خوبتر!",
|
|
isMandatory: true,
|
|
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(),
|
|
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: () {
|
|
// TODO: Implement submit logic
|
|
},
|
|
child: const Text("ثبت تخفیف"),
|
|
),
|
|
),
|
|
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>(
|
|
builder: (context, state) {
|
|
return Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: List.generate(2, (index) {
|
|
final imagePath =
|
|
state.productImages.length > index
|
|
? state.productImages[index]
|
|
: null;
|
|
return GestureDetector(
|
|
onTap: () async {
|
|
final ImagePicker picker = ImagePicker();
|
|
final XFile? image = await picker.pickImage(
|
|
source: ImageSource.gallery,
|
|
);
|
|
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:
|
|
imagePath != null
|
|
? DecorationImage(
|
|
image: FileImage(File(imagePath)),
|
|
fit: BoxFit.cover,
|
|
)
|
|
: null,
|
|
),
|
|
child:
|
|
imagePath == null
|
|
? Center(
|
|
child: SvgPicture.asset(
|
|
Assets.icons.addPic,
|
|
width: 60,
|
|
),
|
|
)
|
|
: null,
|
|
),
|
|
);
|
|
}),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildDiscountTypeDropdown() {
|
|
final List<String> discountTypes = [
|
|
"ساعت خوش",
|
|
"رفیق بازی",
|
|
"محصول جانبی رایگان",
|
|
"کالای مکمل",
|
|
"پلکانی",
|
|
"دعوتنامه طلایی",
|
|
"بازگشت وجه",
|
|
"سایر",
|
|
];
|
|
|
|
return DropdownButtonFormField<String>(
|
|
icon: SvgPicture.asset(
|
|
Assets.icons.arrowDown,
|
|
width: 24,
|
|
color: Colors.black,
|
|
),
|
|
menuMaxHeight: 400,
|
|
hint: Text("ساعت خوش"),
|
|
decoration: _inputDecoration("نوع تخفیف", isRequired: true).copyWith(
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
vertical: 14,
|
|
horizontal: 20,
|
|
),
|
|
),
|
|
borderRadius: BorderRadius.circular(12.0),
|
|
items:
|
|
discountTypes
|
|
.map((type) => DropdownMenuItem(value: type, child: Text(type)))
|
|
.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), // Optional: for better fit
|
|
),
|
|
),
|
|
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),
|
|
),
|
|
Text(
|
|
"شعاع ارسال اعلان تخفیف به مشتریها",
|
|
style: const 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),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
SizedBox(height: 7,),
|
|
BlocBuilder<DiscountBloc, DiscountState>(
|
|
builder: (context, state) {
|
|
return 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: 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: 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),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|