proxibuy_bussiness/lib/presentation/pages/add_discount_page.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),
),
],
),
],
),
],
),
),
),
),
);
}
}