From 2349925ed148836f1e8cea3eee5d1e6a0ff99636 Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Tue, 10 Feb 2026 17:47:39 +0300 Subject: [PATCH] information dialog added. --- lib/extensions/context_extension.dart | 6 + .../create_incident_request_page.dart | 39 ++++ .../single_item_drop_down_menu.dart | 187 +++++++++--------- .../swipe_module/dialoge/info_dialog.dart | 86 ++++++++ 4 files changed, 228 insertions(+), 90 deletions(-) create mode 100644 lib/new_views/swipe_module/dialoge/info_dialog.dart diff --git a/lib/extensions/context_extension.dart b/lib/extensions/context_extension.dart index b7bb652a..d214cf98 100644 --- a/lib/extensions/context_extension.dart +++ b/lib/extensions/context_extension.dart @@ -4,6 +4,7 @@ import 'package:provider/provider.dart'; import 'package:test_sa/controllers/providers/api/user_provider.dart'; import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/new_views/swipe_module/dialoge/confirm_dialog.dart'; +import 'package:test_sa/new_views/swipe_module/dialoge/info_dialog.dart'; import '../controllers/providers/settings/setting_provider.dart'; @@ -36,6 +37,11 @@ extension BuildContextExtension on BuildContext { builder: (BuildContext cxt) => ConfirmDialog(message: message, onTap: onTap, title: title, okTitle: okTitle), ); + void showInfoDialog(String message, {String? title, VoidCallback? onTap, String? okTitle, List content = const []}) => showDialog( + context: this, + builder: (BuildContext cxt) => InfoDialog(message: message, onTap: onTap, title: title, okTitle: okTitle, content: content), + ); + Future showBottomSheet(Widget childWidget, {bool? isDismissible, String? title}) => showModalBottomSheet( context: this, useSafeArea: true, diff --git a/lib/modules/incident_module/create_incident_request_page.dart b/lib/modules/incident_module/create_incident_request_page.dart index a49a4922..71d6e3a8 100644 --- a/lib/modules/incident_module/create_incident_request_page.dart +++ b/lib/modules/incident_module/create_incident_request_page.dart @@ -35,6 +35,7 @@ import 'package:test_sa/views/widgets/equipment/asset_picker.dart'; import 'package:test_sa/views/widgets/images/multi_image_picker.dart'; import '../../models/new_models/department.dart'; +import '../../new_views/swipe_module/dialoge/info_dialog.dart'; import 'incident_attachment_model.dart'; class CreateIncidentRequestPage extends StatefulWidget { @@ -330,6 +331,14 @@ class _CreateIncidentRequestPageState extends State { if (value == null) return "Mandatory"; return null; }, + infoCallback: () { + context.showInfoDialog("Select the category that best describes the incident:", content: [ + InfoContent(title: "Medical Equipment", message: "Malfunction, failure, or issue involving a device."), + InfoContent(title: "EHS (Environment, Health, Safety)", message: "Incidents related to safety hazards, spills, injuries."), + InfoContent(title: "Late Delivery", message: "Delayed receipt of medical equipment, materials, or supplies."), + InfoContent(title: "Mis-conduct", message: "FLOW staff behavior that violates rules, ethics, or procedures."), + ]); + }, backgroundColor: AppColor.fieldBgColor(context), showAsBottomSheet: true, onSelect: (value) { @@ -385,6 +394,9 @@ class _CreateIncidentRequestPageState extends State { if ((value ?? "").isEmpty) return "Mandatory"; return null; }, + suffixIcon: const Icon(Icons.info_outline_rounded, color: AppColor.neutral120).paddingOnly(end: 8).onPress(() { + context.showInfoDialog("Brief explanation of the incident, e.g., “Damaged patient monitor screen“"); + }), style: Theme.of(context).textTheme.titleMedium, backgroundColor: AppColor.fieldBgColor(context), labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)), @@ -403,6 +415,10 @@ class _CreateIncidentRequestPageState extends State { if ((value ?? "").isEmpty) return "Mandatory"; return null; }, + suffixIcon: const Icon(Icons.info_outline_rounded, color: AppColor.neutral120).paddingOnly(end: 8).onPress(() { + context.showInfoDialog( + "Provide a clear and factual description of the incident, including how the issue was identified. Ex. “The ECG cable connector was damaged due to continuos forcfull disconnetionnby end-users. Multiple bent pins were noted during inspection after repeated complains of poor signal quality“"); + }), style: Theme.of(context).textTheme.titleMedium, backgroundColor: AppColor.fieldBgColor(context), labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)), @@ -430,6 +446,19 @@ class _CreateIncidentRequestPageState extends State { if (value == null) return "Mandatory"; return null; }, + infoCallback: () { + context.showInfoDialog("Select the primary cause that best explains why the incident occurred. If “Other“ is selected, specify the details in the text box provided.", content: [ + InfoContent( + title: "Misuse", + message: "Clear, improper use of the device by the end-user despite available knowledge or instructions. Misuse implies that correct handling was possible but not followed."), + InfoContent(title: "Lack of Awareness", message: "End-user was unaware of proper handling or procedures due to missing information or communication."), + InfoContent(title: "Insufficient Training", message: "Formal training on the device was not provided, incomplete, or outdated, leading to misuse."), + InfoContent( + title: "Utility Failure", + message: "A utility issue that disrupted the medical device operation, degrade, performance, or create unsafe conditions. such as (power outage, HVAC, or environment)."), + InfoContent(title: "Other", message: "Any cause that does not fit into the categories above (please specify)."), + ]); + }, backgroundColor: AppColor.fieldBgColor(context), showAsBottomSheet: true, onSelect: (value) { @@ -469,6 +498,9 @@ class _CreateIncidentRequestPageState extends State { if (value == null) return "Mandatory"; return null; }, + infoCallback: () { + context.showInfoDialog("Ensure to provide information of person in Charge (e.g., Charge Nurse, Unit Team Leader), not a general staff member.", content: []); + }, backgroundColor: AppColor.fieldBgColor(context), showAsBottomSheet: true, onSelect: (value) { @@ -502,6 +534,10 @@ class _CreateIncidentRequestPageState extends State { if (involvedPerson?.value == 3 && (value ?? "").isEmpty) return "Mandatory"; return null; }, + suffixIcon: const Icon(Icons.info_outline_rounded, color: AppColor.neutral120).paddingOnly(end: 8).onPress(() { + context.showInfoDialog( + "Required if the person is an Employee. Ensure the correct Employee ID of the Person in Charge (e.g., Charge Nurse, Unit Team Leader) is provided, not a general staff member. (Ex. 34567)"); + }), style: Theme.of(context).textTheme.titleMedium, backgroundColor: AppColor.fieldBgColor(context), labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)), @@ -520,6 +556,9 @@ class _CreateIncidentRequestPageState extends State { if (involvedPerson?.value == 3 && (value ?? "").isEmpty) return "Mandatory"; return null; }, + suffixIcon: const Icon(Icons.info_outline_rounded, color: AppColor.neutral120).paddingOnly(end: 8).onPress(() { + context.showInfoDialog("State the end-user Employee/Patient Full name. Required if the person is an Employee. (Ex. Mary-Anne Joseph)"); + }), style: Theme.of(context).textTheme.titleMedium, backgroundColor: AppColor.fieldBgColor(context), labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)), diff --git a/lib/new_views/common_widgets/single_item_drop_down_menu.dart b/lib/new_views/common_widgets/single_item_drop_down_menu.dart index ce7f7838..3774a4eb 100644 --- a/lib/new_views/common_widgets/single_item_drop_down_menu.dart +++ b/lib/new_views/common_widgets/single_item_drop_down_menu.dart @@ -31,6 +31,7 @@ class SingleItemDropDownMenu exte final Color? titleTextColor; // Now nullable final bool? loading; // Now nullable final int? requestById; + final VoidCallback? infoCallback; final String? Function(T?)? validator; final AutovalidateMode? autovalidateMode; final DropDownMatchType matchBy; @@ -46,6 +47,7 @@ class SingleItemDropDownMenu exte this.disableValue, this.enabled = true, this.height, + this.infoCallback, this.showAsBottomSheet = false, this.showAsFullScreenDialog = true, this.staticData, // Provide a default empty list @@ -187,7 +189,7 @@ class _SingleItemDropDownMenuState( - value: _selectedItem, - iconSize: 24, - isDense: true, - icon: const SizedBox.shrink(), - dropdownColor: AppColor.neutral100, - elevation: 0, - isExpanded: true, - hint: Text( - isEmpty || widget.enabled == false ? context.translation.noDataFound : context.translation.select, - style: Theme.of(context).textTheme.bodyLarge, - ), - style: TextStyle(color: Theme.of(context).primaryColor, fontSize: 12), - underline: const SizedBox.shrink(), - onChanged: (widget.enabled == false || widget.showAsBottomSheet || widget.showAsFullScreenDialog) - ? null - : (T? newValue) { - // Now accepts nullable values - final isNull = newValue?.identifier == "-1"; - setState(() { - _selectedItem = isNull ? null : newValue; - }); - fieldState.didChange(isNull ? null : newValue); - widget.onSelect!(isNull ? null : newValue); - }, - items: ((X == NullableLoadingProvider) ? widget.staticData : provider?.items as List)?.map>((value) { - return DropdownMenuItem( - value: value, - child: Text( - value.name ?? "", // Null-aware operator for value.name - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500, color: context.isDark ? AppColor.white10 : AppColor.black10), + if (widget.enabled) const PositionedDirectional(end: 0, child: Icon(Icons.keyboard_arrow_down_rounded)), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + widget.title, + style: Theme.of(context).textTheme.bodySmall?.copyWith(color: context.isDark ? null : widget.titleTextColor ?? AppColor.neutral20, fontWeight: FontWeight.w500), + ), + DropdownButton( + value: _selectedItem, + iconSize: 24, + isDense: true, + icon: const SizedBox.shrink(), + dropdownColor: AppColor.neutral100, + elevation: 0, + isExpanded: true, + hint: Text( + isEmpty || widget.enabled == false ? context.translation.noDataFound : context.translation.select, + style: Theme.of(context).textTheme.bodyLarge, ), - ); - }).toList(), - ), + style: TextStyle(color: Theme.of(context).primaryColor, fontSize: 12), + underline: const SizedBox.shrink(), + onChanged: (widget.enabled == false || widget.showAsBottomSheet || widget.showAsFullScreenDialog) + ? null + : (T? newValue) { + // Now accepts nullable values + final isNull = newValue?.identifier == "-1"; + setState(() { + _selectedItem = isNull ? null : newValue; + }); + fieldState.didChange(isNull ? null : newValue); + widget.onSelect!(isNull ? null : newValue); + }, + items: ((X == NullableLoadingProvider) ? widget.staticData : provider?.items as List)?.map>((value) { + return DropdownMenuItem( + value: value, + child: Text( + value.name ?? "", // Null-aware operator for value.name + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500, color: context.isDark ? AppColor.white10 : AppColor.black10), + ), + ); + }).toList(), + ), + ], + ).onPress(widget.enabled && widget.showAsFullScreenDialog + ? () { + openDialog(fieldState); + } + : widget.enabled + ? (widget.showAsBottomSheet + ? () async { + final selectedT = await showModalBottomSheet( + // Specify return type + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + clipBehavior: Clip.antiAliasWithSaveLayer, + builder: (BuildContext context) => SelectionBottomSheet( + // Specify generic type + items: ((X == NullableLoadingProvider) ? widget.staticData : provider?.items as List) ?? [], + // Provide default empty list if null + selectedItem: _selectedItem, + title: widget.title, + showCancel: widget.showCancel, + onSelect: (selectedT) { + setState(() { + _selectedItem = selectedT; + }); + fieldState.didChange(selectedT); + widget.onSelect!(selectedT); + }, + builderString: (emp) => emp?.name ?? "", // Null-aware operator for emp.name + ), + ); + // if (selectedT != null) { + // setState(() { + // _selectedItem = selectedT; + // }); + // widget.onSelect!(selectedT); // Non-null assertion after null check + // } + } + : null) + : null), ], - ).onPress(widget.enabled && widget.showAsFullScreenDialog - ? () { - openDialog(fieldState); - } - : widget.enabled - ? (widget.showAsBottomSheet - ? () async { - final selectedT = await showModalBottomSheet( - // Specify return type - context: context, - isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - clipBehavior: Clip.antiAliasWithSaveLayer, - builder: (BuildContext context) => SelectionBottomSheet( - // Specify generic type - items: ((X == NullableLoadingProvider) ? widget.staticData : provider?.items as List) ?? [], - // Provide default empty list if null - selectedItem: _selectedItem, - title: widget.title, - showCancel: widget.showCancel, - onSelect: (selectedT) { - setState(() { - _selectedItem = selectedT; - }); - fieldState.didChange(selectedT); - widget.onSelect!(selectedT); - }, - builderString: (emp) => emp?.name ?? "", // Null-aware operator for emp.name - ), - ); - // if (selectedT != null) { - // setState(() { - // _selectedItem = selectedT; - // }); - // widget.onSelect!(selectedT); // Non-null assertion after null check - // } - } - : null) - : null), + ).expanded, + if (widget.infoCallback != null) const Icon(Icons.info_outline_rounded, color: AppColor.neutral120).onPress(widget.infoCallback), ], ), ), diff --git a/lib/new_views/swipe_module/dialoge/info_dialog.dart b/lib/new_views/swipe_module/dialoge/info_dialog.dart new file mode 100644 index 00000000..90057706 --- /dev/null +++ b/lib/new_views/swipe_module/dialoge/info_dialog.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:test_sa/extensions/context_extension.dart'; +import 'package:test_sa/extensions/int_extensions.dart'; +import 'package:test_sa/extensions/text_extensions.dart'; +import 'package:test_sa/extensions/widget_extensions.dart'; +import 'package:test_sa/new_views/app_style/app_color.dart'; +import 'package:test_sa/new_views/common_widgets/app_filled_button.dart'; + +class InfoContent { + final String? title; + final String? message; + + InfoContent({this.title, this.message}); +} + +class InfoDialog extends StatelessWidget { + final String? title; + final String? message; + final List content; + + final String? okTitle; + final VoidCallback? onTap; + final VoidCallback? onCloseTap; + + const InfoDialog({Key? key, this.title, this.message, this.content = const [], this.okTitle, this.onTap, this.onCloseTap}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: AppColor.background(context), + shape: const RoundedRectangleBorder(), + insetPadding: const EdgeInsets.only(left: 21, right: 21), + child: Padding( + padding: const EdgeInsets.only(left: 20, right: 20, top: 18, bottom: 28), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + title ?? "Information", + style: TextStyle(fontSize: 24, fontWeight: FontWeight.w600, color: AppColor.headingTextColor(context), height: 35 / 24, letterSpacing: -0.96), + ).paddingOnly(top: 16), + ), + IconButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.close), + color: AppColor.iconColor(context), + constraints: const BoxConstraints(), + onPressed: () => onCloseTap ?? Navigator.pop(context), + // onPressed: () => Navigator.pop(context), + ) + ], + ), + message != null ? message!.bodyText(context).custom(color: context.isDark ? AppColor.neutral10 : AppColor.neutral20) : const SizedBox(), + 8.height, + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (cxt, index) => RichText( + text: TextSpan( + text: "${content[index].title}: ", + style: AppTextStyles.heading6.copyWith(fontWeight: FontWeight.w600, color: AppColor.textColor(context)), + children: [ + TextSpan(text: content[index].message, style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral20)), + ], + ), + ), + separatorBuilder: (cxt, index) => 6.height, + itemCount: content.length), + 24.height, + AppFilledButton( + label: okTitle ?? "OK", + onPressed: onTap ?? () => Navigator.pop(context), + textColor: Colors.white, + //color: Ap.green, + ), + ], + ), + ), + ); + } +}