Image Functionality & Discounts & Offers
|
After Width: | Height: | Size: 734 KiB |
|
After Width: | Height: | Size: 246 KiB |
|
After Width: | Height: | Size: 211 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20.9995 12C20.9995 12 20.9995 12 20.9995 12C20.9995 11.5568 20.8047 11.162 20.6411 10.8906C20.461 10.5918 20.2225 10.2892 19.9677 10.0008C19.4559 9.4216 18.7926 8.80434 18.1551 8.25084C17.5127 7.69303 16.8731 7.18002 16.396 6.80767C16.1593 6.62291 15.7406 6.30672 15.5962 6.19767L15.5925 6.19487C15.1478 5.86736 14.5218 5.96236 14.1943 6.40706C13.8668 6.85173 13.9618 7.47768 14.4064 7.80521C14.5396 7.90576 14.9359 8.20515 15.1656 8.38437C15.6259 8.74365 16.2364 9.23357 16.8439 9.76105C17.3225 10.1765 17.7853 10.6034 18.1679 11L3.99951 11C3.44723 11 2.99951 11.4477 2.99951 12C2.99951 12.5523 3.44723 13 3.99951 13L18.1679 13C17.7853 13.3966 17.3225 13.8235 16.8439 14.2389C16.2364 14.7664 15.6259 15.2564 15.1656 15.6156C14.9359 15.7948 14.5396 16.0942 14.4064 16.1948C13.9618 16.5223 13.8668 17.1483 14.1943 17.5929C14.5218 18.0376 15.1478 18.1326 15.5925 17.8051L15.5965 17.8021C15.7409 17.6931 16.1593 17.3771 16.396 17.1923C16.8731 16.82 17.5127 16.307 18.1551 15.7492C18.7926 15.1957 19.4559 14.5784 19.9677 13.9992C20.2225 13.7108 20.461 13.4082 20.6411 13.1094C20.8047 12.8381 20.9995 12.4431 20.9995 12Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.7163 5.99998C10.8032 6 10.8902 6.00001 10.9771 6.00001L10.9771 14C10.9771 14.5523 11.4351 15 12.0001 15C12.5651 15 13.0231 14.5523 13.0231 14L13.0231 6.00001C13.11 6.00001 13.1969 6 13.2837 5.99998C13.4694 5.99994 13.655 5.9999 13.8407 6.00004C14.073 6.00021 14.3505 6.00041 14.5774 5.96723C14.8128 5.93278 15.3669 5.81137 15.6344 5.24525C15.9005 4.68223 15.641 4.19357 15.5138 3.99322C15.3933 3.80322 15.2141 3.59781 15.0652 3.42709C15.0546 3.41493 15.0442 3.40295 15.0339 3.39116C14.481 2.75629 13.6973 1.89011 13.1169 1.41106C12.8307 1.17478 12.47 1.01767 12.0681 1.0014C11.6161 0.983094 11.2039 1.14606 10.883 1.41077C10.3023 1.88992 9.51848 2.75645 8.96575 3.39141C8.9555 3.40319 8.94507 3.41515 8.93448 3.4273C8.78565 3.59799 8.60654 3.80342 8.48601 3.99344C8.35889 4.19385 8.0995 4.68246 8.3656 5.24535C8.63317 5.81135 9.18709 5.93277 9.42259 5.96723C9.6494 6.00041 9.92688 6.00021 10.1591 6.00004C10.3449 5.9999 10.5306 5.99994 10.7163 5.99998Z" fill="white"/>
|
||||
<path d="M6.20302 7.97913C6.7438 7.86701 7.0913 7.33772 6.97918 6.79694C6.86705 6.25615 6.33777 5.90866 5.79698 6.02078C4.89779 6.20722 4.12408 6.54004 3.48187 7.15175C2.67092 7.92418 2.31966 8.90199 2.15626 10.0596C1.99994 11.167 1.99997 12.5725 2 14.3041V14.4558C1.99997 16.1873 1.99994 17.5928 2.15626 18.7003C2.31966 19.8579 2.67092 20.8357 3.48187 21.6081C4.28597 22.374 5.29233 22.6998 6.48389 22.8524C7.63674 23 9.1038 23 10.9298 23H13.0702C14.8962 23 16.3633 23 17.5161 22.8524C18.7077 22.6998 19.714 22.374 20.5181 21.6081C21.3291 20.8357 21.6803 19.8579 21.8437 18.7003C22.0001 17.5928 22 16.1873 22 14.4558V14.3041C22 12.5725 22.0001 11.167 21.8437 10.0596C21.6803 8.90199 21.3291 7.92418 20.5181 7.15175C19.8759 6.54004 19.1022 6.20722 18.203 6.02078C17.6622 5.90866 17.133 6.25615 17.0208 6.79694C16.9087 7.33772 17.2562 7.86701 17.797 7.97913C18.4468 8.11386 18.8417 8.31705 19.1387 8.59994C19.4993 8.94343 19.7339 9.42162 19.8634 10.3391C19.9977 11.2905 20 12.5542 20 14.3799C20 16.2056 19.9977 17.4694 19.8634 18.4208C19.7339 19.3383 19.4993 19.8164 19.1387 20.1599C18.7713 20.51 18.249 20.7422 17.2621 20.8686C16.2511 20.998 14.9126 21 13 21H11C9.08742 21 7.74887 20.998 6.73794 20.8686C5.75099 20.7422 5.22875 20.51 4.86128 20.1599C4.50066 19.8164 4.26614 19.3383 4.13663 18.4208C4.00234 17.4694 4 16.2056 4 14.3799C4 12.5542 4.00234 11.2905 4.13663 10.3391C4.26614 9.42162 4.50066 8.94343 4.86128 8.59994C5.15828 8.31705 5.55322 8.11386 6.20302 7.97913Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@ -0,0 +1,3 @@
|
||||
<svg width="21" height="22" viewBox="0 0 21 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 1C0 0.447715 0.447715 0 1 0L1.93845 0C3.31505 0 4.515 0.936891 4.84887 2.27239L4.85349 2.29084L7.31185 14H7.38C7.39152 14 7.40299 14.0002 7.41441 14.0006L13.7632 14.0006C14.7838 14.0006 15.515 13.8399 16.0562 13.5888C16.5861 13.3429 16.9837 12.9875 17.3096 12.5118C18.0068 11.4943 18.3556 9.98374 18.7807 7.8723C18.9056 7.25167 18.9778 6.88274 18.9961 6.61741C19.002 6.53142 18.983 6.52996 18.9117 6.52452L18.884 6.52225C18.7781 6.51262 18.6477 6.5068 18.4816 6.50374C17.9294 6.49357 17.49 6.03769 17.5002 5.4855C17.5103 4.93331 17.9662 4.49391 18.5184 4.50408C18.8759 4.51066 19.258 4.52934 19.6007 4.61974C19.9829 4.72056 20.3604 4.92167 20.641 5.30777C20.9858 5.78217 21.0219 6.31071 20.9914 6.75472C20.963 7.16653 20.8624 7.66613 20.753 8.20908L20.7414 8.26701C20.3387 10.2673 19.9324 12.2223 18.9595 13.6422C18.4504 14.3853 17.7842 14.9917 16.8981 15.4029C16.0234 15.8088 14.9865 16.0006 13.7632 16.0006L6.92903 16.0006C6.34562 16.0177 5.81269 16.4121 5.59895 17L16 17L15.9984 17.0026C15.9982 17.003 15.9979 17.0035 15.9976 17.0039C15.9984 17.0039 15.9992 17.0039 16 17.0039C17.2426 17.0039 18.25 18.0113 18.25 19.2539C18.25 20.4965 17.2426 21.5039 16 21.5039C14.7574 21.5039 13.75 20.4965 13.75 19.2539C13.75 19.1681 13.7548 19.0833 13.7642 19H13.764H11.2358C11.2452 19.0833 11.25 19.1681 11.25 19.2539C11.25 20.4965 10.2426 21.5039 9 21.5039C7.75736 21.5039 6.75 20.4965 6.75 19.2539C6.75 19.1681 6.75481 19.0833 6.76417 19H4.91143C4.09384 19 3.5 18.3215 3.5 17.5714C3.5 16.2195 4.24057 15.0121 5.35392 14.4081L2.90464 2.74218C2.78811 2.30517 2.39205 2 1.93845 2L1 2C0.447715 2 0 1.55228 0 1ZM12.5 1.5C12.5 0.947715 12.0523 0.5 11.5 0.5C10.9477 0.5 10.5 0.947715 10.5 1.5L10.5 4.5L7.5 4.5C6.94772 4.5 6.5 4.94772 6.5 5.5C6.5 6.05228 6.94772 6.5 7.5 6.5L10.5 6.5V9.5C10.5 10.0523 10.9477 10.5 11.5 10.5C12.0523 10.5 12.5 10.0523 12.5 9.5V6.5L15.5 6.5C16.0523 6.5 16.5 6.05228 16.5 5.5C16.5 4.94772 16.0523 4.5 15.5 4.5L12.5 4.5L12.5 1.5ZM8.25 19.2539C8.25 18.8397 8.58579 18.5039 9 18.5039C9.41421 18.5039 9.75 18.8397 9.75 19.2539C9.75 19.6681 9.41421 20.0039 9 20.0039C8.58579 20.0039 8.25 19.6681 8.25 19.2539ZM16 18.5039C15.5858 18.5039 15.25 18.8397 15.25 19.2539C15.25 19.6681 15.5858 20.0039 16 20.0039C16.4142 20.0039 16.75 19.6681 16.75 19.2539C16.75 18.8397 16.4142 18.5039 16 18.5039Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@ -0,0 +1,52 @@
|
||||
class OffersAndDiscountsResponseModel {
|
||||
int? id;
|
||||
String? title;
|
||||
String? description;
|
||||
String? imageUrl;
|
||||
String? facilityType; // HMC, HMG, or Both
|
||||
String? startDate;
|
||||
String? endDate;
|
||||
String? discount;
|
||||
bool? isActive;
|
||||
|
||||
OffersAndDiscountsResponseModel({
|
||||
this.id,
|
||||
this.title,
|
||||
this.description,
|
||||
this.imageUrl,
|
||||
this.facilityType,
|
||||
this.startDate,
|
||||
this.endDate,
|
||||
this.discount,
|
||||
this.isActive,
|
||||
});
|
||||
|
||||
factory OffersAndDiscountsResponseModel.fromJson(Map<String, dynamic> json) {
|
||||
return OffersAndDiscountsResponseModel(
|
||||
id: json['ID'] as int?,
|
||||
title: json['Title'] as String?,
|
||||
description: json['Description'] as String?,
|
||||
imageUrl: json['ImageURL'] as String?,
|
||||
facilityType: json['FacilityType'] as String?,
|
||||
startDate: json['StartDate'] as String?,
|
||||
endDate: json['EndDate'] as String?,
|
||||
discount: json['Discount'] as String?,
|
||||
isActive: json['IsActive'] as bool?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'ID': id,
|
||||
'Title': title,
|
||||
'Description': description,
|
||||
'ImageURL': imageUrl,
|
||||
'FacilityType': facilityType,
|
||||
'StartDate': startDate,
|
||||
'EndDate': endDate,
|
||||
'Discount': discount,
|
||||
'IsActive': isActive,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,102 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:hmg_patient_app_new/core/api/api_client.dart';
|
||||
import 'package:hmg_patient_app_new/core/api_consts.dart';
|
||||
import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart';
|
||||
import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart';
|
||||
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offers_and_discounts_response_model.dart';
|
||||
import 'package:hmg_patient_app_new/services/logger_service.dart';
|
||||
|
||||
abstract class OffersAndDiscountsRepo {
|
||||
Future<Either<Failure, GenericApiModel<List<OffersAndDiscountsResponseModel>>>> getOffersAndDiscounts();
|
||||
Future<Either<Failure, GenericApiModel<List<OffersAndDiscountsResponseModel>>>> getOffersAndDiscountsHistory();
|
||||
}
|
||||
|
||||
class OffersAndDiscountsRepoImp implements OffersAndDiscountsRepo {
|
||||
final ApiClient apiClient;
|
||||
final LoggerService loggerService;
|
||||
|
||||
OffersAndDiscountsRepoImp({required this.loggerService, required this.apiClient});
|
||||
|
||||
@override
|
||||
Future<Either<Failure, GenericApiModel<List<OffersAndDiscountsResponseModel>>>> getOffersAndDiscounts() async {
|
||||
Map<String, dynamic> mapDevice = {};
|
||||
|
||||
try {
|
||||
GenericApiModel<List<OffersAndDiscountsResponseModel>>? apiResponse;
|
||||
Failure? failure;
|
||||
await apiClient.post(
|
||||
GET_OFFERS_AND_DISCOUNTS, // TODO: Replace with actual API endpoint
|
||||
body: mapDevice,
|
||||
onFailure: (error, statusCode, {messageStatus, failureType}) {
|
||||
failure = failureType;
|
||||
},
|
||||
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
|
||||
try {
|
||||
final list = response['List_OffersAndDiscounts'];
|
||||
if (list == null || list.isEmpty) {
|
||||
throw Exception("offers and discounts list is empty");
|
||||
}
|
||||
|
||||
final offers = list.map((item) => OffersAndDiscountsResponseModel.fromJson(item as Map<String, dynamic>)).toList().cast<OffersAndDiscountsResponseModel>();
|
||||
|
||||
apiResponse = GenericApiModel<List<OffersAndDiscountsResponseModel>>(
|
||||
messageStatus: messageStatus,
|
||||
statusCode: statusCode,
|
||||
errorMessage: null,
|
||||
data: offers,
|
||||
);
|
||||
} catch (e) {
|
||||
failure = DataParsingFailure(e.toString());
|
||||
}
|
||||
},
|
||||
);
|
||||
if (failure != null) return Left(failure!);
|
||||
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
|
||||
return Right(apiResponse!);
|
||||
} catch (e) {
|
||||
return Left(UnknownFailure(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, GenericApiModel<List<OffersAndDiscountsResponseModel>>>> getOffersAndDiscountsHistory() async {
|
||||
Map<String, dynamic> mapDevice = {};
|
||||
|
||||
try {
|
||||
GenericApiModel<List<OffersAndDiscountsResponseModel>>? apiResponse;
|
||||
Failure? failure;
|
||||
await apiClient.post(
|
||||
GET_OFFERS_AND_DISCOUNTS_HISTORY, // TODO: Replace with actual API endpoint
|
||||
body: mapDevice,
|
||||
onFailure: (error, statusCode, {messageStatus, failureType}) {
|
||||
failure = failureType;
|
||||
},
|
||||
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
|
||||
try {
|
||||
final list = response['List_OffersAndDiscountsHistory'];
|
||||
if (list == null || list.isEmpty) {
|
||||
throw Exception("offers and discounts history list is empty");
|
||||
}
|
||||
|
||||
final historyOffers = list.map((item) => OffersAndDiscountsResponseModel.fromJson(item as Map<String, dynamic>)).toList().cast<OffersAndDiscountsResponseModel>();
|
||||
|
||||
apiResponse = GenericApiModel<List<OffersAndDiscountsResponseModel>>(
|
||||
messageStatus: messageStatus,
|
||||
statusCode: statusCode,
|
||||
errorMessage: null,
|
||||
data: historyOffers,
|
||||
);
|
||||
} catch (e) {
|
||||
failure = DataParsingFailure(e.toString());
|
||||
}
|
||||
},
|
||||
);
|
||||
if (failure != null) return Left(failure!);
|
||||
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
|
||||
return Right(apiResponse!);
|
||||
} catch (e) {
|
||||
return Left(UnknownFailure(e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,449 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offers_and_discounts_response_model.dart';
|
||||
import 'package:hmg_patient_app_new/features/offers_and_discounts/offers_and_discounts_repo.dart';
|
||||
import 'package:hmg_patient_app_new/services/error_handler_service.dart';
|
||||
|
||||
class OffersAndDiscountsViewModel extends ChangeNotifier {
|
||||
bool isOffersLoading = false;
|
||||
bool isHistoryLoading = false;
|
||||
List<String> selectedFacilities = ['All Offers']; // Default to 'All Offers'
|
||||
String searchQuery = '';
|
||||
|
||||
OffersAndDiscountsRepo offersAndDiscountsRepo;
|
||||
ErrorHandlerService errorHandlerService;
|
||||
|
||||
List<OffersAndDiscountsResponseModel> offersList = [];
|
||||
List<OffersAndDiscountsResponseModel> historyList = [];
|
||||
|
||||
OffersAndDiscountsViewModel({
|
||||
required this.offersAndDiscountsRepo,
|
||||
required this.errorHandlerService,
|
||||
}) {
|
||||
_initializeDummyData();
|
||||
}
|
||||
|
||||
// Initialize with dummy data for testing
|
||||
void _initializeDummyData() {
|
||||
offersList = [
|
||||
OffersAndDiscountsResponseModel(
|
||||
id: 1,
|
||||
title: 'Summer Health Checkup Package',
|
||||
description: 'Complete health screening with blood tests, X-ray, and consultation. Get 30% off on all diagnostic services.',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1576091160399-112ba8d25d1d?w=800',
|
||||
facilityType: 'Female',
|
||||
startDate: '2026-05-01',
|
||||
endDate: '2026-08-31',
|
||||
discount: '30% OFF',
|
||||
isActive: true,
|
||||
),
|
||||
OffersAndDiscountsResponseModel(
|
||||
id: 2,
|
||||
title: 'Maternity Care Special',
|
||||
description: 'Comprehensive prenatal care package including ultrasound, consultations, and postnatal support.',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1584820927498-cfe5211fd8bf?w=800',
|
||||
facilityType: 'OB-Gyne',
|
||||
startDate: '2026-05-01',
|
||||
endDate: '2026-12-31',
|
||||
discount: '25% OFF',
|
||||
isActive: true,
|
||||
),
|
||||
OffersAndDiscountsResponseModel(
|
||||
id: 3,
|
||||
title: 'Skin Treatment Package',
|
||||
description: 'Advanced dermatology services including acne treatment, anti-aging procedures, and skin rejuvenation.',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1570172619644-dfd03ed5d881?w=800',
|
||||
facilityType: 'Dermatology',
|
||||
startDate: '2026-05-15',
|
||||
endDate: '2026-07-15',
|
||||
discount: '40% OFF',
|
||||
isActive: true,
|
||||
),
|
||||
OffersAndDiscountsResponseModel(
|
||||
id: 4,
|
||||
title: 'CT & MRI Scanning Discount',
|
||||
description: 'State-of-the-art imaging services with latest technology. Book your scan today!',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1516549655169-df83a0774514?w=800',
|
||||
facilityType: 'Radiology',
|
||||
startDate: '2026-05-01',
|
||||
endDate: '2026-06-30',
|
||||
discount: '20% OFF',
|
||||
isActive: true,
|
||||
),
|
||||
OffersAndDiscountsResponseModel(
|
||||
id: 5,
|
||||
title: 'Women\'s Health Screening',
|
||||
description: 'Comprehensive health package designed specifically for women including mammography, bone density scan, and hormone tests.',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1559757175-5700dde675bc?w=800',
|
||||
facilityType: 'Female',
|
||||
startDate: '2026-05-10',
|
||||
endDate: '2026-09-30',
|
||||
discount: '35% OFF',
|
||||
isActive: true,
|
||||
),
|
||||
OffersAndDiscountsResponseModel(
|
||||
id: 6,
|
||||
title: 'Gynecology Consultation Special',
|
||||
description: 'Free follow-up consultation with every initial gynecology visit. Expert care for all women\'s health needs.',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1631217868264-e5b90bb7e133?w=800',
|
||||
facilityType: 'OB-Gyne',
|
||||
startDate: '2026-05-01',
|
||||
endDate: '2026-07-31',
|
||||
discount: 'FREE Follow-up',
|
||||
isActive: true,
|
||||
),
|
||||
OffersAndDiscountsResponseModel(
|
||||
id: 7,
|
||||
title: 'Laser Hair Removal Package',
|
||||
description: 'Professional laser hair removal treatment with latest technology. Multiple sessions available.',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1612349317150-e413f6a5b16d?w=800',
|
||||
facilityType: 'Dermatology',
|
||||
startDate: '2026-05-20',
|
||||
endDate: '2026-10-20',
|
||||
discount: '50% OFF',
|
||||
isActive: true,
|
||||
),
|
||||
OffersAndDiscountsResponseModel(
|
||||
id: 8,
|
||||
title: 'X-Ray & Ultrasound Combo',
|
||||
description: 'Get both X-ray and ultrasound services at a discounted price. Quick results guaranteed.',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1530497610245-94d3c16cda28?w=800',
|
||||
facilityType: 'Radiology',
|
||||
startDate: '2026-05-01',
|
||||
endDate: '2026-08-15',
|
||||
discount: '15% OFF',
|
||||
isActive: true,
|
||||
),
|
||||
OffersAndDiscountsResponseModel(
|
||||
id: 9,
|
||||
title: 'Complete Wellness Package',
|
||||
description: 'Full body checkup including all major tests, consultations, and health assessment report.',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1505751172876-fa1923c5c528?w=800',
|
||||
facilityType: 'Both',
|
||||
startDate: '2026-05-01',
|
||||
endDate: '2026-12-31',
|
||||
discount: '45% OFF',
|
||||
isActive: true,
|
||||
),
|
||||
OffersAndDiscountsResponseModel(
|
||||
id: 10,
|
||||
title: 'Botox & Filler Treatment',
|
||||
description: 'Anti-aging treatments with certified dermatologists. Natural-looking results guaranteed.',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1515377905703-c4788e51af15?w=800',
|
||||
facilityType: 'Dermatology',
|
||||
startDate: '2026-05-15',
|
||||
endDate: '2026-06-15',
|
||||
discount: '30% OFF',
|
||||
isActive: true,
|
||||
),
|
||||
];
|
||||
|
||||
// Past offers for history
|
||||
historyList = [
|
||||
OffersAndDiscountsResponseModel(
|
||||
id: 101,
|
||||
title: 'Winter Health Campaign',
|
||||
description: 'Flu shots and winter wellness packages that were offered during the winter season.',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1584308666744-24d5c474f2ae?w=800',
|
||||
facilityType: 'Both',
|
||||
startDate: '2025-12-01',
|
||||
endDate: '2026-02-28',
|
||||
discount: '25% OFF',
|
||||
isActive: false,
|
||||
),
|
||||
OffersAndDiscountsResponseModel(
|
||||
id: 102,
|
||||
title: 'Valentine\'s Day Couple Checkup',
|
||||
description: 'Special couple health screening packages offered for Valentine\'s Day.',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1516549655169-df83a0774514?w=800',
|
||||
facilityType: 'Both',
|
||||
startDate: '2026-02-01',
|
||||
endDate: '2026-02-14',
|
||||
discount: '40% OFF',
|
||||
isActive: false,
|
||||
),
|
||||
OffersAndDiscountsResponseModel(
|
||||
id: 103,
|
||||
title: 'Spring Skin Renewal',
|
||||
description: 'Spring special dermatology treatments for skin rejuvenation.',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1556228720-195a672e8a03?w=800',
|
||||
facilityType: 'Dermatology',
|
||||
startDate: '2026-03-01',
|
||||
endDate: '2026-04-30',
|
||||
discount: '30% OFF',
|
||||
isActive: false,
|
||||
),
|
||||
OffersAndDiscountsResponseModel(
|
||||
id: 104,
|
||||
title: 'Summer Wellness Package - Active',
|
||||
description: 'Active offer for comprehensive summer health checkup with multiple tests.',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1576091160399-112ba8d25d1d?w=800',
|
||||
facilityType: 'Female',
|
||||
startDate: '2026-05-01',
|
||||
endDate: '2026-08-31',
|
||||
discount: '35% OFF',
|
||||
isActive: true,
|
||||
),
|
||||
OffersAndDiscountsResponseModel(
|
||||
id: 105,
|
||||
title: 'Kids Health Screening - Active',
|
||||
description: 'Currently active pediatric health screening package for children.',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1559839734-2b71ea197ec2?w=800',
|
||||
facilityType: 'Both',
|
||||
startDate: '2026-05-05',
|
||||
endDate: '2026-09-30',
|
||||
discount: '20% OFF',
|
||||
isActive: true,
|
||||
),
|
||||
OffersAndDiscountsResponseModel(
|
||||
id: 106,
|
||||
title: 'Laser Treatment Offer - Expired',
|
||||
description: 'Laser hair removal package that has expired.',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1612349317150-e413f6a5b16d?w=800',
|
||||
facilityType: 'Dermatology',
|
||||
startDate: '2026-01-01',
|
||||
endDate: '2026-03-31',
|
||||
discount: '50% OFF',
|
||||
isActive: false,
|
||||
),
|
||||
OffersAndDiscountsResponseModel(
|
||||
id: 107,
|
||||
title: 'Cardiology Consultation Package',
|
||||
description: 'Heart health checkup with ECG and consultation.',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1628348068343-c6a848d2b6dd?w=800',
|
||||
facilityType: 'Both',
|
||||
startDate: '2025-11-01',
|
||||
endDate: '2026-01-31',
|
||||
discount: '25% OFF',
|
||||
isActive: false,
|
||||
),
|
||||
OffersAndDiscountsResponseModel(
|
||||
id: 108,
|
||||
title: 'Dental Care Special - Active',
|
||||
description: 'Comprehensive dental checkup and cleaning package.',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1606811971618-4486d14f3f99?w=800',
|
||||
facilityType: 'Both',
|
||||
startDate: '2026-04-01',
|
||||
endDate: '2026-12-31',
|
||||
discount: '30% OFF',
|
||||
isActive: true,
|
||||
),
|
||||
OffersAndDiscountsResponseModel(
|
||||
id: 109,
|
||||
title: 'Eye Care Package - Expired',
|
||||
description: 'Complete eye examination with vision testing.',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1516534775068-ba3e7458af70?w=800',
|
||||
facilityType: 'Both',
|
||||
startDate: '2025-10-01',
|
||||
endDate: '2025-12-31',
|
||||
discount: '20% OFF',
|
||||
isActive: false,
|
||||
),
|
||||
OffersAndDiscountsResponseModel(
|
||||
id: 110,
|
||||
title: 'Maternity Care Premium',
|
||||
description: 'Premium prenatal and postnatal care package.',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1584820927498-cfe5211fd8bf?w=800',
|
||||
facilityType: 'OB-Gyne',
|
||||
startDate: '2026-01-15',
|
||||
endDate: '2026-06-30',
|
||||
discount: '40% OFF',
|
||||
isActive: true,
|
||||
),
|
||||
OffersAndDiscountsResponseModel(
|
||||
id: 111,
|
||||
title: 'Blood Test Special - Expired',
|
||||
description: 'Comprehensive blood work panel at discounted rates.',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1579154204601-01588f351e67?w=800',
|
||||
facilityType: 'Both',
|
||||
startDate: '2025-09-01',
|
||||
endDate: '2025-11-30',
|
||||
discount: '15% OFF',
|
||||
isActive: false,
|
||||
),
|
||||
OffersAndDiscountsResponseModel(
|
||||
id: 112,
|
||||
title: 'Vaccination Drive',
|
||||
description: 'Special vaccination package for adults and children.',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1587854692152-cbe660dbde88?w=800',
|
||||
facilityType: 'Both',
|
||||
startDate: '2026-03-01',
|
||||
endDate: '2026-05-05',
|
||||
discount: '10% OFF',
|
||||
isActive: false,
|
||||
),
|
||||
OffersAndDiscountsResponseModel(
|
||||
id: 113,
|
||||
title: 'Radiology Imaging Package - Active',
|
||||
description: 'CT, MRI, and X-ray services at reduced prices.',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1516549655169-df83a0774514?w=800',
|
||||
facilityType: 'Radiology',
|
||||
startDate: '2026-05-01',
|
||||
endDate: '2026-10-31',
|
||||
discount: '25% OFF',
|
||||
isActive: false, // Changed to false - will show as "Availed"
|
||||
),
|
||||
OffersAndDiscountsResponseModel(
|
||||
id: 114,
|
||||
title: 'Physiotherapy Sessions',
|
||||
description: 'Package of 10 physiotherapy sessions.',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1576091160550-2173dba999ef?w=800',
|
||||
facilityType: 'Both',
|
||||
startDate: '2025-12-01',
|
||||
endDate: '2026-04-01',
|
||||
discount: '35% OFF',
|
||||
isActive: false,
|
||||
),
|
||||
OffersAndDiscountsResponseModel(
|
||||
id: 115,
|
||||
title: 'Nutrition Consultation Bundle - Active',
|
||||
description: 'Series of nutrition and diet consultation sessions.',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1490645935967-10de6ba17061?w=800',
|
||||
facilityType: 'Both',
|
||||
startDate: '2026-05-01',
|
||||
endDate: '2026-11-30',
|
||||
discount: '20% OFF',
|
||||
isActive: true,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
// For loading state compatibility with existing code
|
||||
bool get isRegionListLoading => isOffersLoading;
|
||||
|
||||
initOffersAndDiscounts() {
|
||||
// Don't clear the list - keep dummy data until API returns
|
||||
// Don't show loading state since we have dummy data
|
||||
// offersList.clear();
|
||||
// isOffersLoading = true;
|
||||
notifyListeners();
|
||||
getOffersAndDiscounts();
|
||||
}
|
||||
|
||||
setIsOffersLoading(bool val) {
|
||||
isOffersLoading = val;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
setIsHistoryLoading(bool val) {
|
||||
isHistoryLoading = val;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
setSelectedFacility(List<String> facilities) {
|
||||
selectedFacilities = facilities;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
setSearchQuery(String query) {
|
||||
searchQuery = query;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> getOffersAndDiscounts({Function(dynamic)? onSuccess, Function(String)? onError}) async {
|
||||
final result = await offersAndDiscountsRepo.getOffersAndDiscounts();
|
||||
|
||||
result.fold(
|
||||
(failure) async {
|
||||
isOffersLoading = false;
|
||||
notifyListeners();
|
||||
// Keep dummy data if API fails - don't clear the list
|
||||
// await errorHandlerService.handleError(failure: failure);
|
||||
if (onError != null) {
|
||||
onError(failure.toString());
|
||||
}
|
||||
},
|
||||
(response) {
|
||||
isOffersLoading = false;
|
||||
if (response.data != null && response.data!.isNotEmpty) {
|
||||
offersList = response.data!;
|
||||
}
|
||||
// If API returns empty or null, keep the dummy data
|
||||
notifyListeners();
|
||||
if (onSuccess != null) {
|
||||
onSuccess(response);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> getOffersAndDiscountsHistory({Function(dynamic)? onSuccess, Function(String)? onError}) async {
|
||||
isHistoryLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
final result = await offersAndDiscountsRepo.getOffersAndDiscountsHistory();
|
||||
|
||||
result.fold(
|
||||
(failure) async {
|
||||
isHistoryLoading = false;
|
||||
notifyListeners();
|
||||
// Keep dummy data if API fails - don't clear the list
|
||||
// await errorHandlerService.handleError(failure: failure);
|
||||
if (onError != null) {
|
||||
onError(failure.toString());
|
||||
}
|
||||
},
|
||||
(response) {
|
||||
isHistoryLoading = false;
|
||||
if (response.data != null && response.data!.isNotEmpty) {
|
||||
historyList = response.data!;
|
||||
}
|
||||
// If API returns empty or null, keep the dummy data
|
||||
notifyListeners();
|
||||
if (onSuccess != null) {
|
||||
onSuccess(response);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<OffersAndDiscountsResponseModel> get filteredOffers {
|
||||
List<OffersAndDiscountsResponseModel> filtered = offersList;
|
||||
|
||||
// Filter by facility types (multi-select support)
|
||||
if (!selectedFacilities.contains('All Offers')) {
|
||||
filtered = filtered.where((offer) {
|
||||
// Check if offer's facility type matches any of the selected types
|
||||
return selectedFacilities.any((selectedType) =>
|
||||
offer.facilityType == selectedType || offer.facilityType == 'Both'
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// Filter by search query
|
||||
if (searchQuery.isNotEmpty) {
|
||||
filtered = filtered.where((offer) {
|
||||
final titleMatch = offer.title?.toLowerCase().contains(searchQuery.toLowerCase()) ?? false;
|
||||
final descriptionMatch = offer.description?.toLowerCase().contains(searchQuery.toLowerCase()) ?? false;
|
||||
return titleMatch || descriptionMatch;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
List<OffersAndDiscountsResponseModel> get filteredHistory {
|
||||
List<OffersAndDiscountsResponseModel> filtered = historyList;
|
||||
|
||||
// Filter by facility types (multi-select support)
|
||||
if (!selectedFacilities.contains('All Offers')) {
|
||||
filtered = filtered.where((offer) {
|
||||
// Check if offer's facility type matches any of the selected types
|
||||
return selectedFacilities.any((selectedType) =>
|
||||
offer.facilityType == selectedType || offer.facilityType == 'Both'
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// Filter by search query
|
||||
if (searchQuery.isNotEmpty) {
|
||||
filtered = filtered.where((offer) {
|
||||
final titleMatch = offer.title?.toLowerCase().contains(searchQuery.toLowerCase()) ?? false;
|
||||
final descriptionMatch = offer.description?.toLowerCase().contains(searchQuery.toLowerCase()) ?? false;
|
||||
return titleMatch || descriptionMatch;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,262 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_assets.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_state.dart';
|
||||
import 'package:hmg_patient_app_new/core/dependencies.dart';
|
||||
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
|
||||
import 'package:hmg_patient_app_new/core/utils/utils.dart';
|
||||
import 'package:hmg_patient_app_new/theme/colors.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
class OfferAndDiscountsFullScreenSwiperPage extends StatefulWidget {
|
||||
final List<String>? images;
|
||||
final int initialIndex;
|
||||
|
||||
const OfferAndDiscountsFullScreenSwiperPage({
|
||||
super.key,
|
||||
this.images,
|
||||
this.initialIndex = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
State<OfferAndDiscountsFullScreenSwiperPage> createState() => _OfferAndDiscountsFullScreenSwiperPageState();
|
||||
}
|
||||
|
||||
class _OfferAndDiscountsFullScreenSwiperPageState extends State<OfferAndDiscountsFullScreenSwiperPage> {
|
||||
late PageController _pageController;
|
||||
late int _currentPage;
|
||||
late List<String> _promoImages;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Use provided images or default demo images
|
||||
_promoImages = widget.images ??
|
||||
[
|
||||
'assets/images/offersanddiscounts/promo.jpg',
|
||||
'assets/images/offersanddiscounts/promo.jpg',
|
||||
'assets/images/offersanddiscounts/promo.jpg',
|
||||
'assets/images/offersanddiscounts/promo.jpg',
|
||||
];
|
||||
_currentPage = widget.initialIndex;
|
||||
_pageController = PageController(initialPage: widget.initialIndex);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFADA6D0),
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
// Main content area
|
||||
Column(
|
||||
children: [
|
||||
SizedBox(height: 26.h),
|
||||
|
||||
// Page indicators
|
||||
_buildPageIndicators(),
|
||||
|
||||
SizedBox(height: 70.h),
|
||||
|
||||
// Swipeable image section
|
||||
Expanded(
|
||||
child: PageView.builder(
|
||||
controller: _pageController,
|
||||
onPageChanged: (index) {
|
||||
setState(() {
|
||||
_currentPage = index;
|
||||
});
|
||||
},
|
||||
itemCount: _promoImages.length,
|
||||
itemBuilder: (context, index) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 0.h),
|
||||
child: Transform.flip(
|
||||
flipX: getIt.get<AppState>().isArabic(),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16.r),
|
||||
child: Image.asset(
|
||||
_promoImages[index],
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(16.r),
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.image_outlined,
|
||||
size: 100.h,
|
||||
color: Colors.white.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 70.h),
|
||||
// Bottom action buttons
|
||||
_buildBottomActions(),
|
||||
|
||||
SizedBox(height: 12.h),
|
||||
],
|
||||
),
|
||||
|
||||
// Close button (top right)
|
||||
Positioned(
|
||||
top: 26.h + 14.h, // After indicator spacing
|
||||
right: 24.w,
|
||||
child: _buildCloseButton()),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPageIndicators() {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24.w),
|
||||
child: Row(
|
||||
children: List.generate(
|
||||
_promoImages.length,
|
||||
(index) => Expanded(
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(
|
||||
right: index < _promoImages.length - 1 ? 4.w : 0,
|
||||
),
|
||||
height: 4.h,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(2.r),
|
||||
// Fill bar if it's current page or any previous page
|
||||
color: index <= _currentPage ? Colors.white : const Color(0x33000000), // 20% opacity
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCloseButton() {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Container(
|
||||
width: 32.w,
|
||||
height: 32.h,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x33FFFFFF), // 20% opacity
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
color: AppColors.textColor,
|
||||
size: 20.h,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomActions() {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24.w),
|
||||
child: Row(
|
||||
children: [
|
||||
// Buy Now button (takes remaining space)
|
||||
Expanded(
|
||||
child: CustomButton(
|
||||
text: 'Buy Now',
|
||||
icon: AppAssets.shoppingCart,
|
||||
iconColor: AppColors.whiteColor,
|
||||
onPressed: () {
|
||||
// Handle buy now action
|
||||
},
|
||||
backgroundColor: AppColors.primaryRedColor,
|
||||
borderColor: AppColors.primaryRedColor,
|
||||
textColor: AppColors.whiteColor,
|
||||
fontSize: 16.f,
|
||||
isBold: true,
|
||||
borderRadius: 12.r,
|
||||
height: 56.h,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(width: 12.w),
|
||||
|
||||
// Next button
|
||||
_buildIconButton(
|
||||
svgIcon: AppAssets.nextSwiper,
|
||||
onTap: () {
|
||||
if (_currentPage < _promoImages.length - 1) {
|
||||
_pageController.nextPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
SizedBox(width: 12.w),
|
||||
|
||||
_buildIconButton(
|
||||
svgIcon: AppAssets.share,
|
||||
onTap: () async {
|
||||
await Share.share('Check out this amazing offer!');
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIconButton({
|
||||
IconData? icon,
|
||||
String? svgIcon,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: 56.w,
|
||||
height: 56.h,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x57303957), // 34% opacity approximation
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
),
|
||||
child: Center(
|
||||
child: svgIcon != null
|
||||
? Utils.buildSvgWithAssets(
|
||||
icon: svgIcon,
|
||||
iconColor: AppColors.whiteColor,
|
||||
width: 24.h,
|
||||
height: 24.h,
|
||||
)
|
||||
: Icon(
|
||||
icon!,
|
||||
color: AppColors.whiteColor,
|
||||
size: 24.h,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,223 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_assets.dart';
|
||||
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/features/offers_and_discounts/models/offers_and_discounts_response_model.dart';
|
||||
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
|
||||
import 'package:hmg_patient_app_new/theme/colors.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
|
||||
|
||||
class OffersAndDiscountsDetailedPage extends StatelessWidget {
|
||||
final OffersAndDiscountsResponseModel offer;
|
||||
|
||||
const OffersAndDiscountsDetailedPage({
|
||||
super.key,
|
||||
required this.offer,
|
||||
});
|
||||
|
||||
String _formatDateString(String? dateString) {
|
||||
if (dateString == null || dateString.isEmpty) return '';
|
||||
try {
|
||||
DateTime date = DateTime.parse(dateString);
|
||||
return DateFormat('d MMM, yyyy').format(date);
|
||||
} catch (e) {
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the status of the offer based on dates and isActive flag
|
||||
String _getOfferStatus(String? endDate, bool? isActive) {
|
||||
if (endDate == null || endDate.isEmpty) return 'Expired';
|
||||
try {
|
||||
DateTime end = DateTime.parse(endDate);
|
||||
DateTime now = DateTime.now();
|
||||
|
||||
if (now.isAfter(end)) {
|
||||
return 'Expired';
|
||||
} else {
|
||||
if (isActive == true) {
|
||||
return 'Active';
|
||||
} else {
|
||||
return 'Availed';
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
return 'Expired';
|
||||
}
|
||||
}
|
||||
|
||||
Color _getStatusBgColor(String status) {
|
||||
switch (status) {
|
||||
case 'Active':
|
||||
return AppColors.successColor.withValues(alpha: 0.1);
|
||||
case 'Availed':
|
||||
return AppColors.infoColor.withValues(alpha: 0.1);
|
||||
case 'Expired':
|
||||
return AppColors.errorColor.withValues(alpha: 0.1);
|
||||
default:
|
||||
return AppColors.greyColor;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getStatusTextColor(String status) {
|
||||
switch (status) {
|
||||
case 'Active':
|
||||
return AppColors.successColor;
|
||||
case 'Availed':
|
||||
return AppColors.infoColor;
|
||||
case 'Expired':
|
||||
return AppColors.errorColor;
|
||||
default:
|
||||
return AppColors.textColor;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final status = _getOfferStatus(offer.endDate, offer.isActive);
|
||||
|
||||
return CollapsingListView(
|
||||
title: "${LocaleKeys.offers.tr(context: context)} ${LocaleKeys.details.tr(context: context)}",
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Offer Image
|
||||
if (offer.imageUrl != null)
|
||||
Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(24.h),
|
||||
child: Image.network(
|
||||
offer.imageUrl!,
|
||||
width: double.infinity,
|
||||
height: 250.h,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
height: 250.h,
|
||||
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||
color: AppColors.greyColor,
|
||||
borderRadius: 24.h,
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(Icons.image_not_supported, size: 64.h),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
// Discount badge at top right on image
|
||||
// if (offer.discount != null)
|
||||
// Positioned(
|
||||
// top: 16.h,
|
||||
// right: 16.w,
|
||||
// child: Container(
|
||||
// padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
|
||||
// decoration: BoxDecoration(
|
||||
// color: AppColors.primaryRedColor,
|
||||
// borderRadius: BorderRadius.circular(8.r),
|
||||
// ),
|
||||
// child: Text(
|
||||
// offer.discount!,
|
||||
// style: TextStyle(
|
||||
// fontSize: 14.f,
|
||||
// fontWeight: FontWeight.w700,
|
||||
// color: AppColors.whiteColor,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(height: 24.h),
|
||||
|
||||
// Title
|
||||
if (offer.title != null)
|
||||
offer.title!.toText24(
|
||||
isBold: true,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
|
||||
SizedBox(height: 16.h),
|
||||
|
||||
// Chips Row (Valid Till and Status)
|
||||
Wrap(
|
||||
spacing: 12.w,
|
||||
runSpacing: 12.h,
|
||||
children: [
|
||||
// Valid till chip
|
||||
if (offer.endDate != null)
|
||||
AppCustomChipWidget(
|
||||
labelText: "Valid till ${_formatDateString(offer.endDate)}",
|
||||
backgroundColor: AppColors.chipBgColor,
|
||||
textColor: AppColors.textColor,
|
||||
icon: AppAssets.calendar,
|
||||
iconSize: 14.h,
|
||||
iconColor: AppColors.textColor,
|
||||
isEnglishOnly: true,
|
||||
),
|
||||
// Status chip
|
||||
AppCustomChipWidget(
|
||||
labelText: status,
|
||||
backgroundColor: _getStatusBgColor(status),
|
||||
textColor: _getStatusTextColor(status),
|
||||
),
|
||||
// Facility type chip
|
||||
if (offer.facilityType != null)
|
||||
AppCustomChipWidget(
|
||||
labelText: offer.facilityType!,
|
||||
backgroundColor: AppColors.chipBgColor,
|
||||
textColor: AppColors.textColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(height: 32.h),
|
||||
|
||||
// Description Heading
|
||||
LocaleKeys.description.tr(context: context).toText18(
|
||||
isBold: true,
|
||||
weight: FontWeight.w700,
|
||||
),
|
||||
|
||||
SizedBox(height: 12.h),
|
||||
|
||||
// Description Content
|
||||
if (offer.description != null)
|
||||
offer.description!.toText16(
|
||||
color: AppColors.greyTextColor,
|
||||
height: 1.5,
|
||||
),
|
||||
|
||||
SizedBox(height: 32.h),
|
||||
|
||||
// Buy Now Button
|
||||
CustomButton(
|
||||
text: "Buy Now",
|
||||
icon: AppAssets.shoppingCart,
|
||||
iconColor: AppColors.whiteColor,
|
||||
onPressed: () {
|
||||
// Handle buy now action
|
||||
},
|
||||
backgroundColor: AppColors.primaryRedColor,
|
||||
borderColor: AppColors.primaryRedColor,
|
||||
textColor: AppColors.whiteColor,
|
||||
fontSize: 16.f,
|
||||
isBold: true,
|
||||
borderRadius: 12.r,
|
||||
height: 56.h,
|
||||
),
|
||||
|
||||
SizedBox(height: 24.h),
|
||||
],
|
||||
).paddingSymmetrical(24.h, 0.h),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,292 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_assets.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_state.dart';
|
||||
import 'package:hmg_patient_app_new/core/dependencies.dart';
|
||||
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
|
||||
import 'package:hmg_patient_app_new/core/utils/utils.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/features/offers_and_discounts/offers_and_discounts_view_model.dart';
|
||||
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
|
||||
import 'package:hmg_patient_app_new/presentation/offers_and_discounts/offers_and_discounts_detailed_page.dart';
|
||||
import 'package:hmg_patient_app_new/theme/colors.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class OffersAndDiscountsHistoryPage extends StatefulWidget {
|
||||
const OffersAndDiscountsHistoryPage({super.key});
|
||||
|
||||
@override
|
||||
State<OffersAndDiscountsHistoryPage> createState() => _OffersAndDiscountsHistoryPageState();
|
||||
}
|
||||
|
||||
class _OffersAndDiscountsHistoryPageState extends State<OffersAndDiscountsHistoryPage> {
|
||||
late OffersAndDiscountsViewModel offersAndDiscountsViewModel;
|
||||
late AppState appState;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
scheduleMicrotask(() {
|
||||
offersAndDiscountsViewModel.getOffersAndDiscountsHistory();
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
String _formatDateString(String? dateString) {
|
||||
if (dateString == null || dateString.isEmpty) return '';
|
||||
try {
|
||||
DateTime date = DateTime.parse(dateString);
|
||||
return DateFormat('d MMM, yyyy').format(date);
|
||||
} catch (e) {
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the status of the offer based on dates and isActive flag
|
||||
String _getOfferStatus(String? endDate, bool? isActive) {
|
||||
if (endDate == null || endDate.isEmpty) return 'Expired';
|
||||
try {
|
||||
DateTime end = DateTime.parse(endDate);
|
||||
DateTime now = DateTime.now();
|
||||
|
||||
if (now.isAfter(end)) {
|
||||
return 'Expired';
|
||||
} else {
|
||||
// If end date is in future, check isActive flag
|
||||
if (isActive == true) {
|
||||
return 'Active';
|
||||
} else {
|
||||
return 'Availed';
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
return 'Expired';
|
||||
}
|
||||
}
|
||||
|
||||
Color _getStatusBgColor(String status) {
|
||||
switch (status) {
|
||||
case 'Active':
|
||||
return AppColors.successColor.withValues(alpha: 0.1);
|
||||
case 'Availed':
|
||||
return AppColors.infoColor.withValues(alpha: 0.1);
|
||||
case 'Expired':
|
||||
return AppColors.errorColor.withValues(alpha: 0.1);
|
||||
default:
|
||||
return AppColors.greyColor;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getStatusTextColor(String status) {
|
||||
switch (status) {
|
||||
case 'Active':
|
||||
return AppColors.successColor;
|
||||
case 'Availed':
|
||||
return AppColors.infoColor;
|
||||
case 'Expired':
|
||||
return AppColors.errorColor;
|
||||
default:
|
||||
return AppColors.textColor;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
appState = getIt.get<AppState>();
|
||||
offersAndDiscountsViewModel = Provider.of<OffersAndDiscountsViewModel>(context, listen: false);
|
||||
|
||||
return CollapsingListView(
|
||||
title: '${LocaleKeys.order.tr(context: context)} ${LocaleKeys.history.tr(context: context)}',
|
||||
child: SingleChildScrollView(
|
||||
child: Consumer<OffersAndDiscountsViewModel>(builder: (context, offersAndDiscountVM, child) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
offersAndDiscountVM.isHistoryLoading
|
||||
? Container(
|
||||
height: 200.h,
|
||||
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||
color: AppColors.whiteColor,
|
||||
borderRadius: 24.h,
|
||||
hasShadow: true,
|
||||
),
|
||||
child: Center(child: Utils.getLoadingWidget()),
|
||||
).paddingSymmetrical(24.h, 0.h)
|
||||
: ListView.separated(
|
||||
padding: EdgeInsets.only(top: 12.h),
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
itemCount: offersAndDiscountVM.filteredHistory.isNotEmpty ? offersAndDiscountVM.filteredHistory.length : 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (offersAndDiscountVM.filteredHistory.isEmpty) {
|
||||
return Utils.getNoDataWidget(context, noDataText: LocaleKeys.noDataAvailable.tr(context: context));
|
||||
}
|
||||
|
||||
final offer = offersAndDiscountVM.filteredHistory[index];
|
||||
final status = _getOfferStatus(offer.endDate, offer.isActive);
|
||||
|
||||
return AnimationConfiguration.staggeredList(
|
||||
position: index,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
child: SlideAnimation(
|
||||
verticalOffset: 100.0,
|
||||
child: FadeInAnimation(
|
||||
child: AnimatedContainer(
|
||||
duration: Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||
color: AppColors.whiteColor,
|
||||
borderRadius: 24.h,
|
||||
hasShadow: true,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.all(16.h),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Title with space for status chip
|
||||
Padding(
|
||||
padding: EdgeInsets.only(right: 80.w),
|
||||
child: offer.title?.toText18(
|
||||
isBold: true,
|
||||
weight: FontWeight.w700,
|
||||
) ??
|
||||
SizedBox(),
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
|
||||
// Info chips
|
||||
Wrap(
|
||||
spacing: 8.w,
|
||||
runSpacing: 8.h,
|
||||
children: [
|
||||
// Start Date chip
|
||||
if (offer.startDate != null)
|
||||
AppCustomChipWidget(
|
||||
labelText: _formatDateString(offer.startDate),
|
||||
backgroundColor: AppColors.chipBgColor,
|
||||
textColor: AppColors.textColor,
|
||||
icon: AppAssets.calendar,
|
||||
iconSize: 12.h,
|
||||
iconColor: AppColors.textColor,
|
||||
isEnglishOnly: true,
|
||||
),
|
||||
// Hospital/Facility chip
|
||||
if (offer.facilityType != null)
|
||||
AppCustomChipWidget(
|
||||
labelText: offer.facilityType!,
|
||||
backgroundColor: AppColors.chipBgColor,
|
||||
textColor: AppColors.textColor,
|
||||
),
|
||||
// Purchase date chip (using startDate as purchased date)
|
||||
if (offer.startDate != null)
|
||||
AppCustomChipWidget(
|
||||
labelText: "Purchased ${_formatDateString(offer.startDate)}",
|
||||
backgroundColor: AppColors.chipBgColor,
|
||||
textColor: AppColors.textColor,
|
||||
isEnglishOnly: true,
|
||||
),
|
||||
// Valid until chip
|
||||
if (offer.endDate != null)
|
||||
AppCustomChipWidget(
|
||||
labelText: "Valid till ${_formatDateString(offer.endDate)}",
|
||||
backgroundColor: AppColors.chipBgColor,
|
||||
textColor: AppColors.textColor,
|
||||
icon: AppAssets.calendar,
|
||||
iconSize: 12.h,
|
||||
iconColor: AppColors.textColor,
|
||||
isEnglishOnly: true,
|
||||
),
|
||||
// Order ID chip (using offer.id)
|
||||
if (offer.id != null)
|
||||
AppCustomChipWidget(
|
||||
labelText: "ID: #${offer.id}",
|
||||
backgroundColor: AppColors.chipBgColor,
|
||||
textColor: AppColors.textColor,
|
||||
isEnglishOnly: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
|
||||
// Action buttons
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CustomButton(
|
||||
text: 'View Offer Details',
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
CustomPageRoute(
|
||||
page: OffersAndDiscountsDetailedPage(
|
||||
offer: offersAndDiscountVM.filteredOffers[index],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: AppColors.errorColor.withValues(alpha: 0.1),
|
||||
borderColor: Colors.transparent,
|
||||
textColor: AppColors.errorColor,
|
||||
fontSize: 12.f,
|
||||
isBold: true,
|
||||
borderRadius: 12.r,
|
||||
height: 40.h,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
Expanded(
|
||||
child: CustomButton(
|
||||
text: 'View Order Details',
|
||||
onPressed: () {
|
||||
// Handle view order details
|
||||
},
|
||||
backgroundColor: AppColors.whiteColor,
|
||||
borderColor: AppColors.textColor,
|
||||
textColor: AppColors.textColor,
|
||||
fontSize: 12.f,
|
||||
isBold: true,
|
||||
borderRadius: 12.r,
|
||||
height: 40.h,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Status chip at top right
|
||||
Positioned(
|
||||
top: 16.h,
|
||||
right: 16.w,
|
||||
child: AppCustomChipWidget(
|
||||
labelText: status,
|
||||
backgroundColor: _getStatusBgColor(status),
|
||||
textColor: _getStatusTextColor(status),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
).paddingSymmetrical(24.h, 0.h),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h),
|
||||
),
|
||||
SizedBox(height: 24.h),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,279 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_assets.dart';
|
||||
import 'package:hmg_patient_app_new/core/app_state.dart';
|
||||
import 'package:hmg_patient_app_new/core/dependencies.dart';
|
||||
import 'package:hmg_patient_app_new/core/enums.dart';
|
||||
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
|
||||
import 'package:hmg_patient_app_new/core/utils/utils.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/features/offers_and_discounts/offers_and_discounts_view_model.dart';
|
||||
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
|
||||
import 'package:hmg_patient_app_new/presentation/offers_and_discounts/offers_and_discounts_detailed_page.dart';
|
||||
import 'package:hmg_patient_app_new/presentation/offers_and_discounts/offers_and_discounts_history_page.dart';
|
||||
import 'package:hmg_patient_app_new/presentation/offers_and_discounts/widgets/offers_and_discount_type_selection_widget.dart';
|
||||
import 'package:hmg_patient_app_new/theme/colors.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
|
||||
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../widgets/input_widget.dart';
|
||||
|
||||
class OffersAndDiscountsPage extends StatefulWidget {
|
||||
const OffersAndDiscountsPage({super.key});
|
||||
|
||||
@override
|
||||
State<OffersAndDiscountsPage> createState() => _OffersAndDiscountsPageState();
|
||||
}
|
||||
|
||||
class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
|
||||
late OffersAndDiscountsViewModel offersAndDiscountsViewModel;
|
||||
late AppState appState;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
scheduleMicrotask(() {
|
||||
offersAndDiscountsViewModel.initOffersAndDiscounts();
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
appState = getIt.get<AppState>();
|
||||
offersAndDiscountsViewModel = Provider.of<OffersAndDiscountsViewModel>(context, listen: false);
|
||||
|
||||
return CollapsingListView(
|
||||
title: "${LocaleKeys.offers.tr(context: context)} & ${LocaleKeys.discount.tr(context: context)}",
|
||||
history: () {
|
||||
Navigator.of(context).push(
|
||||
CustomPageRoute(
|
||||
page: OffersAndDiscountsHistoryPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
child: Consumer<OffersAndDiscountsViewModel>(builder: (context, offersAndDiscountVM, child) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Search Input
|
||||
TextInputWidget(
|
||||
labelText: LocaleKeys.search.tr(context: context),
|
||||
hintText: LocaleKeys.search.tr(context: context),
|
||||
controller: _searchController,
|
||||
onChange: (value) {
|
||||
offersAndDiscountVM.setSearchQuery(value!);
|
||||
},
|
||||
isEnable: true,
|
||||
prefix: null,
|
||||
autoFocus: false,
|
||||
isBorderAllowed: false,
|
||||
keyboardType: TextInputType.text,
|
||||
isAllowLeadingIcon: true,
|
||||
selectionType: SelectionTypeEnum.search,
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: ResponsiveExtension(10).h,
|
||||
horizontal: ResponsiveExtension(15).h,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
OffersAndDiscountTypeSelectionWidget(
|
||||
selectedOffers: offersAndDiscountVM.selectedFacilities,
|
||||
onOfferClicked: (selectedValues) {
|
||||
offersAndDiscountVM.setSelectedFacility(selectedValues);
|
||||
},
|
||||
),
|
||||
|
||||
SizedBox(height: 16.h),
|
||||
|
||||
// Offers Grid
|
||||
offersAndDiscountVM.isOffersLoading
|
||||
? GridView.builder(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24.w),
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 16.w,
|
||||
mainAxisSpacing: 16.h,
|
||||
childAspectRatio: 0.55, // Adjust this to control card height
|
||||
),
|
||||
itemCount: 6,
|
||||
// Show 6 loading placeholders
|
||||
itemBuilder: (context, index) {
|
||||
return Container(
|
||||
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||
color: AppColors.whiteColor,
|
||||
borderRadius: 24.h,
|
||||
hasShadow: true,
|
||||
),
|
||||
child: Center(child: Utils.getLoadingWidget()),
|
||||
);
|
||||
},
|
||||
)
|
||||
: offersAndDiscountVM.filteredOffers.isNotEmpty
|
||||
? GridView.builder(
|
||||
padding: EdgeInsets.symmetric(horizontal: 0.w),
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2, crossAxisSpacing: 16.w, mainAxisSpacing: 16.h, childAspectRatio: 0.55),
|
||||
itemCount: offersAndDiscountVM.filteredOffers.length,
|
||||
itemBuilder: (context, index) {
|
||||
return AnimationConfiguration.staggeredGrid(
|
||||
position: index,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
columnCount: 2,
|
||||
child: ScaleAnimation(
|
||||
child: FadeInAnimation(
|
||||
child: AnimatedContainer(
|
||||
duration: Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||
color: AppColors.whiteColor,
|
||||
borderRadius: 24.h,
|
||||
hasShadow: true,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (offersAndDiscountVM.filteredOffers[index].imageUrl != null)
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24.h)),
|
||||
child: SizedBox.expand(
|
||||
child: Image.network(
|
||||
offersAndDiscountVM.filteredOffers[index].imageUrl!,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
color: AppColors.greyColor,
|
||||
child: Center(
|
||||
child: Icon(Icons.image_not_supported, size: 32.h),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
).onPress(() {
|
||||
Navigator.of(context).push(
|
||||
CustomPageRoute(
|
||||
page: OffersAndDiscountsDetailedPage(
|
||||
offer: offersAndDiscountVM.filteredOffers[index],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
// Discount badge at top right
|
||||
// if (offersAndDiscountVM.filteredOffers[index].discount != null)
|
||||
// Positioned(
|
||||
// top: 8.h,
|
||||
// right: 8.w,
|
||||
// child: Container(
|
||||
// padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
|
||||
// decoration: BoxDecoration(
|
||||
// color: AppColors.primaryRedColor,
|
||||
// borderRadius: BorderRadius.circular(6.r),
|
||||
// ),
|
||||
// child: Text(
|
||||
// offersAndDiscountVM.filteredOffers[index].discount!,
|
||||
// style: TextStyle(
|
||||
// fontSize: 10.f,
|
||||
// fontWeight: FontWeight.w700,
|
||||
// color: AppColors.whiteColor,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(12.h),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (offersAndDiscountVM.filteredOffers[index].title != null)
|
||||
Text(
|
||||
offersAndDiscountVM.filteredOffers[index].title!,
|
||||
style: TextStyle(
|
||||
fontSize: 14.f,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.textColor,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
// Valid till chip
|
||||
if (offersAndDiscountVM.filteredOffers[index].endDate != null)
|
||||
AppCustomChipWidget(
|
||||
labelText: "Valid till ${Utils.formatDateToDisplay(offersAndDiscountVM.filteredOffers[index].endDate ?? "")}",
|
||||
// labelText: "Valid till ${_formatDateString(offersAndDiscountVM.filteredOffers[index].endDate)}",
|
||||
backgroundColor: AppColors.chipBgColor,
|
||||
textColor: AppColors.textColor,
|
||||
icon: AppAssets.calendar,
|
||||
iconSize: 12.h,
|
||||
iconColor: AppColors.textColor,
|
||||
isEnglishOnly: true,
|
||||
labelPadding: EdgeInsetsDirectional.only(start: 4.w, end: 8.w),
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
CustomButton(
|
||||
text: 'Buy Now',
|
||||
icon: AppAssets.shoppingCart,
|
||||
iconColor: AppColors.whiteColor,
|
||||
onPressed: () {
|
||||
// Handle buy now action
|
||||
},
|
||||
backgroundColor: AppColors.primaryRedColor,
|
||||
borderColor: AppColors.primaryRedColor,
|
||||
textColor: AppColors.whiteColor,
|
||||
fontSize: 14.f,
|
||||
isBold: true,
|
||||
borderRadius: 12.r,
|
||||
height: 40.h,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24.w),
|
||||
child: Utils.getNoDataWidget(context, noDataText: LocaleKeys.noDataAvailable.tr(context: context)),
|
||||
),
|
||||
SizedBox(height: 24.h),
|
||||
],
|
||||
).paddingSymmetrical(24.h, 0.h);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,113 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
|
||||
import 'package:hmg_patient_app_new/theme/colors.dart';
|
||||
|
||||
class OffersAndDiscountTypeSelectionWidget extends StatelessWidget {
|
||||
final List<String> selectedOffers;
|
||||
final Function(List<String>) onOfferClicked;
|
||||
|
||||
const OffersAndDiscountTypeSelectionWidget({
|
||||
super.key,
|
||||
required this.selectedOffers,
|
||||
required this.onOfferClicked,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
_buildOfferTypeCard(
|
||||
context: context,
|
||||
title: "All Offers",
|
||||
facilityType: 'All Offers',
|
||||
isSelected: selectedOffers.contains('All Offers'),
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
_buildOfferTypeCard(
|
||||
context: context,
|
||||
title: 'Female',
|
||||
facilityType: 'Female',
|
||||
isSelected: selectedOffers.contains('Female'),
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
_buildOfferTypeCard(
|
||||
context: context,
|
||||
title: 'OB-Gyne',
|
||||
facilityType: 'OB-Gyne',
|
||||
isSelected: selectedOffers.contains('OB-Gyne'),
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
_buildOfferTypeCard(
|
||||
context: context,
|
||||
title: 'Dermatology',
|
||||
facilityType: 'Dermatology',
|
||||
isSelected: selectedOffers.contains('Dermatology'),
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
_buildOfferTypeCard(
|
||||
context: context,
|
||||
title: 'Radiology',
|
||||
facilityType: 'Radiology',
|
||||
isSelected: selectedOffers.contains('Radiology'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOfferTypeCard({
|
||||
required BuildContext context,
|
||||
required String title,
|
||||
required String facilityType,
|
||||
required bool isSelected,
|
||||
}) {
|
||||
return AnimatedContainer(
|
||||
duration: Duration(milliseconds: 200),
|
||||
padding: EdgeInsets.symmetric(vertical: 12.h, horizontal: 16.w),
|
||||
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
||||
color: isSelected ? AppColors.bgRedLightColor : AppColors.whiteColor,
|
||||
borderRadius: 12.r,
|
||||
hasShadow: true,
|
||||
side: isSelected ? BorderSide(color: AppColors.primaryRedColor, width: 2) : BorderSide(color: AppColors.borderGrayColor, width: 1),
|
||||
),
|
||||
child: Center(
|
||||
child: title.toText14(
|
||||
color: isSelected ? AppColors.primaryRedColor : AppColors.textColor,
|
||||
weight: isSelected ? FontWeight.w700 : FontWeight.w500,
|
||||
isBold: isSelected,
|
||||
),
|
||||
),
|
||||
).onPress(() {
|
||||
List<String> updatedSelection = List.from(selectedOffers);
|
||||
|
||||
if (facilityType == 'All Offers') {
|
||||
if (updatedSelection.contains('All Offers')) {
|
||||
updatedSelection.clear();
|
||||
} else {
|
||||
updatedSelection.clear();
|
||||
updatedSelection.add('All Offers');
|
||||
}
|
||||
} else {
|
||||
// Remove "All Offers" if any specific type is selected
|
||||
updatedSelection.remove('All Offers');
|
||||
|
||||
// Toggle the clicked facility type
|
||||
if (updatedSelection.contains(facilityType)) {
|
||||
updatedSelection.remove(facilityType);
|
||||
// If no selection left, default to "All Offers"
|
||||
if (updatedSelection.isEmpty) {
|
||||
updatedSelection.add('All Offers');
|
||||
}
|
||||
} else {
|
||||
updatedSelection.add(facilityType);
|
||||
}
|
||||
}
|
||||
|
||||
onOfferClicked(updatedSelection);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,135 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
|
||||
import 'package:hmg_patient_app_new/presentation/offers_and_discounts/offer_and_discounts_full_screen_swiper_page.dart';
|
||||
import 'package:hmg_patient_app_new/theme/colors.dart';
|
||||
import 'package:smooth_corner/smooth_corner.dart';
|
||||
|
||||
class OffersAndDiscountsCarousel extends StatefulWidget {
|
||||
const OffersAndDiscountsCarousel({super.key});
|
||||
|
||||
@override
|
||||
State<OffersAndDiscountsCarousel> createState() => _OffersAndDiscountsCarouselState();
|
||||
}
|
||||
|
||||
class _OffersAndDiscountsCarouselState extends State<OffersAndDiscountsCarousel> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
Timer? _autoScrollTimer;
|
||||
|
||||
// List of offer images - you can add more images here
|
||||
final List<String> offerImages = [
|
||||
'assets/images/offersanddiscounts/img1.png',
|
||||
'assets/images/offersanddiscounts/img2.png',
|
||||
'assets/images/offersanddiscounts/img3.png',
|
||||
'assets/images/offersanddiscounts/img1.png',
|
||||
'assets/images/offersanddiscounts/img2.png',
|
||||
'assets/images/offersanddiscounts/img3.png',
|
||||
'assets/images/offersanddiscounts/img1.png',
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// _startAutoScroll();
|
||||
}
|
||||
|
||||
void _startAutoScroll() {
|
||||
_autoScrollTimer = Timer.periodic(const Duration(milliseconds: 50), (timer) {
|
||||
if (_scrollController.hasClients) {
|
||||
final maxScroll = _scrollController.position.maxScrollExtent;
|
||||
final currentScroll = _scrollController.position.pixels;
|
||||
final delta = 1.0; // Scroll speed
|
||||
|
||||
if (currentScroll >= maxScroll) {
|
||||
// Reset to beginning for infinite scroll effect
|
||||
_scrollController.jumpTo(0);
|
||||
} else {
|
||||
_scrollController.animateTo(
|
||||
currentScroll + delta,
|
||||
duration: const Duration(milliseconds: 50),
|
||||
curve: Curves.linear,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_autoScrollTimer?.cancel();
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 79.h,
|
||||
child: ListView.separated(
|
||||
controller: _scrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: EdgeInsets.symmetric(horizontal: 0.w),
|
||||
itemCount: offerImages.length * 100,
|
||||
itemBuilder: (context, index) {
|
||||
final imageIndex = index % offerImages.length;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const OfferAndDiscountsFullScreenSwiperPage()),
|
||||
);
|
||||
},
|
||||
child: _buildOfferItem(offerImages[imageIndex]),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => SizedBox(width: 12.w),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOfferItem(String imagePath) {
|
||||
return Container(
|
||||
width: 79.w,
|
||||
height: 79.h,
|
||||
decoration: ShapeDecoration(
|
||||
shape: SmoothRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(13.r),
|
||||
smoothness: 0.6,
|
||||
side: BorderSide(
|
||||
color: Color(0xff2E3039),
|
||||
width: 2.w,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(13.r),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: AppColors.textColor, width: 2.w),
|
||||
borderRadius: BorderRadius.circular(11.r),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(2.0),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(11.r),
|
||||
child: Image.asset(
|
||||
imagePath,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
color: AppColors.greyColor,
|
||||
child: Icon(
|
||||
Icons.image,
|
||||
color: AppColors.textColorLight,
|
||||
size: 32.h,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||