Manos Chorianopoulos

MerchantModel, CouponSetItemModel, MyRewardsVC fixes

......@@ -19,6 +19,43 @@ public class MyRewardsOfferCollectionViewCell: UICollectionViewCell {
@IBOutlet weak var expirationLabel: UILabel!
@IBOutlet weak var logoImage: UIImageView!
var postImageURL: String? {
didSet {
if let url = postImageURL {
self.bannerImage.image = UIImage() // UIImage(named: "loading")
UIImage.loadImageUsingCacheWithUrlString(url) { image in
// set the image only when we are still displaying the content for the image we finished downloading
if url == self.postImageURL {
self.bannerImage.image = image
}
}
}
else {
self.bannerImage.image = nil
}
}
}
var logoImageURL: String? {
didSet {
if let url = logoImageURL {
self.logoImage.image = UIImage() // UIImage(named: "loading")
UIImage.loadImageUsingCacheWithUrlString(url) { image in
// set the image only when we are still displaying the content for the image we finished downloading
if url == self.logoImageURL {
self.logoImage.image = image
}
}
}
else {
self.logoImage.image = nil
}
}
}
public override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
......@@ -68,12 +105,7 @@ public class MyRewardsOfferCollectionViewCell: UICollectionViewCell {
func configureCell(data: CouponSetItemModel) {
// Use coupon set preview image
let imageName = data._img_preview
if !imageName.isEmpty {
bannerImage.image = UIImage(named: imageName, in: Bundle.frameworkResourceBundle, compatibleWith: nil)
} else {
bannerImage.image = nil
}
self.postImageURL = data._img_preview
// Default to not favorite for coupon sets
favoriteImage.image = UIImage(named: "favorite_empty", in: Bundle.frameworkResourceBundle, compatibleWith: nil)
......@@ -93,7 +125,7 @@ public class MyRewardsOfferCollectionViewCell: UICollectionViewCell {
case "plus_one":
return 0x007AFF
default:
return 0x6C757D
return 0xEE417D
}
}()
discountView.backgroundColor = UIColor(rgb: discountColor)
......@@ -106,20 +138,16 @@ public class MyRewardsOfferCollectionViewCell: UICollectionViewCell {
subtitleLabel.font = UIFont(name: "PingLCG-Regular", size: 14)
subtitleLabel.textColor = UIColor(rgb: 0x00111B)
expirationLabel.text = "έως " + data.formattedExpiration(format: "dd-MM")
expirationLabel.text = "έως " + data.formattedEndDate(format: "dd-MM")
expirationLabel.font = UIFont(name: "PingLCG-Regular", size: 13)
expirationLabel.textColor = UIColor(rgb: 0x00111B)
// Use first image from img array if available
if let imgArray = data._img, !imgArray.isEmpty {
let logoName = imgArray[0]
if !logoName.isEmpty {
logoImage.image = UIImage(named: logoName, in: Bundle.frameworkResourceBundle, compatibleWith: nil)
} else {
logoImage.image = nil
}
// Use merchant logo from bound merchant data
if let merchant = data._merchant, !merchant._img_preview.isEmpty {
// Use merchant's img_preview for logo
self.logoImageURL = merchant._img_preview
} else {
logoImage.image = nil
self.logoImageURL = nil
}
}
......
......@@ -78,6 +78,9 @@ public class CouponSetItemModel {
private var third_party_service: String?
private var category: String?
// Bound merchant data for performance
private var merchant: MerchantModel?
public init(dictionary: [String: Any]) {
// Existing fields
self.uuid = dictionary["uuid"] as? String? ?? ""
......@@ -236,6 +239,12 @@ public class CouponSetItemModel {
public var _third_party_service: String { get { return self.third_party_service ?? "" } }
public var _category: String { get { return self.category ?? "" } }
// Bound merchant data accessor
public var _merchant: MerchantModel? {
get { return self.merchant }
set(newValue) { self.merchant = newValue }
}
// Formatted expiration date for display
public var _expiration_formatted: String {
guard let expiration = self.expiration, !expiration.isEmpty else {
......@@ -272,6 +281,46 @@ public class CouponSetItemModel {
return ""
}
/// Format start date with custom format
/// - Parameter format: DateFormatter format string (e.g., "dd/MM/yyyy", "MMM yyyy", "dd-MM")
/// - Returns: Formatted date string or empty string if invalid
public func formattedStartDate(format: String) -> String {
guard let startDate = self.start_date, !startDate.isEmpty else {
return ""
}
let inputFormatter = DateFormatter()
inputFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
if let date = inputFormatter.date(from: startDate) {
let outputFormatter = DateFormatter()
outputFormatter.dateFormat = format
return outputFormatter.string(from: date)
}
return ""
}
/// Format end date with custom format
/// - Parameter format: DateFormatter format string (e.g., "dd/MM/yyyy", "MMM yyyy", "dd-MM")
/// - Returns: Formatted date string or empty string if invalid
public func formattedEndDate(format: String) -> String {
guard let endDate = self.end_date, !endDate.isEmpty else {
return ""
}
let inputFormatter = DateFormatter()
inputFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
if let date = inputFormatter.date(from: endDate) {
let outputFormatter = DateFormatter()
outputFormatter.dateFormat = format
return outputFormatter.string(from: date)
}
return ""
}
}
public class RedeemedMerchantDetailsModel: Codable {
......
......@@ -10,7 +10,8 @@ import Foundation
// MARK: - Merchant Models
public class MerchantModel: Codable {
public class MerchantModel {
// Existing fields
private var address: String?
private var id: String?
private var store_id: String?
......@@ -48,7 +49,35 @@ public class MerchantModel: Codable {
private var show_map: Bool?
private var eshop: Bool?
// NEW FIELDS - Missing from API response
// Image array - multiple merchant images
private var img: [String]?
// Business metadata - stored as JSON string for safety
private var merchant_metadata_raw: String?
// Contact details
private var telephones_raw: String?
// Business operations
private var working_hours: String?
private var year_established: Int?
private var currency: String?
private var bank: String?
// E-commerce fields
private var min_order_price: Double?
private var min_order_price_takeaway: Double?
private var min_price: Double?
private var max_price: Double?
// SEO and categorization
private var url_name: String?
private var tags_raw: String? // Store as comma-separated string
private var product_raw: String? // Store as JSON string
public init() {
// Existing fields
self.address = ""
self.id = ""
self.store_id = ""
......@@ -85,9 +114,26 @@ public class MerchantModel: Codable {
self.hidden = false
self.show_map = false
self.eshop = false
// New fields - initialize with appropriate defaults
self.img = []
self.merchant_metadata_raw = ""
self.telephones_raw = ""
self.working_hours = ""
self.year_established = nil
self.currency = ""
self.bank = ""
self.min_order_price = nil
self.min_order_price_takeaway = nil
self.min_price = nil
self.max_price = nil
self.url_name = ""
self.tags_raw = ""
self.product_raw = ""
}
public init(dictionary: [String: Any]) {
// Parse existing fields
self.address = dictionary["address"] as? String? ?? ""
self.id = dictionary["id"] as? String? ?? ""
self.store_id = dictionary["store_id"] as? String? ?? ""
......@@ -123,6 +169,7 @@ public class MerchantModel: Codable {
self.default_shown = dictionary["default_shown"] as? Bool? ?? false
self.hidden = dictionary["hidden"] as? Bool? ?? false
// Parse extra_fields
if let extra_fields = dictionary["extra_fields"] as? [String: Any] {
self.show_map = extra_fields["show_map"] as? Bool? ?? false
self.eshop = extra_fields["eshop"] as? Bool? ?? false
......@@ -130,6 +177,72 @@ public class MerchantModel: Codable {
self.show_map = false
self.eshop = false
}
// Parse NEW FIELDS - with safe type handling
// Parse img array safely
if let imgArray = dictionary["img"] as? [String] {
self.img = imgArray
} else {
self.img = []
}
// Parse merchant_metadata as JSON string for safety
if let metadata = dictionary["merchant_metadata"] as? [String: Any] {
do {
let jsonData = try JSONSerialization.data(withJSONObject: metadata, options: [])
if let jsonString = String(data: jsonData, encoding: .utf8) {
self.merchant_metadata_raw = jsonString
} else {
self.merchant_metadata_raw = ""
}
} catch {
self.merchant_metadata_raw = ""
}
} else {
self.merchant_metadata_raw = ""
}
// Parse contact details
self.telephones_raw = dictionary["telephones_raw"] as? String? ?? ""
// Parse business operations
self.working_hours = dictionary["working_hours"] as? String? ?? ""
self.year_established = dictionary["year_established"] as? Int?
self.currency = dictionary["currency"] as? String? ?? ""
self.bank = dictionary["bank"] as? String? ?? ""
// Parse e-commerce fields
self.min_order_price = dictionary["min_order_price"] as? Double?
self.min_order_price_takeaway = dictionary["min_order_price_takeaway"] as? Double?
self.min_price = dictionary["min_price"] as? Double?
self.max_price = dictionary["max_price"] as? Double?
// Parse SEO and categorization
self.url_name = dictionary["url_name"] as? String? ?? ""
// Parse tags array to comma-separated string
if let tagsArray = dictionary["tags"] as? [String] {
self.tags_raw = tagsArray.joined(separator: ",")
} else {
self.tags_raw = ""
}
// Parse product object as JSON string
if let product = dictionary["product"] as? [String: Any] {
do {
let jsonData = try JSONSerialization.data(withJSONObject: product, options: [])
if let jsonString = String(data: jsonData, encoding: .utf8) {
self.product_raw = jsonString
} else {
self.product_raw = ""
}
} catch {
self.product_raw = ""
}
} else {
self.product_raw = ""
}
}
public var _address: String {
......@@ -311,4 +424,133 @@ public class MerchantModel: Codable {
get { return self.eshop ?? false }
set(newValue) { self.eshop = newValue }
}
// MARK: - NEW FIELD ACCESSORS
// Image array accessor
public var _img: [String] {
get { return self.img ?? [] }
set(newValue) { self.img = newValue }
}
// Business metadata accessor (raw JSON string)
public var _merchant_metadata_raw: String {
get { return self.merchant_metadata_raw ?? "" }
set(newValue) { self.merchant_metadata_raw = newValue }
}
// Contact details
public var _telephones_raw: String {
get { return self.telephones_raw ?? "" }
set(newValue) { self.telephones_raw = newValue }
}
// Business operations
public var _working_hours: String {
get { return self.working_hours ?? "" }
set(newValue) { self.working_hours = newValue }
}
public var _year_established: Int? {
get { return self.year_established }
set(newValue) { self.year_established = newValue }
}
public var _currency: String {
get { return self.currency ?? "" }
set(newValue) { self.currency = newValue }
}
public var _bank: String {
get { return self.bank ?? "" }
set(newValue) { self.bank = newValue }
}
// E-commerce fields
public var _min_order_price: Double? {
get { return self.min_order_price }
set(newValue) { self.min_order_price = newValue }
}
public var _min_order_price_takeaway: Double? {
get { return self.min_order_price_takeaway }
set(newValue) { self.min_order_price_takeaway = newValue }
}
public var _min_price: Double? {
get { return self.min_price }
set(newValue) { self.min_price = newValue }
}
public var _max_price: Double? {
get { return self.max_price }
set(newValue) { self.max_price = newValue }
}
// SEO and categorization
public var _url_name: String {
get { return self.url_name ?? "" }
set(newValue) { self.url_name = newValue }
}
public var _tags_raw: String {
get { return self.tags_raw ?? "" }
set(newValue) { self.tags_raw = newValue }
}
public var _product_raw: String {
get { return self.product_raw ?? "" }
set(newValue) { self.product_raw = newValue }
}
// MARK: - COMPUTED PROPERTIES FOR COMPLEX DATA
// Computed property to get metadata as dictionary
public var merchantMetadata: [String: Any]? {
guard let rawString = merchant_metadata_raw,
!rawString.isEmpty,
let data = rawString.data(using: .utf8),
let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
return nil
}
return dict
}
// Computed property to get tags as array
public var tagsArray: [String] {
guard let rawTags = tags_raw, !rawTags.isEmpty else { return [] }
return rawTags.components(separatedBy: ",").filter { !$0.isEmpty }
}
// Computed property to get product info as dictionary
public var productInfo: [String: Any]? {
guard let rawString = product_raw,
!rawString.isEmpty,
let data = rawString.data(using: .utf8),
let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
return nil
}
return dict
}
// Computed property to check if merchant has progress completed
public var hasProgressCompleted: Bool {
return merchantMetadata?["progressCompleted"] as? Bool ?? false
}
// Computed property to get main image URL (first in img array or fallback to img_preview)
public var mainImageURL: String {
if let imgArray = img, !imgArray.isEmpty {
return imgArray.first ?? _img_preview
}
return _img_preview
}
// Computed property to get pin image URL (second in img array if available)
public var pinImageURL: String? {
if let imgArray = img, imgArray.count > 1 {
return imgArray[1]
}
return nil
}
}
......
......@@ -256,6 +256,12 @@ import UIKit
if belongsToCategory {
processedCouponSets.insert(couponSet._uuid)
// BIND MERCHANT DATA: Find and bind the merchant to this coupon set
if let merchant = categoryMerchants.first(where: { $0._uuid == couponSet._merchant_uuid }) {
couponSet._merchant = merchant
print(" 🔗 Bound merchant '\(merchant._name)' to coupon set '\(couponSet._name)'")
}
}
return belongsToCategory
......@@ -285,6 +291,14 @@ import UIKit
if !unmatchedCouponSets.isEmpty {
print(" ⚠️ Found \(unmatchedCouponSets.count) unmatched coupon sets - adding to 'Άλλες Προσφορές' section")
// BIND MERCHANT DATA for unmatched coupon sets too
for couponSet in unmatchedCouponSets {
if let merchant = merchants.first(where: { $0._uuid == couponSet._merchant_uuid }) {
couponSet._merchant = merchant
print(" 🔗 Bound merchant '\(merchant._name)' to unmatched coupon set '\(couponSet._name)'")
}
}
let unmatchedSection = SectionModel(
sectionType: .myRewardsHorizontalCouponsets,
title: "Άλλες Προσφορές", // "Other Offers"
......