medical file changes

pull/109/head
aamir-csol 1 month ago
parent 0f17a655ed
commit 19365375f6

@ -53,6 +53,7 @@ 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/common_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart';
import 'package:hmg_patient_app_new/widgets/expandable_list_widget.dart';
import 'package:hmg_patient_app_new/widgets/input_widget.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
@ -148,20 +149,6 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 16.h),
TextInputWidget(
labelText: LocaleKeys.search.tr(context: context),
hintText: "Type any record".needTranslation,
controller: TextEditingController(),
keyboardType: TextInputType.number,
isEnable: true,
prefix: null,
autoFocus: false,
isBorderAllowed: false,
isAllowLeadingIcon: true,
padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 8.h),
leadingIcon: AppAssets.student_card,
).paddingSymmetrical(24.w, 0.0),
SizedBox(height: 16.h),
Container(
width: double.infinity,
@ -236,8 +223,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
iconColor: insuranceVM.isInsuranceExpired ? AppColors.primaryRedColor : AppColors.successColor,
textColor: insuranceVM.isInsuranceExpired ? AppColors.primaryRedColor : AppColors.successColor,
iconSize: 12.w,
backgroundColor:
insuranceVM.isInsuranceExpired ? AppColors.primaryRedColor.withOpacity(0.1) : AppColors.successColor.withOpacity(0.1),
backgroundColor: insuranceVM.isInsuranceExpired ? AppColors.primaryRedColor.withOpacity(0.1) : AppColors.successColor.withOpacity(0.1),
labelPadding: EdgeInsetsDirectional.only(end: 8.w),
);
}),
@ -248,27 +234,108 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
),
).paddingSymmetrical(24.w, 0.0),
SizedBox(height: 16.h),
Consumer<MedicalFileViewModel>(builder: (context, medicalFileVM, child) {
return Column(
children: [
CustomTabBar(
activeTextColor: AppColors.primaryRedColor,
activeBackgroundColor: AppColors.primaryRedColor.withValues(alpha: .1),
tabs: [
CustomTabBarModel(AppAssets.myFilesBottom, LocaleKeys.general.tr(context: context).needTranslation),
CustomTabBarModel(AppAssets.insurance, LocaleKeys.insurance.tr(context: context)),
CustomTabBarModel(AppAssets.requests, LocaleKeys.request.tr(context: context).needTranslation),
CustomTabBarModel(AppAssets.more, "More".needTranslation),
],
onTabChange: (index) {
medicalFileVM.onTabChanged(index);
},
).paddingSymmetrical(24.w, 0.0),
SizedBox(height: 24.h),
getSelectedTabData(medicalFileVM.selectedTabIndex),
],
);
}),
TextInputWidget(
labelText: LocaleKeys.search.tr(context: context),
hintText: "Type any record".needTranslation,
controller: TextEditingController(),
keyboardType: TextInputType.number,
isEnable: true,
prefix: null,
autoFocus: false,
isBorderAllowed: false,
isAllowLeadingIcon: true,
padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 8.h),
leadingIcon: AppAssets.student_card,
).paddingSymmetrical(24.w, 0.0),
SizedBox(height: 16.h),
// Using CustomExpandableList
CustomExpandableList(
expansionMode: ExpansionMode.exactlyOne,
dividerColor: Color(0xFF2B353E1A),
items: [
ExpandableListItem(
title: "Medical Services".toText20(weight: FontWeight.w500),
children: [
SizedBox(height: 10.h),
getSelectedTabData(0),
],
),
ExpandableListItem(
title: "Medical Reports".toText20(
weight: FontWeight.w500,
),
children: [
SizedBox(height: 10.h),
getSelectedTabData(2),
]),
ExpandableListItem(
title: "Insurance & Payments".toText20(weight: FontWeight.w500),
children: [
SizedBox(height: 10.h),
getSelectedTabData(1),
],
),
ExpandableListItem(
title: "Tracker & Others".toText20(weight: FontWeight.w500),
children: [
Text("Blood Report"),
SizedBox(height: 8),
Text("X-Ray Report"),
],
),
],
theme: ExpandableListTheme.custom(
defaultTrailingIcon: Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, height: 22.h, width: 22.w, iconColor: AppColors.textColor),
),
).paddingSymmetrical(16.w, 0.0),
// ExpansionTileList(
// shrinkWrap: true,
// trailing: Icon(Icons.arrow_drop_down),
// expansionMode: ExpansionMode.exactlyOne,
// children: <ExpansionTile>[
// ExpansionTile(
// title: "Medical Services".toText20(weight: FontWeight.w600, color: AppColors.textColor),
// iconColor: Color(0xFF2E3039),
// trailing: Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, height: 22.h, width: 22.w),
// children: <Widget>[Text('Child 1')],
// ),
// ExpansionTile(
// title: "Medical Reports".toText20(weight: FontWeight.w600, color: AppColors.textColor),
// children: <Widget>[Text('Child 2')],
// ),
// ExpansionTile(
// title: "Insurance & Payments".toText20(weight: FontWeight.w600, color: AppColors.textColor),
// children: <Widget>[Text('Child 3')],
// ),
// ExpansionTile(
// title: "Tracker & Others".toText20(weight: FontWeight.w600, color: AppColors.textColor),
// children: <Widget>[Text('Child 4')],
// ),
// ],
// ),
// Consumer<MedicalFileViewModel>(builder: (context, medicalFileVM, child) {
// return Column(
// children: [
// CustomTabBar(
// activeTextColor: AppColors.primaryRedColor,
// activeBackgroundColor: AppColors.primaryRedColor.withValues(alpha: .1),
// tabs: [
// CustomTabBarModel(AppAssets.myFilesBottom, LocaleKeys.general.tr(context: context).needTranslation),
// CustomTabBarModel(AppAssets.insurance, LocaleKeys.insurance.tr(context: context)),
// CustomTabBarModel(AppAssets.requests, LocaleKeys.request.tr(context: context).needTranslation),
// CustomTabBarModel(AppAssets.more, "More".needTranslation),
// ],
// onTabChange: (index) {
// medicalFileVM.onTabChanged(index);
// },
// ).paddingSymmetrical(24.w, 0.0),
// SizedBox(height: 24.h),
// getSelectedTabData(medicalFileVM.selectedTabIndex),
// ],
// );
// }),
],
),
);
@ -324,7 +391,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
"Appointments & visits".needTranslation.toText18(isBold: true),
"Appointments & visits".needTranslation.toText16(weight: FontWeight.w500, letterSpacing: -0.2),
Row(
children: [
LocaleKeys.viewAll.tr().toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500),
@ -333,7 +400,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
],
),
],
).paddingSymmetrical(24.w, 0.h).onPress(() {
).paddingSymmetrical(0.w, 0.h).onPress(() {
Navigator.of(context).push(
CustomPageRoute(
page: MyAppointmentsPage(),
@ -356,8 +423,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
? Container(
padding: EdgeInsets.all(12.w),
width: MediaQuery.of(context).size.width,
decoration:
RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 12.r, hasShadow: true),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 12.r, hasShadow: true),
child: Column(
children: [
Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon, width: 32.h, height: 32.h),
@ -415,10 +481,10 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
},
separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 12.h),
),
).paddingSymmetrical(24.w, 0);
).paddingSymmetrical(0.w, 0);
}),
SizedBox(height: 24.h),
"Lab & Radiology".needTranslation.toText18(isBold: true).paddingSymmetrical(24.w, 0.h),
"Lab & Radiology".needTranslation.toText16(weight: FontWeight.w500, letterSpacing: -0.2),
SizedBox(height: 16.h),
Row(
children: [
@ -437,11 +503,12 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
);
}),
),
SizedBox(width: 16.h),
SizedBox(width: 8.w),
Expanded(
child: LabRadCard(
icon: AppAssets.radiology_icon,
labelText: LocaleKeys.radiology.tr(context: context),
labelText: "${LocaleKeys.radiology.tr(context: context)} Results".needTranslation,
// labOrderTests: ["Complete blood count", "Creatinine", "Blood Sugar",
// labOrderTests: ["Chest X-ray", "Abdominal Ultrasound", "Dental X-ray"],
labOrderTests: [],
isLoading: false,
@ -454,13 +521,13 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
}),
),
],
).paddingSymmetrical(24.w, 0.h),
).paddingSymmetrical(0.w, 0.h),
SizedBox(height: 24.h),
"Active Medications & Prescriptions".needTranslation.toText18(isBold: true).paddingSymmetrical(24.w, 0.h),
"Active Medications & Prescriptions".needTranslation.toText16(weight: FontWeight.w500, letterSpacing: -0.2),
SizedBox(height: 16.h),
Consumer<PrescriptionsViewModel>(builder: (context, prescriptionVM, child) {
return prescriptionVM.isPrescriptionsOrdersLoading
? const CommonShimmerWidget().paddingSymmetrical(24.w, 0.h)
? const CommonShimmerWidget().paddingSymmetrical(0.w, 0.h)
: prescriptionVM.patientPrescriptionOrders.isNotEmpty
? Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
@ -503,13 +570,11 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
spacing: 3.w,
runSpacing: 4.w,
children: [
AppCustomChipWidget(
labelText: prescriptionVM.patientPrescriptionOrders[index].clinicDescription!),
AppCustomChipWidget(labelText: prescriptionVM.patientPrescriptionOrders[index].clinicDescription!),
AppCustomChipWidget(
icon: AppAssets.doctor_calendar_icon,
labelText: DateUtil.formatDateToDate(
DateUtil.convertStringToDate(
prescriptionVM.patientPrescriptionOrders[index].appointmentDate),
DateUtil.convertStringToDate(prescriptionVM.patientPrescriptionOrders[index].appointmentDate),
false,
),
),
@ -522,19 +587,13 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
Transform.flip(
flipX: appState.isArabic(),
child: Utils.buildSvgWithAssets(
icon: AppAssets.forward_arrow_icon_small,
width: 15.w,
height: 15.h,
fit: BoxFit.contain,
iconColor: AppColors.textColor)),
icon: AppAssets.forward_arrow_icon_small, width: 15.w, height: 15.h, fit: BoxFit.contain, iconColor: AppColors.textColor)),
],
).onPress(() {
prescriptionVM.setPrescriptionsDetailsLoading();
Navigator.of(context).push(
CustomPageRoute(
page: PrescriptionDetailPage(
isFromAppointments: false,
prescriptionsResponseModel: prescriptionVM.patientPrescriptionOrders[index]),
page: PrescriptionDetailPage(isFromAppointments: false, prescriptionsResponseModel: prescriptionVM.patientPrescriptionOrders[index]),
),
);
}),
@ -607,14 +666,14 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
width: 62.w,
height: 62.h,
),
).paddingSymmetrical(24.w, 0.h);
).paddingSymmetrical(0.w, 0.h);
}),
SizedBox(height: 24.h),
//My Doctor Section
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
LocaleKeys.myDoctor.tr(context: context).toText18(isBold: true),
LocaleKeys.myDoctor.tr(context: context).toText16(weight: FontWeight.w500, letterSpacing: -0.2),
Row(
children: [
LocaleKeys.viewAll.tr().toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500),
@ -630,7 +689,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
);
}),
],
).paddingSymmetrical(24.w, 0.h),
).paddingSymmetrical(0.w, 0.h),
SizedBox(height: 16.h),
Consumer<MyAppointmentsViewModel>(builder: (context, myAppointmentsVM, child) {
return myAppointmentsVM.isPatientMyDoctorsLoading
@ -644,10 +703,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
fit: BoxFit.cover,
).circle(100).toShimmer2(isShow: true, radius: 50.r),
SizedBox(height: 8.h),
("Dr. John Smith Smith Smith")
.toString()
.toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2)
.toShimmer2(isShow: true),
("Dr. John Smith Smith Smith").toString().toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2).toShimmer2(isShow: true),
],
)
: myAppointmentsVM.patientMyDoctorsList.isEmpty
@ -665,7 +721,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
width: 62.w,
height: 62.h,
),
).paddingSymmetrical(24.w, 0.h)
).paddingSymmetrical(0.w, 0.h)
: SizedBox(
height: 100.h,
child: ListView.separated(
@ -730,10 +786,10 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
},
separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 8.h),
),
).paddingSymmetrical(24.w, 0);
).paddingSymmetrical(0.w, 0);
}),
SizedBox(height: 24.h),
"Others".needTranslation.toText18(isBold: true).paddingSymmetrical(24.w, 0.h),
"Others".needTranslation.toText16(weight: FontWeight.w500, letterSpacing: -0.2),
SizedBox(height: 16.h),
GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
@ -786,7 +842,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
);
}),
],
).paddingSymmetrical(24.w, 0.0),
).paddingSymmetrical(0.w, 0.0),
SizedBox(height: 24.h),
],
);
@ -801,7 +857,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
labOrder: null,
index: index,
isLoading: true,
).paddingSymmetrical(24.w, 0.0)
).paddingSymmetrical(0.w, 0.0)
: insuranceVM.patientInsuranceList.isNotEmpty
? PatientInsuranceCard(
insuranceCardDetailsModel: insuranceVM.patientInsuranceList.first,
@ -828,14 +884,9 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
text: "${LocaleKeys.updateInsurance.tr(context: context)} ${LocaleKeys.updateInsuranceSubtitle.tr(context: context)}",
onPressed: () {
insuranceViewModel.setIsInsuranceUpdateDetailsLoading(true);
insuranceViewModel.getPatientInsuranceDetailsForUpdate(appState.getAuthenticatedUser()!.patientId.toString(),
appState.getAuthenticatedUser()!.patientIdentificationNo.toString());
showCommonBottomSheetWithoutHeight(context,
child: PatientInsuranceCardUpdateCard(),
callBackFunc: () {},
title: "",
isCloseButtonVisible: false,
isFullScreen: false);
insuranceViewModel.getPatientInsuranceDetailsForUpdate(
appState.getAuthenticatedUser()!.patientId.toString(), appState.getAuthenticatedUser()!.patientIdentificationNo.toString());
showCommonBottomSheetWithoutHeight(context, child: PatientInsuranceCardUpdateCard(), callBackFunc: () {}, title: "", isCloseButtonVisible: false, isFullScreen: false);
},
backgroundColor: AppColors.bgGreenColor.withOpacity(0.20),
borderColor: AppColors.bgGreenColor.withOpacity(0.0),
@ -847,7 +898,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
height: isFoldable ? 50.h : 40.h,
).paddingOnly(left: 12.w, right: 12.w, bottom: 12.h),
),
).paddingSymmetrical(24.w, 0.h);
).paddingSymmetrical(0.w, 0.h);
}),
SizedBox(height: 10.h),
GridView(
@ -902,7 +953,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
iconSize: 36.w,
),
],
).paddingSymmetrical(24.w, 0.0),
).paddingSymmetrical(0.w, 0.0),
SizedBox(height: 16.h),
],
);
@ -915,12 +966,12 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
? PatientSickLeaveCard(
patientSickLeavesResponseModel: PatientSickLeavesResponseModel(),
isLoading: true,
).paddingSymmetrical(24.w, 0.0)
).paddingSymmetrical(0.w, 0.0)
: medicalFileVM.patientSickLeaveList.isNotEmpty
? PatientSickLeaveCard(
patientSickLeavesResponseModel: medicalFileVM.patientSickLeaveList.first,
isLoading: false,
).paddingSymmetrical(24.w, 0.0)
).paddingSymmetrical(0.w, 0.0)
: Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
@ -934,7 +985,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
width: 62.w,
height: 62.h,
),
).paddingSymmetrical(24.w, 0.h);
).paddingSymmetrical(0.w, 0.h);
}),
SizedBox(height: 16.h),
GridView(
@ -987,7 +1038,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
);
}),
],
).paddingSymmetrical(24.w, 0.0),
).paddingSymmetrical(0.w, 0.0),
SizedBox(height: 24.h),
],
);

