retailer screen added.
parent
782ad51823
commit
ca6ce85d7d
@ -0,0 +1,305 @@
|
|||||||
|
import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_category_model.dart';
|
||||||
|
|
||||||
|
class WalaPlusRetailersResponse {
|
||||||
|
int? page;
|
||||||
|
int? pagesCount;
|
||||||
|
int? perPage;
|
||||||
|
List<WalaPlusRetailerModel>? retailers;
|
||||||
|
|
||||||
|
WalaPlusRetailersResponse({
|
||||||
|
this.page,
|
||||||
|
this.pagesCount,
|
||||||
|
this.perPage,
|
||||||
|
this.retailers,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory WalaPlusRetailersResponse.fromJson(Map<String, dynamic> json) {
|
||||||
|
List<WalaPlusRetailerModel> retailersList = [];
|
||||||
|
if (json['retailers'] != null && json['retailers'] is List) {
|
||||||
|
json['retailers'].forEach((v) {
|
||||||
|
retailersList.add(WalaPlusRetailerModel.fromJson(v));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return WalaPlusRetailersResponse(
|
||||||
|
page: json['page'] is int ? json['page'] : int.tryParse(json['page']?.toString() ?? ''),
|
||||||
|
pagesCount: json['pages_count'] is int ? json['pages_count'] : int.tryParse(json['pages_count']?.toString() ?? ''),
|
||||||
|
perPage: json['per_page'] is int ? json['per_page'] : int.tryParse(json['per_page']?.toString() ?? ''),
|
||||||
|
retailers: retailersList,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
Map<String, dynamic> data = <String, dynamic>{};
|
||||||
|
data['page'] = page;
|
||||||
|
data['pages_count'] = pagesCount;
|
||||||
|
data['per_page'] = perPage;
|
||||||
|
if (retailers != null) {
|
||||||
|
data['retailers'] = retailers!.map((v) => v.toJson()).toList();
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WalaPlusRetailerModel {
|
||||||
|
String? id;
|
||||||
|
String? name;
|
||||||
|
String? offerTitle;
|
||||||
|
String? offerType;
|
||||||
|
String? description;
|
||||||
|
String? info;
|
||||||
|
String? image;
|
||||||
|
String? imageThumb;
|
||||||
|
String? logo;
|
||||||
|
String? logoThumb;
|
||||||
|
String? website;
|
||||||
|
String? type;
|
||||||
|
dynamic usage;
|
||||||
|
dynamic weight;
|
||||||
|
dynamic geoId;
|
||||||
|
String? pin;
|
||||||
|
String? barcodeStandard;
|
||||||
|
String? arHowToUse;
|
||||||
|
String? enHowToUse;
|
||||||
|
String? howToUse;
|
||||||
|
WalaPlusRetailerContact? contact;
|
||||||
|
dynamic offerWeight;
|
||||||
|
List<WalaPlusRetailerLocation>? locations;
|
||||||
|
List<WalaPlusCategoryModel>? categories;
|
||||||
|
|
||||||
|
WalaPlusRetailerModel({
|
||||||
|
this.id,
|
||||||
|
this.name,
|
||||||
|
this.offerTitle,
|
||||||
|
this.offerType,
|
||||||
|
this.description,
|
||||||
|
this.info,
|
||||||
|
this.image,
|
||||||
|
this.imageThumb,
|
||||||
|
this.logo,
|
||||||
|
this.logoThumb,
|
||||||
|
this.website,
|
||||||
|
this.type,
|
||||||
|
this.usage,
|
||||||
|
this.weight,
|
||||||
|
this.geoId,
|
||||||
|
this.pin,
|
||||||
|
this.barcodeStandard,
|
||||||
|
this.arHowToUse,
|
||||||
|
this.enHowToUse,
|
||||||
|
this.howToUse,
|
||||||
|
this.contact,
|
||||||
|
this.offerWeight,
|
||||||
|
this.locations,
|
||||||
|
this.categories,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory WalaPlusRetailerModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
List<WalaPlusRetailerLocation> locationsList = [];
|
||||||
|
if (json['locations'] != null && json['locations'] is List) {
|
||||||
|
json['locations'].forEach((v) {
|
||||||
|
locationsList.add(WalaPlusRetailerLocation.fromJson(v));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
List<WalaPlusCategoryModel> categoriesList = [];
|
||||||
|
if (json['categories'] != null && json['categories'] is List) {
|
||||||
|
json['categories'].forEach((v) {
|
||||||
|
categoriesList.add(WalaPlusCategoryModel.fromJson(v));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
WalaPlusRetailerContact? contactObj;
|
||||||
|
if (json['contact'] != null) {
|
||||||
|
if (json['contact'] is Map) {
|
||||||
|
contactObj = WalaPlusRetailerContact.fromJson(Map<String, dynamic>.from(json['contact']));
|
||||||
|
} else if (json['contact'] is String && json['contact'].toString().trim().isNotEmpty) {
|
||||||
|
contactObj = WalaPlusRetailerContact(website: json['contact'].toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return WalaPlusRetailerModel(
|
||||||
|
id: json['id']?.toString(),
|
||||||
|
name: json['name']?.toString(),
|
||||||
|
offerTitle: json['offer_title']?.toString(),
|
||||||
|
offerType: json['offer_type']?.toString(),
|
||||||
|
description: json['description']?.toString(),
|
||||||
|
info: json['info']?.toString(),
|
||||||
|
image: json['image']?.toString(),
|
||||||
|
imageThumb: json['image_thumb']?.toString(),
|
||||||
|
logo: json['logo']?.toString(),
|
||||||
|
logoThumb: json['logo_thumb']?.toString(),
|
||||||
|
website: json['website']?.toString(),
|
||||||
|
type: json['type']?.toString(),
|
||||||
|
usage: json['usage'],
|
||||||
|
weight: json['weight'],
|
||||||
|
geoId: json['geo_id'],
|
||||||
|
pin: json['pin']?.toString(),
|
||||||
|
barcodeStandard: json['barcode_standard']?.toString(),
|
||||||
|
arHowToUse: json['ar_how_to_use']?.toString(),
|
||||||
|
enHowToUse: json['en_how_to_use']?.toString(),
|
||||||
|
howToUse: json['how_to_use']?.toString(),
|
||||||
|
contact: contactObj,
|
||||||
|
offerWeight: json['offer_weight'],
|
||||||
|
locations: locationsList,
|
||||||
|
categories: categoriesList,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
Map<String, dynamic> data = <String, dynamic>{};
|
||||||
|
data['id'] = id;
|
||||||
|
data['name'] = name;
|
||||||
|
data['offer_title'] = offerTitle;
|
||||||
|
data['offer_type'] = offerType;
|
||||||
|
data['description'] = description;
|
||||||
|
data['info'] = info;
|
||||||
|
data['image'] = image;
|
||||||
|
data['image_thumb'] = imageThumb;
|
||||||
|
data['logo'] = logo;
|
||||||
|
data['logo_thumb'] = logoThumb;
|
||||||
|
data['website'] = website;
|
||||||
|
data['type'] = type;
|
||||||
|
data['usage'] = usage;
|
||||||
|
data['weight'] = weight;
|
||||||
|
data['geo_id'] = geoId;
|
||||||
|
data['pin'] = pin;
|
||||||
|
data['barcode_standard'] = barcodeStandard;
|
||||||
|
data['ar_how_to_use'] = arHowToUse;
|
||||||
|
data['en_how_to_use'] = enHowToUse;
|
||||||
|
data['how_to_use'] = howToUse;
|
||||||
|
data['contact'] = contact?.toJson();
|
||||||
|
data['offer_weight'] = offerWeight;
|
||||||
|
if (locations != null) {
|
||||||
|
data['locations'] = locations!.map((v) => v.toJson()).toList();
|
||||||
|
}
|
||||||
|
if (categories != null) {
|
||||||
|
data['categories'] = categories!.map((v) => v.toJson()).toList();
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? get resolvedWebsite {
|
||||||
|
if (contact?.website != null && contact!.website!.trim().isNotEmpty) {
|
||||||
|
return contact!.website!.trim();
|
||||||
|
}
|
||||||
|
if (website != null && website!.trim().isNotEmpty) {
|
||||||
|
return website!.trim();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String get displayCity {
|
||||||
|
if (locations != null && locations!.isNotEmpty) {
|
||||||
|
for (var loc in locations!) {
|
||||||
|
if (loc.city != null && loc.city!.trim().isNotEmpty) {
|
||||||
|
return loc.city!.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (type?.toLowerCase() == 'online') {
|
||||||
|
return 'Online';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get displayUsage {
|
||||||
|
if (usage != null && usage.toString().isNotEmpty) {
|
||||||
|
return 'Usage : $usage';
|
||||||
|
}
|
||||||
|
if (type != null && type!.isNotEmpty) {
|
||||||
|
if (type!.toLowerCase() == 'online') return 'Online';
|
||||||
|
if (type!.toLowerCase() == 'store') return 'In Store';
|
||||||
|
return type!;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WalaPlusRetailerContact {
|
||||||
|
List<String>? emails;
|
||||||
|
List<String>? phones;
|
||||||
|
String? website;
|
||||||
|
|
||||||
|
WalaPlusRetailerContact({this.emails, this.phones, this.website});
|
||||||
|
|
||||||
|
factory WalaPlusRetailerContact.fromJson(Map<String, dynamic> json) {
|
||||||
|
List<String> emailsList = [];
|
||||||
|
if (json['emails'] != null && json['emails'] is List) {
|
||||||
|
json['emails'].forEach((v) => emailsList.add(v.toString()));
|
||||||
|
}
|
||||||
|
List<String> phonesList = [];
|
||||||
|
if (json['phones'] != null && json['phones'] is List) {
|
||||||
|
json['phones'].forEach((v) => phonesList.add(v.toString()));
|
||||||
|
}
|
||||||
|
return WalaPlusRetailerContact(
|
||||||
|
emails: emailsList,
|
||||||
|
phones: phonesList,
|
||||||
|
website: json['website']?.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'emails': emails,
|
||||||
|
'phones': phones,
|
||||||
|
'website': website,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WalaPlusRetailerLocation {
|
||||||
|
String? address;
|
||||||
|
String? city;
|
||||||
|
String? cityNameAr;
|
||||||
|
String? cityNameEn;
|
||||||
|
String? contact;
|
||||||
|
String? countryNameAr;
|
||||||
|
String? countryNameEn;
|
||||||
|
double? latitude;
|
||||||
|
double? longitude;
|
||||||
|
dynamic placeId;
|
||||||
|
|
||||||
|
WalaPlusRetailerLocation({
|
||||||
|
this.address,
|
||||||
|
this.city,
|
||||||
|
this.cityNameAr,
|
||||||
|
this.cityNameEn,
|
||||||
|
this.contact,
|
||||||
|
this.countryNameAr,
|
||||||
|
this.countryNameEn,
|
||||||
|
this.latitude,
|
||||||
|
this.longitude,
|
||||||
|
this.placeId,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory WalaPlusRetailerLocation.fromJson(Map<String, dynamic> json) {
|
||||||
|
return WalaPlusRetailerLocation(
|
||||||
|
address: json['address']?.toString(),
|
||||||
|
city: json['city']?.toString(),
|
||||||
|
cityNameAr: json['city_name_ar']?.toString(),
|
||||||
|
cityNameEn: json['city_name_en']?.toString(),
|
||||||
|
contact: json['contact']?.toString(),
|
||||||
|
countryNameAr: json['country_name_ar']?.toString(),
|
||||||
|
countryNameEn: json['country_name_en']?.toString(),
|
||||||
|
latitude: json['latitude'] is num ? (json['latitude'] as num).toDouble() : double.tryParse(json['latitude']?.toString() ?? ''),
|
||||||
|
longitude: json['longitude'] is num ? (json['longitude'] as num).toDouble() : double.tryParse(json['longitude']?.toString() ?? ''),
|
||||||
|
placeId: json['place_id'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
Map<String, dynamic> data = <String, dynamic>{};
|
||||||
|
data['address'] = address;
|
||||||
|
data['city'] = city;
|
||||||
|
data['city_name_ar'] = cityNameAr;
|
||||||
|
data['city_name_en'] = cityNameEn;
|
||||||
|
data['contact'] = contact;
|
||||||
|
data['country_name_ar'] = countryNameAr;
|
||||||
|
data['country_name_en'] = countryNameEn;
|
||||||
|
data['latitude'] = latitude;
|
||||||
|
data['longitude'] = longitude;
|
||||||
|
data['place_id'] = placeId;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,630 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.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/consts.dart';
|
||||||
|
import 'package:mohem_flutter_app/config/routes.dart';
|
||||||
|
import 'package:mohem_flutter_app/extensions/int_extensions.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:shimmer/shimmer.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 = false;
|
||||||
|
String? _effectiveRetailerId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_retailer = widget.retailer;
|
||||||
|
_effectiveRetailerId = widget.retailerId ?? widget.retailer?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
if (_retailer == null && _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 (_retailer == null && _effectiveRetailerId != null && !_isLoading) {
|
||||||
|
_fetchRetailerDetails();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _fetchRetailerDetails() async {
|
||||||
|
if (_effectiveRetailerId == null) return;
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
WalaPlusRetailerModel? result = await OffersAndDiscountsApiClient().getWalaPlusRetailerById(
|
||||||
|
retailerId: _effectiveRetailerId!,
|
||||||
|
context: context,
|
||||||
|
);
|
||||||
|
if (result != null && mounted) {
|
||||||
|
setState(() {
|
||||||
|
_retailer = result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error fetching WalaPlus retailer by ID: $e");
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onUseOffer() async {
|
||||||
|
String? website = _retailer?.resolvedWebsite;
|
||||||
|
if (website != null && website.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
Uri uri = Uri.parse(website);
|
||||||
|
if (!uri.hasScheme) {
|
||||||
|
uri = Uri.parse("https://$website");
|
||||||
|
}
|
||||||
|
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error launching website: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If promo pin exists, display a dialog with the promo code to copy
|
||||||
|
if (_retailer?.pin != null && _retailer!.pin!.isNotEmpty) {
|
||||||
|
_showPromoCodeDialog(_retailer!.pin!);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise show terms/usage dialog
|
||||||
|
_showTermsDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showPromoCodeDialog(String pin) {
|
||||||
|
bool isAr = AppState().isArabic(context);
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (cxt) => AlertDialog(
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
|
title: Text(
|
||||||
|
isAr ? "كود الخصم" : "Promo Code",
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFF3F4F6),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(color: const Color(0xFF20A4A8), width: 1.2),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
pin,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 2,
|
||||||
|
color: Color(0xFF1E293B),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.copy_rounded, color: Color(0xFF20A4A8)),
|
||||||
|
onPressed: () {
|
||||||
|
Clipboard.setData(ClipboardData(text: pin));
|
||||||
|
Navigator.pop(cxt);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(isAr ? "تم نسخ الكود بنجاح" : "Promo code copied"),
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(cxt),
|
||||||
|
child: Text(isAr ? "إغلاق" : "Close", style: const TextStyle(color: Color(0xFF20A4A8))),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showTermsDialog() {
|
||||||
|
bool isAr = AppState().isArabic(context);
|
||||||
|
String content = _retailer?.howToUse ??
|
||||||
|
_retailer?.arHowToUse ??
|
||||||
|
_retailer?.enHowToUse ??
|
||||||
|
_retailer?.usage?.toString() ??
|
||||||
|
(isAr ? "تطبق الشروط والأحكام الخاصة بالعرض." : "Terms and conditions apply for this offer.");
|
||||||
|
|
||||||
|
// Clean html tags if simple
|
||||||
|
content = content.replaceAll(RegExp(r'<[^>]*>|&[^;]+;'), ' ').trim();
|
||||||
|
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||||
|
),
|
||||||
|
builder: (cxt) => SafeArea(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
isAr ? "الشروط والأحكام" : "Terms and Conditions",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF1E293B),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
onPressed: () => Navigator.pop(cxt),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
content,
|
||||||
|
style: const TextStyle(fontSize: 14, color: Color(0xFF4B5563), height: 1.5),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
bool isAr = AppState().isArabic(context);
|
||||||
|
String title = _retailer?.name ?? (isAr ? "تفاصيل العرض" : "Offer Details");
|
||||||
|
|
||||||
|
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 && _retailer == null
|
||||||
|
? _buildLoadingSkeleton()
|
||||||
|
: Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
physics: const BouncingScrollPhysics(),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(22),
|
||||||
|
border: Border.all(color: const Color(0xFFF1F5F9), width: 1.2),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.04),
|
||||||
|
blurRadius: 12,
|
||||||
|
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?.image != null && _retailer!.image!.isNotEmpty)
|
||||||
|
? Image.network(
|
||||||
|
_retailer!.image!,
|
||||||
|
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)
|
||||||
|
Center(
|
||||||
|
child: Text(
|
||||||
|
_retailer?.name ?? "",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF1E293B),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
18.height,
|
||||||
|
|
||||||
|
// 4. 3 Info Badges Row (Store Type, Validity, City)
|
||||||
|
_buildInfoBadgesRow(isAr),
|
||||||
|
22.height,
|
||||||
|
|
||||||
|
// 5. Description Section
|
||||||
|
Text(
|
||||||
|
isAr ? "الوصف:" : "description:",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF1E293B),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
8.height,
|
||||||
|
Text(
|
||||||
|
_getCleanDescription(),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
color: Color(0xFF4B5563),
|
||||||
|
height: 1.45,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
16.height,
|
||||||
|
|
||||||
|
// 6. Terms and Conditions Link (Right aligned)
|
||||||
|
Align(
|
||||||
|
alignment: isAr ? Alignment.centerLeft : Alignment.centerRight,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: _showTermsDialog,
|
||||||
|
child: Text(
|
||||||
|
isAr ? "الشروط والأحكام" : "terms and conditions",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF20A4A8),
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 7. Bottom Action Button
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 10, 16, 20),
|
||||||
|
color: Colors.white,
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 50,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: _onUseOffer,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFF20A4A8),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
isAr ? "استخدام العرض" : "Use Offer",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBrandStrip(bool isAr) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// Left: Human Resources branding
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: const [
|
||||||
|
Text(
|
||||||
|
"الموارد البشرية",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Color(0xFFED1C2B),
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"Human Resources",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Color(0xFFED1C2B),
|
||||||
|
fontSize: 8.5,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Center: Retailer Logo
|
||||||
|
if (_retailer?.logo != null && _retailer!.logo!.isNotEmpty)
|
||||||
|
Image.network(
|
||||||
|
_retailer!.logo!,
|
||||||
|
height: 36,
|
||||||
|
width: 36,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
errorBuilder: (_, __, ___) => const SizedBox(width: 36, height: 36),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const SizedBox(width: 36, height: 36),
|
||||||
|
|
||||||
|
// Right: Dr Sulaiman Al Habib branding with red cross
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: const [
|
||||||
|
Text(
|
||||||
|
"سليمان الحبيب",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Color(0xFF4B5563),
|
||||||
|
fontSize: 9,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"DR SULAIMAN AL HABIB",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Color(0xFF4B5563),
|
||||||
|
fontSize: 8,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
SvgPicture.asset(
|
||||||
|
"assets/icons/mazaya_brand.svg",
|
||||||
|
height: 22,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInfoBadgesRow(bool isAr) {
|
||||||
|
String storeTypeText = (_retailer?.type?.toLowerCase() == 'online')
|
||||||
|
? (isAr ? "عبر الإنترنت" : "online")
|
||||||
|
: (isAr ? "في المتجر" : "in store");
|
||||||
|
|
||||||
|
IconData storeIcon = (_retailer?.type?.toLowerCase() == 'online')
|
||||||
|
? Icons.language_rounded
|
||||||
|
: Icons.storefront_rounded;
|
||||||
|
|
||||||
|
String cityText = _retailer?.displayCity ?? "";
|
||||||
|
if (cityText.isEmpty) {
|
||||||
|
cityText = isAr ? "الرياض" : "Riyadh";
|
||||||
|
}
|
||||||
|
|
||||||
|
String validityText = isAr ? "٣ أشهر" : "3 Month";
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _buildInfoCard(
|
||||||
|
icon: storeIcon,
|
||||||
|
label: storeTypeText,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
12.width,
|
||||||
|
Expanded(
|
||||||
|
child: _buildInfoCard(
|
||||||
|
icon: Icons.check_circle_outline_rounded,
|
||||||
|
label: validityText,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
12.width,
|
||||||
|
Expanded(
|
||||||
|
child: _buildInfoCard(
|
||||||
|
icon: Icons.location_on_rounded,
|
||||||
|
label: cityText,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInfoCard({required IconData icon, required String label}) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFF4F6F9),
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: const Color(0xFF20A4A8), size: 26),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF334155),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getCleanDescription() {
|
||||||
|
String desc = _retailer?.description ?? _retailer?.info ?? _retailer?.offerTitle ?? "";
|
||||||
|
if (desc.isEmpty) {
|
||||||
|
return AppState().isArabic(context) ? "لا توجد تفاصيل إضافية متاحة." : "No additional details available.";
|
||||||
|
}
|
||||||
|
// Remove HTML tags if string contains them
|
||||||
|
return desc.replaceAll(RegExp(r'<[^>]*>|&[^;]+;'), ' ').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLoadingSkeleton() {
|
||||||
|
return Shimmer.fromColors(
|
||||||
|
baseColor: const Color(0xFFE2E8F0),
|
||||||
|
highlightColor: const Color(0xFFF8FAFC),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(22),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: 190,
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
height: 24,
|
||||||
|
width: 160,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Row(
|
||||||
|
children: List.generate(
|
||||||
|
3,
|
||||||
|
(index) => Expanded(
|
||||||
|
child: Container(
|
||||||
|
height: 75,
|
||||||
|
margin: EdgeInsets.symmetric(horizontal: index == 1 ? 8 : 0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Container(
|
||||||
|
height: 16,
|
||||||
|
width: 100,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Container(
|
||||||
|
height: 14,
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Container(
|
||||||
|
height: 14,
|
||||||
|
width: 220,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue