Manos Chorianopoulos

MerchantModel, CouponSetItemModel, MyRewardsVC fixes

...@@ -19,6 +19,43 @@ public class MyRewardsOfferCollectionViewCell: UICollectionViewCell { ...@@ -19,6 +19,43 @@ public class MyRewardsOfferCollectionViewCell: UICollectionViewCell {
19 @IBOutlet weak var expirationLabel: UILabel! 19 @IBOutlet weak var expirationLabel: UILabel!
20 @IBOutlet weak var logoImage: UIImageView! 20 @IBOutlet weak var logoImage: UIImageView!
21 21
22 + var postImageURL: String? {
23 + didSet {
24 + if let url = postImageURL {
25 + self.bannerImage.image = UIImage() // UIImage(named: "loading")
26 +
27 + UIImage.loadImageUsingCacheWithUrlString(url) { image in
28 + // set the image only when we are still displaying the content for the image we finished downloading
29 + if url == self.postImageURL {
30 + self.bannerImage.image = image
31 + }
32 + }
33 + }
34 + else {
35 + self.bannerImage.image = nil
36 + }
37 + }
38 + }
39 +
40 + var logoImageURL: String? {
41 + didSet {
42 + if let url = logoImageURL {
43 + self.logoImage.image = UIImage() // UIImage(named: "loading")
44 +
45 + UIImage.loadImageUsingCacheWithUrlString(url) { image in
46 + // set the image only when we are still displaying the content for the image we finished downloading
47 + if url == self.logoImageURL {
48 + self.logoImage.image = image
49 + }
50 + }
51 + }
52 + else {
53 + self.logoImage.image = nil
54 + }
55 + }
56 + }
57 +
58 +
22 public override func awakeFromNib() { 59 public override func awakeFromNib() {
23 super.awakeFromNib() 60 super.awakeFromNib()
24 // Initialization code 61 // Initialization code
...@@ -68,12 +105,7 @@ public class MyRewardsOfferCollectionViewCell: UICollectionViewCell { ...@@ -68,12 +105,7 @@ public class MyRewardsOfferCollectionViewCell: UICollectionViewCell {
68 105
69 func configureCell(data: CouponSetItemModel) { 106 func configureCell(data: CouponSetItemModel) {
70 // Use coupon set preview image 107 // Use coupon set preview image
71 - let imageName = data._img_preview 108 + self.postImageURL = data._img_preview
72 - if !imageName.isEmpty {
73 - bannerImage.image = UIImage(named: imageName, in: Bundle.frameworkResourceBundle, compatibleWith: nil)
74 - } else {
75 - bannerImage.image = nil
76 - }
77 109
78 // Default to not favorite for coupon sets 110 // Default to not favorite for coupon sets
79 favoriteImage.image = UIImage(named: "favorite_empty", in: Bundle.frameworkResourceBundle, compatibleWith: nil) 111 favoriteImage.image = UIImage(named: "favorite_empty", in: Bundle.frameworkResourceBundle, compatibleWith: nil)
...@@ -93,7 +125,7 @@ public class MyRewardsOfferCollectionViewCell: UICollectionViewCell { ...@@ -93,7 +125,7 @@ public class MyRewardsOfferCollectionViewCell: UICollectionViewCell {
93 case "plus_one": 125 case "plus_one":
94 return 0x007AFF 126 return 0x007AFF
95 default: 127 default:
96 - return 0x6C757D 128 + return 0xEE417D
97 } 129 }
98 }() 130 }()
99 discountView.backgroundColor = UIColor(rgb: discountColor) 131 discountView.backgroundColor = UIColor(rgb: discountColor)
...@@ -106,20 +138,16 @@ public class MyRewardsOfferCollectionViewCell: UICollectionViewCell { ...@@ -106,20 +138,16 @@ public class MyRewardsOfferCollectionViewCell: UICollectionViewCell {
106 subtitleLabel.font = UIFont(name: "PingLCG-Regular", size: 14) 138 subtitleLabel.font = UIFont(name: "PingLCG-Regular", size: 14)
107 subtitleLabel.textColor = UIColor(rgb: 0x00111B) 139 subtitleLabel.textColor = UIColor(rgb: 0x00111B)
108 140
109 - expirationLabel.text = "έως " + data.formattedExpiration(format: "dd-MM") 141 + expirationLabel.text = "έως " + data.formattedEndDate(format: "dd-MM")
110 expirationLabel.font = UIFont(name: "PingLCG-Regular", size: 13) 142 expirationLabel.font = UIFont(name: "PingLCG-Regular", size: 13)
111 expirationLabel.textColor = UIColor(rgb: 0x00111B) 143 expirationLabel.textColor = UIColor(rgb: 0x00111B)
112 144
113 - // Use first image from img array if available 145 + // Use merchant logo from bound merchant data
114 - if let imgArray = data._img, !imgArray.isEmpty { 146 + if let merchant = data._merchant, !merchant._img_preview.isEmpty {
115 - let logoName = imgArray[0] 147 + // Use merchant's img_preview for logo
116 - if !logoName.isEmpty { 148 + self.logoImageURL = merchant._img_preview
117 - logoImage.image = UIImage(named: logoName, in: Bundle.frameworkResourceBundle, compatibleWith: nil)
118 - } else {
119 - logoImage.image = nil
120 - }
121 } else { 149 } else {
122 - logoImage.image = nil 150 + self.logoImageURL = nil
123 } 151 }
124 } 152 }
125 153
......
...@@ -78,6 +78,9 @@ public class CouponSetItemModel { ...@@ -78,6 +78,9 @@ public class CouponSetItemModel {
78 private var third_party_service: String? 78 private var third_party_service: String?
79 private var category: String? 79 private var category: String?
80 80
81 + // Bound merchant data for performance
82 + private var merchant: MerchantModel?
83 +
81 public init(dictionary: [String: Any]) { 84 public init(dictionary: [String: Any]) {
82 // Existing fields 85 // Existing fields
83 self.uuid = dictionary["uuid"] as? String? ?? "" 86 self.uuid = dictionary["uuid"] as? String? ?? ""
...@@ -236,6 +239,12 @@ public class CouponSetItemModel { ...@@ -236,6 +239,12 @@ public class CouponSetItemModel {
236 public var _third_party_service: String { get { return self.third_party_service ?? "" } } 239 public var _third_party_service: String { get { return self.third_party_service ?? "" } }
237 public var _category: String { get { return self.category ?? "" } } 240 public var _category: String { get { return self.category ?? "" } }
238 241
242 + // Bound merchant data accessor
243 + public var _merchant: MerchantModel? {
244 + get { return self.merchant }
245 + set(newValue) { self.merchant = newValue }
246 + }
247 +
239 // Formatted expiration date for display 248 // Formatted expiration date for display
240 public var _expiration_formatted: String { 249 public var _expiration_formatted: String {
241 guard let expiration = self.expiration, !expiration.isEmpty else { 250 guard let expiration = self.expiration, !expiration.isEmpty else {
...@@ -272,6 +281,46 @@ public class CouponSetItemModel { ...@@ -272,6 +281,46 @@ public class CouponSetItemModel {
272 281
273 return "" 282 return ""
274 } 283 }
284 +
285 + /// Format start date with custom format
286 + /// - Parameter format: DateFormatter format string (e.g., "dd/MM/yyyy", "MMM yyyy", "dd-MM")
287 + /// - Returns: Formatted date string or empty string if invalid
288 + public func formattedStartDate(format: String) -> String {
289 + guard let startDate = self.start_date, !startDate.isEmpty else {
290 + return ""
291 + }
292 +
293 + let inputFormatter = DateFormatter()
294 + inputFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
295 +
296 + if let date = inputFormatter.date(from: startDate) {
297 + let outputFormatter = DateFormatter()
298 + outputFormatter.dateFormat = format
299 + return outputFormatter.string(from: date)
300 + }
301 +
302 + return ""
303 + }
304 +
305 + /// Format end date with custom format
306 + /// - Parameter format: DateFormatter format string (e.g., "dd/MM/yyyy", "MMM yyyy", "dd-MM")
307 + /// - Returns: Formatted date string or empty string if invalid
308 + public func formattedEndDate(format: String) -> String {
309 + guard let endDate = self.end_date, !endDate.isEmpty else {
310 + return ""
311 + }
312 +
313 + let inputFormatter = DateFormatter()
314 + inputFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
315 +
316 + if let date = inputFormatter.date(from: endDate) {
317 + let outputFormatter = DateFormatter()
318 + outputFormatter.dateFormat = format
319 + return outputFormatter.string(from: date)
320 + }
321 +
322 + return ""
323 + }
275 } 324 }
276 325
277 public class RedeemedMerchantDetailsModel: Codable { 326 public class RedeemedMerchantDetailsModel: Codable {
......
...@@ -10,7 +10,8 @@ import Foundation ...@@ -10,7 +10,8 @@ import Foundation
10 10
11 // MARK: - Merchant Models 11 // MARK: - Merchant Models
12 12
13 -public class MerchantModel: Codable { 13 +public class MerchantModel {
14 + // Existing fields
14 private var address: String? 15 private var address: String?
15 private var id: String? 16 private var id: String?
16 private var store_id: String? 17 private var store_id: String?
...@@ -48,7 +49,35 @@ public class MerchantModel: Codable { ...@@ -48,7 +49,35 @@ public class MerchantModel: Codable {
48 private var show_map: Bool? 49 private var show_map: Bool?
49 private var eshop: Bool? 50 private var eshop: Bool?
50 51
52 + // NEW FIELDS - Missing from API response
53 + // Image array - multiple merchant images
54 + private var img: [String]?
55 +
56 + // Business metadata - stored as JSON string for safety
57 + private var merchant_metadata_raw: String?
58 +
59 + // Contact details
60 + private var telephones_raw: String?
61 +
62 + // Business operations
63 + private var working_hours: String?
64 + private var year_established: Int?
65 + private var currency: String?
66 + private var bank: String?
67 +
68 + // E-commerce fields
69 + private var min_order_price: Double?
70 + private var min_order_price_takeaway: Double?
71 + private var min_price: Double?
72 + private var max_price: Double?
73 +
74 + // SEO and categorization
75 + private var url_name: String?
76 + private var tags_raw: String? // Store as comma-separated string
77 + private var product_raw: String? // Store as JSON string
78 +
51 public init() { 79 public init() {
80 + // Existing fields
52 self.address = "" 81 self.address = ""
53 self.id = "" 82 self.id = ""
54 self.store_id = "" 83 self.store_id = ""
...@@ -85,9 +114,26 @@ public class MerchantModel: Codable { ...@@ -85,9 +114,26 @@ public class MerchantModel: Codable {
85 self.hidden = false 114 self.hidden = false
86 self.show_map = false 115 self.show_map = false
87 self.eshop = false 116 self.eshop = false
117 +
118 + // New fields - initialize with appropriate defaults
119 + self.img = []
120 + self.merchant_metadata_raw = ""
121 + self.telephones_raw = ""
122 + self.working_hours = ""
123 + self.year_established = nil
124 + self.currency = ""
125 + self.bank = ""
126 + self.min_order_price = nil
127 + self.min_order_price_takeaway = nil
128 + self.min_price = nil
129 + self.max_price = nil
130 + self.url_name = ""
131 + self.tags_raw = ""
132 + self.product_raw = ""
88 } 133 }
89 134
90 public init(dictionary: [String: Any]) { 135 public init(dictionary: [String: Any]) {
136 + // Parse existing fields
91 self.address = dictionary["address"] as? String? ?? "" 137 self.address = dictionary["address"] as? String? ?? ""
92 self.id = dictionary["id"] as? String? ?? "" 138 self.id = dictionary["id"] as? String? ?? ""
93 self.store_id = dictionary["store_id"] as? String? ?? "" 139 self.store_id = dictionary["store_id"] as? String? ?? ""
...@@ -123,6 +169,7 @@ public class MerchantModel: Codable { ...@@ -123,6 +169,7 @@ public class MerchantModel: Codable {
123 self.default_shown = dictionary["default_shown"] as? Bool? ?? false 169 self.default_shown = dictionary["default_shown"] as? Bool? ?? false
124 self.hidden = dictionary["hidden"] as? Bool? ?? false 170 self.hidden = dictionary["hidden"] as? Bool? ?? false
125 171
172 + // Parse extra_fields
126 if let extra_fields = dictionary["extra_fields"] as? [String: Any] { 173 if let extra_fields = dictionary["extra_fields"] as? [String: Any] {
127 self.show_map = extra_fields["show_map"] as? Bool? ?? false 174 self.show_map = extra_fields["show_map"] as? Bool? ?? false
128 self.eshop = extra_fields["eshop"] as? Bool? ?? false 175 self.eshop = extra_fields["eshop"] as? Bool? ?? false
...@@ -130,6 +177,72 @@ public class MerchantModel: Codable { ...@@ -130,6 +177,72 @@ public class MerchantModel: Codable {
130 self.show_map = false 177 self.show_map = false
131 self.eshop = false 178 self.eshop = false
132 } 179 }
180 +
181 + // Parse NEW FIELDS - with safe type handling
182 +
183 + // Parse img array safely
184 + if let imgArray = dictionary["img"] as? [String] {
185 + self.img = imgArray
186 + } else {
187 + self.img = []
188 + }
189 +
190 + // Parse merchant_metadata as JSON string for safety
191 + if let metadata = dictionary["merchant_metadata"] as? [String: Any] {
192 + do {
193 + let jsonData = try JSONSerialization.data(withJSONObject: metadata, options: [])
194 + if let jsonString = String(data: jsonData, encoding: .utf8) {
195 + self.merchant_metadata_raw = jsonString
196 + } else {
197 + self.merchant_metadata_raw = ""
198 + }
199 + } catch {
200 + self.merchant_metadata_raw = ""
201 + }
202 + } else {
203 + self.merchant_metadata_raw = ""
204 + }
205 +
206 + // Parse contact details
207 + self.telephones_raw = dictionary["telephones_raw"] as? String? ?? ""
208 +
209 + // Parse business operations
210 + self.working_hours = dictionary["working_hours"] as? String? ?? ""
211 + self.year_established = dictionary["year_established"] as? Int?
212 + self.currency = dictionary["currency"] as? String? ?? ""
213 + self.bank = dictionary["bank"] as? String? ?? ""
214 +
215 + // Parse e-commerce fields
216 + self.min_order_price = dictionary["min_order_price"] as? Double?
217 + self.min_order_price_takeaway = dictionary["min_order_price_takeaway"] as? Double?
218 + self.min_price = dictionary["min_price"] as? Double?
219 + self.max_price = dictionary["max_price"] as? Double?
220 +
221 + // Parse SEO and categorization
222 + self.url_name = dictionary["url_name"] as? String? ?? ""
223 +
224 + // Parse tags array to comma-separated string
225 + if let tagsArray = dictionary["tags"] as? [String] {
226 + self.tags_raw = tagsArray.joined(separator: ",")
227 + } else {
228 + self.tags_raw = ""
229 + }
230 +
231 + // Parse product object as JSON string
232 + if let product = dictionary["product"] as? [String: Any] {
233 + do {
234 + let jsonData = try JSONSerialization.data(withJSONObject: product, options: [])
235 + if let jsonString = String(data: jsonData, encoding: .utf8) {
236 + self.product_raw = jsonString
237 + } else {
238 + self.product_raw = ""
239 + }
240 + } catch {
241 + self.product_raw = ""
242 + }
243 + } else {
244 + self.product_raw = ""
245 + }
133 } 246 }
134 247
135 public var _address: String { 248 public var _address: String {
...@@ -311,4 +424,133 @@ public class MerchantModel: Codable { ...@@ -311,4 +424,133 @@ public class MerchantModel: Codable {
311 get { return self.eshop ?? false } 424 get { return self.eshop ?? false }
312 set(newValue) { self.eshop = newValue } 425 set(newValue) { self.eshop = newValue }
313 } 426 }
427 +
428 + // MARK: - NEW FIELD ACCESSORS
429 +
430 + // Image array accessor
431 + public var _img: [String] {
432 + get { return self.img ?? [] }
433 + set(newValue) { self.img = newValue }
434 + }
435 +
436 + // Business metadata accessor (raw JSON string)
437 + public var _merchant_metadata_raw: String {
438 + get { return self.merchant_metadata_raw ?? "" }
439 + set(newValue) { self.merchant_metadata_raw = newValue }
440 + }
441 +
442 + // Contact details
443 + public var _telephones_raw: String {
444 + get { return self.telephones_raw ?? "" }
445 + set(newValue) { self.telephones_raw = newValue }
446 + }
447 +
448 + // Business operations
449 + public var _working_hours: String {
450 + get { return self.working_hours ?? "" }
451 + set(newValue) { self.working_hours = newValue }
452 + }
453 +
454 + public var _year_established: Int? {
455 + get { return self.year_established }
456 + set(newValue) { self.year_established = newValue }
457 + }
458 +
459 + public var _currency: String {
460 + get { return self.currency ?? "" }
461 + set(newValue) { self.currency = newValue }
462 + }
463 +
464 + public var _bank: String {
465 + get { return self.bank ?? "" }
466 + set(newValue) { self.bank = newValue }
467 + }
468 +
469 + // E-commerce fields
470 + public var _min_order_price: Double? {
471 + get { return self.min_order_price }
472 + set(newValue) { self.min_order_price = newValue }
473 + }
474 +
475 + public var _min_order_price_takeaway: Double? {
476 + get { return self.min_order_price_takeaway }
477 + set(newValue) { self.min_order_price_takeaway = newValue }
478 + }
479 +
480 + public var _min_price: Double? {
481 + get { return self.min_price }
482 + set(newValue) { self.min_price = newValue }
483 + }
484 +
485 + public var _max_price: Double? {
486 + get { return self.max_price }
487 + set(newValue) { self.max_price = newValue }
488 + }
489 +
490 + // SEO and categorization
491 + public var _url_name: String {
492 + get { return self.url_name ?? "" }
493 + set(newValue) { self.url_name = newValue }
494 + }
495 +
496 + public var _tags_raw: String {
497 + get { return self.tags_raw ?? "" }
498 + set(newValue) { self.tags_raw = newValue }
499 + }
500 +
501 + public var _product_raw: String {
502 + get { return self.product_raw ?? "" }
503 + set(newValue) { self.product_raw = newValue }
504 + }
505 +
506 + // MARK: - COMPUTED PROPERTIES FOR COMPLEX DATA
507 +
508 + // Computed property to get metadata as dictionary
509 + public var merchantMetadata: [String: Any]? {
510 + guard let rawString = merchant_metadata_raw,
511 + !rawString.isEmpty,
512 + let data = rawString.data(using: .utf8),
513 + let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
514 + return nil
515 + }
516 + return dict
517 + }
518 +
519 + // Computed property to get tags as array
520 + public var tagsArray: [String] {
521 + guard let rawTags = tags_raw, !rawTags.isEmpty else { return [] }
522 + return rawTags.components(separatedBy: ",").filter { !$0.isEmpty }
523 + }
524 +
525 + // Computed property to get product info as dictionary
526 + public var productInfo: [String: Any]? {
527 + guard let rawString = product_raw,
528 + !rawString.isEmpty,
529 + let data = rawString.data(using: .utf8),
530 + let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
531 + return nil
532 + }
533 + return dict
534 + }
535 +
536 + // Computed property to check if merchant has progress completed
537 + public var hasProgressCompleted: Bool {
538 + return merchantMetadata?["progressCompleted"] as? Bool ?? false
539 + }
540 +
541 + // Computed property to get main image URL (first in img array or fallback to img_preview)
542 + public var mainImageURL: String {
543 + if let imgArray = img, !imgArray.isEmpty {
544 + return imgArray.first ?? _img_preview
545 + }
546 + return _img_preview
547 + }
548 +
549 + // Computed property to get pin image URL (second in img array if available)
550 + public var pinImageURL: String? {
551 + if let imgArray = img, imgArray.count > 1 {
552 + return imgArray[1]
553 + }
554 + return nil
555 + }
314 } 556 }
......
...@@ -256,6 +256,12 @@ import UIKit ...@@ -256,6 +256,12 @@ import UIKit
256 256
257 if belongsToCategory { 257 if belongsToCategory {
258 processedCouponSets.insert(couponSet._uuid) 258 processedCouponSets.insert(couponSet._uuid)
259 +
260 + // BIND MERCHANT DATA: Find and bind the merchant to this coupon set
261 + if let merchant = categoryMerchants.first(where: { $0._uuid == couponSet._merchant_uuid }) {
262 + couponSet._merchant = merchant
263 + print(" 🔗 Bound merchant '\(merchant._name)' to coupon set '\(couponSet._name)'")
264 + }
259 } 265 }
260 266
261 return belongsToCategory 267 return belongsToCategory
...@@ -285,6 +291,14 @@ import UIKit ...@@ -285,6 +291,14 @@ import UIKit
285 if !unmatchedCouponSets.isEmpty { 291 if !unmatchedCouponSets.isEmpty {
286 print(" ⚠️ Found \(unmatchedCouponSets.count) unmatched coupon sets - adding to 'Άλλες Προσφορές' section") 292 print(" ⚠️ Found \(unmatchedCouponSets.count) unmatched coupon sets - adding to 'Άλλες Προσφορές' section")
287 293
294 + // BIND MERCHANT DATA for unmatched coupon sets too
295 + for couponSet in unmatchedCouponSets {
296 + if let merchant = merchants.first(where: { $0._uuid == couponSet._merchant_uuid }) {
297 + couponSet._merchant = merchant
298 + print(" 🔗 Bound merchant '\(merchant._name)' to unmatched coupon set '\(couponSet._name)'")
299 + }
300 + }
301 +
288 let unmatchedSection = SectionModel( 302 let unmatchedSection = SectionModel(
289 sectionType: .myRewardsHorizontalCouponsets, 303 sectionType: .myRewardsHorizontalCouponsets,
290 title: "Άλλες Προσφορές", // "Other Offers" 304 title: "Άλλες Προσφορές", // "Other Offers"
......