@ -27,55 +27,62 @@ class LabRadCard extends StatelessWidget {
Widget build(BuildContext context) {
AppState appState = getIt.get<AppState>();
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 12.r, hasShadow: false),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 18.r, hasShadow: false),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Utils.buildSvgWithAssets(
icon: icon,
width: 40.w,
height: 40.h,
fit: BoxFit.contain,
fit: BoxFit.cover,
).toShimmer2(isShow: false, radius: 12.r),
SizedBox(width: 8.w),
Flexible(child: labelText.toText14(isBold: true).toShimmer2(isShow: false, radius: 6.r, height: 32.h)),
],
),
SizedBox(height: 16.h),
labOrderTests.isNotEmpty
? ListView.separated(
scrollDirection: Axis.vertical,
padding: EdgeInsets.zero,
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemBuilder: (cxt, index) {
return labOrderTests[index]
.toText12(isBold: true, maxLine: 1)
.toShimmer2(isShow: false, radius: 6.r, height: 24.h, width: 120.w)
.toShimmer2(isShow: isLoading);
},
separatorBuilder: (cxt, index) => SizedBox(height: 8.h),
itemCount: 3,
)
: "You don't have any records yet".needTranslation.toText13(
color: AppColors.greyTextColor, isCenter: true),
SizedBox(height: 16.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox.shrink(),
Flexible(
child: labelText.toText12(isBold: true, maxLine: 2),
),
Transform.flip(
flipX: appState.isArabic(),
child: Utils.buildSvgWithAssets(
icon: AppAssets.forward_arrow_icon_small, width: 15.w, height: 15.h, fit: BoxFit.contain, iconColor: AppColors.textColor)
.toShimmer2(isShow: false, radius: 12.r),
child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon_small, width: 10.w, height: 10.h, fit: BoxFit.contain, iconColor: AppColors.textColor),
),
],
)
),
// SizedBox(height: 16.h),
// labOrderTests.isNotEmpty
// ? ListView.separated(
// scrollDirection: Axis.vertical,
// padding: EdgeInsets.zero,
// physics: NeverScrollableScrollPhysics(),
// shrinkWrap: true,
// itemBuilder: (cxt, index) {
// return labOrderTests[index]
// .toText12(isBold: true, maxLine: 1)
// .toShimmer2(isShow: false, radius: 6.r, height: 24.h, width: 120.w)
// .toShimmer2(isShow: isLoading);
// },
// separatorBuilder: (cxt, index) => SizedBox(height: 8.h),
// itemCount: 3,
// )
// : "You don't have any records yet".needTranslation.toText13(
// color: AppColors.greyTextColor, isCenter: true),
// SizedBox(height: 16.h),
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// SizedBox.shrink(),
// Transform.flip(
// flipX: appState.isArabic(),
// child: Utils.buildSvgWithAssets(
// icon: AppAssets.forward_arrow_icon_small, width: 15.w, height: 15.h, fit: BoxFit.contain, iconColor: AppColors.textColor)
// .toShimmer2(isShow: false, radius: 12.r),
// ),
// ],
// )
],
).paddingAll(16.w),
).paddingAll(12.w),
);
}
}

