You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
hmg-mohemm-flutter-app/lib/models/offers_and_discounts/wala_plus_retailer_model.dart

982 lines
36 KiB
Dart

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import 'dart:convert';
import 'package:easy_localization/easy_localization.dart';
import 'package:mohem_flutter_app/generated/locale_keys.g.dart';
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;
String? countryNameAr;
String? countryNameEn;
WalaPlusRetailersResponse({this.page, this.pagesCount, this.perPage, this.retailers, this.countryNameAr, this.countryNameEn});
factory WalaPlusRetailersResponse.fromJson(Map<String, dynamic> json) {
List<WalaPlusRetailerModel> retailersList = [];
dynamic retailersData = json['retailers'] ?? json['Retailers'] ?? json['data'] ?? json['items'];
if (retailersData is String) {
try {
retailersData = jsonDecode(retailersData);
} catch (_) {}
}
if (retailersData is List) {
retailersData.forEach((v) {
if (v is Map<String, dynamic>) {
retailersList.add(WalaPlusRetailerModel.fromJson(v));
} else if (v is Map) {
retailersList.add(WalaPlusRetailerModel.fromJson(Map<String, dynamic>.from(v)));
}
});
}
return WalaPlusRetailersResponse(
page: json['page'] is int ? json['page'] : int.tryParse(json['page']?.toString() ?? json['WP_Page']?.toString() ?? '1'),
pagesCount: json['pages_count'] is int ? json['pages_count'] : int.tryParse(json['pages_count']?.toString() ?? json['pagesCount']?.toString() ?? json['totalPages']?.toString() ?? '1'),
perPage: json['per_page'] is int ? json['per_page'] : int.tryParse(json['per_page']?.toString() ?? json['perPage']?.toString() ?? json['WP_Per_page']?.toString() ?? '10'),
retailers: retailersList,
countryNameAr: WalaPlusRetailerModel.decodeUnicodeString(json['country_name_ar'] ?? json['countryNameAr'] ?? json['country_ar']),
countryNameEn: WalaPlusRetailerModel.decodeUnicodeString(json['country_name_en'] ?? json['countryNameEn'] ?? json['country_en']),
);
}
Map<String, dynamic> toJson() {
Map<String, dynamic> data = <String, dynamic>{};
data['page'] = page;
data['pages_count'] = pagesCount;
data['per_page'] = perPage;
data['country_name_ar'] = countryNameAr;
data['country_name_en'] = countryNameEn;
if (retailers != null) {
data['retailers'] = retailers!.map((v) => v.toJson()).toList();
}
return data;
}
}
class WalaPlusRetailersByIDResponse {
int? page;
int? pagesCount;
int? perPage;
List<WalaPlusRetailerLocation>? locations;
List<WalaPlusOfferModel>? offers;
List<WalaPlusRetailerModel>? retailer;
WalaPlusRetailersByIDResponse({this.page, this.pagesCount, this.perPage, this.locations, this.offers, this.retailer});
factory WalaPlusRetailersByIDResponse.fromJson(Map<String, dynamic> json) {
List<WalaPlusRetailerLocation> locList = WalaPlusRetailerModel.parseLocationsList(json['locations'] ?? json['Locations'] ?? json['location'] ?? json['Location']);
List<WalaPlusOfferModel> offList = WalaPlusRetailerModel.parseOffersList(json['offers'] ?? json['Offers']);
List<WalaPlusRetailerModel> retList = [];
dynamic retData = json['retailer'] ?? json['Retailer'];
if (retData is List) {
for (var r in retData) {
if (r is Map<String, dynamic>) {
var model = WalaPlusRetailerModel.fromJson(r);
if (model.locations == null || model.locations!.isEmpty) model.locations = locList;
if (model.offers == null || model.offers!.isEmpty) model.offers = offList;
if ((model.website == null || model.website!.isEmpty || model.website == "null") && (json['website'] != null || json['merchant_website'] != null || json['url'] != null)) {
model.website = (json['website'] ?? json['merchant_website'] ?? json['url'])?.toString();
}
if (model.isFavorite == null && (json.containsKey('isfavorite') || json.containsKey('is_favorite'))) {
var favVal = json['isfavorite'] ?? json['is_favorite'];
model.isFavorite = favVal == true || favVal == 1 || favVal == 'true' || favVal == '1';
}
retList.add(model);
} else if (r is Map) {
var model = WalaPlusRetailerModel.fromJson(Map<String, dynamic>.from(r));
if (model.locations == null || model.locations!.isEmpty) model.locations = locList;
if (model.offers == null || model.offers!.isEmpty) model.offers = offList;
if ((model.website == null || model.website!.isEmpty || model.website == "null") && (json['website'] != null || json['merchant_website'] != null || json['url'] != null)) {
model.website = (json['website'] ?? json['merchant_website'] ?? json['url'])?.toString();
}
if (model.isFavorite == null && (json.containsKey('isfavorite') || json.containsKey('is_favorite'))) {
var favVal = json['isfavorite'] ?? json['is_favorite'];
model.isFavorite = favVal == true || favVal == 1 || favVal == 'true' || favVal == '1';
}
retList.add(model);
}
}
} else if (retData is Map) {
var model = WalaPlusRetailerModel.fromJson(Map<String, dynamic>.from(retData));
if (model.locations == null || model.locations!.isEmpty) model.locations = locList;
if (model.offers == null || model.offers!.isEmpty) model.offers = offList;
if ((model.website == null || model.website!.isEmpty || model.website == "null") && (json['website'] != null || json['merchant_website'] != null || json['url'] != null)) {
model.website = (json['website'] ?? json['merchant_website'] ?? json['url'])?.toString();
}
if (model.isFavorite == null && (json.containsKey('isfavorite') || json.containsKey('is_favorite'))) {
var favVal = json['isfavorite'] ?? json['is_favorite'];
model.isFavorite = favVal == true || favVal == 1 || favVal == 'true' || favVal == '1';
}
retList.add(model);
}
return WalaPlusRetailersByIDResponse(
page: json['page'] is int ? json['page'] : int.tryParse(json['page']?.toString() ?? json['WP_Page']?.toString() ?? ''),
pagesCount: json['pages_count'] is int ? json['pages_count'] : int.tryParse(json['pages_count']?.toString() ?? json['pagesCount']?.toString() ?? ''),
perPage: json['per_page'] is int ? json['per_page'] : int.tryParse(json['per_page']?.toString() ?? json['perPage']?.toString() ?? ''),
locations: locList,
offers: offList,
retailer: retList,
);
}
WalaPlusRetailerModel? get primaryRetailer {
if (retailer != null && retailer!.isNotEmpty) {
return retailer!.first;
}
return null;
}
Map<String, dynamic> toJson() {
Map<String, dynamic> data = <String, dynamic>{};
data['page'] = page;
data['pages_count'] = pagesCount;
data['per_page'] = perPage;
if (locations != null) {
data['locations'] = locations!.map((v) => v.toJson()).toList();
}
if (offers != null) {
data['offers'] = offers!.map((v) => v.toJson()).toList();
}
if (retailer != null) {
data['retailer'] = retailer!.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<WalaPlusOfferModel>? offers;
List<WalaPlusRetailerLocation>? locations;
List<WalaPlusCategoryModel>? categories;
String? validity;
String? validTo;
String? validUntil;
String? endDate;
String? expiryDate;
String? duration;
String? email;
String? phone;
String? countryNameAr;
String? countryNameEn;
bool? isFavorite;
WalaPlusRetailerModel({
this.id,
this.name,
this.offerTitle,
this.offerType,
this.description,
this.info,
this.image,
this.imageThumb,
this.logo,
this.logoThumb,
this.website,
this.isFavorite,
this.type,
this.usage,
this.weight,
this.geoId,
this.pin,
this.barcodeStandard,
this.arHowToUse,
this.enHowToUse,
this.howToUse,
this.contact,
this.offerWeight,
this.offers,
this.locations,
this.categories,
this.validity,
this.validTo,
this.validUntil,
this.endDate,
this.expiryDate,
this.duration,
this.email,
this.phone,
this.countryNameAr,
this.countryNameEn,
});
static List<WalaPlusOfferModel> parseOffersList(data) {
List<WalaPlusOfferModel> list = [];
if (data == null) return list;
if (data is String) {
try {
data = jsonDecode(data);
} catch (_) {}
}
if (data is List) {
for (var v in data) {
if (v is Map<String, dynamic>) {
list.add(WalaPlusOfferModel.fromJson(v));
} else if (v is Map) {
list.add(WalaPlusOfferModel.fromJson(Map<String, dynamic>.from(v)));
}
}
}
return list;
}
static List<WalaPlusRetailerLocation> parseLocationsList(data) {
List<WalaPlusRetailerLocation> list = [];
if (data == null) return list;
if (data is String) {
try {
data = jsonDecode(data);
} catch (_) {}
}
if (data is List) {
for (var v in data) {
if (v is Map<String, dynamic>) {
list.add(WalaPlusRetailerLocation.fromJson(v));
} else if (v is Map) {
list.add(WalaPlusRetailerLocation.fromJson(Map<String, dynamic>.from(v)));
}
}
}
return list;
}
factory WalaPlusRetailerModel.fromJson(Map<String, dynamic> json) {
Map<String, dynamic> targetJson = json;
dynamic offersSource = json['offers'] ?? json['Offers'];
dynamic locationsSource = json['locations'] ?? json['Locations'] ?? json['location'] ?? json['Location'];
// If json is the wrapper object from WalaPlus_RetailersByID containing 'retailer' key
if (json['retailer'] != null || json['Retailer'] != null) {
dynamic retailerData = json['retailer'] ?? json['Retailer'];
if (retailerData is List && retailerData.isNotEmpty) {
var first = retailerData.first;
if (first is Map<String, dynamic>) {
targetJson = first;
} else if (first is Map) {
targetJson = Map<String, dynamic>.from(first);
}
} else if (retailerData is Map<String, dynamic>) {
targetJson = retailerData;
} else if (retailerData is Map) {
targetJson = Map<String, dynamic>.from(retailerData);
}
if (locationsSource == null || (locationsSource is List && locationsSource.isEmpty)) {
locationsSource = targetJson['locations'] ?? targetJson['Locations'] ?? targetJson['location'] ?? targetJson['Location'];
}
if (offersSource == null || (offersSource is List && offersSource.isEmpty)) {
offersSource = targetJson['offers'] ?? targetJson['Offers'];
}
}
List<WalaPlusOfferModel> offersList = parseOffersList(offersSource ?? targetJson['offers'] ?? targetJson['Offers']);
List<WalaPlusRetailerLocation> locationsList = parseLocationsList(locationsSource ?? targetJson['locations'] ?? targetJson['Locations'] ?? targetJson['location'] ?? targetJson['Location']);
List<WalaPlusCategoryModel> categoriesList = [];
if (targetJson['categories'] != null && targetJson['categories'] is List) {
targetJson['categories'].forEach((v) {
categoriesList.add(WalaPlusCategoryModel.fromJson(v));
});
}
WalaPlusRetailerContact? contactObj;
if (targetJson['contact'] != null) {
if (targetJson['contact'] is Map) {
contactObj = WalaPlusRetailerContact.fromJson(Map<String, dynamic>.from(targetJson['contact']));
} else if (targetJson['contact'] is String && targetJson['contact'].toString().trim().isNotEmpty) {
contactObj = WalaPlusRetailerContact(website: targetJson['contact'].toString());
}
}
String? foundValidity =
targetJson['validity']?.toString() ??
targetJson['valid_to']?.toString() ??
targetJson['valid_until']?.toString() ??
targetJson['end_date']?.toString() ??
targetJson['expiry_date']?.toString() ??
targetJson['expire_date']?.toString() ??
targetJson['duration']?.toString() ??
targetJson['validity_period']?.toString() ??
targetJson['valid_duration']?.toString() ??
targetJson['offer_validity']?.toString();
if (foundValidity == null || foundValidity == "null" || foundValidity.trim().isEmpty) {
for (var entry in targetJson.entries) {
String k = entry.key.toLowerCase();
if ((k.contains('valid') || k.contains('expir') || k.contains('duration')) && !k.contains('isverificationcodevalid') && !k.contains('barcode')) {
if (entry.value != null && entry.value.toString().trim().isNotEmpty && entry.value.toString() != "null") {
foundValidity = entry.value.toString().trim();
break;
}
}
}
}
String? foundWebsite = targetJson['website']?.toString() ??
targetJson['merchant_website']?.toString() ??
targetJson['merchantWebsite']?.toString() ??
targetJson['website_url']?.toString() ??
targetJson['websiteUrl']?.toString() ??
targetJson['web_site']?.toString() ??
targetJson['url']?.toString() ??
targetJson['merchant_url']?.toString() ??
targetJson['merchantUrl']?.toString() ??
targetJson['merchantLink']?.toString() ??
targetJson['merchant_link']?.toString() ??
targetJson['link']?.toString() ??
contactObj?.website;
if (foundWebsite == null || foundWebsite.trim().isEmpty || foundWebsite == "null") {
if (targetJson['merchant'] is Map) {
var m = targetJson['merchant'];
foundWebsite = (m['website'] ?? m['merchant_website'] ?? m['url'] ?? m['link'])?.toString();
}
}
if (foundWebsite == null || foundWebsite.trim().isEmpty || foundWebsite == "null") {
for (var entry in targetJson.entries) {
String k = entry.key.toLowerCase();
if ((k.contains('website') || k.contains('web_site') || k == 'url' || k.contains('merchant_url')) && !k.contains('image') && !k.contains('logo')) {
if (entry.value != null && entry.value.toString().trim().isNotEmpty && entry.value.toString() != "null") {
foundWebsite = entry.value.toString().trim();
break;
}
}
}
}
bool? parsedIsFavorite;
if (targetJson.containsKey('isfavorite')) {
parsedIsFavorite = targetJson['isfavorite'] == true ||
targetJson['isfavorite'] == 1 ||
targetJson['isfavorite'] == 'true' ||
targetJson['isfavorite'] == '1';
} else if (targetJson.containsKey('is_favorite')) {
parsedIsFavorite = targetJson['is_favorite'] == true ||
targetJson['is_favorite'] == 1 ||
targetJson['is_favorite'] == 'true' ||
targetJson['is_favorite'] == '1';
} else if (targetJson.containsKey('isFavorite')) {
parsedIsFavorite = targetJson['isFavorite'] == true ||
targetJson['isFavorite'] == 1 ||
targetJson['isFavorite'] == 'true' ||
targetJson['isFavorite'] == '1';
} else if (targetJson.containsKey('favorite')) {
parsedIsFavorite = targetJson['favorite'] == true ||
targetJson['favorite'] == 1 ||
targetJson['favorite'] == 'true' ||
targetJson['favorite'] == '1';
}
return WalaPlusRetailerModel(
id:
targetJson['id']?.toString() ??
targetJson['WP_RetailerID']?.toString() ??
targetJson['retailer_id']?.toString() ??
targetJson['RetailerID']?.toString() ??
targetJson['retailerId']?.toString(),
name: decodeUnicodeString(targetJson['name']),
offerTitle: decodeUnicodeString(targetJson['offer_title']),
offerType: targetJson['offer_type']?.toString(),
description: decodeUnicodeString(targetJson['description']),
info: decodeUnicodeString(targetJson['info']),
image: targetJson['image']?.toString(),
imageThumb: targetJson['image_thumb']?.toString(),
logo: targetJson['logo']?.toString(),
logoThumb: targetJson['logo_thumb']?.toString(),
website: foundWebsite,
isFavorite: parsedIsFavorite,
type: targetJson['type']?.toString(),
usage: targetJson['usage'] is String ? decodeUnicodeString(targetJson['usage']) : targetJson['usage'],
weight: targetJson['weight'],
geoId: targetJson['geo_id'],
pin: targetJson['pin']?.toString(),
barcodeStandard: targetJson['barcode_standard']?.toString(),
arHowToUse: decodeUnicodeString(targetJson['ar_how_to_use']),
enHowToUse: decodeUnicodeString(targetJson['en_how_to_use']),
howToUse: decodeUnicodeString(targetJson['how_to_use']),
contact: contactObj,
offerWeight: targetJson['offer_weight'],
offers: offersList,
locations: locationsList,
categories: categoriesList,
validity: foundValidity,
validTo: targetJson['valid_to']?.toString() ?? targetJson['validTo']?.toString(),
validUntil: targetJson['valid_until']?.toString() ?? targetJson['validUntil']?.toString(),
endDate: targetJson['end_date']?.toString() ?? targetJson['endDate']?.toString(),
expiryDate: targetJson['expiry_date']?.toString() ?? targetJson['expiryDate']?.toString(),
duration: targetJson['duration']?.toString(),
email: targetJson['email']?.toString() ?? targetJson['contact_email']?.toString() ?? contactObj?.primaryEmail,
phone: targetJson['phone']?.toString() ?? targetJson['contact_phone']?.toString() ?? targetJson['mobile']?.toString() ?? contactObj?.primaryPhone,
countryNameAr: decodeUnicodeString(targetJson['country_name_ar'] ?? targetJson['countryNameAr'] ?? targetJson['country_ar']),
countryNameEn: decodeUnicodeString(targetJson['country_name_en'] ?? targetJson['countryNameEn'] ?? targetJson['country_en']),
);
}
static String? decodeUnicodeString(Object? val) {
if (val == null) return null;
String s = val.toString().trim();
if (s.isEmpty || s == "null") return null;
while ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
if (s.length >= 2) {
s = s.substring(1, s.length - 1).trim();
} else {
break;
}
}
s = s.replaceAll(r'\/', '/').replaceAll(r'\"', '"').replaceAll(r"\'", "'");
s = s.replaceAllMapped(RegExp(r'\\u([0-9a-fA-F]{4})'), (match) {
try {
var codeUnit = int.parse(match.group(1)!, radix: 16);
return String.fromCharCode(codeUnit);
} catch (_) {
return match.group(0)!;
}
});
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', '“')
.replaceAll('&quot;', '"')
.replaceAll('&apos;', "'")
.replaceAll('&#39;', "'")
.replaceAll('&nbsp;', ' ')
.replaceAll('&amp;', '&');
return s;
}
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;
data['validity'] = validity;
data['valid_to'] = validTo;
data['valid_until'] = validUntil;
data['end_date'] = endDate;
data['expiry_date'] = expiryDate;
data['duration'] = duration;
data['email'] = email;
data['phone'] = phone;
data['country_name_ar'] = countryNameAr;
data['country_name_en'] = countryNameEn;
data['is_favorite'] = isFavorite;
data['isfavorite'] = isFavorite;
if (offers != null) {
data['offers'] = offers!.map((v) => v.toJson()).toList();
}
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 displayImage {
if (image != null && image!.trim().isNotEmpty && image != "null") return image!.trim();
if (imageThumb != null && imageThumb!.trim().isNotEmpty && imageThumb != "null") return imageThumb!.trim();
return null;
}
String? get displayLogo {
if (logo != null && logo!.trim().isNotEmpty && logo != "null") return logo!.trim();
if (logoThumb != null && logoThumb!.trim().isNotEmpty && logoThumb != "null") return logoThumb!.trim();
return null;
}
String? get resolvedWebsite {
if (website != null && website!.trim().isNotEmpty && website != "null") {
return website!.trim();
}
if (contact?.website != null && contact!.website!.trim().isNotEmpty && contact!.website != "null") {
return contact!.website!.trim();
}
return null;
}
bool get hasPhysicalLocation {
if (locations != null && locations!.isNotEmpty) {
for (var loc in locations!) {
if ((loc.cityNameEn != null && loc.cityNameEn!.trim().isNotEmpty && loc.cityNameEn != "null") ||
(loc.cityNameAr != null && loc.cityNameAr!.trim().isNotEmpty && loc.cityNameAr != "null") ||
(loc.city != null && loc.city!.trim().isNotEmpty && loc.city != "null")) {
return true;
}
}
}
return false;
}
// Resolves a single location's city using the same 3-pass priority this used to apply
// globally: specific city name first, then a non-generic-country city, then any city at all.
String? _resolveLocationCity(WalaPlusRetailerLocation loc, bool isAr) {
String? cName = isAr ? loc.cityNameAr : loc.cityNameEn;
if (cName != null && cName.trim().isNotEmpty && cName != "null") {
return cName.trim();
}
if (loc.city != null && loc.city!.trim().isNotEmpty && loc.city != "null") {
String c = loc.city!.trim();
if (c.toLowerCase() != 'saudi arabia' && c != 'المملكة العربية السعودية') {
return c;
}
}
if (loc.city != null && loc.city!.trim().isNotEmpty && loc.city != "null") {
return loc.city!.trim();
}
return null;
}
// Multiple distinct cities across this retailer's locations -> "Multiple Cities" instead of
// just picking the first one found, so the badge doesn't silently hide the other branches.
String getDisplayCity({bool isAr = false}) {
if (locations == null || locations!.isEmpty) return '';
Set<String> distinctCities = <String>{};
for (var loc in locations!) {
String? resolved = _resolveLocationCity(loc, isAr);
if (resolved != null) distinctCities.add(resolved);
}
if (distinctCities.isEmpty) return '';
if (distinctCities.length > 1) return LocaleKeys.multipleCities.tr();
return distinctCities.first;
}
List<String> getDistinctCities({bool isAr = false}) {
if (locations == null || locations!.isEmpty) return <String>[];
Set<String> distinctCities = <String>{};
for (var loc in locations!) {
String? resolved = _resolveLocationCity(loc, isAr);
if (resolved != null && resolved.trim().isNotEmpty && resolved != "null") {
distinctCities.add(resolved.trim());
}
}
return distinctCities.toList();
}
String get displayCity => getDisplayCity(isAr: false);
String get displayUsage {
if (usage != null) {
String u = usage.toString().trim();
if (int.tryParse(u) != null) {
return 'usage:$u';
}
if (!u.contains('<') && !u.contains('{') && u.isNotEmpty && u.length <= 12 && u.toLowerCase() != 'online' && u.toLowerCase() != 'store') {
return 'usage:$u';
}
}
return '';
}
String getDisplayValidity({bool isAr = false}) {
if (validity != null && validity!.trim().isNotEmpty && validity != "null") {
return validity!.trim();
}
if (validTo != null && validTo!.trim().isNotEmpty && validTo != "null") {
return validTo!.trim();
}
if (validUntil != null && validUntil!.trim().isNotEmpty && validUntil != "null") {
return validUntil!.trim();
}
if (endDate != null && endDate!.trim().isNotEmpty && endDate != "null") {
return endDate!.trim();
}
if (expiryDate != null && expiryDate!.trim().isNotEmpty && expiryDate != "null") {
return expiryDate!.trim();
}
if (duration != null && duration!.trim().isNotEmpty && duration != "null") {
return duration!.trim();
}
return LocaleKeys.offerValid.tr();
}
String? get resolvedEmail {
if (email != null && email!.trim().isNotEmpty && email != "null") {
return email!.trim();
}
if (contact?.primaryEmail != null && contact!.primaryEmail!.trim().isNotEmpty && contact!.primaryEmail != "null") {
return contact!.primaryEmail!.trim();
}
if (locations != null && locations!.isNotEmpty) {
for (var loc in locations!) {
if (loc.contact != null && loc.contact!.contains('@')) {
return loc.contact!.trim();
}
}
}
String combined = "${description ?? ''} ${info ?? ''} ${howToUse ?? ''} ${arHowToUse ?? ''} ${enHowToUse ?? ''}";
RegExp emailRegex = RegExp(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}');
Match? match = emailRegex.firstMatch(combined);
if (match != null) {
return match.group(0);
}
return null;
}
String? get resolvedPhone {
if (phone != null && phone!.trim().isNotEmpty && phone != "null") {
return phone!.trim();
}
if (contact?.primaryPhone != null && contact!.primaryPhone!.trim().isNotEmpty && contact!.primaryPhone != "null") {
return contact!.primaryPhone!.trim();
}
if (locations != null && locations!.isNotEmpty) {
for (var loc in locations!) {
if (loc.contact != null && loc.contact!.trim().isNotEmpty && loc.contact != "null" && !loc.contact!.contains('@')) {
return loc.contact!.trim();
}
}
}
String combined = "${description ?? ''} ${info ?? ''} ${howToUse ?? ''} ${arHowToUse ?? ''} ${enHowToUse ?? ''}";
RegExp phoneRegex = RegExp(r'(?:\+?966|0)?(?:5\d{8}|1\d{7}|9200\d{5}|800\d{6,7})');
Match? match = phoneRegex.firstMatch(combined);
if (match != null) {
return match.group(0);
}
return null;
}
// type can be a comma-separated combo (e.g. "online,instore") when a retailer offers both -
// this splits and normalizes every part instead of only ever matching a single exact value,
// so a combined retailer is correctly recognized as both rather than neither/just one.
List<String> get normalizedTypes {
if (type != null && type!.trim().isNotEmpty) {
Set<String> result = <String>{};
for (String part in type!.split(',')) {
String t = part.trim().toLowerCase();
if (t.isEmpty) continue;
if (t == 'online') {
result.add('online');
} else if (t == 'instore' || t == 'in_store' || t == 'in store' || t == 'store') {
result.add('instore');
} else {
result.add(t);
}
}
if (result.isNotEmpty) return result.toList();
}
if (locations != null && locations!.isNotEmpty) {
return <String>['instore'];
}
return <String>[];
}
bool get isOnlineStore => normalizedTypes.contains('online');
bool get isInstore => normalizedTypes.contains('instore');
// Kept for any existing single-value callers - returns the first normalized type found.
String? get normalizedType {
List<String> types = normalizedTypes;
return types.isEmpty ? null : types.first;
}
String _displayTypeLabel(String norm) {
if (norm == 'online') return LocaleKeys.online.tr();
if (norm == 'instore') return LocaleKeys.inStore.tr();
return norm;
}
String? getDisplayType() {
List<String> types = normalizedTypes;
if (types.isNotEmpty) return _displayTypeLabel(types.first);
if (type != null && type!.trim().isNotEmpty) return type!.trim();
return null;
}
// One label per distinct type (e.g. ["Online", "In Store"]) - used to render a separate chip
// for each, instead of collapsing a dual-type retailer down to a single badge.
List<String> getDisplayTypes() {
List<String> types = normalizedTypes;
if (types.isNotEmpty) return types.map(_displayTypeLabel).toList();
if (type != null && type!.trim().isNotEmpty) return <String>[type!.trim()];
return <String>[];
}
}
class WalaPlusRetailerContact {
String? email;
List<String>? emails;
String? phone;
List<String>? phones;
String? website;
String? address;
String? instagram;
String? twitter;
WalaPlusRetailerContact({this.email, this.emails, this.phone, this.phones, this.website, this.address, this.instagram, this.twitter});
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()));
}
String? singleEmail = json['email']?.toString() ?? json['email_address']?.toString();
if (singleEmail != null && singleEmail.trim().isNotEmpty && singleEmail != "null") {
if (!emailsList.contains(singleEmail.trim())) {
emailsList.add(singleEmail.trim());
}
}
List<String> phonesList = [];
if (json['phones'] != null && json['phones'] is List) {
json['phones'].forEach((v) => phonesList.add(v.toString()));
}
String? singlePhone = json['phone']?.toString() ?? json['mobile']?.toString() ?? json['phone_number']?.toString();
if (singlePhone != null && singlePhone.trim().isNotEmpty && singlePhone != "null") {
if (!phonesList.contains(singlePhone.trim())) {
phonesList.add(singlePhone.trim());
}
}
return WalaPlusRetailerContact(
email: singleEmail,
emails: emailsList,
phone: singlePhone,
phones: phonesList,
website: WalaPlusRetailerModel.decodeUnicodeString(json['website']),
address: WalaPlusRetailerModel.decodeUnicodeString(json['address']),
instagram: json['instagram']?.toString(),
twitter: json['twitter']?.toString(),
);
}
Map<String, dynamic> toJson() {
return {'email': email, 'emails': emails, 'phone': phone, 'phones': phones, 'website': website, 'address': address, 'instagram': instagram, 'twitter': twitter};
}
String? get primaryEmail {
if (email != null && email!.trim().isNotEmpty && email != "null") return email!.trim();
if (emails != null && emails!.isNotEmpty) {
for (var e in emails!) {
if (e.trim().isNotEmpty && e != "null") return e.trim();
}
}
return null;
}
String? get primaryPhone {
if (phone != null && phone!.trim().isNotEmpty && phone != "null") return phone!.trim();
if (phones != null && phones!.isNotEmpty) {
for (var p in phones!) {
if (p.trim().isNotEmpty && p != "null") return p.trim();
}
}
return null;
}
}
class WalaPlusRetailerLocation {
dynamic id;
String? address;
String? city;
String? cityNameAr;
String? cityNameEn;
String? contact;
String? countryNameAr;
String? countryNameEn;
String? distance;
double? latitude;
double? longitude;
dynamic placeId;
WalaPlusRetailerLocation({
this.id,
this.address,
this.city,
this.cityNameAr,
this.cityNameEn,
this.contact,
this.countryNameAr,
this.countryNameEn,
this.distance,
this.latitude,
this.longitude,
this.placeId,
});
factory WalaPlusRetailerLocation.fromJson(Map<String, dynamic> json) {
return WalaPlusRetailerLocation(
id: json['id'],
address: WalaPlusRetailerModel.decodeUnicodeString(json['address']),
city: WalaPlusRetailerModel.decodeUnicodeString(json['city']),
cityNameAr: WalaPlusRetailerModel.decodeUnicodeString(json['city_name_ar']),
cityNameEn: WalaPlusRetailerModel.decodeUnicodeString(json['city_name_en']),
contact: json['contact']?.toString(),
countryNameAr: WalaPlusRetailerModel.decodeUnicodeString(json['country_name_ar']),
countryNameEn: WalaPlusRetailerModel.decodeUnicodeString(json['country_name_en']),
distance: WalaPlusRetailerModel.decodeUnicodeString(json['distance']),
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['id'] = id;
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['distance'] = distance;
data['latitude'] = latitude;
data['longitude'] = longitude;
data['place_id'] = placeId;
return data;
}
// distance comes from the API as a raw numeric string (e.g. "14.729670498669968") -
// round to max 2 decimal places and append the localized "KM Away" label.
String? getDisplayDistance() {
if (distance == null) return null;
double? km = double.tryParse(distance!.trim());
if (km == null) return null;
String formatted = km.toStringAsFixed(2);
return LocaleKeys.kmAway.tr(args: [formatted]);
}
String getLocalizedCity(bool isAr) {
if (isAr) {
if (cityNameAr != null && cityNameAr!.trim().isNotEmpty && cityNameAr != "null") {
return cityNameAr!.trim();
}
} else {
if (cityNameEn != null && cityNameEn!.trim().isNotEmpty && cityNameEn != "null") {
return cityNameEn!.trim();
}
}
if (city != null && city!.trim().isNotEmpty && city != "null") {
return city!.trim();
}
return isAr ? (cityNameAr ?? "") : (cityNameEn ?? "");
}
String getLocalizedCountry(bool isAr) {
if (isAr) {
if (countryNameAr != null && countryNameAr!.trim().isNotEmpty && countryNameAr != "null") {
return countryNameAr!.trim();
}
} else {
if (countryNameEn != null && countryNameEn!.trim().isNotEmpty && countryNameEn != "null") {
return countryNameEn!.trim();
}
}
return "";
}
}
class WalaPlusOfferModel {
String? id;
String? retailerId;
String? title;
String? image;
String? imageThumb;
String? type;
String? description;
dynamic price;
dynamic usage;
String? expiredAt;
WalaPlusOfferModel({this.id, this.retailerId, this.title, this.image, this.imageThumb, this.type, this.description, this.price, this.usage, this.expiredAt});
factory WalaPlusOfferModel.fromJson(Map<String, dynamic> json) {
return WalaPlusOfferModel(
id: json['id']?.toString(),
retailerId: (json['retailer_id'] ?? json['retailerId'] ?? json['retailerID'])?.toString(),
title: WalaPlusRetailerModel.decodeUnicodeString(json['title']),
image: json['image']?.toString(),
imageThumb: (json['image_thumb'] ?? json['imageThumb'])?.toString(),
type: json['type']?.toString(),
description: WalaPlusRetailerModel.decodeUnicodeString(json['description']),
price: json['price'],
usage: json['usage'],
expiredAt: (json['expired_at'] ?? json['expiredAt'] ?? json['expiry_date'])?.toString(),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'retailer_id': retailerId,
'title': title,
'image': image,
'image_thumb': imageThumb,
'type': type,
'description': description,
'price': price,
'usage': usage,
'expired_at': expiredAt,
};
}
String? get displayImage {
if (image != null && image!.trim().isNotEmpty && image != "null") return image!.trim();
if (imageThumb != null && imageThumb!.trim().isNotEmpty && imageThumb != "null") return imageThumb!.trim();
return null;
}
}