information dialog added.

design_3.0_incident_module
Sikander Saleem 17 hours ago
parent 6e1b63cef8
commit 2349925ed1

@ -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<InfoContent> 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,

@ -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<CreateIncidentRequestPage> {
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<CreateIncidentRequestPage> {
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<CreateIncidentRequestPage> {
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<CreateIncidentRequestPage> {
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<CreateIncidentRequestPage> {
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<CreateIncidentRequestPage> {
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<CreateIncidentRequestPage> {
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)),

@ -31,6 +31,7 @@ class SingleItemDropDownMenu<T extends Base, X extends LoadingListNotifier> 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<T extends Base, X extends LoadingListNotifier> 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<T extends Base, X extends LoadingListNotifier
},
child: Container(
height: widget.height ?? 60.toScreenHeight,
padding: EdgeInsets.symmetric(horizontal: 16.toScreenWidth),
padding: EdgeInsets.only(left: 16.toScreenWidth, right: 8.toScreenWidth),
decoration: BoxDecoration(
color: context.isDark && (widget.enabled == false || isEmpty)
? AppColor.neutral20
@ -197,99 +199,104 @@ class _SingleItemDropDownMenuState<T extends Base, X extends LoadingListNotifier
borderRadius: BorderRadius.circular(10),
boxShadow: widget.showShadow ? [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10)] : null,
),
child: Stack(
alignment: Alignment.center,
child: Row(
children: [
if (widget.enabled) const PositionedDirectional(end: 0, child: Icon(Icons.keyboard_arrow_down_rounded)),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
Stack(
alignment: Alignment.center,
children: [
Text(
widget.title,
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: context.isDark ? null : widget.titleTextColor ?? AppColor.neutral20, fontWeight: FontWeight.w500),
),
DropdownButton<T>(
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<T>)?.map<DropdownMenuItem<T>>((value) {
return DropdownMenuItem<T>(
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<T>(
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<T>)?.map<DropdownMenuItem<T>>((value) {
return DropdownMenuItem<T>(
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<T?>(
// Specify return type
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
),
),
clipBehavior: Clip.antiAliasWithSaveLayer,
builder: (BuildContext context) => SelectionBottomSheet<T>(
// Specify generic type
items: ((X == NullableLoadingProvider) ? widget.staticData : provider?.items as List<T>) ?? [],
// 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<T?>(
// Specify return type
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
),
),
clipBehavior: Clip.antiAliasWithSaveLayer,
builder: (BuildContext context) => SelectionBottomSheet<T>(
// Specify generic type
items: ((X == NullableLoadingProvider) ? widget.staticData : provider?.items as List<T>) ?? [],
// 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),
],
),
),

@ -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<InfoContent> 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,
),
],
),
),
);
}
}
Loading…
Cancel
Save