@ -0,0 +1,301 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
// ==================== MAIN CUSTOM WIDGET ====================
class CustomExpandableList extends StatefulWidget {
final List<ExpandableListItem> items;
final ExpansionMode expansionMode;
final EdgeInsetsGeometry? itemPadding;
final EdgeInsetsGeometry? contentPadding;
final Color? dividerColor;
final double dividerHeight;
final bool showDividers;
final Duration animationDuration;
final Curve animationCurve;
final ExpandableListTheme? theme;
final Function(int index, bool isExpanded)? onItemToggled;
const CustomExpandableList({
Key? key,
required this.items,
this.expansionMode = ExpansionMode.multiple,
this.itemPadding,
this.contentPadding,
this.dividerColor,
this.dividerHeight = 1.0,
this.showDividers = true,
this.animationDuration = const Duration(milliseconds: 300),
this.animationCurve = Curves.easeInOut,
this.theme,
this.onItemToggled,
}) : super(key: key);
@override
_CustomExpandableListState createState() => _CustomExpandableListState();
}
class _CustomExpandableListState extends State<CustomExpandableList> {
late List<bool> _expandedStates;
@override
void initState() {
super.initState();
_expandedStates = List<bool>.generate(
widget.items.length,
(index) => widget.items[index].initiallyExpanded,
);
}
void _toggleItem(int index) {
setState(() {
if (widget.expansionMode == ExpansionMode.exactlyOne) {
// Close all others, open this one
for (int i = 0; i < _expandedStates.length; i++) {
_expandedStates[i] = (i == index) ? !_expandedStates[i] : false;
}
} else if (widget.expansionMode == ExpansionMode.atMostOne) {
// Toggle this one, close if opening another
bool wasExpanded = _expandedStates[index];
for (int i = 0; i < _expandedStates.length; i++) {
if (i == index) {
_expandedStates[i] = !wasExpanded;
} else if (!wasExpanded) {
_expandedStates[i] = false;
}
}
} else {
// Multiple - just toggle the clicked item
_expandedStates[index] = !_expandedStates[index];
}
});
widget.onItemToggled?.call(index, _expandedStates[index]);
}
@override
Widget build(BuildContext context) {
final theme = widget.theme ?? ExpandableListTheme.defaultTheme(context);
return Column(
children: List.generate(widget.items.length, (index) {
final item = widget.items[index];
final isExpanded = _expandedStates[index];
final isLast = index == widget.items.length - 1;
return Column(
children: [
_ExpandableListItemWidget(
title: item.title,
isExpanded: isExpanded,
onTap: () => _toggleItem(index),
padding: widget.itemPadding,
contentPadding: widget.contentPadding,
theme: theme,
leading: item.leading,
trailing: item.trailing,
backgroundColor: item.backgroundColor,
expandedBackgroundColor: item.expandedBackgroundColor,
animationDuration: widget.animationDuration,
animationCurve: widget.animationCurve,
children: item.children,
),
if (widget.showDividers && !isLast)
Divider(
height: widget.dividerHeight,
color: widget.dividerColor ?? theme.dividerColor,
).paddingSymmetrical(12.h,0),
],
);
}),
);
}
}
// ==================== SUPPORTING WIDGETS ====================
class _ExpandableListItemWidget extends StatelessWidget {
final Widget title;
final List<Widget> children;
final bool isExpanded;
final VoidCallback onTap;
final EdgeInsetsGeometry? padding;
final EdgeInsetsGeometry? contentPadding;
final ExpandableListTheme theme;
final Widget? leading;
final Widget? trailing;
final Color? backgroundColor;
final Color? expandedBackgroundColor;
final Duration animationDuration;
final Curve animationCurve;
const _ExpandableListItemWidget({
super.key,
required this.title,
required this.children,
required this.isExpanded,
required this.onTap,
this.padding,
this.contentPadding,
required this.theme,
this.leading,
this.trailing,
this.backgroundColor,
this.expandedBackgroundColor,
required this.animationDuration,
required this.animationCurve,
});
@override
Widget build(BuildContext context) {
return Container(
color: isExpanded
? (expandedBackgroundColor ?? theme.expandedBackgroundColor)
: (backgroundColor ?? theme.backgroundColor),
child: Column(
children: [
// Header
InkWell(
onTap: onTap,
child: Padding(
padding: padding ?? theme.itemPadding,
child: Row(
children: [
if (leading != null) ...[
leading!,
SizedBox(width: theme.leadingSpacing),
],
Expanded(child: title),
if (trailing != null)
trailing!
else
AnimatedRotation(
turns: isExpanded ? 0.5 : 0.0,
duration: animationDuration,
curve: animationCurve,
child: theme.defaultTrailingIcon,
),
],
),
),
),
// Content
AnimatedSize(
duration: animationDuration,
curve: animationCurve,
child: Container(
constraints: BoxConstraints(
minHeight: isExpanded ? 0.0 : 0.0,
maxHeight: isExpanded ? double.infinity : 0.0,
),
child: isExpanded
? Padding(
padding: contentPadding ?? theme.contentPadding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
),
)
: SizedBox.shrink(),
),
),
],
),
);
}
}
// ==================== DATA MODELS ====================
class ExpandableListItem {
final Widget title;
final List<Widget> children;
final Widget? leading;
final Widget? trailing;
final bool initiallyExpanded;
final Color? backgroundColor;
final Color? expandedBackgroundColor;
const ExpandableListItem({
required this.title,
required this.children,
this.leading,
this.trailing,
this.initiallyExpanded = false,
this.backgroundColor,
this.expandedBackgroundColor,
});
}
// ==================== THEME ====================
class ExpandableListTheme {
final Color backgroundColor;
final Color expandedBackgroundColor;
final Color dividerColor;
final Widget defaultTrailingIcon;
final EdgeInsetsGeometry itemPadding;
final EdgeInsetsGeometry contentPadding;
final double leadingSpacing;
const ExpandableListTheme({
required this.backgroundColor,
required this.expandedBackgroundColor,
required this.dividerColor,
required this.defaultTrailingIcon,
required this.itemPadding,
required this.contentPadding,
this.leadingSpacing = 12.0,
});
factory ExpandableListTheme.defaultTheme(BuildContext context) {
final textColor = Theme.of(context).textTheme.bodyLarge?.color ?? Colors.black;
return ExpandableListTheme(
backgroundColor: Colors.transparent,
expandedBackgroundColor: Colors.grey.shade50,
dividerColor: Colors.grey.shade300,
defaultTrailingIcon: Icon(
Icons.keyboard_arrow_down,
color: textColor,
size: 24,
),
itemPadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
contentPadding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 16.0),
);
}
factory ExpandableListTheme.custom({
Color? backgroundColor,
Color? expandedBackgroundColor,
Color? dividerColor,
Widget? defaultTrailingIcon,
EdgeInsetsGeometry? itemPadding,
EdgeInsetsGeometry? contentPadding,
double? leadingSpacing,
}) {
return ExpandableListTheme(
backgroundColor: backgroundColor ?? Colors.transparent,
expandedBackgroundColor: expandedBackgroundColor ?? Colors.grey.shade50,
dividerColor: dividerColor ?? Colors.grey.shade300,
defaultTrailingIcon: defaultTrailingIcon ??
Icon(Icons.keyboard_arrow_down, color: Colors.black, size: 24),
itemPadding: itemPadding ?? const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
contentPadding: contentPadding ?? const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 16.0),
leadingSpacing: leadingSpacing ?? 12.0,
);
}
}
// ==================== ENUMS ====================
enum ExpansionMode {
multiple, // Multiple items can be expanded
exactlyOne, // Exactly one item expanded at a time
atMostOne, // Zero or one item expanded at a time
}
Loading…
Cancel
Save