313 lines
13 KiB
Dart
313 lines
13 KiB
Dart
// food_merchant_page.dart — قائمة مطعم واحد + إضافة أصناف للسلة
|
|
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
|
|
import '../../constant/box_name.dart';
|
|
import '../../constant/colors.dart';
|
|
import '../../constant/style.dart';
|
|
import '../../main.dart';
|
|
import '../../views/widgets/error_snakbar.dart';
|
|
import '../../controller/food/food_controller.dart';
|
|
import '../../controller/food/food_models.dart';
|
|
import '../widgets/my_scafold.dart';
|
|
import 'food_cart_page.dart';
|
|
|
|
class FoodMerchantPage extends StatelessWidget {
|
|
final int merchantId;
|
|
const FoodMerchantPage({super.key, required this.merchantId});
|
|
|
|
bool get _isAr => box.read(BoxName.lang) == 'ar';
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final c = Get.find<FoodController>();
|
|
c.openMerchant(merchantId);
|
|
|
|
return GetBuilder<FoodController>(
|
|
builder: (c) {
|
|
final merchant = c.selectedMerchant;
|
|
return MyScafolld(
|
|
title: merchant?.nameAr ?? (_isAr ? 'المطعم' : 'Restaurant'),
|
|
isleading: true,
|
|
action: Stack(
|
|
clipBehavior: Clip.none,
|
|
children: [
|
|
IconButton(
|
|
icon: Icon(Icons.shopping_cart_rounded, color: AppColor.primaryColor),
|
|
onPressed: () => Get.to(() => const FoodCartPage()),
|
|
),
|
|
if (c.cartItemsCount > 0)
|
|
Positioned(
|
|
right: 4,
|
|
top: 4,
|
|
child: Container(
|
|
padding: const EdgeInsets.all(4),
|
|
decoration: const BoxDecoration(color: AppColor.redColor, shape: BoxShape.circle),
|
|
constraints: const BoxConstraints(minWidth: 18, minHeight: 18),
|
|
child: Text(c.cartItemsCount.toString(),
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
body: [
|
|
c.isLoadingMenu
|
|
? const Center(child: CircularProgressIndicator())
|
|
: merchant == null
|
|
? Center(child: Text(_isAr ? 'تعذر تحميل المطعم' : 'Failed to load restaurant'))
|
|
: _content(c, merchant),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _content(FoodController c, FoodMerchant merchant) {
|
|
return ListView(
|
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
|
|
children: [
|
|
if (!merchant.isOpen)
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(12),
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
decoration: BoxDecoration(
|
|
color: AppColor.redColor.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Text(
|
|
_isAr ? 'هذا المطعم مغلق حالياً' : 'This restaurant is currently closed',
|
|
style: TextStyle(color: AppColor.redColor, fontWeight: FontWeight.bold),
|
|
),
|
|
),
|
|
if (merchant.descriptionAr != null && merchant.descriptionAr!.isNotEmpty)
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 12),
|
|
child: Text(merchant.descriptionAr!, style: AppStyle.subtitle),
|
|
),
|
|
Text(
|
|
_isAr
|
|
? 'الحد الأدنى للطلب: ${(merchant.minOrderAmount / 1000).toStringAsFixed(3)} د.أ'
|
|
: 'Minimum order: ${(merchant.minOrderAmount / 1000).toStringAsFixed(3)} JOD',
|
|
style: AppStyle.subtitle.copyWith(color: AppColor.grayColor),
|
|
),
|
|
const SizedBox(height: 16),
|
|
...c.categories.map((cat) => _categorySection(c, merchant, cat)),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _categorySection(FoodController c, FoodMerchant merchant, FoodMenuCategory cat) {
|
|
if (cat.items.isEmpty) return const SizedBox.shrink();
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(cat.nameAr, style: AppStyle.headTitle2.copyWith(fontSize: 18)),
|
|
const SizedBox(height: 8),
|
|
...cat.items.map((item) => _itemTile(c, merchant, item)),
|
|
const SizedBox(height: 16),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _itemTile(FoodController c, FoodMerchant merchant, FoodMenuItem item) {
|
|
return Card(
|
|
margin: const EdgeInsets.only(bottom: 10),
|
|
color: AppColor.cardColor,
|
|
elevation: 0,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(14),
|
|
side: BorderSide(color: AppColor.borderColor),
|
|
),
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(14),
|
|
onTap: !item.isAvailable || !merchant.isOpen
|
|
? null
|
|
: () => _openItemSheet(c, merchant, item),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(12),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(item.nameAr, style: AppStyle.title.copyWith(fontWeight: FontWeight.bold)),
|
|
if (item.descriptionAr != null && item.descriptionAr!.isNotEmpty) ...[
|
|
const SizedBox(height: 4),
|
|
Text(item.descriptionAr!,
|
|
style: AppStyle.subtitle, maxLines: 2, overflow: TextOverflow.ellipsis),
|
|
],
|
|
const SizedBox(height: 6),
|
|
Text(foodFormatPrice(item.price),
|
|
style: AppStyle.title.copyWith(color: AppColor.accentColor, fontWeight: FontWeight.bold)),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
if (item.imageUrl != null && item.imageUrl!.isNotEmpty)
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(10),
|
|
child: Image.network(item.imageUrl!, width: 72, height: 72, fit: BoxFit.cover,
|
|
errorBuilder: (_, __, ___) => Container(width: 72, height: 72, color: AppColor.borderColor)),
|
|
),
|
|
if (!item.isAvailable)
|
|
Padding(
|
|
padding: const EdgeInsets.only(left: 8),
|
|
child: Text(_isAr ? 'غير متاح' : 'Unavailable',
|
|
style: TextStyle(color: AppColor.redColor, fontSize: 12, fontWeight: FontWeight.bold)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _openItemSheet(FoodController c, FoodMerchant merchant, FoodMenuItem item) {
|
|
final Map<int, List<String>> selected = {};
|
|
int quantity = 1;
|
|
|
|
Get.bottomSheet(
|
|
isScrollControlled: true,
|
|
StatefulBuilder(
|
|
builder: (context, setState) {
|
|
int unitPrice = item.price;
|
|
for (final group in item.options) {
|
|
for (final choiceId in (selected[group.id] ?? [])) {
|
|
final choice = group.choices.firstWhere((ch) => ch.id == choiceId,
|
|
orElse: () => FoodOptionChoice(id: '', labelAr: '', price: 0));
|
|
unitPrice += choice.price;
|
|
}
|
|
}
|
|
|
|
bool canSubmit = true;
|
|
for (final group in item.options) {
|
|
if (group.isRequired && (selected[group.id] ?? []).isEmpty) canSubmit = false;
|
|
}
|
|
|
|
return Container(
|
|
constraints: BoxConstraints(maxHeight: Get.height * 0.85),
|
|
decoration: BoxDecoration(
|
|
color: AppColor.secondaryColor,
|
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const SizedBox(height: 12),
|
|
Container(width: 40, height: 4,
|
|
decoration: BoxDecoration(color: Colors.grey.withOpacity(0.3), borderRadius: BorderRadius.circular(2))),
|
|
Expanded(
|
|
child: ListView(
|
|
padding: const EdgeInsets.all(20),
|
|
children: [
|
|
Text(item.nameAr, style: AppStyle.headTitle2.copyWith(fontSize: 20)),
|
|
if (item.descriptionAr != null && item.descriptionAr!.isNotEmpty) ...[
|
|
const SizedBox(height: 8),
|
|
Text(item.descriptionAr!, style: AppStyle.subtitle),
|
|
],
|
|
const SizedBox(height: 16),
|
|
...item.options.map((group) => _optionGroupWidget(group, selected, setState)),
|
|
const SizedBox(height: 16),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
IconButton(
|
|
icon: const Icon(Icons.remove_circle_outline_rounded),
|
|
onPressed: quantity > 1 ? () => setState(() => quantity--) : null,
|
|
),
|
|
Text(quantity.toString(), style: AppStyle.headTitle2.copyWith(fontSize: 20)),
|
|
IconButton(
|
|
icon: const Icon(Icons.add_circle_outline_rounded),
|
|
onPressed: () => setState(() => quantity++),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Padding(
|
|
padding: EdgeInsets.fromLTRB(20, 8, 20, MediaQuery.of(context).padding.bottom + 16),
|
|
child: SizedBox(
|
|
width: double.infinity,
|
|
height: 52,
|
|
child: ElevatedButton(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: canSubmit ? AppColor.primaryColor : AppColor.grayColor,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
|
),
|
|
onPressed: !canSubmit
|
|
? null
|
|
: () {
|
|
final ok = c.addToCartForMerchant(merchant.id, item,
|
|
quantity: quantity, options: selected);
|
|
Get.back();
|
|
if (ok) {
|
|
mySnackbarSuccess(_isAr ? 'أُضيف إلى السلة' : 'Added to cart');
|
|
}
|
|
},
|
|
child: Text(
|
|
'${_isAr ? "إضافة" : "Add"} · ${foodFormatPrice(unitPrice * quantity)}',
|
|
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _optionGroupWidget(
|
|
FoodItemOptionGroup group, Map<int, List<String>> selected, StateSetter setState) {
|
|
final isSingle = group.maxSelect <= 1;
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 12),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Text(group.groupNameAr, style: AppStyle.title.copyWith(fontWeight: FontWeight.bold)),
|
|
if (group.isRequired) ...[
|
|
const SizedBox(width: 6),
|
|
Text(_isAr ? '(مطلوب)' : '(required)',
|
|
style: TextStyle(color: AppColor.redColor, fontSize: 12)),
|
|
],
|
|
],
|
|
),
|
|
...group.choices.map((choice) {
|
|
final isChecked = (selected[group.id] ?? []).contains(choice.id);
|
|
return CheckboxListTile(
|
|
contentPadding: EdgeInsets.zero,
|
|
dense: true,
|
|
value: isChecked,
|
|
controlAffinity: ListTileControlAffinity.leading,
|
|
title: Text(choice.labelAr),
|
|
secondary: choice.price > 0 ? Text('+${foodFormatPrice(choice.price)}') : null,
|
|
onChanged: (checked) {
|
|
setState(() {
|
|
final list = List<String>.from(selected[group.id] ?? []);
|
|
if (checked == true) {
|
|
if (isSingle) list.clear();
|
|
if (!isSingle && list.length >= group.maxSelect) return;
|
|
list.add(choice.id);
|
|
} else {
|
|
list.remove(choice.id);
|
|
}
|
|
selected[group.id] = list;
|
|
});
|
|
},
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|