Skip to content

Instantly share code, notes, and snippets.

@naiplawan
Created February 20, 2026 03:42
Show Gist options
  • Select an option

  • Save naiplawan/6ac7dd61159553a7df58927fdf2257fd to your computer and use it in GitHub Desktop.

Select an option

Save naiplawan/6ac7dd61159553a7df58927fdf2257fd to your computer and use it in GitHub Desktop.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pwa_ocs_flutter/shared/widgets/custom_form_field.dart';
import 'package:pwa_ocs_flutter/shared/widgets/loading_view.dart';
import 'package:pwa_ocs_flutter/shared/widgets/photo_widget.dart';
import 'package:pwa_ocs_flutter/shared/widgets/section_header.dart';
import 'package:pwa_ocs_flutter/shared/widgets/step_navigation.dart';
import 'package:pwa_ocs_flutter/ui/core/ui/main_scaffold.dart';
import 'package:pwa_ocs_flutter/ui/core/ui/low_pressure_water_meter_form_dialog.dart';
import 'package:pwa_ocs_flutter/ui/core/ui/low_pressure_water_pump_form_dialog.dart';
import 'package:pwa_ocs_flutter/features/p602/domain/entities/models/low_pressure_water.dart';
import 'package:pwa_ocs_flutter/features/p602/domain/entities/models/low_pressure_water_pump.dart';
import 'package:pwa_ocs_flutter/features/p600/presentation/bloc/p600_viewmodel.dart';
import 'package:pwa_ocs_flutter/features/p602/presentation/bloc/p602_viewmodel.dart';
import 'package:pwa_ocs_flutter/shared/constants/form_options.dart';
class P602 extends StatelessWidget {
P602({super.key});
Future<void> _handleSubmit(
BuildContext context,
P602ViewModel controller,
P600State args,
) async {
final result = await controller.onSubmit();
if (!context.mounted) return;
if (result == 'success') {
showDialog(
context: context,
barrierDismissible: false,
builder: (dialogContext) => AlertDialog(
title: const Text('ส่งข้อมูลสำเร็จ', textAlign: TextAlign.center),
content: Column(
mainAxisSize: MainAxisSize.min,
spacing: 16,
children: const [
Icon(Icons.check_circle, size: 64, color: Colors.green),
Text('กำลังกลับไปหน้ารายการ...'),
],
),
),
);
await Future.delayed(const Duration(seconds: 2));
if (!context.mounted) return;
Navigator.of(context).pushReplacementNamed('/600', arguments: args);
} else if (result == 'invalid') {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: const [
Icon(Icons.warning_amber_rounded, color: Colors.white),
SizedBox(width: 12),
Expanded(child: Text('กรุณากรอกข้อมูลให้ครบทุกขั้นตอน')),
],
),
backgroundColor: Colors.orange[600],
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.all(16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
duration: const Duration(seconds: 4),
action: SnackBarAction(
label: 'ปิด',
textColor: Colors.white,
onPressed: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
},
),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
spacing: 12,
children: const [
Icon(Icons.error_outline, color: Colors.white),
Expanded(child: Text('ส่งข้อมูลไม่สำเร็จ')),
],
),
backgroundColor: Colors.red[600],
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.all(16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
action: SnackBarAction(
label: 'ปิด',
textColor: Colors.white,
onPressed: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
},
),
),
);
}
}
@override
Widget build(context) {
final args = ModalRoute.of(context)!.settings.arguments as P600State;
return ChangeNotifierProvider<P602ViewModel>(
create: (context) => P602ViewModel(
args.item.plantId,
args.item.operationDateTimestamp.split('T')[0],
args.item.workScheduleId,
initialStep: args.item.submanu[args.index].activitySeq,
),
child: Consumer<P602ViewModel>(
builder: (context, controller, child) {
if (controller.isLoading) {
return MainScaffold(
title: args.item.plantName,
subtitle: args.item.operationDate,
child: const LoadingView(),
);
} else if (controller.error != null) {
return MainScaffold(
title: args.item.plantName,
subtitle: args.item.operationDate,
child: Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: Text('Error: ${controller.error}'),
),
),
);
}
final items = [
_buildBase(controller),
if (controller.lowPressureId != null) ...[
if (controller.currentStep == 1) ..._buildStep1(controller, args),
if (controller.currentStep == 2)
..._buildStep2(context, controller, args),
if (controller.currentStep == 3)
..._buildStep3(context, controller, args),
],
];
return MainScaffold(
title: args.item.plantName,
subtitle: args.item.operationDate,
child: Form(
key: controller.key,
child: Container(
padding: EdgeInsets.symmetric(vertical: 16.0),
child: Column(
spacing: 16.0,
children: [
Expanded(
child: RefreshIndicator(
onRefresh: controller.fetchAndUpdateCache,
child: ListView.separated(
padding: EdgeInsets.symmetric(
vertical: 24,
horizontal: 16,
),
itemCount: items.length,
itemBuilder: (context, index) => items[index],
separatorBuilder: (context, index) =>
SizedBox(height: 24.0),
),
),
),
StepNavigator(
onNext: controller.next,
current: controller.currentStep,
onPrevious: controller.prev,
total: P602ViewModel.MAX_STEP,
submitting: controller.isSubmitting,
submitable: controller.isFormValid(),
onSubmit: () => _handleSubmit(context, controller, args),
),
],
),
),
),
);
},
),
);
}
Widget _buildBase(P602ViewModel controller) => CustomDropdownField(
label: "เลือกสถานี",
key: Key('low_pressure_id'),
value: controller.lowPressureId,
onChanged: controller.selectLowPressureWater,
items: controller.lowPressureWaterList
.map(
(option) => DropdownMenuItem(
child: Text(option.lowPressureName),
value: option.lowPressureId,
),
)
.toList(),
);
List<Widget> _buildStep1(P602ViewModel controller, P600State args) => [
SectionHeader(title: _activityTitle(args, 0)),
CustomSelectorField(
key: Key('cleanliness_status'),
initialValue: controller.cleanlinessStatus,
enabled: controller.lowPressureId != null,
onSelectionChanged: controller.selectCleanlinessStatus,
options: normalAbnormalCodedOptions,
),
if (controller.cleanlinessStatus == "I") ...[
CustomFormField(
label: "โปรดระบุ",
key: Key('cleanliness_remark'),
enabled: controller.lowPressureId != null,
controller: controller.cleanlinessRemark,
),
],
PhotoUploadWidget(
title: "ถ่ายรูปสภาพแวดล้อม",
maxPhotos: 5,
photos: controller.environmentPhotos,
onPhotoTaken: controller.addEnvironmentPhoto,
onRemovePhoto: controller.removeEnvironmentPhoto,
),
];
List<Widget> _buildStep2(
BuildContext context,
P602ViewModel controller,
P600State args,
) {
final selected = _findSelectedLowPressure(controller);
return [
SectionHeader(title: _activityTitle(args, 1)),
if (selected == null)
const Text('ไม่พบข้อมูลสถานีที่เลือก')
else
Wrap(
runSpacing: 16,
children: selected.waterPump.asMap().entries.map((entry) {
final index = entry.key;
final pump = entry.value;
return SizedBox(
width: (MediaQuery.of(context).size.width - 32) / 4,
child: TextButton(
style: TextButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
onPressed: () {
final initialPressureWaterPump = controller
.getDraftLowPressureWaterPump(pump);
showDialog(
context: context,
builder: (dialogContext) {
final formKey = GlobalKey<FormState>();
return LowPressureWaterPumpFormDialog(
formKey: formKey,
pressureWaterPump: initialPressureWaterPump,
onSubmit: (lowPressurePump) async {
try {
// Show loading indicator
showDialog(
context: dialogContext,
barrierDismissible: false,
builder: (_) => const Center(
child: CircularProgressIndicator(),
),
);
// Call API to update pump
await controller.updateLowPressureWaterPump(
index,
lowPressurePump,
);
// Close loading indicator
if (dialogContext.mounted) {
Navigator.of(dialogContext).pop();
}
// Show success message
if (dialogContext.mounted) {
ScaffoldMessenger.of(dialogContext).showSnackBar(
const SnackBar(
content: Text('บันทึกข้อมูลเครื่องสูบสำเร็จ'),
backgroundColor: Colors.green,
duration: Duration(seconds: 2),
),
);
}
} catch (e) {
// Close loading indicator
if (dialogContext.mounted) {
Navigator.of(dialogContext).pop();
}
// Show error message
if (dialogContext.mounted) {
ScaffoldMessenger.of(dialogContext).showSnackBar(
SnackBar(
content: Text(
'เกิดข้อผิดพลาด: ${e.toString()}',
),
backgroundColor: Colors.red,
duration: const Duration(seconds: 3),
),
);
}
}
},
);
},
);
},
child: Column(
children: [
Image.asset(
_getPumpImagePath(pump),
width: 64,
height: 64,
fit: BoxFit.contain,
alignment: Alignment.center,
),
SizedBox(height: 8),
Text(
pump.waterPumpName ?? '',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
height: 1.4,
color: Color(0xFF191C20),
),
textAlign: TextAlign.center,
),
],
),
),
);
}).toList(),
),
];
}
List<Widget> _buildStep3(
BuildContext context,
P602ViewModel controller,
P600State args,
) {
final selected = _findSelectedLowPressure(controller);
return [
SectionHeader(title: _activityTitle(args, 2)),
if (selected == null)
const Text('ไม่พบข้อมูลสถานีที่เลือก')
else
Wrap(
runSpacing: 16,
children: selected.waterMeter.map((meter) {
return SizedBox(
width: (MediaQuery.of(context).size.width - 32) / 4,
child: TextButton(
style: TextButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
onPressed: () {
final initialLowPressureWaterMeter = controller
.getDraftLowPressureWaterMeter(meter);
showDialog(
context: context,
builder: (dialogContext) {
final formKey = GlobalKey<FormState>();
return LowPressureWaterMeterFormDialog(
formKey: formKey,
pressureWaterMeter: initialLowPressureWaterMeter,
onSubmit: (lowPressureMeter) async {
try {
// Show loading indicator
showDialog(
context: dialogContext,
barrierDismissible: false,
builder: (_) => const Center(
child: CircularProgressIndicator(),
),
);
// Call API to update meter
await controller.updateMeter(lowPressureMeter);
// Close loading indicator
if (dialogContext.mounted) {
Navigator.of(dialogContext).pop();
}
// Show success message
if (dialogContext.mounted) {
ScaffoldMessenger.of(dialogContext).showSnackBar(
const SnackBar(
content: Text('บันทึกข้อมูลมาตรวัดน้ำสำเร็จ'),
backgroundColor: Colors.green,
duration: Duration(seconds: 2),
),
);
}
} catch (e) {
// Close loading indicator
if (dialogContext.mounted) {
Navigator.of(dialogContext).pop();
}
// Show error message
if (dialogContext.mounted) {
ScaffoldMessenger.of(dialogContext).showSnackBar(
SnackBar(
content: Text(
'เกิดข้อผิดพลาด: ${e.toString()}',
),
backgroundColor: Colors.red,
duration: const Duration(seconds: 3),
),
);
}
}
},
);
},
);
},
child: Column(
children: [
Image.asset(
'images/raw-water-meter${meter.rawWaterMeterStatus != null && meter.rawWaterMeterStatus!.isNotEmpty ? '-${meter.rawWaterMeterStatus!.toLowerCase()}' : ""}.png',
width: 64,
height: 64,
fit: BoxFit.contain,
alignment: Alignment.center,
),
SizedBox(height: 8),
Text(
meter.rawWaterMeterName ?? '',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
height: 1.4,
color: Color(0xFF191C20),
),
textAlign: TextAlign.center,
),
],
),
),
);
}).toList(),
),
];
}
String _activityTitle(P600State args, int index) {
if (index >= 0 && index < args.item.submanu.length) {
final title = args.item.submanu[index].activityName;
return title;
}
return 'ไม่ระบุหัวข้อ';
}
LowPressureWater? _findSelectedLowPressure(P602ViewModel controller) {
for (final item in controller.lowPressureWaterList) {
if (item.lowPressureId == controller.lowPressureId) {
return item;
}
}
return null;
}
/// Get the correct image asset path for water pump based on status
/// Logic (Priority order):
/// 1. machineCondition = "1" → raw-water-pump-1.png (พักเครื่อง)
/// 2. machineCondition = "2" → raw-water-pump-2.png (สำรอง)
/// 3. machineCondition = "3" → raw-water-pump-3.png (ใช้งานไม่ได้)
/// 4. machineCondition = "4" → raw-water-pump-4.png (แจ้งซ่อม)
/// 5. machinePowerStatus = "A" → Red (ใช้งาน)
/// 6. Default → Blue (ไม่มีข้อมูล)
String _getPumpImagePath(LowPressureWaterPump pump) {
// Priority 1: Check machinePowerStatus = "A" → Red (Active/Running)
if (pump.machinePowerStatus == 'A') {
return 'images/raw-water-pump-a.png'; // ใช้งาน - สีแดง
}
// Priority 2: Check machineCondition for specific image
if (pump.machineCondition != null && pump.machineCondition!.isNotEmpty) {
final status = pump.machineCondition!;
if (status == '1') {
return 'images/raw-water-pump-1.png'; // พักเครื่อง - สีเขียว
} else if (status == '2') {
return 'images/raw-water-pump-2.png'; // สำรอง - สีเขียว
} else if (status == '3') {
return 'images/raw-water-pump-3.png'; // ใช้งานไม่ได้ - สีเทา
} else if (status == '4') {
return 'images/raw-water-pump-4.png'; // แจ้งซ่อม - สีเทา
}
}
// Default: Blue - ไม่มีข้อมูล
return 'images/raw-water-pump-i.png';
}
}
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pwa_ocs_flutter/core/services/status_mapping_service.dart';
import 'package:pwa_ocs_flutter/shared/widgets/custom_form_field.dart';
import 'package:pwa_ocs_flutter/shared/widgets/loading_view.dart';
import 'package:pwa_ocs_flutter/shared/widgets/photo_widget.dart';
import 'package:pwa_ocs_flutter/shared/widgets/section_header.dart';
import 'package:pwa_ocs_flutter/shared/widgets/step_navigation.dart';
import 'package:pwa_ocs_flutter/shared/widgets/p607_filter_update_modal.dart';
import 'package:pwa_ocs_flutter/ui/core/ui/main_scaffold.dart';
import 'package:pwa_ocs_flutter/features/p600/presentation/bloc/p600_viewmodel.dart';
import 'package:pwa_ocs_flutter/features/p607/presentation/bloc/p607_viewmodel.dart';
class P607 extends StatelessWidget {
P607({super.key});
@override
Widget build(context) {
final args = ModalRoute.of(context)?.settings.arguments as P600State?;
if (args == null) {
return const Scaffold(
body: Center(child: Text('Error: Missing required arguments')),
);
}
return ChangeNotifierProvider<P607ViewModel>(
create: (context) => P607ViewModel(
plantId: args.item.plantId,
operationDate: args.item.operationDateTimestamp.split('T')[0],
workScheduleId: args.item.workScheduleId,
initialStep: args.item.submanu[args.index].activitySeq,
),
child: Consumer<P607ViewModel>(
builder: (context, controller, child) {
if (controller.isLoading) {
return MainScaffold(
title: args.item.plantName,
subtitle: args.item.operationDate,
child: const LoadingView(),
);
}
if (controller.error != null) {
return MainScaffold(
title: args.item.plantName,
subtitle: args.item.operationDate,
child: Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(controller.error!),
),
),
);
}
final items = [
..._buildBase(controller),
if (controller.selectedPlant != null) ...[
if (controller.step == 1) ..._buildStep11(controller, args),
if (controller.step == 2) ..._buildStep12(controller, args),
if (controller.step == 3)
..._buildStep2(context, controller, args),
if (controller.step == 4) ..._buildStep3(controller, args),
if (controller.step == 5) ..._buildStep4(controller, args),
],
];
return MainScaffold(
title: args.item.plantName,
subtitle: args.item.operationDate,
child: Form(
key: controller.key,
child: Container(
padding: EdgeInsets.symmetric(vertical: 16.0),
child: Column(
spacing: 16.0,
children: [
Expanded(
child: RefreshIndicator(
onRefresh: controller.fetchAndUpdateCache,
child: ListView.separated(
padding: EdgeInsets.symmetric(
vertical: 24,
horizontal: 16,
),
itemCount: items.length,
itemBuilder: (context, index) => items[index],
separatorBuilder: (context, index) =>
SizedBox(height: 24.0),
),
),
),
Builder(
builder: (context) => StepNavigator(
total: P607ViewModel.MAX_STEP,
submitable: controller.hasPlants,
submitting: controller.isSubmitting,
onNext: () {
// Navigation doesn't typically need error handling
controller.next();
},
current: controller.step,
onPrevious: () {
// Navigation doesn't typically need error handling
controller.prev();
},
onSubmit: () async {
await controller.executeWithErrorHandling(
context,
() async {
// Submit the form data to the server
await controller.submitForm();
// Show success dialog
if (context.mounted) {
showDialog(
context: context,
barrierDismissible: false,
builder: (dialogContext) => const AlertDialog(
title: Text(
'ส่งข้อมูลสำเร็จ',
textAlign: TextAlign.center,
),
content: Column(
mainAxisSize: MainAxisSize.min,
spacing: 16,
children: [
Icon(
Icons.check_circle,
size: 64,
color: Colors.green,
),
Text('กำลังกลับไปหน้ารายการ...'),
],
),
),
);
// Wait 2 seconds before navigating back
await Future.delayed(
const Duration(seconds: 2),
);
if (context.mounted) {
Navigator.of(context).pushReplacementNamed(
'/600',
arguments: args,
);
}
}
},
operationContext: 'P607.submit',
);
},
),
),
],
),
),
),
);
},
),
);
}
List<Widget> _buildBase(P607ViewModel controller) => [
Builder(
builder: (context) {
final uniquePlants = <String>{}; // Track objectNames we've seen
final items = <DropdownMenuItem<String>>[];
// Create dropdown items from water treatment plants, handling duplicate objectNames
for (final plant in controller.plantItems) {
final objectName = plant.objectName.trim();
if (objectName.isEmpty || uniquePlants.contains(objectName)) {
continue;
}
uniquePlants.add(objectName);
items.add(
DropdownMenuItem<String>(
value: objectName,
child: Text(objectName),
),
);
}
final currentValue = controller.selectedPlant?.objectName.trim();
final dropdownValue = items.any((entry) => entry.value == currentValue)
? currentValue
: null;
return CustomDropdownField<String>(
label: "เลือกระบบผลิต",
value: dropdownValue,
onChanged: controller.plantItems.isNotEmpty
? (value) {
// Find the plant that matches the selected objectName
final selectedPlant = controller.plantItems.firstWhere(
(plant) => plant.objectName.trim() == value?.trim(),
orElse: () => controller.plantItems.first,
);
// Select the plant directly by objectName
controller.selectPlant(selectedPlant.objectName);
}
: null,
enabled: controller.plantItems.isNotEmpty,
hintText: controller.plantItems.isNotEmpty
? null
: 'ไม่พบข้อมูลระบบผลิต',
items: items,
);
},
),
];
List<Widget> _buildStep11(P607ViewModel controller, P600State args) => [
SectionHeader(title: "การเก็บตัวอย่างน้ำก่อนกรอง"),
Builder(
builder: (context) => PhotoUploadWidget(
title: 'การเก็บตัวอย่างน้ำก่อนกรอง',
photos: controller.currentEquipmentPhotos,
maxPhotos: 5,
enabled: controller.selectedPlant != null,
onPhotoTaken: (photo) async {
await controller.executeWithErrorHandling(
context,
() async => controller.addEquipmentPhoto(photo),
operationContext: 'P607.addEquipmentPhoto',
);
},
onRemovePhoto: (index) async {
await controller.executeWithErrorHandling(
context,
() async => controller.removeEquipmentPhoto(index),
operationContext: 'P607.removeEquipmentPhoto',
);
},
),
),
];
List<Widget> _buildStep12(P607ViewModel controller, P600State args) => [
SectionHeader(title: "ตรวจสอบประตูน้ำและอุปกรณ์ภายในโรงกรอง"),
CustomSelectorField<bool>(
label: 'สถานะ',
enabled: controller.selectedPlant != null,
initialValue: controller.inspectStatus,
onSelectionChanged: controller.updateInspectStatus,
options: [
RadioOption(value: true, label: "ปกติ"),
RadioOption(value: false, label: "ไม่ปกติ"),
],
),
if (controller.inspectStatus == false) ...[
CustomFormField(
label: 'หมายเหตุ (โปรดระบุ)',
enabled: controller.selectedPlant != null,
controller: controller.inspectReasonController,
maxLines: 3,
),
],
];
List<Widget> _buildStep2(
BuildContext context,
P607ViewModel controller,
P600State args,
) {
final filters = controller.filterGroupItems;
return [
SectionHeader(title: _activityTitle(args, 1)),
if (filters.isEmpty)
const Text('ไม่พบข้อมูลหม้อกรอง')
else
Wrap(
runSpacing: 16,
children: filters.asMap().entries.map((entry) {
final filter = entry.value;
return SizedBox(
width: (MediaQuery.of(context).size.width - 32) / 4,
child: TextButton(
style: TextButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
onPressed: () async {
// Select the filter first
controller.selectFilter(filter.filterName);
// Get draft filter data
final draftFilter = controller.getDraftFilter(filter);
// Show the modal and wait for result
if (context.mounted) {
final updatedFilter = await P607FilterUpdateModal.show(
context,
draftFilter,
controller.filterId,
args.item.operationDateTimestamp.split('T')[0],
args.item.workScheduleId,
filter.seq ?? 1, // Pass seq from filter, default to 1
);
// Update filter if user submitted changes
if (updatedFilter != null && context.mounted) {
await controller.executeWithErrorHandling(
context,
() async => controller.updateFilter(updatedFilter),
operationContext: 'P607.updateFilter',
);
}
}
},
child: Column(
children: [
Image.asset(
'images/filter_machine.png',
width: 64,
height: 64,
fit: BoxFit.contain,
alignment: Alignment.center,
),
SizedBox(height: 8),
Text(
controller.displayFilterLabel(filter.filterName),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
height: 1.4,
color: Color(0xFF191C20),
),
textAlign: TextAlign.center,
),
],
),
),
);
}).toList(),
),
];
}
List<Widget> _buildStep3(P607ViewModel controller, P600State args) {
final filters = controller.filterGroupItems;
return [
SectionHeader(title: _activityTitle(args, 2)),
if (filters.isEmpty)
const Text('ไม่พบข้อมูลหม้อกรอง')
else
DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade300, width: 1),
),
child: Column(
children: [
Container(
padding: const EdgeInsets.symmetric(
vertical: 12,
horizontal: 16,
),
decoration: const BoxDecoration(
color: Color(0xFF035BA9),
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
),
child: Row(
children: const [
_HeaderCell(label: 'กะทำงาน', flex: 1),
_HeaderCell(label: 'หม้อกรอง', flex: 2),
_HeaderCell(label: 'สถานะ', flex: 3),
],
),
),
...filters.asMap().entries.map((entry) {
final index = entry.key;
final filter = entry.value;
final statusCode = controller.filterStatusFor(
filter.filterName,
);
final statusLabel = controller.filterStatusLabel(statusCode);
final statusRemark = controller.filterStatusRemark(
filter.filterName,
);
final remarkText =
statusRemark != null && statusRemark.isNotEmpty
? '$statusLabel ($statusRemark)'
: statusLabel;
return Container(
decoration: BoxDecoration(
color: index.isEven ? Colors.white : Colors.grey[50],
border: Border(
bottom: BorderSide(color: Colors.grey.shade200, width: 1),
),
),
padding: const EdgeInsets.symmetric(
vertical: 12,
horizontal: 16,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
flex: 1,
child: Text(
controller.currentShiftLabel,
style: const TextStyle(fontSize: 14),
textAlign: TextAlign.center,
),
),
Expanded(
flex: 2,
child: Text(
controller.displayFilterNumber(filter.filterName),
style: const TextStyle(fontSize: 14),
textAlign: TextAlign.center,
),
),
Expanded(
flex: 3,
child: Row(
children: [
Expanded(
child: Text(
remarkText,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF191C20),
),
),
),
_StatusDot(statusCode: statusCode),
],
),
),
],
),
);
}),
],
),
),
];
}
List<Widget> _buildStep4(P607ViewModel controller, P600State args) => [
SectionHeader(title: _activityTitle(args, 3)),
Builder(
builder: (context) => PhotoUploadWidget(
title: 'การเก็บตัวอย่างน้ำหลังกรอง',
photos: controller.currentAfterFilteringPhotos,
maxPhotos: 5,
enabled: controller.selectedPlant != null,
onPhotoTaken: (photo) async {
await controller.executeWithErrorHandling(
context,
() async => controller.addAfterFilteringPhoto(photo),
operationContext: 'P607.addAfterFilteringPhoto',
);
},
onRemovePhoto: (index) async {
await controller.executeWithErrorHandling(
context,
() async => controller.removeAfterFilteringPhoto(index),
operationContext: 'P607.removeAfterFilteringPhoto',
);
},
),
),
];
String _activityTitle(P600State args, int index) {
if (index >= 0 && index < args.item.submanu.length) {
return args.item.submanu[index].activityName;
}
return 'ไม่ระบุหัวข้อ';
}
}
class _HeaderCell extends StatelessWidget {
final String label;
final int flex;
const _HeaderCell({required this.label, this.flex = 1});
@override
Widget build(BuildContext context) {
return Expanded(
flex: flex,
child: Text(
label,
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
);
}
}
class _StatusDot extends StatelessWidget {
final String? statusCode;
final StatusMappingService _statusMapping = StatusMappingService();
_StatusDot({required this.statusCode});
@override
Widget build(BuildContext context) {
return Container(
width: 14,
height: 14,
margin: const EdgeInsets.only(left: 8),
decoration: BoxDecoration(
color: _statusMapping.getStatusColor(statusCode),
shape: BoxShape.circle,
),
);
}
}
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pwa_ocs_flutter/ui/core/ui/high_pressure_water_meter_form_dialog.dart';
import 'package:pwa_ocs_flutter/ui/core/ui/high_pressure_water_pump_form_dialog.dart';
import 'package:pwa_ocs_flutter/features/p609/domain/entities/high_pressure_water.dart';
import 'package:pwa_ocs_flutter/shared/widgets/custom_form_field.dart';
import 'package:pwa_ocs_flutter/shared/widgets/loading_view.dart';
import 'package:pwa_ocs_flutter/shared/widgets/section_header.dart';
import 'package:pwa_ocs_flutter/shared/widgets/view_only_step_navigator.dart';
import 'package:pwa_ocs_flutter/ui/core/ui/main_scaffold.dart';
import 'package:pwa_ocs_flutter/features/p600/presentation/bloc/p600_viewmodel.dart';
import 'package:pwa_ocs_flutter/features/p609/presentation/bloc/p609_viewmodel.dart';
class P609 extends StatelessWidget {
P609({super.key});
@override
Widget build(context) {
final args = ModalRoute.of(context)!.settings.arguments as P600State;
return ChangeNotifierProvider<P609ViewModel>(
create: (context) => P609ViewModel(
args.item.plantId,
args.item.operationDateTimestamp.split('T')[0],
args.item.workScheduleId,
initialStep: args.item.submanu[args.index].activitySeq,
),
child: Consumer<P609ViewModel>(
builder: (context, controller, child) {
if (controller.isLoading) {
return MainScaffold(
title: args.item.plantName,
subtitle: args.item.operationDate,
child: const LoadingView(),
);
} else if (controller.error != null) {
return MainScaffold(
title: args.item.plantName,
subtitle: args.item.operationDate,
child: Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: Text('Error: ${controller.error}'),
),
),
);
}
final items = [
_buildBase(controller),
if (controller.objectId != null) ...[
if (controller.currentStep == 1)
..._buildStep1(context, controller, args),
if (controller.currentStep == 2)
..._buildStep2(context, controller, args),
],
];
return MainScaffold(
title: args.item.plantName,
subtitle: args.item.operationDate,
child: Form(
key: controller.key,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Column(
spacing: 16.0,
children: [
Expanded(
child: RefreshIndicator(
onRefresh: controller.fetchAndUpdateCache,
child: ListView.separated(
padding: const EdgeInsets.symmetric(
vertical: 24,
horizontal: 16,
),
itemCount: items.length,
itemBuilder: (context, index) => items[index],
separatorBuilder: (context, index) =>
const SizedBox(height: 24.0),
),
),
),
ViewOnlyStepNavigator(
onNext: controller.next,
current: controller.currentStep,
onPrevious: controller.prev,
total: P609ViewModel.MAX_STEP,
),
],
),
),
),
);
},
),
);
}
Widget _buildBase(P609ViewModel controller) => CustomDropdownField(
label: "เลือกโรงสูบน้ำแรงสูง",
key: const Key('object_id'),
value: controller.objectId,
onChanged: controller.selectHighPressureWater,
items: controller.highPressureWaterList
.map(
(option) => DropdownMenuItem(
child: Text(option.objectName ?? ''),
value: option.objectId,
),
)
.toList(),
);
List<Widget> _buildStep1(
BuildContext context,
P609ViewModel controller,
P600State args,
) {
final selectedHighPressure = _findSelectedHighPressure(controller);
return [
SectionHeader(title: _activityTitle(args, 0)),
if (selectedHighPressure == null)
const Text('ไม่พบข้อมูลโรงสูบที่เลือก')
else
Wrap(
runSpacing: 16,
children: selectedHighPressure.highPressurePumpList
.asMap()
.entries
.map((entry) {
final index = entry.key;
final pump = entry.value;
return SizedBox(
width: (MediaQuery.of(context).size.width - 32) / 4,
child: TextButton(
style: TextButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
onPressed: () {
final initialPressureWaterPump = controller
.getDraftHighPressureWaterPump(pump);
showDialog(
context: context,
builder: (dialogContext) {
final formKey = GlobalKey<FormState>();
return HighPressureWaterPumpFormDialog(
formKey: formKey,
pressureWaterPump: initialPressureWaterPump,
plantId: controller.plantId,
operationDate: controller.operationDate,
workScheduleId: controller.workScheduleId,
onSubmit: (highPressurePump) =>
controller.updateHighPressureWaterPump(
index,
highPressurePump,
),
);
},
);
},
child: Column(
children: [
Image.asset(
_getPumpImagePath(pump),
width: 64,
height: 64,
fit: BoxFit.contain,
alignment: Alignment.center,
),
const SizedBox(height: 8),
Text(
pump.pumpName ?? '',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
height: 1.4,
color: Color(0xFF191C20),
),
textAlign: TextAlign.center,
),
],
),
),
);
})
.toList(),
),
];
}
List<Widget> _buildStep2(
BuildContext context,
P609ViewModel controller,
P600State args,
) {
final selectedHighPressure = _findSelectedHighPressure(controller);
return [
SectionHeader(title: _activityTitle(args, 1)),
if (selectedHighPressure == null)
const Text('ไม่พบข้อมูลโรงสูบที่เลือก')
else
Wrap(
runSpacing: 16,
children: selectedHighPressure.highPressureMeterList.asMap().entries.map((
entry,
) {
final index = entry.key;
final meter = entry.value;
return SizedBox(
width: (MediaQuery.of(context).size.width - 32) / 4,
child: TextButton(
style: TextButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
onPressed: () {
final initialPressureWaterMeter = controller
.getDraftHighPressureWaterMeter(meter);
showDialog(
context: context,
builder: (dialogContext) {
final formKey = GlobalKey<FormState>();
return HighPressureWaterMeterFormDialog(
formKey: formKey,
pressureWaterMeter: initialPressureWaterMeter,
plantId: controller.plantId,
operationDate: controller.operationDate,
workScheduleId: controller.workScheduleId,
onSubmit: (highPressureMeter) =>
controller.updateHighPressureWaterMeter(
index,
highPressureMeter,
),
);
},
);
},
child: Column(
children: [
Image.asset(
'images/raw-water-meter${meter.distributionPumpStatus != null && meter.distributionPumpStatus!.isNotEmpty ? '-${meter.distributionPumpStatus!.toLowerCase()}' : ""}.png',
width: 64,
height: 64,
fit: BoxFit.contain,
alignment: Alignment.center,
),
const SizedBox(height: 8),
Text(
meter.meterName ?? '',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
height: 1.4,
color: Color(0xFF191C20),
),
textAlign: TextAlign.center,
),
],
),
),
);
}).toList(),
),
];
}
String _activityTitle(P600State args, int index) {
if (index >= 0 && index < args.item.submanu.length) {
final name = args.item.submanu[index].activityName;
return name.isNotEmpty ? name : 'ไม่ระบุหัวข้อ';
}
return 'ไม่ระบุหัวข้อ';
}
HighPressureWater? _findSelectedHighPressure(P609ViewModel controller) {
for (final item in controller.highPressureWaterList) {
if (item.objectId == controller.objectId) {
return item;
}
}
return null;
}
/// Get the correct image asset path for water pump based on status
/// Logic (Priority order):
/// 1. machineRemark = "1" → raw-water-pump-1.png (พักเครื่อง)
/// 2. machineRemark = "2" → raw-water-pump-2.png (สำรอง)
/// 3. machineRemark = "3" → raw-water-pump-3.png (ใช้งานไม่ได้)
/// 4. machineRemark = "4" → raw-water-pump-4.png (แจ้งซ่อม)
/// 5. pumpStatus = "A" → Red (ใช้งาน)
/// 6. pumpStatus = "I" with no machineRemark → Blue (ไม่ใช้งาน)
/// 7. Default → Blue (ไม่มีข้อมูล)
String _getPumpImagePath(dynamic pump) {
// DEBUG: Log current pump status and machine remark
debugPrint(
'🔍 P609: Pump "${pump.pumpName ?? 'Unknown'}" - pumpStatus: "${pump.pumpStatus ?? ''}", machineRemark: "${pump.operationStatusRemark ?? ''}"',
);
// Priority 1: Check operation machineRemark field (highest priority)
final machineRemark = pump.operationStatusRemark;
if (machineRemark != null && machineRemark.isNotEmpty) {
debugPrint(' 🎯 Machine remark found: $machineRemark');
if (machineRemark == '1') {
return 'images/raw-water-pump-1.png'; // พักเครื่อง - สีเขียว
} else if (machineRemark == '2') {
return 'images/raw-water-pump-2.png'; // สำรอง - สีเขียว
} else if (machineRemark == '3') {
return 'images/raw-water-pump-3.png'; // ใช้งานไม่ได้ - สีเทา
} else if (machineRemark == '4') {
return 'images/raw-water-pump-4.png'; // แจ้งซ่อม - สีเทา
}
}
// Priority 2: Check pumpStatus = "A" → Red (Active/Running)
final pumpStatus = pump.pumpStatus;
if (pumpStatus != null && pumpStatus == 'A') {
debugPrint(' 🎯 Pump status is A (Active)');
return 'images/raw-water-pump-a.png'; // ใช้งาน - สีแดง
}
// Priority 3: Check pumpStatus = "I" with no machineRemark → Blue (Inactive)
if (pumpStatus != null &&
pumpStatus == 'I' &&
(machineRemark == null || machineRemark.isEmpty)) {
debugPrint(' 🎯 Pump status is I (Inactive) with no machine remark');
return 'images/raw-water-pump-i.png'; // ไม่ใช้งาน - สีน้ำเงิน
}
// Default: Blue - ไม่มีข้อมูล
debugPrint(' 🎯 Default case - using blue image');
return 'images/raw-water-pump-i.png';
}
}
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pwa_ocs_flutter/ui/core/ui/main_scaffold.dart';
import 'package:pwa_ocs_flutter/shared/widgets/loading_view.dart';
import 'package:pwa_ocs_flutter/shared/widgets/photo_widget.dart';
import 'package:pwa_ocs_flutter/shared/widgets/section_header.dart';
import 'package:pwa_ocs_flutter/features/p600/presentation/bloc/p600_viewmodel.dart';
import 'package:pwa_ocs_flutter/features/p610/presentation/bloc/p610_viewmodel.dart';
import 'package:pwa_ocs_flutter/shared/widgets/step_navigation.dart';
import 'package:pwa_ocs_flutter/shared/widgets/custom_form_field.dart';
import 'package:pwa_ocs_flutter/ui/core/ui/reverse_osmosis_pump_form_dialog.dart';
import 'package:pwa_ocs_flutter/features/p610/data/datasources/p610_service.dart';
class P610 extends StatefulWidget {
const P610({super.key});
@override
State<P610> createState() => _P610State();
}
class _P610State extends State<P610> {
// Cache text controllers to avoid recreating them on each rebuild
final Map<String, TextEditingController> _textControllers = {};
@override
void dispose() {
// Dispose all text controllers
for (final controller in _textControllers.values) {
controller.dispose();
}
super.dispose();
}
Future<void> _handleSubmit(
BuildContext context,
P610ViewModel controller,
P600State args,
) async {
final result = await controller.onSubmit(
args.item.plantId,
args.item.operationDateTimestamp.split('T')[0],
);
if (!context.mounted) return;
if (result == 'invalid') {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: const [
Icon(Icons.warning_amber_rounded, color: Colors.white),
SizedBox(width: 12),
Expanded(child: Text('กรุณากรอกข้อมูลให้ครบทุกขั้นตอน')),
],
),
backgroundColor: Colors.orange[600],
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.all(16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
duration: const Duration(seconds: 4),
action: SnackBarAction(
label: 'ปิด',
textColor: Colors.white,
onPressed: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
},
),
),
);
return;
}
if (result == 'success') {
showDialog(
context: context,
barrierDismissible: false,
builder: (dialogContext) => AlertDialog(
title: const Text('ส่งข้อมูลสำเร็จ', textAlign: TextAlign.center),
content: Column(
mainAxisSize: MainAxisSize.min,
spacing: 16,
children: const [
Icon(Icons.check_circle, size: 64, color: Colors.green),
Text('กำลังกลับไปหน้ารายการ...'),
],
),
),
);
await Future.delayed(const Duration(seconds: 2));
if (!context.mounted) return;
Navigator.of(context).pushReplacementNamed('/600', arguments: args);
return;
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
spacing: 12,
children: const [
Icon(Icons.error_outline, color: Colors.white),
Expanded(child: Text('ส่งข้อมูลไม่สำเร็จ')),
],
),
backgroundColor: Colors.red[600],
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.all(16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
action: SnackBarAction(
label: 'ปิด',
textColor: Colors.white,
onPressed: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
},
),
),
);
}
@override
Widget build(BuildContext context) {
final args = ModalRoute.of(context)!.settings.arguments as P600State;
return ChangeNotifierProvider<P610ViewModel>(
create: (context) => P610ViewModel(
args.item.plantId,
args.item.operationDateTimestamp,
args.item.workScheduleId,
args.item.submanu[args.index].activitySeq,
),
child: Consumer<P610ViewModel>(
builder: (context, controller, child) {
if (controller.isLoading) {
return MainScaffold(
title: args.item.plantName,
subtitle: args.item.operationDateTimestamp.split('T')[0],
child: const LoadingView(),
);
}
if (controller.errorMessage != null) {
return MainScaffold(
title: args.item.plantName,
subtitle: args.item.operationDateTimestamp.split('T')[0],
child: Center(child: Text('Error: ${controller.errorMessage}')),
);
}
final items = [
if (controller.step == 1) ..._buildStep1(controller),
if (controller.step == 2) ..._buildStep2(context, controller, args),
];
return MainScaffold(
title: args.item.plantName,
subtitle: args.item.operationDate,
child: Form(
key: controller.key,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Column(
children: [
Expanded(
child: RefreshIndicator(
onRefresh: controller.fetchAndUpdateCache,
child: ListView.separated(
padding: const EdgeInsets.symmetric(
vertical: 12,
horizontal: 16,
),
itemCount: items.length,
itemBuilder: (context, index) => items[index],
separatorBuilder: (context, index) =>
const SizedBox(height: 24.0),
),
),
),
StepNavigator(
total: P610ViewModel.MAX_STEP,
submitting: false,
submitable: controller.isFormValid(),
onNext: controller.next,
current: controller.step,
onPrevious: controller.prev,
onSubmit: () => _handleSubmit(context, controller, args),
),
],
),
),
),
);
},
),
);
}
List<Widget> _buildStep1(P610ViewModel controller) => [
SectionHeader(title: 'สระพักตะกอน'),
CustomDropdownField<String>(
label: 'เลือกสระพักตะกอน',
value: controller.selectedTankId,
onChanged: controller.sedimentationTanks.isNotEmpty
? controller.selectTank
: null,
enabled: controller.sedimentationTanks.isNotEmpty,
hintText: controller.sedimentationTanks.isEmpty
? 'ไม่พบข้อมูลสระพักตะกอน'
: null,
items: controller.tankDropdownOptions,
),
if (controller.selectedTankId != null) ...[
Builder(
builder: (context) {
return PhotoUploadWidget(
title: 'ถ่ายรูปสระพักตะกอน',
photos: controller.photoList1,
maxPhotos: 5,
enabled: controller.selectedTankId != null,
onPhotoTaken: controller.addTankPhoto,
onRemovePhoto: controller.removeTankPhoto,
);
},
),
],
];
List<Widget> _buildStep2(
BuildContext context,
P610ViewModel controller,
P600State args,
) {
final selectedStation = controller.selectedStation;
return [
SectionHeader(title: 'โรงสูบน้ำย้อนกลับ'),
CustomDropdownField<String>(
label: 'เลือกโรงสูบน้ำย้อนกลับ',
value: controller.selectedStationId,
onChanged: controller.reverseOsmosisStations.isNotEmpty
? controller.selectStation
: null,
enabled: controller.reverseOsmosisStations.isNotEmpty,
hintText: controller.reverseOsmosisStations.isEmpty
? 'ไม่พบข้อมูลโรงสูบน้ำย้อนกลับ'
: null,
items: controller.stationDropdownOptions,
),
if (selectedStation == null)
const Text('ไม่พบข้อมูลสถานีที่เลือก')
else
Wrap(
runSpacing: 16,
children: controller.pumpsForSelectedStation.asMap().entries.map((
entry,
) {
final index = entry.key;
final pump = entry.value;
return SizedBox(
width: (MediaQuery.of(context).size.width - 32) / 4,
child: TextButton(
style: TextButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
onPressed: () {
final initialPump = controller.getDraftReverseOsmosisPump(
pump,
);
showDialog(
context: context,
builder: (context) {
final formKey = GlobalKey<FormState>();
return ReverseOsmosisPumpFormDialog(
formKey: formKey,
pump: initialPump,
operationDate: args.item.operationDateTimestamp.split(
'T',
)[0],
workScheduleId: args.item.workScheduleId,
onSubmit: (updatedPump) => controller
.updateReverseOsmosisPump(index, updatedPump),
);
},
);
},
child: Column(
children: [
Image.asset(
_getPumpImagePath(pump),
width: 64,
height: 64,
fit: BoxFit.contain,
alignment: Alignment.center,
),
const SizedBox(height: 8),
Text(
pump.reverseOsmosisPumpName,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
height: 1.4,
color: Color(0xFF191C20),
),
textAlign: TextAlign.center,
),
],
),
),
);
}).toList(),
),
];
}
/// Get the correct image asset path for water pump based on status
/// Logic (Priority order):
/// 1. machineRemark = "1" → raw-water-pump-1.png (พักเครื่อง)
/// 2. machineRemark = "2" → raw-water-pump-2.png (สำรอง)
/// 3. machineRemark = "3" → raw-water-pump-3.png (ใช้งานไม่ได้)
/// 4. machineRemark = "4" → raw-water-pump-4.png (แจ้งซ่อม)
/// 5. pumpStatus = "A" → Red (ใช้งาน)
/// 6. pumpStatus = "I" with no machineRemark → Blue (ไม่ใช้งาน)
/// 7. Default → Blue (ไม่มีข้อมูล)
String _getPumpImagePath(ReverseOsmosisPump pump) {
// DEBUG: Log the current pump status and machine remark
debugPrint(
'🔍 P610: Pump "${pump.reverseOsmosisPumpName}" - pumpStatus: "${pump.pumpStatus}", machineRemark: "${pump.machineRemark}"',
);
// Priority 1: Check machineRemark for specific image (highest priority)
if (pump.machineRemark.isNotEmpty) {
final remark = pump.machineRemark;
debugPrint(' 🎯 Machine remark found: $remark');
if (remark == '1') {
return 'images/raw-water-pump-1.png'; // พักเครื่อง - สีเขียว
} else if (remark == '2') {
return 'images/raw-water-pump-2.png'; // สำรอง - สีเขียว
} else if (remark == '3') {
return 'images/raw-water-pump-3.png'; // ใช้งานไม่ได้ - สีเทา
} else if (remark == '4') {
return 'images/raw-water-pump-4.png'; // แจ้งซ่อม - สีเทา
}
}
// Priority 2: Check pumpStatus = "A" → Red (Active/Running)
if (pump.pumpStatus == 'A') {
debugPrint(' 🎯 Pump status is A (Active)');
return 'images/raw-water-pump-a.png'; // ใช้งาน - สีแดง
}
// Priority 3: Check pumpStatus = "I" with no machineRemark → Blue (Inactive)
if (pump.pumpStatus == 'I' && pump.machineRemark.isEmpty) {
debugPrint(' 🎯 Pump status is I (Inactive) with no machine remark');
return 'images/raw-water-pump-i.png'; // ไม่ใช้งาน - สีน้ำเงิน
}
// Default: Blue - ไม่มีข้อมูล
debugPrint(' 🎯 Default case - using blue image');
return 'images/raw-water-pump-i.png';
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment