|
|
import 'dart:developer';
|
|
|
import 'dart:ui' as ui;
|
|
|
import 'package:easy_localization/easy_localization.dart';
|
|
|
import 'package:flutter/material.dart';
|
|
|
import 'package:flutter_html/flutter_html.dart';
|
|
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
|
|
import 'package:flutter_svg/flutter_svg.dart';
|
|
|
import 'package:mohem_flutter_app/api/offers_and_discounts_api_client.dart';
|
|
|
import 'package:mohem_flutter_app/app_state/app_state.dart';
|
|
|
import 'package:mohem_flutter_app/classes/colors.dart';
|
|
|
import 'package:mohem_flutter_app/classes/consts.dart';
|
|
|
import 'package:mohem_flutter_app/classes/utils.dart';
|
|
|
import 'package:mohem_flutter_app/config/routes.dart';
|
|
|
import 'package:mohem_flutter_app/extensions/int_extensions.dart';
|
|
|
import 'package:mohem_flutter_app/extensions/string_extensions.dart';
|
|
|
import 'package:mohem_flutter_app/extensions/widget_extensions.dart';
|
|
|
import 'package:mohem_flutter_app/generated/locale_keys.g.dart';
|
|
|
import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_retailer_model.dart';
|
|
|
import 'package:mohem_flutter_app/widgets/app_bar_widget.dart';
|
|
|
import 'package:mohem_flutter_app/widgets/app_in_app_webview_screen.dart';
|
|
|
import 'package:mohem_flutter_app/widgets/button/default_button.dart';
|
|
|
import 'package:mohem_flutter_app/widgets/info_chip_widget.dart';
|
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
|
|
|
|
class WalaPlusRetailerDetailsScreen extends StatefulWidget {
|
|
|
final WalaPlusRetailerModel? retailer;
|
|
|
final String? retailerId;
|
|
|
|
|
|
const WalaPlusRetailerDetailsScreen({Key? key, this.retailer, this.retailerId}) : super(key: key);
|
|
|
|
|
|
@override
|
|
|
State<WalaPlusRetailerDetailsScreen> createState() => _WalaPlusRetailerDetailsScreenState();
|
|
|
}
|
|
|
|
|
|
class _WalaPlusRetailerDetailsScreenState extends State<WalaPlusRetailerDetailsScreen> {
|
|
|
WalaPlusRetailerModel? _retailer;
|
|
|
bool _isLoading = true;
|
|
|
bool _hasDetailsData = false;
|
|
|
String? _effectiveRetailerId;
|
|
|
bool _hasFetched = false;
|
|
|
bool _isRedeeming = false;
|
|
|
|
|
|
@override
|
|
|
void initState() {
|
|
|
super.initState();
|
|
|
_retailer = widget.retailer;
|
|
|
_effectiveRetailerId = widget.retailerId ?? widget.retailer?.id;
|
|
|
}
|
|
|
|
|
|
@override
|
|
|
void didChangeDependencies() {
|
|
|
super.didChangeDependencies();
|
|
|
if (_effectiveRetailerId == null) {
|
|
|
Object? args = ModalRoute.of(context)?.settings.arguments;
|
|
|
if (args is WalaPlusRetailerModel) {
|
|
|
_retailer ??= args;
|
|
|
_effectiveRetailerId = args.id;
|
|
|
} else if (args is String) {
|
|
|
_effectiveRetailerId = args;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (!_hasFetched && _effectiveRetailerId != null) {
|
|
|
_hasFetched = true;
|
|
|
_fetchRetailerDetails();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
Future<void> _fetchRetailerDetails() async {
|
|
|
if (_effectiveRetailerId == null) {
|
|
|
if (mounted) {
|
|
|
setState(() {
|
|
|
_isLoading = false;
|
|
|
});
|
|
|
}
|
|
|
return;
|
|
|
}
|
|
|
setState(() {
|
|
|
_isLoading = true;
|
|
|
});
|
|
|
|
|
|
try {
|
|
|
debugPrint("WalaPlusRetailerDetailsScreen: fetching details for retailer ID: $_effectiveRetailerId");
|
|
|
WalaPlusRetailerModel? result = await OffersAndDiscountsApiClient().getWalaPlusRetailerById(retailerId: _effectiveRetailerId!, context: context);
|
|
|
if (result != null && mounted) {
|
|
|
if ((result.image == null || result.image!.isEmpty || result.image == "null") && _retailer?.image != null) {
|
|
|
result.image = _retailer!.image;
|
|
|
}
|
|
|
if ((result.imageThumb == null || result.imageThumb!.isEmpty || result.imageThumb == "null") && _retailer?.imageThumb != null) {
|
|
|
result.imageThumb = _retailer!.imageThumb;
|
|
|
}
|
|
|
if ((result.logo == null || result.logo!.isEmpty || result.logo == "null") && _retailer?.logo != null) {
|
|
|
result.logo = _retailer!.logo;
|
|
|
}
|
|
|
if ((result.logoThumb == null || result.logoThumb!.isEmpty || result.logoThumb == "null") && _retailer?.logoThumb != null) {
|
|
|
result.logoThumb = _retailer!.logoThumb;
|
|
|
}
|
|
|
if ((result.name == null || result.name!.isEmpty) && _retailer?.name != null) {
|
|
|
result.name = _retailer!.name;
|
|
|
}
|
|
|
if ((result.type == null || result.type!.isEmpty) && _retailer?.type != null) {
|
|
|
result.type = _retailer!.type;
|
|
|
}
|
|
|
if ((result.locations == null || result.locations!.isEmpty) && _retailer?.locations != null && _retailer!.locations!.isNotEmpty) {
|
|
|
result.locations = _retailer!.locations;
|
|
|
}
|
|
|
if ((result.offers == null || result.offers!.isEmpty) && _retailer?.offers != null && _retailer!.offers!.isNotEmpty) {
|
|
|
result.offers = _retailer!.offers;
|
|
|
}
|
|
|
if ((result.validity == null || result.validity!.isEmpty) && _retailer?.validity != null) {
|
|
|
result.validity = _retailer!.validity;
|
|
|
}
|
|
|
if ((result.offerTitle == null || result.offerTitle!.isEmpty) && _retailer?.offerTitle != null) {
|
|
|
result.offerTitle = _retailer!.offerTitle;
|
|
|
}
|
|
|
if (result.contact == null && _retailer?.contact != null) {
|
|
|
result.contact = _retailer!.contact;
|
|
|
}
|
|
|
if ((result.email == null || result.email!.isEmpty) && _retailer?.email != null) {
|
|
|
result.email = _retailer!.email;
|
|
|
}
|
|
|
if ((result.phone == null || result.phone!.isEmpty) && _retailer?.phone != null) {
|
|
|
result.phone = _retailer!.phone;
|
|
|
}
|
|
|
|
|
|
setState(() {
|
|
|
_retailer = result;
|
|
|
_hasDetailsData = true;
|
|
|
});
|
|
|
}
|
|
|
} catch (e) {
|
|
|
debugPrint("Error fetching WalaPlus retailer by ID: $e");
|
|
|
} finally {
|
|
|
if (mounted) {
|
|
|
setState(() {
|
|
|
_isLoading = false;
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
Future<void> _openInAppBrowser(String url, {String? offerType}) async {
|
|
|
String title = _retailer?.name ?? LocaleKeys.offerDetails.tr();
|
|
|
bool shouldStartScanning = offerType?.trim().toLowerCase() == "discount";
|
|
|
|
|
|
await Navigator.push(
|
|
|
context,
|
|
|
MaterialPageRoute(builder: (BuildContext context) => AppInAppWebViewScreen(url: url, title: title, onPageLoaded: shouldStartScanning ? _clickStartScanningButton : null)),
|
|
|
);
|
|
|
}
|
|
|
|
|
|
// Vue's @click="startScanning()" is a component method, not something on window,
|
|
|
// so we find the actual button by its text and click it (lets Vue's own handler fire).
|
|
|
// Vue may still be mounting when onLoadStop fires, so poll briefly before giving up.
|
|
|
// Only called for offerType "discount" - "promo" offers just show a promo code, no scanner.
|
|
|
void _clickStartScanningButton(InAppWebViewController controller) {
|
|
|
controller.evaluateJavascript(
|
|
|
source: r"""
|
|
|
(function() {
|
|
|
var attempts = 0;
|
|
|
var maxAttempts = 10; // ~0.5s at 50ms
|
|
|
var timer = setInterval(function() {
|
|
|
attempts++;
|
|
|
var buttons = document.querySelectorAll('button');
|
|
|
for (var i = 0; i < buttons.length; i++) {
|
|
|
var text = (buttons[i].textContent || '').replace(/\s+/g, ' ').trim();
|
|
|
if (text.indexOf('Start Scanning') !== -1) {
|
|
|
buttons[i].click();
|
|
|
clearInterval(timer);
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
|
if (attempts >= maxAttempts) {
|
|
|
clearInterval(timer);
|
|
|
}
|
|
|
}, 50);
|
|
|
})();
|
|
|
""",
|
|
|
);
|
|
|
}
|
|
|
|
|
|
void _onUseOffer() {
|
|
|
if (_isRedeeming) return;
|
|
|
|
|
|
List<WalaPlusOfferModel> offers = _retailer?.offers ?? [];
|
|
|
log("offers.length: ${offers.length}");
|
|
|
|
|
|
if (offers.length > 1) {
|
|
|
_showOffersBottomSheet(offers);
|
|
|
} else if (offers.length == 1) {
|
|
|
WalaPlusOfferModel singleOffer = offers.first;
|
|
|
String offerId = (singleOffer.id != null && singleOffer.id!.isNotEmpty) ? singleOffer.id! : (_retailer?.id ?? _effectiveRetailerId ?? "");
|
|
|
String offerType = (singleOffer.type != null && singleOffer.type!.isNotEmpty) ? singleOffer.type! : (_retailer?.offerType ?? "promo");
|
|
|
_redeemOffer(offerId: offerId, offerType: offerType);
|
|
|
} else {
|
|
|
String offerId = _retailer?.id ?? _effectiveRetailerId ?? "";
|
|
|
if (offerId.isEmpty) {
|
|
|
Utils.showToast(LocaleKeys.offerIdNotAvailable.tr());
|
|
|
return;
|
|
|
}
|
|
|
_redeemOffer(offerId: offerId, offerType: _retailer?.offerType ?? "promo");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
Future<void> _redeemOffer({required String offerId, required String offerType}) async {
|
|
|
if (_isRedeeming) return;
|
|
|
if (offerId.isEmpty) {
|
|
|
Utils.showToast(LocaleKeys.offerIdNotAvailable.tr());
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
setState(() {
|
|
|
_isRedeeming = true;
|
|
|
});
|
|
|
Utils.showLoading(context);
|
|
|
|
|
|
try {
|
|
|
Map<String, dynamic>? res = await OffersAndDiscountsApiClient().redeemWalaPlusOffer(offerId: offerId, offerType: offerType, context: context);
|
|
|
|
|
|
Utils.hideLoading(context);
|
|
|
|
|
|
String? transactionUrl;
|
|
|
if (res != null) {
|
|
|
transactionUrl = res['Transaction_Url'] ?? res['TransactionUrl'] ?? res['transaction_url'] ?? res['transactionUrl'] ?? res['Url'] ?? res['url'];
|
|
|
}
|
|
|
|
|
|
log("transactionUrl: ${transactionUrl.toString()}");
|
|
|
|
|
|
if (transactionUrl != null && transactionUrl.trim().isNotEmpty) {
|
|
|
await _openInAppBrowser(transactionUrl.trim(), offerType: offerType);
|
|
|
} else {
|
|
|
String? error = res?['Error']?.toString() ?? res?['error']?.toString() ?? res?['ErrorMessage']?.toString() ?? res?['message']?.toString();
|
|
|
if (error != null && error.isNotEmpty && error != "null") {
|
|
|
Utils.showToast(error);
|
|
|
} else {
|
|
|
Utils.showToast(LocaleKeys.unableToRedeemOffer.tr());
|
|
|
}
|
|
|
}
|
|
|
} catch (e) {
|
|
|
Utils.hideLoading(context);
|
|
|
debugPrint("Error redeeming offer: $e");
|
|
|
Utils.showToast(LocaleKeys.errorRedeemingOffer.tr());
|
|
|
} finally {
|
|
|
if (mounted) {
|
|
|
setState(() {
|
|
|
_isRedeeming = false;
|
|
|
});
|
|
|
} else {
|
|
|
_isRedeeming = false;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
void _showOffersBottomSheet(List<WalaPlusOfferModel> offers) {
|
|
|
bool isAr = Directionality.of(context) == ui.TextDirection.rtl || AppState().isArabic(context);
|
|
|
|
|
|
showModalBottomSheet(
|
|
|
context: context,
|
|
|
isScrollControlled: true,
|
|
|
backgroundColor: Colors.transparent,
|
|
|
builder: (BuildContext sheetContext) {
|
|
|
return Container(
|
|
|
constraints: BoxConstraints(maxHeight: MediaQuery.of(sheetContext).size.height * 0.78),
|
|
|
decoration: const BoxDecoration(color: Color(0xFFF8F9FA), borderRadius: BorderRadius.vertical(top: Radius.circular(24))),
|
|
|
child: Column(
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
children: [
|
|
|
12.height,
|
|
|
Center(child: Container(width: 44, height: 4.5, decoration: BoxDecoration(color: const Color(0xFFCBD5E1), borderRadius: BorderRadius.circular(2.5)))),
|
|
|
12.height,
|
|
|
Padding(
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
|
child: Row(
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
|
children: [
|
|
|
Expanded(
|
|
|
child: Column(
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
children: [
|
|
|
LocaleKeys.availableOffers.tr().toText18(isBold: true, color: const Color(0xFF1E293B)),
|
|
|
4.height,
|
|
|
LocaleKeys.selectOfferToRedeem.tr().toText12(color: const Color(0xFF64748B)),
|
|
|
],
|
|
|
),
|
|
|
),
|
|
|
IconButton(
|
|
|
icon: const Icon(Icons.close_rounded, color: Color(0xFF64748B), size: 24),
|
|
|
onPressed: () => Navigator.pop(sheetContext),
|
|
|
padding: EdgeInsets.zero,
|
|
|
constraints: const BoxConstraints(),
|
|
|
),
|
|
|
],
|
|
|
),
|
|
|
),
|
|
|
14.height,
|
|
|
const Divider(height: 1, color: Color(0xFFF1F5F9)),
|
|
|
Flexible(
|
|
|
child: ListView.separated(
|
|
|
shrinkWrap: true,
|
|
|
physics: const BouncingScrollPhysics(),
|
|
|
padding: const EdgeInsets.fromLTRB(20, 16, 20, 24),
|
|
|
itemCount: offers.length,
|
|
|
separatorBuilder: (_, __) => 14.height,
|
|
|
itemBuilder: (BuildContext _, int index) {
|
|
|
WalaPlusOfferModel offer = offers[index];
|
|
|
return _buildOfferBottomSheetCard(sheetContext, offer, isAr);
|
|
|
},
|
|
|
),
|
|
|
),
|
|
|
],
|
|
|
),
|
|
|
);
|
|
|
},
|
|
|
);
|
|
|
}
|
|
|
|
|
|
Widget _buildOfferBottomSheetCard(BuildContext sheetContext, WalaPlusOfferModel offer, bool isAr) {
|
|
|
String title = offer.title ?? "";
|
|
|
String description = offer.description ?? "";
|
|
|
String type = (offer.type != null && offer.type!.isNotEmpty) ? offer.type! : "discount";
|
|
|
String? expiredAt = offer.expiredAt;
|
|
|
|
|
|
bool hasDifferentDescription = description.trim().isNotEmpty && description.trim() != title.trim();
|
|
|
|
|
|
return Container(
|
|
|
decoration: BoxDecoration(
|
|
|
color: Colors.white,
|
|
|
borderRadius: BorderRadius.circular(16),
|
|
|
boxShadow: [BoxShadow(color: const Color(0xff000000).withOpacity(.05), blurRadius: 10, offset: const Offset(0, 3))],
|
|
|
),
|
|
|
padding: const EdgeInsets.all(16),
|
|
|
child: Column(
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
children: [
|
|
|
Row(
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
children: [
|
|
|
Container(
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
|
|
decoration: BoxDecoration(color: const Color(0xFFE6F7F0), borderRadius: BorderRadius.circular(8)),
|
|
|
child: type.toUpperCase().toText10(isBold: true, color: const Color(0xFF00A651)),
|
|
|
),
|
|
|
if (expiredAt != null && expiredAt.trim().isNotEmpty && expiredAt != "null") ...[
|
|
|
Row(
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
children: [const Icon(Icons.event_outlined, size: 12, color: Color(0xFF64748B)), const SizedBox(width: 4), expiredAt.toText10(color: const Color(0xFF64748B))],
|
|
|
),
|
|
|
],
|
|
|
],
|
|
|
),
|
|
|
10.height,
|
|
|
title.toText14(isBold: true, color: const Color(0xFF1E293B), maxlines: 2),
|
|
|
if (hasDifferentDescription) ...[6.height, description.toText12(color: const Color(0xFF64748B), maxLine: 3)],
|
|
|
DefaultButton(
|
|
|
LocaleKeys.useOffer.tr(),
|
|
|
() {
|
|
|
Navigator.pop(sheetContext);
|
|
|
String offerId = (offer.id != null && offer.id!.isNotEmpty) ? offer.id! : (_retailer?.id ?? _effectiveRetailerId ?? "");
|
|
|
String offerType = (offer.type != null && offer.type!.isNotEmpty) ? offer.type! : (_retailer?.offerType ?? "promo");
|
|
|
_redeemOffer(offerId: offerId, offerType: offerType);
|
|
|
},
|
|
|
height: 42,
|
|
|
borderRadius: 10,
|
|
|
fontSize: 14,
|
|
|
),
|
|
|
],
|
|
|
),
|
|
|
);
|
|
|
}
|
|
|
|
|
|
@override
|
|
|
Widget build(BuildContext context) {
|
|
|
bool isAr = Directionality.of(context) == ui.TextDirection.rtl || AppState().isArabic(context);
|
|
|
String title = _retailer?.name ?? LocaleKeys.offerDetails.tr();
|
|
|
|
|
|
return Scaffold(
|
|
|
backgroundColor: const Color(0xFFF8F9FA),
|
|
|
appBar: AppBarWidget(
|
|
|
context,
|
|
|
title: title,
|
|
|
showHomeButton: true,
|
|
|
onHomeTapped: () {
|
|
|
if (ApiConsts.isDependetLogin) {
|
|
|
Navigator.popUntil(context, ModalRoute.withName(AppRoutes.dashboardScreenForDependet));
|
|
|
} else {
|
|
|
Navigator.popUntil(context, ModalRoute.withName(AppRoutes.dashboard));
|
|
|
}
|
|
|
},
|
|
|
),
|
|
|
body:
|
|
|
(_isLoading && !_hasDetailsData)
|
|
|
? Center(child: Image.asset("assets/images/logos/loading_mohemm_logo.gif", height: 96.0, width: 96.0))
|
|
|
: Column(
|
|
|
children: [
|
|
|
if (_isLoading && _hasDetailsData) const LinearProgressIndicator(minHeight: 2.5, backgroundColor: Colors.transparent, valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF168A89))),
|
|
|
Expanded(
|
|
|
child: SingleChildScrollView(
|
|
|
physics: const BouncingScrollPhysics(),
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
|
|
child: Column(
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
children: [
|
|
|
Container(
|
|
|
decoration: BoxDecoration(
|
|
|
color: Colors.white,
|
|
|
borderRadius: BorderRadius.circular(20),
|
|
|
boxShadow: [BoxShadow(color: const Color(0xff000000).withOpacity(.05), blurRadius: 14, offset: const Offset(0, 4))],
|
|
|
),
|
|
|
padding: const EdgeInsets.all(16),
|
|
|
child: Column(
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
children: [
|
|
|
// 1. Top Banner Image with rounded corners
|
|
|
ClipRRect(
|
|
|
borderRadius: BorderRadius.circular(16),
|
|
|
child: SizedBox(
|
|
|
height: 190,
|
|
|
width: double.infinity,
|
|
|
child:
|
|
|
(_retailer?.displayImage != null && _retailer!.displayImage!.isNotEmpty)
|
|
|
? Image.network(
|
|
|
_retailer!.displayImage!,
|
|
|
fit: BoxFit.cover,
|
|
|
errorBuilder: (_, __, ___) => Container(color: const Color(0xFFF3F4F6), child: const Icon(Icons.broken_image, size: 48, color: Color(0xFF9CA3AF))),
|
|
|
)
|
|
|
: Container(color: const Color(0xFFF3F4F6), child: const Icon(Icons.image_outlined, size: 48, color: Color(0xFF9CA3AF))),
|
|
|
),
|
|
|
),
|
|
|
14.height,
|
|
|
|
|
|
// 2. Brand Strip (Human Resources - Retailer Logo - Dr Sulaiman Al Habib)
|
|
|
_buildBrandStrip(isAr),
|
|
|
18.height,
|
|
|
|
|
|
// 3. Retailer Name (Centered, Bold) & Offer Title Subtitle
|
|
|
(_retailer?.name ?? "").toText22(isBold: true, isCentered: false, color: const Color(0xFF1E293B)),
|
|
|
// if (_retailer?.offerTitle != null && _retailer!.offerTitle!.trim().isNotEmpty && _retailer!.offerTitle!.trim() != _retailer!.name?.trim()) ...[
|
|
|
// 6.height,
|
|
|
// Center(child: _cleanHtml(_retailer!.offerTitle!).toText16(isCentered: true, color: const Color(0xFF168A89))),
|
|
|
// ],
|
|
|
18.height,
|
|
|
|
|
|
// 4. 3 Info Badges Row (Store Type, Validity, City)
|
|
|
_buildInfoBadgesRow(isAr),
|
|
|
10.height,
|
|
|
|
|
|
// 5. Description Section (parsed as HTML)
|
|
|
// "${LocaleKeys.description.tr()}:".toText16(isBold: true, color: const Color(0xFF1E293B)),
|
|
|
// 8.height,
|
|
|
// Html(
|
|
|
// data: _getHowToUseHtml(isAr),
|
|
|
// style: {"body": Style(fontSize: FontSize(14.0), color: const Color(0xFF4B5563))},
|
|
|
// onLinkTap: (String? url, Map<String, String> attributes, _) async {
|
|
|
// if (url != null && url.isNotEmpty) {
|
|
|
// try {
|
|
|
// Uri uri = Uri.parse(url);
|
|
|
// if (!uri.hasScheme) {
|
|
|
// uri = Uri.parse("https://$url");
|
|
|
// }
|
|
|
// await launchUrl(uri, mode: LaunchMode.externalApplication);
|
|
|
// } catch (e) {
|
|
|
// debugPrint("Error launching url: $e");
|
|
|
// }
|
|
|
// }
|
|
|
// },
|
|
|
// ),
|
|
|
],
|
|
|
),
|
|
|
),
|
|
|
|
|
|
// 6. Locations Section (below description)
|
|
|
_buildLocationsSection(isAr),
|
|
|
|
|
|
// 7. Contact Information (Email & Phone Number)
|
|
|
_buildContactSection(isAr),
|
|
|
|
|
|
16.height,
|
|
|
],
|
|
|
),
|
|
|
),
|
|
|
),
|
|
|
|
|
|
// 7. Bottom Action Button
|
|
|
Container(
|
|
|
padding: const EdgeInsets.fromLTRB(16, 10, 16, 20),
|
|
|
color: Colors.white,
|
|
|
child: SizedBox(width: double.infinity, height: 50, child: DefaultButton(LocaleKeys.useOffer.tr(), _isRedeeming ? null : _onUseOffer, height: 50, borderRadius: 12, fontSize: 16)),
|
|
|
),
|
|
|
],
|
|
|
),
|
|
|
);
|
|
|
}
|
|
|
|
|
|
Widget _buildBrandStrip(bool isAr) {
|
|
|
String? logoUrl = _retailer?.displayLogo;
|
|
|
return Row(
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
|
children: <Widget>[
|
|
|
// Left: Retailer Logo
|
|
|
if (logoUrl != null && logoUrl.isNotEmpty)
|
|
|
Container(
|
|
|
decoration: BoxDecoration(
|
|
|
color: Colors.white,
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
boxShadow: [BoxShadow(color: const Color(0xff000000).withOpacity(.04), blurRadius: 4, offset: const Offset(0, 2))],
|
|
|
),
|
|
|
padding: const EdgeInsets.all(2),
|
|
|
child: Image.network(logoUrl, height: 36, width: 36, fit: BoxFit.contain, errorBuilder: (_, __, ___) => const SizedBox(width: 36, height: 36)),
|
|
|
)
|
|
|
else
|
|
|
const SizedBox(width: 36, height: 36),
|
|
|
|
|
|
// Right: Mazaya Brand Logo
|
|
|
SvgPicture.asset("assets/icons/mazaya_brand.svg", height: 48, fit: BoxFit.contain),
|
|
|
],
|
|
|
);
|
|
|
}
|
|
|
|
|
|
Widget _buildInfoBadgesRow(bool isAr) {
|
|
|
bool isOnline = (_retailer?.type?.toLowerCase() == 'online');
|
|
|
String storeTypeText = isOnline ? LocaleKeys.online.tr() : LocaleKeys.inStore.tr();
|
|
|
|
|
|
IconData storeIcon = isOnline ? Icons.language_rounded : Icons.storefront_rounded;
|
|
|
|
|
|
String cityText = _getDisplayCity(isAr);
|
|
|
String validityText = _retailer?.getDisplayValidity() ?? LocaleKeys.offerValid.tr();
|
|
|
|
|
|
return Wrap(
|
|
|
spacing: 12,
|
|
|
runSpacing: 10,
|
|
|
children: [
|
|
|
InfoChipWidget(
|
|
|
text: storeTypeText,
|
|
|
icon: storeIcon,
|
|
|
backgroundColor: isOnline ? const Color(0xFFEFF6FF) : const Color(0xFFF5F3FF),
|
|
|
textColor: isOnline ? const Color(0xFF2563EB) : const Color(0xFF7C3AED),
|
|
|
),
|
|
|
InfoChipWidget(text: validityText, backgroundColor: const Color(0xFFE6F7F0), textColor: const Color(0xFF00A651)),
|
|
|
InfoChipWidget(
|
|
|
text: cityText,
|
|
|
svgIcon: "assets/icons/location.svg",
|
|
|
isBold: false,
|
|
|
backgroundColor: const Color(0xFFF3F4F6),
|
|
|
textColor: const Color(0xFF4B5563),
|
|
|
iconColor: const Color(0xFF64748B),
|
|
|
iconTextGap: 2.5,
|
|
|
),
|
|
|
],
|
|
|
);
|
|
|
}
|
|
|
|
|
|
String _getDisplayCity(bool isAr) {
|
|
|
if (_retailer?.locations != null && _retailer!.locations!.isNotEmpty) {
|
|
|
// 1. First pass: look for a specific city name (e.g. Riyadh, Jeddah, Hail)
|
|
|
for (var loc in _retailer!.locations!) {
|
|
|
String? cName = isAr ? loc.cityNameAr : loc.cityNameEn;
|
|
|
if (cName != null && cName.trim().isNotEmpty && cName != "null") {
|
|
|
return cName.trim();
|
|
|
}
|
|
|
}
|
|
|
// 2. Second pass: fallback to 'city' field if not generic country
|
|
|
for (var loc in _retailer!.locations!) {
|
|
|
if (loc.city != null && loc.city!.trim().isNotEmpty && loc.city != "null") {
|
|
|
String c = loc.city!.trim();
|
|
|
if (c.toLowerCase() != 'saudi arabia' && c != 'المملكة العربية السعودية') {
|
|
|
return c;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
// 3. Third pass: any non-empty city
|
|
|
for (var loc in _retailer!.locations!) {
|
|
|
if (loc.city != null && loc.city!.trim().isNotEmpty && loc.city != "null") {
|
|
|
return loc.city!.trim();
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
if (_retailer?.type?.toLowerCase() == 'online') {
|
|
|
return LocaleKeys.ksa.tr();
|
|
|
}
|
|
|
return LocaleKeys.riyadh.tr();
|
|
|
}
|
|
|
|
|
|
String _getHowToUseHtml(bool isAr) {
|
|
|
String? content;
|
|
|
|
|
|
// 1. Language-specific how-to-use field
|
|
|
if (isAr) {
|
|
|
if (_retailer?.arHowToUse != null && _retailer!.arHowToUse!.trim().isNotEmpty && _retailer!.arHowToUse != "null") {
|
|
|
content = _retailer!.arHowToUse!.trim();
|
|
|
}
|
|
|
} else {
|
|
|
if (_retailer?.enHowToUse != null && _retailer!.enHowToUse!.trim().isNotEmpty && _retailer!.enHowToUse != "null") {
|
|
|
content = _retailer!.enHowToUse!.trim();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 2. In WalaPlus API, 'usage' field often holds the rich HTML formatted instructions
|
|
|
if (content == null || content.isEmpty) {
|
|
|
if (_retailer?.usage != null) {
|
|
|
String u = _retailer!.usage.toString().trim();
|
|
|
if (u.isNotEmpty && u != "null") {
|
|
|
// If usage contains HTML tags or step keywords, prioritize it
|
|
|
if (u.contains('<') || u.toLowerCase().contains('step') || u.contains('خطوة') || u.toLowerCase().contains('discount')) {
|
|
|
content = u;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 3. Fallback to generic 'howToUse'
|
|
|
if (content == null || content.isEmpty) {
|
|
|
if (_retailer?.howToUse != null && _retailer!.howToUse!.trim().isNotEmpty && _retailer!.howToUse != "null") {
|
|
|
content = _retailer!.howToUse!.trim();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 4. Fallback to 'usage' even if plain text
|
|
|
if (content == null || content.isEmpty) {
|
|
|
if (_retailer?.usage != null) {
|
|
|
String u = _retailer!.usage.toString().trim();
|
|
|
if (u.isNotEmpty && u != "null" && int.tryParse(u) == null) {
|
|
|
content = u;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 5. Fallback to the other language field
|
|
|
if (content == null || content.isEmpty) {
|
|
|
if (isAr) {
|
|
|
if (_retailer?.enHowToUse != null && _retailer!.enHowToUse!.trim().isNotEmpty && _retailer!.enHowToUse != "null") {
|
|
|
content = _retailer!.enHowToUse!.trim();
|
|
|
}
|
|
|
} else {
|
|
|
if (_retailer?.arHowToUse != null && _retailer!.arHowToUse!.trim().isNotEmpty && _retailer!.arHowToUse != "null") {
|
|
|
content = _retailer!.arHowToUse!.trim();
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 6. Fallback to description or info
|
|
|
if (content == null || content.isEmpty) {
|
|
|
if (_retailer?.description != null && _retailer!.description!.trim().isNotEmpty && _retailer!.description != "null") {
|
|
|
content = _retailer!.description!.trim();
|
|
|
} else if (_retailer?.info != null && _retailer!.info!.trim().isNotEmpty && _retailer!.info != "null") {
|
|
|
content = _retailer!.info!.trim();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (content == null || content.isEmpty) {
|
|
|
return "<p>${LocaleKeys.noInstructionsAvailable.tr()}</p>";
|
|
|
}
|
|
|
|
|
|
return _formatHtmlInstructions(content);
|
|
|
}
|
|
|
|
|
|
String _formatHtmlInstructions(String raw) {
|
|
|
String s = raw.trim();
|
|
|
|
|
|
// 1. Strip surrounding quotes (handles multiple layers e.g. "\"text\"")
|
|
|
while ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
|
|
if (s.length >= 2) {
|
|
|
s = s.substring(1, s.length - 1).trim();
|
|
|
} else {
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 2. Unescape JSON slashes & quotes
|
|
|
s = s.replaceAll(r'\/', '/').replaceAll(r'\"', '"').replaceAll(r"\'", "'");
|
|
|
|
|
|
// 3. Decode \uXXXX Unicode escape sequences
|
|
|
s = s.replaceAllMapped(RegExp(r'\\u([0-9a-fA-F]{4})'), (match) {
|
|
|
try {
|
|
|
int codeUnit = int.parse(match.group(1)!, radix: 16);
|
|
|
return String.fromCharCode(codeUnit);
|
|
|
} catch (_) {
|
|
|
return match.group(0)!;
|
|
|
}
|
|
|
});
|
|
|
|
|
|
// 4. Decode common specific unicode escapes (quotes, bullets, dashes, etc.)
|
|
|
s = s
|
|
|
.replaceAll(r'\u201c', '“')
|
|
|
.replaceAll(r'\u201d', '”')
|
|
|
.replaceAll(r'\u2019', '’')
|
|
|
.replaceAll(r'\u2018', '‘')
|
|
|
.replaceAll(r'\u00a0', ' ')
|
|
|
.replaceAll(r'\u2022', '•')
|
|
|
.replaceAll(r'\u2013', '–')
|
|
|
.replaceAll(r'\u2014', '—')
|
|
|
.replaceAll(r'\u200f', '')
|
|
|
.replaceAll(r'\u200e', '')
|
|
|
.replaceAll(r'\u061f', '؟')
|
|
|
.replaceAll(r'\u201', '“'); // Fallback if truncated to 3 digits
|
|
|
|
|
|
// 5. Decode HTML entities
|
|
|
s = s.replaceAll('"', '"').replaceAll(''', "'").replaceAll(''', "'").replaceAll(' ', ' ').replaceAll('&', '&');
|
|
|
|
|
|
// 6. Check if string contains HTML tags
|
|
|
bool hasHtmlTags = RegExp(r'<[a-zA-Z][^>]*>').hasMatch(s);
|
|
|
|
|
|
if (!hasHtmlTags) {
|
|
|
// Plain text: format nicely into structured HTML with bold step titles and line breaks
|
|
|
s = s.replaceAll(r'\r\n', '\n').replaceAll(r'\n', '\n');
|
|
|
|
|
|
// Separate step markers
|
|
|
RegExp stepRegex = RegExp(
|
|
|
r'(\b(?:First|Second|Third|Fourth|Fifth|Sixth|Seventh|Eighth|Ninth|Tenth)\s+step\s*:?|'
|
|
|
r'\b(?:1st|2nd|3rd|4th|5th|6th|7th|8th|9th|10th)\s+step\s*:?|'
|
|
|
r'(?:الخطوة\s+(?:الأولى|الثانية|الثالثة|الرابعة|الخامسة|السادسة|السابعة|الثامنة|التاسعة|العاشرة))\s*:?)',
|
|
|
caseSensitive: false,
|
|
|
);
|
|
|
|
|
|
s = s.replaceAllMapped(stepRegex, (match) {
|
|
|
String matchText = match.group(0)!.trim();
|
|
|
return '<br><br><strong>$matchText</strong> ';
|
|
|
});
|
|
|
|
|
|
// Split remaining newlines to <br>
|
|
|
s = s.replaceAll('\n', '<br>');
|
|
|
|
|
|
// Clean leading <br> tags
|
|
|
s = s.replaceFirst(RegExp(r'^(?:<br\s*/?>)+'), '');
|
|
|
|
|
|
s = '<p>$s</p>';
|
|
|
}
|
|
|
|
|
|
return s;
|
|
|
}
|
|
|
|
|
|
Widget _buildLocationsSection(bool isAr) {
|
|
|
List<WalaPlusRetailerLocation> locations = _retailer?.locations ?? [];
|
|
|
if (locations.isEmpty) {
|
|
|
return const SizedBox.shrink();
|
|
|
}
|
|
|
|
|
|
int totalCount = locations.length;
|
|
|
List<WalaPlusRetailerLocation> previewLocations = totalCount <= 2 ? locations : locations.take(2).toList();
|
|
|
|
|
|
return Column(
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
children: [
|
|
|
18.height,
|
|
|
// Header
|
|
|
Row(
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
|
children: [
|
|
|
Row(
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
children: [
|
|
|
// Container(
|
|
|
// padding: const EdgeInsets.all(6),
|
|
|
// decoration: BoxDecoration(color: const Color(0xFF168A89).withOpacity(0.1), borderRadius: BorderRadius.circular(8)),
|
|
|
// child: const Icon(Icons.storefront_rounded, size: 18, color: Color(0xFF168A89)),
|
|
|
// ),
|
|
|
8.width,
|
|
|
LocaleKeys.locations.tr().toText16(isBold: false, color: const Color(0xFF202B33)),
|
|
|
// 8.width,
|
|
|
// Container(
|
|
|
// padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
|
|
// decoration: BoxDecoration(color: const Color(0xFFF1F5F9), borderRadius: BorderRadius.circular(10)),
|
|
|
// child: '$totalCount'.toText11(isBold: true, color: const Color(0xFF64748B)),
|
|
|
// ),
|
|
|
],
|
|
|
),
|
|
|
if (totalCount > 2)
|
|
|
InkWell(
|
|
|
onTap: () => _showAllLocationsBottomSheet(locations, isAr),
|
|
|
borderRadius: BorderRadius.circular(6),
|
|
|
child: Padding(
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
|
|
|
child: Row(
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
children: [
|
|
|
LocaleKeys.showAllLocations.tr(args: ['$totalCount']).toText12(color: const Color(0xFF047857), underlineGap: 1, isUnderLine: true, isBold: false),
|
|
|
// const SizedBox(width: 2),
|
|
|
// Icon(isAr ? Icons.chevron_left_rounded : Icons.chevron_right_rounded, size: 18, color: const Color(0xFF168A89)),
|
|
|
],
|
|
|
),
|
|
|
),
|
|
|
),
|
|
|
],
|
|
|
),
|
|
|
12.height,
|
|
|
// Preview Cards
|
|
|
ListView.separated(
|
|
|
shrinkWrap: true,
|
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
|
padding: EdgeInsets.zero,
|
|
|
itemCount: previewLocations.length,
|
|
|
separatorBuilder: (_, __) => 10.height,
|
|
|
itemBuilder: (BuildContext _, int index) {
|
|
|
return _buildLocationCard(previewLocations[index], isAr);
|
|
|
},
|
|
|
),
|
|
|
// If more than 2, show the banner to browse all
|
|
|
// if (totalCount > 2) ...[
|
|
|
// 10.height,
|
|
|
// InkWell(
|
|
|
// onTap: () => _showAllLocationsBottomSheet(locations, isAr),
|
|
|
// borderRadius: BorderRadius.circular(16),
|
|
|
// child: Container(
|
|
|
// padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
|
|
|
// decoration: BoxDecoration(
|
|
|
// color: Colors.white,
|
|
|
// borderRadius: BorderRadius.circular(16),
|
|
|
// boxShadow: [BoxShadow(color: const Color(0xff000000).withOpacity(.05), blurRadius: 10, offset: const Offset(0, 3))],
|
|
|
// ),
|
|
|
// child: Row(
|
|
|
// children: [
|
|
|
// Container(
|
|
|
// padding: const EdgeInsets.all(9),
|
|
|
// decoration: BoxDecoration(color: const Color(0xFF168A89).withOpacity(0.08), shape: BoxShape.circle),
|
|
|
// child: const Icon(Icons.map_outlined, color: Color(0xFF168A89), size: 18),
|
|
|
// ),
|
|
|
// 12.width,
|
|
|
// Expanded(
|
|
|
// child: Column(
|
|
|
// crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
// children: [
|
|
|
// LocaleKeys.showAllLocations.tr(args: ['$totalCount']).toText14(isBold: true, color: const Color(0xFF168A89)),
|
|
|
// 2.height,
|
|
|
// LocaleKeys.browseAllBranches.tr(args: ['$totalCount']).toText11(color: const Color(0xFF64748B)),
|
|
|
// ],
|
|
|
// ),
|
|
|
// ),
|
|
|
// Icon(isAr ? Icons.arrow_back_ios_rounded : Icons.arrow_forward_ios_rounded, size: 14, color: const Color(0xFF168A89)),
|
|
|
// ],
|
|
|
// ),
|
|
|
// ),
|
|
|
// ),
|
|
|
// ],
|
|
|
],
|
|
|
);
|
|
|
}
|
|
|
|
|
|
Widget _buildLocationCard(WalaPlusRetailerLocation loc, bool isAr) {
|
|
|
String cityText = loc.getLocalizedCity(isAr);
|
|
|
if (cityText.isEmpty) {
|
|
|
cityText = loc.city ?? "";
|
|
|
}
|
|
|
String addressText = loc.address ?? "";
|
|
|
|
|
|
String primaryTitle = addressText.isNotEmpty ? addressText : cityText;
|
|
|
String? secondaryText = (addressText.isNotEmpty && cityText.isNotEmpty && addressText.toLowerCase() != cityText.toLowerCase()) ? cityText : null;
|
|
|
|
|
|
return Container(
|
|
|
padding: const EdgeInsets.all(14),
|
|
|
decoration: BoxDecoration(
|
|
|
color: Colors.white,
|
|
|
borderRadius: BorderRadius.circular(16),
|
|
|
boxShadow: <BoxShadow>[BoxShadow(color: const Color(0xff000000).withOpacity(.05), blurRadius: 10, offset: const Offset(0, 3))],
|
|
|
),
|
|
|
child: Column(
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
children: [
|
|
|
// Row(
|
|
|
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
// crossAxisAlignment: CrossAxisAlignment.center,
|
|
|
// children: [
|
|
|
// // City Pill Badge
|
|
|
// if (cityText.isNotEmpty)
|
|
|
// Container(
|
|
|
// padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3.5),
|
|
|
// decoration: BoxDecoration(color: const Color(0xFF168A89).withOpacity(0.08), borderRadius: BorderRadius.circular(6)),
|
|
|
// child: Row(
|
|
|
// mainAxisSize: MainAxisSize.min,
|
|
|
// children: [const Icon(Icons.location_on, size: 12, color: Color(0xFF168A89)), const SizedBox(width: 4), cityText.toText11(isBold: true, color: const Color(0xFF168A89))],
|
|
|
// ),
|
|
|
// )
|
|
|
// else
|
|
|
// const SizedBox.shrink(),
|
|
|
// ],
|
|
|
// ),
|
|
|
// 10.height,
|
|
|
// Branch title & pin icon
|
|
|
InkWell(
|
|
|
onTap: () => _openMapForLocation(loc),
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
child: Row(
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
children: [
|
|
|
Expanded(
|
|
|
child: Row(
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
children: [
|
|
|
Container(
|
|
|
margin: const EdgeInsets.only(top: 2),
|
|
|
width: 36,
|
|
|
height: 36,
|
|
|
alignment: Alignment.center,
|
|
|
decoration: const BoxDecoration(color: Color(0xFFF3F4F6), shape: BoxShape.circle),
|
|
|
child: SvgPicture.asset("assets/icons/location.svg", width: 16, height: 16),
|
|
|
),
|
|
|
8.width,
|
|
|
Expanded(
|
|
|
child: Column(
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
children: [
|
|
|
primaryTitle.toText16(isBold: false, color: const Color(0xFF202B33), maxlines: 2),
|
|
|
if (secondaryText != null) ...[2.height, secondaryText.toText12(color: const Color(0xFF057958), maxLine: 1)],
|
|
|
],
|
|
|
),
|
|
|
),
|
|
|
],
|
|
|
),
|
|
|
),
|
|
|
|
|
|
12.width,
|
|
|
SvgPicture.asset("assets/icons/location.svg", width: 18, height: 18),
|
|
|
|
|
|
// Container(
|
|
|
// padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
|
|
// decoration: BoxDecoration(
|
|
|
// gradient: const LinearGradient(
|
|
|
// transform: GradientRotation(.83),
|
|
|
// begin: Alignment.topRight,
|
|
|
// end: Alignment.bottomLeft,
|
|
|
// colors: [MyColors.gradiantEndColor, MyColors.gradiantStartColor],
|
|
|
// ),
|
|
|
// borderRadius: BorderRadius.circular(8),
|
|
|
// ),
|
|
|
// child: Row(
|
|
|
// mainAxisSize: MainAxisSize.min,
|
|
|
// children: [const Icon(Icons.directions_outlined, color: Colors.white, size: 14), const SizedBox(width: 4), LocaleKeys.viewOnMap.tr().toText11(color: Colors.white, isBold: true)],
|
|
|
// ),
|
|
|
// ),
|
|
|
],
|
|
|
),
|
|
|
),
|
|
|
],
|
|
|
),
|
|
|
);
|
|
|
}
|
|
|
|
|
|
void _showAllLocationsBottomSheet(List<WalaPlusRetailerLocation> locations, bool isAr) {
|
|
|
Map<String, int> cityCounts = {};
|
|
|
for (var loc in locations) {
|
|
|
String c = loc.getLocalizedCity(isAr);
|
|
|
if (c.isEmpty) c = loc.city ?? "";
|
|
|
if (c.trim().isNotEmpty) {
|
|
|
cityCounts[c.trim()] = (cityCounts[c.trim()] ?? 0) + 1;
|
|
|
}
|
|
|
}
|
|
|
List<String> sortedCities = cityCounts.keys.toList()..sort((a, b) => (cityCounts[b] ?? 0).compareTo(cityCounts[a] ?? 0));
|
|
|
|
|
|
String selectedCity = "ALL";
|
|
|
String searchQuery = "";
|
|
|
TextEditingController searchController = TextEditingController();
|
|
|
|
|
|
showModalBottomSheet(
|
|
|
context: context,
|
|
|
isScrollControlled: true,
|
|
|
backgroundColor: Colors.transparent,
|
|
|
builder: (BuildContext sheetContext) {
|
|
|
return StatefulBuilder(
|
|
|
builder: (BuildContext _, StateSetter setSheetState) {
|
|
|
List<WalaPlusRetailerLocation> filtered =
|
|
|
locations.where((loc) {
|
|
|
String c = loc.getLocalizedCity(isAr);
|
|
|
if (c.isEmpty) c = loc.city ?? "";
|
|
|
if (selectedCity != "ALL" && c.trim() != selectedCity) {
|
|
|
return false;
|
|
|
}
|
|
|
if (searchQuery.isNotEmpty) {
|
|
|
String q = searchQuery.toLowerCase();
|
|
|
String addr = (loc.address ?? "").toLowerCase();
|
|
|
String cityEn = (loc.cityNameEn ?? "").toLowerCase();
|
|
|
String cityAr = (loc.cityNameAr ?? "").toLowerCase();
|
|
|
String genericCity = (loc.city ?? "").toLowerCase();
|
|
|
if (!addr.contains(q) && !cityEn.contains(q) && !cityAr.contains(q) && !genericCity.contains(q)) {
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
return true;
|
|
|
}).toList();
|
|
|
|
|
|
return Container(
|
|
|
constraints: BoxConstraints(maxHeight: MediaQuery.of(sheetContext).size.height * 0.85),
|
|
|
decoration: const BoxDecoration(color: Color(0xFFF8F9FA), borderRadius: BorderRadius.vertical(top: Radius.circular(24))),
|
|
|
child: Column(
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
children: [
|
|
|
12.height,
|
|
|
Center(child: Container(width: 44, height: 4.5, decoration: BoxDecoration(color: const Color(0xFFCBD5E1), borderRadius: BorderRadius.circular(2.5)))),
|
|
|
12.height,
|
|
|
Padding(
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
|
child: Row(
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
|
children: [
|
|
|
Expanded(
|
|
|
child: Column(
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
children: [
|
|
|
LocaleKeys.locations.tr().toText18(isBold: true, color: const Color(0xFF1E293B)),
|
|
|
3.height,
|
|
|
LocaleKeys.branchesCount.tr(args: ['${locations.length}']).toText12(color: const Color(0xFF64748B)),
|
|
|
],
|
|
|
),
|
|
|
),
|
|
|
IconButton(
|
|
|
icon: const Icon(Icons.close_rounded, color: Color(0xFF64748B), size: 24),
|
|
|
onPressed: () => Navigator.pop(sheetContext),
|
|
|
padding: EdgeInsets.zero,
|
|
|
constraints: const BoxConstraints(),
|
|
|
),
|
|
|
],
|
|
|
),
|
|
|
),
|
|
|
12.height,
|
|
|
Padding(
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
|
child: Container(
|
|
|
height: 44,
|
|
|
decoration: BoxDecoration(
|
|
|
color: Colors.white,
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
boxShadow: [BoxShadow(color: const Color(0xff000000).withOpacity(.04), blurRadius: 8, offset: const Offset(0, 2))],
|
|
|
),
|
|
|
child: Row(
|
|
|
children: [
|
|
|
12.width,
|
|
|
const Icon(Icons.search_rounded, size: 20, color: Color(0xFF94A3B8)),
|
|
|
8.width,
|
|
|
Expanded(
|
|
|
child: TextField(
|
|
|
controller: searchController,
|
|
|
style: const TextStyle(fontSize: 13, color: Color(0xFF1E293B)),
|
|
|
decoration: InputDecoration(
|
|
|
hintText: LocaleKeys.searchBranches.tr(),
|
|
|
hintStyle: const TextStyle(fontSize: 13, color: Color(0xFF94A3B8)),
|
|
|
border: InputBorder.none,
|
|
|
isDense: true,
|
|
|
contentPadding: EdgeInsets.zero,
|
|
|
),
|
|
|
onChanged: (val) {
|
|
|
setSheetState(() {
|
|
|
searchQuery = val.trim();
|
|
|
});
|
|
|
},
|
|
|
),
|
|
|
),
|
|
|
if (searchQuery.isNotEmpty)
|
|
|
GestureDetector(
|
|
|
onTap: () {
|
|
|
searchController.clear();
|
|
|
setSheetState(() {
|
|
|
searchQuery = "";
|
|
|
});
|
|
|
},
|
|
|
child: const Padding(padding: EdgeInsets.symmetric(horizontal: 10), child: Icon(Icons.cancel, size: 18, color: Color(0xFF94A3B8))),
|
|
|
),
|
|
|
],
|
|
|
),
|
|
|
),
|
|
|
),
|
|
|
if (sortedCities.length > 1) ...[
|
|
|
10.height,
|
|
|
SizedBox(
|
|
|
height: 38,
|
|
|
child: ListView.separated(
|
|
|
scrollDirection: Axis.horizontal,
|
|
|
physics: const BouncingScrollPhysics(),
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
|
itemCount: sortedCities.length + 1,
|
|
|
separatorBuilder: (_, __) => 8.width,
|
|
|
itemBuilder: (BuildContext _, int index) {
|
|
|
if (index == 0) {
|
|
|
bool isSelected = selectedCity == "ALL";
|
|
|
return GestureDetector(
|
|
|
onTap: () {
|
|
|
setSheetState(() {
|
|
|
selectedCity = "ALL";
|
|
|
});
|
|
|
},
|
|
|
child: Container(
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
|
alignment: Alignment.center,
|
|
|
decoration: BoxDecoration(borderRadius: BorderRadius.circular(6), color: isSelected ? MyColors.darkIconColor : MyColors.lightGreyEAColor),
|
|
|
child: '${LocaleKeys.allCities.tr()} (${locations.length})'.toText12(color: isSelected ? MyColors.white : MyColors.black),
|
|
|
),
|
|
|
);
|
|
|
}
|
|
|
String cName = sortedCities[index - 1];
|
|
|
int cCount = cityCounts[cName] ?? 0;
|
|
|
bool isSelected = selectedCity == cName;
|
|
|
return GestureDetector(
|
|
|
onTap: () {
|
|
|
setSheetState(() {
|
|
|
selectedCity = cName;
|
|
|
});
|
|
|
},
|
|
|
child: Container(
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
|
alignment: Alignment.center,
|
|
|
decoration: BoxDecoration(borderRadius: BorderRadius.circular(6), color: isSelected ? MyColors.darkIconColor : MyColors.lightGreyEAColor),
|
|
|
child: '$cName ($cCount)'.toText12(color: isSelected ? MyColors.white : MyColors.black),
|
|
|
),
|
|
|
);
|
|
|
},
|
|
|
),
|
|
|
),
|
|
|
],
|
|
|
12.height,
|
|
|
const Divider(height: 1, color: Color(0xFFF1F5F9)),
|
|
|
Flexible(
|
|
|
child:
|
|
|
filtered.isEmpty
|
|
|
? Center(
|
|
|
child: Padding(
|
|
|
padding: const EdgeInsets.all(32),
|
|
|
child: Column(
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
children: [
|
|
|
const Icon(Icons.location_off_outlined, size: 44, color: Color(0xFF94A3B8)),
|
|
|
12.height,
|
|
|
LocaleKeys.noBranchesFound.tr().toText14(isBold: true, color: const Color(0xFF64748B)),
|
|
|
],
|
|
|
),
|
|
|
),
|
|
|
)
|
|
|
: ListView.separated(
|
|
|
shrinkWrap: true,
|
|
|
physics: const BouncingScrollPhysics(),
|
|
|
padding: const EdgeInsets.fromLTRB(20, 16, 20, 24),
|
|
|
itemCount: filtered.length,
|
|
|
separatorBuilder: (_, __) => 10.height,
|
|
|
itemBuilder: (BuildContext _, int index) {
|
|
|
return _buildLocationCard(filtered[index], isAr);
|
|
|
},
|
|
|
),
|
|
|
),
|
|
|
],
|
|
|
),
|
|
|
);
|
|
|
},
|
|
|
);
|
|
|
},
|
|
|
);
|
|
|
}
|
|
|
|
|
|
Future<void> _openMapForLocation(WalaPlusRetailerLocation loc) async {
|
|
|
bool hasCoordinates = loc.latitude != null && loc.longitude != null && loc.latitude != 0 && loc.longitude != 0;
|
|
|
|
|
|
Uri googleMapsUrl;
|
|
|
if (hasCoordinates) {
|
|
|
googleMapsUrl = Uri.parse("https://www.google.com/maps/search/?api=1&query=${loc.latitude},${loc.longitude}");
|
|
|
} else {
|
|
|
String query = "${loc.address ?? ''} ${loc.city ?? ''}".trim();
|
|
|
if (query.isEmpty) {
|
|
|
query = _retailer?.name ?? "";
|
|
|
}
|
|
|
googleMapsUrl = Uri.parse("https://www.google.com/maps/search/?api=1&query=${Uri.encodeComponent(query)}");
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
if (await canLaunchUrl(googleMapsUrl)) {
|
|
|
await launchUrl(googleMapsUrl, mode: LaunchMode.externalApplication);
|
|
|
} else {
|
|
|
await launchUrl(googleMapsUrl);
|
|
|
}
|
|
|
} catch (e) {
|
|
|
debugPrint("Error launching maps: $e");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
Widget _buildContactSection(bool isAr) {
|
|
|
String? email = _retailer?.resolvedEmail;
|
|
|
String? phone = _retailer?.resolvedPhone;
|
|
|
|
|
|
if ((email == null || email.isEmpty) && (phone == null || phone.isEmpty)) {
|
|
|
return const SizedBox.shrink();
|
|
|
}
|
|
|
|
|
|
return Column(
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
children: [
|
|
|
18.height,
|
|
|
LocaleKeys.contactInfo.tr().toText16(isBold: false, color: const Color(0xFF1E293B)).paddingOnly(right: 8, left: 8),
|
|
|
10.height,
|
|
|
if (phone != null && phone.isNotEmpty) ...[
|
|
|
InkWell(
|
|
|
onTap: () async {
|
|
|
try {
|
|
|
Uri phoneUri = Uri(scheme: 'tel', path: phone);
|
|
|
await launchUrl(phoneUri);
|
|
|
} catch (e) {
|
|
|
debugPrint("Error launching phone: $e");
|
|
|
}
|
|
|
},
|
|
|
borderRadius: BorderRadius.circular(14),
|
|
|
child: Container(
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
|
|
decoration: BoxDecoration(
|
|
|
color: Colors.white,
|
|
|
borderRadius: BorderRadius.circular(14),
|
|
|
boxShadow: [BoxShadow(color: const Color(0xff000000).withOpacity(.05), blurRadius: 10, offset: const Offset(0, 3))],
|
|
|
),
|
|
|
child: Row(
|
|
|
children: [
|
|
|
Container(
|
|
|
width: 36,
|
|
|
height: 36,
|
|
|
alignment: Alignment.center,
|
|
|
decoration: const BoxDecoration(color: Color(0xFFF3F4F6), shape: BoxShape.circle),
|
|
|
child: const Icon(Icons.phone_outlined, color: Color(0xFF737373), size: 20),
|
|
|
),
|
|
|
12.width,
|
|
|
Expanded(
|
|
|
child: Column(
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
children: [LocaleKeys.phoneNumber.tr().toText14(color: const Color(0xFF202020)), const SizedBox(height: 2), phone.toText14(color: const Color(0xFF057958))],
|
|
|
),
|
|
|
),
|
|
|
// Icon(isAr ? Icons.arrow_back_ios_rounded : Icons.arrow_forward_ios_rounded, size: 13, color: const Color(0xFF94A3B8)),
|
|
|
],
|
|
|
),
|
|
|
),
|
|
|
),
|
|
|
if (email != null && email.isNotEmpty) 10.height,
|
|
|
],
|
|
|
if (email != null && email.isNotEmpty) ...[
|
|
|
InkWell(
|
|
|
onTap: () async {
|
|
|
try {
|
|
|
Uri emailUri = Uri(scheme: 'mailto', path: email);
|
|
|
await launchUrl(emailUri);
|
|
|
} catch (e) {
|
|
|
debugPrint("Error launching email: $e");
|
|
|
}
|
|
|
},
|
|
|
borderRadius: BorderRadius.circular(14),
|
|
|
child: Container(
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
|
|
decoration: BoxDecoration(
|
|
|
color: Colors.white,
|
|
|
borderRadius: BorderRadius.circular(14),
|
|
|
boxShadow: [BoxShadow(color: const Color(0xff000000).withOpacity(.05), blurRadius: 10, offset: const Offset(0, 3))],
|
|
|
),
|
|
|
child: Row(
|
|
|
children: [
|
|
|
Container(
|
|
|
width: 36,
|
|
|
height: 36,
|
|
|
alignment: Alignment.center,
|
|
|
decoration: const BoxDecoration(color: Color(0xFFF3F4F6), shape: BoxShape.circle),
|
|
|
child: const Icon(Icons.email_outlined, color: Color(0xFF737373), size: 20),
|
|
|
),
|
|
|
12.width,
|
|
|
Expanded(
|
|
|
child: Column(
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
children: [LocaleKeys.email.tr().toText14(color: const Color(0xFF202020)), const SizedBox(height: 2), email.toText14(color: const Color(0xFF057958))],
|
|
|
),
|
|
|
),
|
|
|
// Icon(isAr ? Icons.arrow_back_ios_rounded : Icons.arrow_forward_ios_rounded, size: 13, color: const Color(0xFF94A3B8)),
|
|
|
],
|
|
|
),
|
|
|
),
|
|
|
),
|
|
|
],
|
|
|
],
|
|
|
);
|
|
|
}
|
|
|
|
|
|
String _cleanHtml(String text) {
|
|
|
String decoded = WalaPlusRetailerModel.decodeUnicodeString(text) ?? text;
|
|
|
return decoded
|
|
|
.replaceAll(RegExp(r'<br\s*/?>', caseSensitive: false), '\n')
|
|
|
.replaceAll(RegExp(r'</p>', caseSensitive: false), '\n\n')
|
|
|
.replaceAll(RegExp(r'<[^>]*>'), '')
|
|
|
.replaceAll(' ', ' ')
|
|
|
.replaceAll('&', '&')
|
|
|
.replaceAll('"', '"')
|
|
|
.replaceAll(''', "'")
|
|
|
.replaceAll('<', '<')
|
|
|
.replaceAll('>', '>')
|
|
|
.trim();
|
|
|
}
|
|
|
}
|