Manos Chorianopoulos

add ProfileViewController dynamic coupons data

1 +{
2 + "images" : [
3 + {
4 + "filename" : "barcode_2.png",
5 + "idiom" : "universal",
6 + "scale" : "1x"
7 + },
8 + {
9 + "filename" : "barcode_2 1.png",
10 + "idiom" : "universal",
11 + "scale" : "2x"
12 + },
13 + {
14 + "filename" : "barcode_2 2.png",
15 + "idiom" : "universal",
16 + "scale" : "3x"
17 + }
18 + ],
19 + "info" : {
20 + "author" : "xcode",
21 + "version" : 1
22 + }
23 +}
...@@ -32,6 +32,40 @@ public class ProfileCouponTableViewCell: UITableViewCell { ...@@ -32,6 +32,40 @@ public class ProfileCouponTableViewCell: UITableViewCell {
32 32
33 } 33 }
34 34
35 + // MARK: - Image Loading Helpers
36 +
37 + private var bannerImageURL: String? {
38 + didSet {
39 + if let url = bannerImageURL, !url.isEmpty {
40 + self.bannerImage.image = UIImage()
41 + UIImage.loadImageUsingCacheWithUrlString(url) { [weak self] image in
42 + if url == self?.bannerImageURL {
43 + self?.bannerImage.image = image
44 + }
45 + }
46 + } else {
47 + self.bannerImage.image = nil
48 + }
49 + }
50 + }
51 +
52 + private var logoImageURL: String? {
53 + didSet {
54 + if let url = logoImageURL, !url.isEmpty {
55 + self.logoImage.image = UIImage()
56 + UIImage.loadImageUsingCacheWithUrlString(url) { [weak self] image in
57 + if url == self?.logoImageURL {
58 + self?.logoImage.image = image
59 + }
60 + }
61 + } else {
62 + self.logoImage.image = nil
63 + }
64 + }
65 + }
66 +
67 + // MARK: - Configure with OfferModel (legacy, kept for backward compatibility)
68 +
35 func configureCell(data: OfferModel) { 69 func configureCell(data: OfferModel) {
36 bannerImage.image = UIImage(named: data.bannerImage, in: Bundle.frameworkResourceBundle, compatibleWith: nil) 70 bannerImage.image = UIImage(named: data.bannerImage, in: Bundle.frameworkResourceBundle, compatibleWith: nil)
37 favoriteImage.image = UIImage(named: data.isFavorite ? "favorite_filled" : "favorite_empty", in: Bundle.frameworkResourceBundle, compatibleWith: nil) 71 favoriteImage.image = UIImage(named: data.isFavorite ? "favorite_filled" : "favorite_empty", in: Bundle.frameworkResourceBundle, compatibleWith: nil)
...@@ -64,6 +98,68 @@ public class ProfileCouponTableViewCell: UITableViewCell { ...@@ -64,6 +98,68 @@ public class ProfileCouponTableViewCell: UITableViewCell {
64 98
65 logoImage.image = UIImage(named: data.merchantLogo, in: Bundle.frameworkResourceBundle, compatibleWith: nil) 99 logoImage.image = UIImage(named: data.merchantLogo, in: Bundle.frameworkResourceBundle, compatibleWith: nil)
66 } 100 }
101 +
102 + // MARK: - Configure with CouponItemModel (dynamic data)
103 +
104 + func configureCell(data: CouponItemModel) {
105 + // Banner image — load from couponset_data img_preview (remote URL)
106 + if let imgPreview = data.couponset_data?._img_preview, !imgPreview.isEmpty {
107 + self.bannerImageURL = imgPreview
108 + } else {
109 + bannerImage.image = nil
110 + }
111 +
112 + // Favorite — default to not favorite for now
113 + favoriteImage.image = UIImage(named: "favorite_empty", in: Bundle.frameworkResourceBundle, compatibleWith: nil)
114 +
115 + // Discount label — use coupon discount or couponset discount
116 + let discountText = data.discount ?? data.couponset_data?._discount ?? ""
117 + discountLabel.text = discountText
118 + discountLabel.font = UIFont(name: "PingLCG-Bold", size: 25)
119 + discountLabel.textColor = UIColor(rgb: 0xF2F2F2)
120 +
121 + // Discount view color based on discount type
122 + let discountType = data.couponset_data?._discount_type ?? ""
123 + let discountColor: UInt = {
124 + switch discountType {
125 + case "percentage":
126 + return 0xFF6B35
127 + case "value":
128 + return 0x28A745
129 + case "plus_one":
130 + return 0x007AFF
131 + default:
132 + return 0xEE417D
133 + }
134 + }()
135 + discountView.backgroundColor = UIColor(rgb: discountColor)
136 +
137 + // Title — from couponset_data name
138 + titleLabel.text = data.couponset_data?._name ?? ""
139 + titleLabel.font = UIFont(name: "PingLCG-Bold", size: 22)
140 + titleLabel.textColor = UIColor(rgb: 0x000F1E)
141 +
142 + // Subtitle — from couponset_data short_description
143 + subtitleLabel.text = data.couponset_data?._short_description ?? ""
144 + subtitleLabel.font = UIFont(name: "PingLCG-Regular", size: 16)
145 + subtitleLabel.textColor = UIColor(rgb: 0x00111B)
146 +
147 + // Expiration — already formatted as "dd/MM/yyyy" by CouponItemModel
148 + if let expiration = data.expiration, !expiration.isEmpty {
149 + expirationLabel.text = "έως " + expiration
150 + } else {
151 + expirationLabel.text = ""
152 + }
153 + expirationLabel.font = UIFont(name: "PingLCG-Regular", size: 13)
154 + expirationLabel.textColor = UIColor(rgb: 0x00111B)
155 +
156 + // Logo — load from merchant_details img_preview (remote URL)
157 + if let merchantImgPreview = data.merchant_details?._img_preview, !merchantImgPreview.isEmpty {
158 + self.logoImageURL = merchantImgPreview
159 + } else {
160 + logoImage.image = nil
161 + }
162 + }
67 163
68 public override func setSelected(_ selected: Bool, animated: Bool) { 164 public override func setSelected(_ selected: Bool, animated: Bool) {
69 super.setSelected(selected, animated: animated) 165 super.setSelected(selected, animated: animated)
......
...@@ -81,6 +81,32 @@ public class CouponSetItemModel { ...@@ -81,6 +81,32 @@ public class CouponSetItemModel {
81 // Bound merchant data for performance 81 // Bound merchant data for performance
82 private var merchant: MerchantModel? 82 private var merchant: MerchantModel?
83 83
84 + // MARK: - Multi-Format Date Parsing Helper
85 +
86 + /// Supported date input formats for parsing dates from various API responses
87 + private static let supportedDateFormats = [
88 + "yyyy-MM-dd HH:mm:ss", // "2027-01-01 03:21:00"
89 + "yyyy-MM-dd'T'HH:mm:ss", // "2027-01-01T15:00:00" (ISO 8601)
90 + "yyyy-MM-dd HH:mm", // "2026-06-30 11:59"
91 + "yyyy-MM-dd'T'HH:mm:ssZZZZZ", // "2027-01-01T15:00:00+03:00" (ISO 8601 with timezone)
92 + "yyyy-MM-dd HH:mm:ss.SSSSSS", // "2022-08-04T14:06:31.110522"
93 + "yyyy-MM-dd'T'HH:mm:ss.SSSSSS" // "2022-08-04T14:06:31.110522" (ISO 8601 with microseconds)
94 + ]
95 +
96 + /// Try multiple date formats and return the first successful parse
97 + /// - Parameter dateString: The date string to parse
98 + /// - Returns: Parsed Date or nil if no format matched
99 + private static func parseDate(_ dateString: String) -> Date? {
100 + let formatter = DateFormatter()
101 + for format in supportedDateFormats {
102 + formatter.dateFormat = format
103 + if let date = formatter.date(from: dateString) {
104 + return date
105 + }
106 + }
107 + return nil
108 + }
109 +
84 public init(dictionary: [String: Any]) { 110 public init(dictionary: [String: Any]) {
85 // Existing fields 111 // Existing fields
86 self.uuid = dictionary["uuid"] as? String? ?? "" 112 self.uuid = dictionary["uuid"] as? String? ?? ""
...@@ -253,18 +279,17 @@ public class CouponSetItemModel { ...@@ -253,18 +279,17 @@ public class CouponSetItemModel {
253 return "" 279 return ""
254 } 280 }
255 281
256 - let dateFormatter = DateFormatter() 282 + if let date = CouponSetItemModel.parseDate(expiration) {
257 - dateFormatter.dateFormat = "yyyy-MM-dd HH:mm" 283 + let outputFormatter = DateFormatter()
258 - 284 + outputFormatter.dateFormat = "dd/MM/yyyy"
259 - if let date = dateFormatter.date(from: expiration) { 285 + return outputFormatter.string(from: date)
260 - dateFormatter.dateFormat = "dd/MM/yyyy"
261 - return dateFormatter.string(from: date)
262 } 286 }
263 287
264 return "" 288 return ""
265 } 289 }
266 290
267 /// Format expiration date with custom format 291 /// Format expiration date with custom format
292 + /// Supports multiple input formats: "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd HH:mm", etc.
268 /// - Parameter format: DateFormatter format string (e.g., "dd-MM", "dd/MM/yyyy") 293 /// - Parameter format: DateFormatter format string (e.g., "dd-MM", "dd/MM/yyyy")
269 /// - Returns: Formatted date string or empty string if invalid 294 /// - Returns: Formatted date string or empty string if invalid
270 public func formattedExpiration(format: String) -> String { 295 public func formattedExpiration(format: String) -> String {
...@@ -272,10 +297,7 @@ public class CouponSetItemModel { ...@@ -272,10 +297,7 @@ public class CouponSetItemModel {
272 return "" 297 return ""
273 } 298 }
274 299
275 - let inputFormatter = DateFormatter() 300 + if let date = CouponSetItemModel.parseDate(expiration) {
276 - inputFormatter.dateFormat = "yyyy-MM-dd HH:mm"
277 -
278 - if let date = inputFormatter.date(from: expiration) {
279 let outputFormatter = DateFormatter() 301 let outputFormatter = DateFormatter()
280 outputFormatter.dateFormat = format 302 outputFormatter.dateFormat = format
281 return outputFormatter.string(from: date) 303 return outputFormatter.string(from: date)
...@@ -285,6 +307,7 @@ public class CouponSetItemModel { ...@@ -285,6 +307,7 @@ public class CouponSetItemModel {
285 } 307 }
286 308
287 /// Format start date with custom format 309 /// Format start date with custom format
310 + /// Supports multiple input formats: "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss", etc.
288 /// - Parameter format: DateFormatter format string (e.g., "dd/MM/yyyy", "MMM yyyy", "dd-MM") 311 /// - Parameter format: DateFormatter format string (e.g., "dd/MM/yyyy", "MMM yyyy", "dd-MM")
289 /// - Returns: Formatted date string or empty string if invalid 312 /// - Returns: Formatted date string or empty string if invalid
290 public func formattedStartDate(format: String) -> String { 313 public func formattedStartDate(format: String) -> String {
...@@ -292,10 +315,7 @@ public class CouponSetItemModel { ...@@ -292,10 +315,7 @@ public class CouponSetItemModel {
292 return "" 315 return ""
293 } 316 }
294 317
295 - let inputFormatter = DateFormatter() 318 + if let date = CouponSetItemModel.parseDate(startDate) {
296 - inputFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
297 -
298 - if let date = inputFormatter.date(from: startDate) {
299 let outputFormatter = DateFormatter() 319 let outputFormatter = DateFormatter()
300 outputFormatter.dateFormat = format 320 outputFormatter.dateFormat = format
301 return outputFormatter.string(from: date) 321 return outputFormatter.string(from: date)
...@@ -305,6 +325,7 @@ public class CouponSetItemModel { ...@@ -305,6 +325,7 @@ public class CouponSetItemModel {
305 } 325 }
306 326
307 /// Format end date with custom format 327 /// Format end date with custom format
328 + /// Supports multiple input formats: "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss", etc.
308 /// - Parameter format: DateFormatter format string (e.g., "dd/MM/yyyy", "MMM yyyy", "dd-MM") 329 /// - Parameter format: DateFormatter format string (e.g., "dd/MM/yyyy", "MMM yyyy", "dd-MM")
309 /// - Returns: Formatted date string or empty string if invalid 330 /// - Returns: Formatted date string or empty string if invalid
310 public func formattedEndDate(format: String) -> String { 331 public func formattedEndDate(format: String) -> String {
...@@ -312,10 +333,7 @@ public class CouponSetItemModel { ...@@ -312,10 +333,7 @@ public class CouponSetItemModel {
312 return "" 333 return ""
313 } 334 }
314 335
315 - let inputFormatter = DateFormatter() 336 + if let date = CouponSetItemModel.parseDate(endDate) {
316 - inputFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
317 -
318 - if let date = inputFormatter.date(from: endDate) {
319 let outputFormatter = DateFormatter() 337 let outputFormatter = DateFormatter()
320 outputFormatter.dateFormat = format 338 outputFormatter.dateFormat = format
321 return outputFormatter.string(from: date) 339 return outputFormatter.string(from: date)
...@@ -488,14 +506,36 @@ public class CouponItemModel { ...@@ -488,14 +506,36 @@ public class CouponItemModel {
488 self.expiration = "" 506 self.expiration = ""
489 } 507 }
490 508
491 - let createdString = dictionary["created"] as? String? ?? "" 509 + // Extract created date: try changes_dates.created first (universal coupons), then top-level created
492 - let dateFormatter2 = DateFormatter() 510 + let createdString: String?
493 - dateFormatter2.dateFormat = "yyyy-MM-dd HH:mm:ss.SSSSSS" 511 + if let changes_dates = dictionary["changes_dates"] as? [String: Any],
494 - if let date = dateFormatter2.date(from: createdString ?? "") { 512 + let changesCreated = changes_dates["created"] as? String, !changesCreated.isEmpty {
495 - dateFormatter2.dateFormat = "dd/MM/yyyy" 513 + createdString = changesCreated
496 - let resultString = dateFormatter2.string(from: date)
497 - self.created = resultString
498 } else { 514 } else {
515 + createdString = dictionary["created"] as? String? ?? ""
516 + }
517 +
518 + let dateFormatter2 = DateFormatter()
519 + // Try multiple date formats for created date
520 + let createdFormats = [
521 + "yyyy-MM-dd HH:mm:ss.SSSSSS", // "2026-03-02 12:01:08.365179"
522 + "yyyy-MM-dd HH:mm:ss", // "2026-03-02 12:01:08"
523 + "yyyy-MM-dd'T'HH:mm:ss.SSSSSS", // ISO 8601 with microseconds
524 + "yyyy-MM-dd'T'HH:mm:ss" // ISO 8601
525 + ]
526 +
527 + var createdParsed = false
528 + for format in createdFormats {
529 + dateFormatter2.dateFormat = format
530 + if let date = dateFormatter2.date(from: createdString ?? "") {
531 + dateFormatter2.dateFormat = "dd/MM/yyyy"
532 + let resultString = dateFormatter2.string(from: date)
533 + self.created = resultString
534 + createdParsed = true
535 + break
536 + }
537 + }
538 + if !createdParsed {
499 self.created = "" 539 self.created = ""
500 } 540 }
501 541
......
...@@ -80,7 +80,7 @@ import UIKit ...@@ -80,7 +80,7 @@ import UIKit
80 80
81 @IBOutlet weak var websiteButton: UIButton! 81 @IBOutlet weak var websiteButton: UIButton!
82 82
83 - var coupon: OfferModel? 83 + var coupon: CouponItemModel?
84 private var isDetailsExpanded = false 84 private var isDetailsExpanded = false
85 private var isCouponCodeExpanded = false 85 private var isCouponCodeExpanded = false
86 private var isCouponQRExpanded = false 86 private var isCouponQRExpanded = false
...@@ -104,7 +104,7 @@ import UIKit ...@@ -104,7 +104,7 @@ import UIKit
104 couponCodeArrowImage.image = UIImage(named: "arrow_down", in: Bundle.frameworkResourceBundle, compatibleWith: nil) 104 couponCodeArrowImage.image = UIImage(named: "arrow_down", in: Bundle.frameworkResourceBundle, compatibleWith: nil)
105 copyButtonImage.image = UIImage(named: "copy", in: Bundle.frameworkResourceBundle, compatibleWith: nil) 105 copyButtonImage.image = UIImage(named: "copy", in: Bundle.frameworkResourceBundle, compatibleWith: nil)
106 couponQRArrowImage.image = UIImage(named: "arrow_down", in: Bundle.frameworkResourceBundle, compatibleWith: nil) 106 couponQRArrowImage.image = UIImage(named: "arrow_down", in: Bundle.frameworkResourceBundle, compatibleWith: nil)
107 - couponQRImage.image = UIImage(named: "barcode", in: Bundle.frameworkResourceBundle, compatibleWith: nil) 107 + couponQRImage.image = UIImage(named: "barcode_2", in: Bundle.frameworkResourceBundle, compatibleWith: nil)
108 termsButtonArrowImage.image = UIImage(named: "arrow_down", in: Bundle.frameworkResourceBundle, compatibleWith: nil) 108 termsButtonArrowImage.image = UIImage(named: "arrow_down", in: Bundle.frameworkResourceBundle, compatibleWith: nil)
109 109
110 infoLabel.font = UIFont(name: "PingLCG-Regular", size: 13) 110 infoLabel.font = UIFont(name: "PingLCG-Regular", size: 13)
...@@ -147,54 +147,101 @@ import UIKit ...@@ -147,54 +147,101 @@ import UIKit
147 websiteButton.layer.borderColor = UIColor(rgb: 0x000F1E).cgColor 147 websiteButton.layer.borderColor = UIColor(rgb: 0x000F1E).cgColor
148 websiteButton.layer.cornerRadius = 4.0 148 websiteButton.layer.cornerRadius = 4.0
149 149
150 - // Configure the view with offer data 150 + // Configure the view with coupon data
151 - if let offer = coupon { 151 + if let couponData = coupon {
152 - setupUI(with: offer) 152 + setupUI(with: couponData)
153 + }
154 + }
155 +
156 + // MARK: - Image Loading Helper
157 +
158 + private func loadRemoteImage(_ urlString: String, into imageView: UIImageView) {
159 + guard !urlString.isEmpty else {
160 + imageView.image = nil
161 + return
162 + }
163 + imageView.image = UIImage()
164 + UIImage.loadImageUsingCacheWithUrlString(urlString) { image in
165 + imageView.image = image
153 } 166 }
154 } 167 }
155 168
156 - private func setupUI(with coupon: OfferModel) { 169 + private func setupUI(with coupon: CouponItemModel) {
157 - couponImage.image = UIImage(named: coupon.bannerImage, in: Bundle.frameworkResourceBundle, compatibleWith: nil) 170 + // Banner image — load from couponset_data img_preview (remote URL)
158 - favoriteImage.image = UIImage(named: coupon.isFavorite ? "favorite2_filled" : "favorite2_empty", in: Bundle.frameworkResourceBundle, compatibleWith: nil) 171 + if let imgPreview = coupon.couponset_data?._img_preview, !imgPreview.isEmpty {
172 + loadRemoteImage(imgPreview, into: couponImage)
173 + } else {
174 + couponImage.image = nil
175 + }
159 176
177 + // Favorite — default to not favorite for now
178 + favoriteImage.image = UIImage(named: "favorite2_empty", in: Bundle.frameworkResourceBundle, compatibleWith: nil)
179 +
180 + // Title — from couponset_data name
160 titleLabel.font = UIFont(name: "PingLCG-Bold", size: 24) 181 titleLabel.font = UIFont(name: "PingLCG-Bold", size: 24)
161 titleLabel.textColor = UIColor(rgb: 0xF2709D) 182 titleLabel.textColor = UIColor(rgb: 0xF2709D)
162 - titleLabel.text = coupon.title 183 + titleLabel.text = coupon.couponset_data?._name ?? ""
163 184
185 + // Subtitle — from couponset_data short_description
164 subtitleLabel.font = UIFont(name: "PingLCG-Regular", size: 18) 186 subtitleLabel.font = UIFont(name: "PingLCG-Regular", size: 18)
165 subtitleLabel.textColor = UIColor(rgb: 0x020E1C) 187 subtitleLabel.textColor = UIColor(rgb: 0x020E1C)
166 - subtitleLabel.text = coupon.description 188 + subtitleLabel.text = coupon.couponset_data?._short_description ?? ""
167 189
190 + // Expiration — already formatted as "dd/MM/yyyy" by CouponItemModel
168 expirationLabel.font = UIFont(name: "PingLCG-Regular", size: 14) 191 expirationLabel.font = UIFont(name: "PingLCG-Regular", size: 14)
169 expirationLabel.textColor = UIColor(rgb: 0x020E1C) 192 expirationLabel.textColor = UIColor(rgb: 0x020E1C)
170 -// expirationLabel.text = ("Η προσφορά ισχύει " + coupon.expirationDate) 193 + if let expiration = coupon.expiration, !expiration.isEmpty {
171 - expirationLabel.text = "Η προσφορά ισχύει έως 30-09-2025" 194 + expirationLabel.text = "Η προσφορά ισχύει έως " + expiration
195 + } else {
196 + expirationLabel.text = ""
197 + }
172 198
173 - setupExpandableDetails() 199 + // Description — from couponset_data description
200 + setupExpandableDetails(with: coupon)
174 201
202 + // Coupon Code section
175 couponCodeTitleLabel.font = UIFont(name: "PingLCG-Regular", size: 16) 203 couponCodeTitleLabel.font = UIFont(name: "PingLCG-Regular", size: 16)
176 couponCodeTitleLabel.textColor = UIColor(rgb: 0x000F1E) 204 couponCodeTitleLabel.textColor = UIColor(rgb: 0x000F1E)
177 couponCodeTitleLabel.text = "Κωδικός Κουπονιού" 205 couponCodeTitleLabel.text = "Κωδικός Κουπονιού"
178 206
179 couponCodeValueLabel.font = UIFont(name: "PingLCG-Bold", size: 24) 207 couponCodeValueLabel.font = UIFont(name: "PingLCG-Bold", size: 24)
180 couponCodeValueLabel.textColor = UIColor(rgb: 0x000F1E) 208 couponCodeValueLabel.textColor = UIColor(rgb: 0x000F1E)
181 - couponCodeValueLabel.text = "coupons_ab" 209 + couponCodeValueLabel.text = coupon.coupon ?? ""
182 copyButton.addTarget(self, action: #selector(copyButtonTapped), for: .touchUpInside) 210 copyButton.addTarget(self, action: #selector(copyButtonTapped), for: .touchUpInside)
183 211
212 + // QR Code section
184 couponQRTitleLabel.font = UIFont(name: "PingLCG-Regular", size: 16) 213 couponQRTitleLabel.font = UIFont(name: "PingLCG-Regular", size: 16)
185 couponQRTitleLabel.textColor = UIColor(rgb: 0x000F1E) 214 couponQRTitleLabel.textColor = UIColor(rgb: 0x000F1E)
186 couponQRTitleLabel.text = "QR Κουπονιού" 215 couponQRTitleLabel.text = "QR Κουπονιού"
187 216
217 + // Terms — from couponset_data terms
188 termsLabel.font = UIFont(name: "PingLCG-Regular", size: 16) 218 termsLabel.font = UIFont(name: "PingLCG-Regular", size: 16)
189 termsLabel.textColor = UIColor(rgb: 0x020E1C) 219 termsLabel.textColor = UIColor(rgb: 0x020E1C)
190 - termsLabel.text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas sed ex euismod, feugiat justo eu, faucibus urna. Nulla sodales euismod arcu volutpat finibus. Etiam id urna at justo facilisis tempor. Morbi dignissim erat vitae magna sodales dignissim ac in mauris. Mauris tempor convallis tortor, interdum hendrerit turpis eleifend at. Praesent." 220 + let termsText = coupon.couponset_data?._terms ?? ""
221 + termsLabel.text = termsText.isEmpty ? "Δεν υπάρχουν διαθέσιμοι όροι χρήσης." : termsText
191 } 222 }
192 223
193 private var fullDetailsText = "" 224 private var fullDetailsText = ""
194 private var shouldTruncaitDetails = false 225 private var shouldTruncaitDetails = false
195 226
196 - private func setupExpandableDetails() { 227 + private func setupExpandableDetails(with coupon: CouponItemModel? = nil) {
197 - fullDetailsText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas sed ex euismod, feugiat justo eu, faucibus urna. Nulla sodales euismod arcu volutpat finibus. Etiam id urna at justo facilisis tempor. Morbi dignissim erat vitae magna sodales dignissim ac in mauris. Mauris tempor convallis tortor, interdum hendrerit turpis eleifend at. Praesent." 228 + // Use couponset_data description if available, otherwise merchant body, or fallback
229 + if let couponData = coupon {
230 + let description = couponData.couponset_data?._description ?? ""
231 + // let merchantBody = couponData.merchant_details?._body ?? ""
232 +
233 + if !description.isEmpty {
234 + fullDetailsText = description
235 + }
236 + // else if !merchantBody.isEmpty {
237 + // fullDetailsText = merchantBody
238 + // }
239 + else {
240 + fullDetailsText = ""
241 + }
242 + } else {
243 + fullDetailsText = ""
244 + }
198 245
199 detailsLabel.font = UIFont(name: "PingLCG-Regular", size: 18) 246 detailsLabel.font = UIFont(name: "PingLCG-Regular", size: 18)
200 detailsLabel.textColor = UIColor(rgb: 0x020E1C) 247 detailsLabel.textColor = UIColor(rgb: 0x020E1C)
......
...@@ -23,7 +23,15 @@ import UIKit ...@@ -23,7 +23,15 @@ import UIKit
23 super.init(coder: coder) 23 super.init(coder: coder)
24 } 24 }
25 25
26 - // MARK: - Dummy Data 26 + // MARK: - Dynamic Data
27 + var allCoupons: [CouponItemModel] = []
28 +
29 + // MARK: - Loading State
30 + private var isLoading: Bool = true
31 + private var loadingIndicator: UIActivityIndicatorView?
32 +
33 + // MARK: - Dummy Data (commented out - replaced by dynamic allCoupons)
34 + /*
27 let allOffers: [OfferModel] = [ 35 let allOffers: [OfferModel] = [
28 // Προτάσεις για εσένα 36 // Προτάσεις για εσένα
29 OfferModel( 37 OfferModel(
...@@ -184,6 +192,7 @@ import UIKit ...@@ -184,6 +192,7 @@ import UIKit
184 redeemed: true 192 redeemed: true
185 ) 193 )
186 ] 194 ]
195 + */
187 196
188 let couponFilters: [CouponFilterModel] = [ 197 let couponFilters: [CouponFilterModel] = [
189 CouponFilterModel(title: "Ενεργά"), 198 CouponFilterModel(title: "Ενεργά"),
...@@ -217,7 +226,11 @@ import UIKit ...@@ -217,7 +226,11 @@ import UIKit
217 tableView.estimatedRowHeight = 200 226 tableView.estimatedRowHeight = 200
218 tableView.rowHeight = UITableView.automaticDimension 227 tableView.rowHeight = UITableView.automaticDimension
219 228
220 - initializeSections() 229 + // Set up loading indicator
230 + setupLoadingIndicator()
231 +
232 + // Fetch coupons from API
233 + fetchCoupons()
221 } 234 }
222 235
223 // NEW: Safe XIB registration method 236 // NEW: Safe XIB registration method
...@@ -240,53 +253,123 @@ import UIKit ...@@ -240,53 +253,123 @@ import UIKit
240 } 253 }
241 254
242 } 255 }
256 +
257 + // MARK: - Loading Indicator
258 +
259 + private func setupLoadingIndicator() {
260 + let indicator = UIActivityIndicatorView(style: .large)
261 + indicator.color = UIColor(rgb: 0x000F1E)
262 + indicator.translatesAutoresizingMaskIntoConstraints = false
263 + indicator.hidesWhenStopped = true
264 + view.addSubview(indicator)
265 +
266 + NSLayoutConstraint.activate([
267 + indicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
268 + indicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
269 + ])
270 +
271 + self.loadingIndicator = indicator
272 + }
273 +
274 + private func showLoading() {
275 + isLoading = true
276 + loadingIndicator?.startAnimating()
277 + tableView.alpha = 0.3
278 + tableView.isUserInteractionEnabled = false
279 + }
280 +
281 + private func hideLoading() {
282 + isLoading = false
283 + loadingIndicator?.stopAnimating()
284 + tableView.alpha = 1.0
285 + tableView.isUserInteractionEnabled = true
286 + }
287 +
288 + // MARK: - Data Fetching
289 +
290 + private func fetchCoupons() {
291 + showLoading()
292 +
293 + // Initialize sections with empty data first (so table view can render skeleton)
294 + initializeSections()
295 +
296 + WarplySDK.shared.getCouponsUniversal({ [weak self] couponsData in
297 + guard let self = self else { return }
298 +
299 + if let coupons = couponsData {
300 + self.allCoupons = coupons
301 + print("✅ [ProfileVC] Fetched \(coupons.count) coupons")
302 +
303 + // Debug: Print coupon statuses
304 + let activeCount = coupons.filter { $0.status == 1 }.count
305 + let redeemedCount = coupons.filter { $0.status == 0 }.count
306 + let expiredCount = coupons.filter { $0.status == -1 }.count
307 + print(" Active: \(activeCount), Redeemed: \(redeemedCount), Expired: \(expiredCount)")
308 + } else {
309 + self.allCoupons = []
310 + print("⚠️ [ProfileVC] No coupons received")
311 + }
312 +
313 + self.initializeSections()
314 + self.hideLoading()
315 + }, failureCallback: { [weak self] errorCode in
316 + guard let self = self else { return }
317 +
318 + print("❌ [ProfileVC] Failed to fetch coupons, error: \(errorCode)")
319 + self.allCoupons = []
320 + self.initializeSections()
321 + self.hideLoading()
322 + })
323 + }
243 324
244 - // MARK: Function 325 + // MARK: - Section Initialization
326 +
245 func initializeSections() { 327 func initializeSections() {
246 - // Προτάσεις για εσένα 328 + // Προτάσεις για εσένα — empty for now
247 - let forYouOffers = allOffers.filter { $0.category == "Προτάσεις για εσένα" } 329 + let forYouOffers: [CouponItemModel] = []
248 forYouOffersSection = SectionModel( 330 forYouOffersSection = SectionModel(
249 sectionType: .myRewardsHorizontalCouponsets, 331 sectionType: .myRewardsHorizontalCouponsets,
250 title: "Προτάσεις για εσένα", 332 title: "Προτάσεις για εσένα",
251 items: forYouOffers, 333 items: forYouOffers,
252 - itemType: .offers 334 + itemType: .coupons
253 ) 335 )
254 336
255 - // Active Offers 337 + // Active Coupons — status == 1
256 - let activeOffers = allOffers.filter { $0.active ?? false } 338 + let activeCoupons = allCoupons.filter { $0.status == 1 }
257 activeOffersSection = SectionModel( 339 activeOffersSection = SectionModel(
258 sectionType: .profileCoupon, 340 sectionType: .profileCoupon,
259 title: "Ενεργά", 341 title: "Ενεργά",
260 - items: activeOffers, 342 + items: activeCoupons,
261 - itemType: .offers 343 + itemType: .coupons
262 ) 344 )
263 345
346 + // Set initial filtered section to active
264 filteredOffersSection = activeOffersSection 347 filteredOffersSection = activeOffersSection
265 348
266 - // Favorite Offers 349 + // Favorite Coupons — empty for now
267 - let favoriteOffers = allOffers.filter { $0.isFavorite } 350 + let favoriteCoupons: [CouponItemModel] = []
268 favoriteOffersSection = SectionModel( 351 favoriteOffersSection = SectionModel(
269 sectionType: .profileCoupon, 352 sectionType: .profileCoupon,
270 title: "Αγαπημένα", 353 title: "Αγαπημένα",
271 - items: favoriteOffers, 354 + items: favoriteCoupons,
272 - itemType: .offers 355 + itemType: .coupons
273 ) 356 )
274 357
275 - // Redeemed Offers 358 + // Redeemed Coupons — status == 0
276 - let redeemedOffers = allOffers.filter { $0.redeemed ?? false } 359 + let redeemedCoupons = allCoupons.filter { $0.status == 0 }
277 redeemedOffersSection = SectionModel( 360 redeemedOffersSection = SectionModel(
278 sectionType: .profileCoupon, 361 sectionType: .profileCoupon,
279 title: "Εξαργυρωμένα", 362 title: "Εξαργυρωμένα",
280 - items: redeemedOffers, 363 + items: redeemedCoupons,
281 - itemType: .offers 364 + itemType: .coupons
282 ) 365 )
283 366
284 self.tableView.reloadData() 367 self.tableView.reloadData()
285 } 368 }
286 369
287 - private func openCouponViewController(with offer: OfferModel) { 370 + private func openCouponViewController(with coupon: CouponItemModel) {
288 let vc = SwiftWarplyFramework.CouponViewController(nibName: "CouponViewController", bundle: Bundle.frameworkBundle) 371 let vc = SwiftWarplyFramework.CouponViewController(nibName: "CouponViewController", bundle: Bundle.frameworkBundle)
289 - vc.coupon = offer 372 + vc.coupon = coupon
290 373
291 self.navigationController?.pushViewController(vc, animated: true) 374 self.navigationController?.pushViewController(vc, animated: true)
292 } 375 }
...@@ -328,28 +411,13 @@ extension ProfileViewController: UITableViewDelegate, UITableViewDataSource { ...@@ -328,28 +411,13 @@ extension ProfileViewController: UITableViewDelegate, UITableViewDataSource {
328 411
329 public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { 412 public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
330 return nil 413 return nil
331 -// if (section <= 3) {
332 -// return nil
333 -// } else {
334 -// // Return clear view for spacing
335 -//// let headerView = UIView()
336 -// let headerView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 19))
337 -// headerView.backgroundColor = UIColor.white
338 -// return headerView
339 -// }
340 } 414 }
341 415
342 public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { 416 public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
343 return 0.0 417 return 0.0
344 -// if (section <= 3) {
345 -// return 0.0
346 -// } else {
347 -// return 19.0
348 -// }
349 } 418 }
350 419
351 public func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { 420 public func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
352 -// return CGFloat.leastNormalMagnitude
353 return 0.0 421 return 0.0
354 } 422 }
355 423
...@@ -360,9 +428,6 @@ extension ProfileViewController: UITableViewDelegate, UITableViewDataSource { ...@@ -360,9 +428,6 @@ extension ProfileViewController: UITableViewDelegate, UITableViewDataSource {
360 public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 428 public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
361 if (indexPath.section == 0) { 429 if (indexPath.section == 0) {
362 let cell = tableView.dequeueReusableCell(withIdentifier: "ProfileHeaderTableViewCell", for: indexPath) as! ProfileHeaderTableViewCell 430 let cell = tableView.dequeueReusableCell(withIdentifier: "ProfileHeaderTableViewCell", for: indexPath) as! ProfileHeaderTableViewCell
363 -// cell.delegate = self // Set the banner offers delegate
364 -// cell.configureCell(data: self.bannerOffersSection)
365 -// cell.parent = self
366 return cell 431 return cell
367 432
368 } else if (indexPath.section == 1) { 433 } else if (indexPath.section == 1) {
...@@ -389,8 +454,8 @@ extension ProfileViewController: UITableViewDelegate, UITableViewDataSource { ...@@ -389,8 +454,8 @@ extension ProfileViewController: UITableViewDelegate, UITableViewDataSource {
389 let cell = tableView.dequeueReusableCell(withIdentifier: "ProfileCouponTableViewCell", for: indexPath) as! ProfileCouponTableViewCell 454 let cell = tableView.dequeueReusableCell(withIdentifier: "ProfileCouponTableViewCell", for: indexPath) as! ProfileCouponTableViewCell
390 if let items = self.filteredOffersSection?.items, 455 if let items = self.filteredOffersSection?.items,
391 indexPath.row < items.count, 456 indexPath.row < items.count,
392 - let offer = items[indexPath.row] as? OfferModel { 457 + let coupon = items[indexPath.row] as? CouponItemModel {
393 - cell.configureCell(data: offer) 458 + cell.configureCell(data: coupon)
394 } 459 }
395 return cell 460 return cell
396 } 461 }
...@@ -403,8 +468,8 @@ extension ProfileViewController: UITableViewDelegate, UITableViewDataSource { ...@@ -403,8 +468,8 @@ extension ProfileViewController: UITableViewDelegate, UITableViewDataSource {
403 } else { 468 } else {
404 if let items = self.filteredOffersSection?.items, 469 if let items = self.filteredOffersSection?.items,
405 indexPath.row < items.count, 470 indexPath.row < items.count,
406 - let offer = items[indexPath.row] as? OfferModel { 471 + let coupon = items[indexPath.row] as? CouponItemModel {
407 - openCouponViewController(with: offer) 472 + openCouponViewController(with: coupon)
408 } 473 }
409 } 474 }
410 } 475 }
...@@ -413,8 +478,8 @@ extension ProfileViewController: UITableViewDelegate, UITableViewDataSource { ...@@ -413,8 +478,8 @@ extension ProfileViewController: UITableViewDelegate, UITableViewDataSource {
413 // Add delegate conformance 478 // Add delegate conformance
414 extension ProfileViewController: MyRewardsOffersScrollTableViewCellDelegate { 479 extension ProfileViewController: MyRewardsOffersScrollTableViewCellDelegate {
415 func didSelectOffer(_ offer: OfferModel) { 480 func didSelectOffer(_ offer: OfferModel) {
416 - // Navigate to CouponViewController 481 + // Legacy OfferModel handling — no longer used but kept for protocol conformance
417 - openCouponViewController(with: offer) 482 + print("⚠️ [ProfileVC] didSelectOffer called with legacy OfferModel — should not happen")
418 } 483 }
419 484
420 func didSelectCouponSet(_ couponSet: CouponSetItemModel) { 485 func didSelectCouponSet(_ couponSet: CouponSetItemModel) {
......