Update codebase
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
// 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;
|
||||
});
|
||||
},
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user