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