improvements
parent
d1a94df33d
commit
d3dadb70c3
@ -0,0 +1,457 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:test_sa/controllers/api_routes/urls.dart';
|
||||||
|
import 'package:test_sa/extensions/context_extension.dart';
|
||||||
|
import 'package:test_sa/extensions/int_extensions.dart';
|
||||||
|
import 'package:test_sa/extensions/widget_extensions.dart';
|
||||||
|
import 'package:test_sa/extensions/text_extensions.dart';
|
||||||
|
import 'package:test_sa/models/ppm/recurrent_wo.dart';
|
||||||
|
import 'package:test_sa/new_views/app_style/app_color.dart';
|
||||||
|
import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
|
||||||
|
import 'package:test_sa/views/widgets/buttons/app_back_button.dart';
|
||||||
|
import 'package:test_sa/modules/cm_module/cm_request_utils.dart';
|
||||||
|
import 'package:test_sa/views/widgets/total_working_time_detail_bottomsheet.dart';
|
||||||
|
|
||||||
|
class CleanerPreviewDialog extends StatelessWidget {
|
||||||
|
final RecurrentWoData model;
|
||||||
|
final double totalWorkingHours;
|
||||||
|
final VoidCallback onConfirm;
|
||||||
|
|
||||||
|
const CleanerPreviewDialog({
|
||||||
|
Key? key,
|
||||||
|
required this.model,
|
||||||
|
required this.totalWorkingHours,
|
||||||
|
required this.onConfirm,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
static void show(
|
||||||
|
BuildContext context, {
|
||||||
|
required RecurrentWoData model,
|
||||||
|
required double totalWorkingHours,
|
||||||
|
required VoidCallback onConfirm,
|
||||||
|
}) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (dialogContext) => CleanerPreviewDialog(
|
||||||
|
model: model,
|
||||||
|
totalWorkingHours: totalWorkingHours,
|
||||||
|
onConfirm: onConfirm,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Dialog(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
insetPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Container(
|
||||||
|
constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.85),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// Header matching DefaultAppBar style
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(14),
|
||||||
|
topRight: Radius.circular(14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: 'Confirm Details'.heading3(context).custom(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Icon(
|
||||||
|
Icons.close,
|
||||||
|
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
|
||||||
|
size: 24,
|
||||||
|
).onPress(() => Navigator.pop(context)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Divider(height: 1, color: context.isDark ? AppColor.neutral60 : AppColor.neutral100),
|
||||||
|
// Content
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildPreviewSection(context, 'Task Information', [
|
||||||
|
_buildPreviewRow(context, context.translation.title, model.title ?? '-'),
|
||||||
|
_buildPreviewRow(context, context.translation.taskNo, model.taskNo ?? '-'),
|
||||||
|
_buildPreviewRow(context, context.translation.site, model.site?.siteName ?? '-'),
|
||||||
|
_buildPreviewRow(context, context.translation.department, model.department?.name ?? '-'),
|
||||||
|
_buildPreviewRow(context, context.translation.floor, model.floor?.name ?? '-'),
|
||||||
|
_buildPreviewRow(context, context.translation.room, model.room?.name ?? '-'),
|
||||||
|
_buildPreviewRow(context, context.translation.assignedEmployee, model.engineer?.userName ?? '-'),
|
||||||
|
]),
|
||||||
|
16.height,
|
||||||
|
// Total Working Time Section - Using WorkingTimeTile design
|
||||||
|
_buildPreviewSection(context, 'Total Working Time', [
|
||||||
|
_buildWorkingTimeTile(context),
|
||||||
|
]),
|
||||||
|
16.height,
|
||||||
|
if (model.planRecurrentTaskCheckLists != null && model.planRecurrentTaskCheckLists!.isNotEmpty) ...[
|
||||||
|
_buildPreviewSection(context, 'Checklist', [
|
||||||
|
...model.planRecurrentTaskCheckLists!
|
||||||
|
.map(
|
||||||
|
(item) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
item.isChecked ? Icons.check_circle : Icons.circle_outlined,
|
||||||
|
color: item.isChecked ? AppColor.primary10 : AppColor.neutral120,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
8.width,
|
||||||
|
RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: item.title,
|
||||||
|
style: AppTextStyles.bodyText2.copyWith(
|
||||||
|
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (item.isMandatory)
|
||||||
|
TextSpan(
|
||||||
|
text: ' *',
|
||||||
|
style: AppTextStyles.bodyText2.copyWith(
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).expanded,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
// Show comment if item is checked and has a comment
|
||||||
|
if (item.isChecked && item.comment.isNotEmpty == true) ...[
|
||||||
|
8.height,
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
margin: const EdgeInsets.only(left: 28),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.isDark ? AppColor.neutral60.withValues(alpha: 0.3) : AppColor.neutral100.withValues(alpha: 0.5),
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
'Comment:'.bodyText2(context).custom(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: context.isDark ? AppColor.neutral30 : AppColor.neutral120,
|
||||||
|
),
|
||||||
|
4.height,
|
||||||
|
item.comment.bodyText2(context).custom(
|
||||||
|
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
]),
|
||||||
|
16.height,
|
||||||
|
],
|
||||||
|
if (model.comment != null && model.comment!.isNotEmpty) ...[
|
||||||
|
_buildPreviewSection(context, context.translation.comment, [
|
||||||
|
model.comment!.bodyText(context),
|
||||||
|
]),
|
||||||
|
16.height,
|
||||||
|
],
|
||||||
|
if (model.imageRoom != null && model.imageRoom!.isNotEmpty) ...[
|
||||||
|
_buildPreviewSection(context, 'Captured Image', [
|
||||||
|
_buildImagePreview(context, model.imageRoom!),
|
||||||
|
]),
|
||||||
|
16.height,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Footer with Confirm button
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
border: Border(top: BorderSide(color: context.isDark ? AppColor.neutral60 : AppColor.neutral100)),
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
bottomLeft: Radius.circular(14),
|
||||||
|
bottomRight: Radius.circular(14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: AppFilledButton(
|
||||||
|
label: context.translation.complete,
|
||||||
|
buttonColor: AppColor.primary10,
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
onConfirm();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPreviewSection(BuildContext context, String title, List<Widget> children) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
title.heading6(context).custom(fontWeight: FontWeight.w600),
|
||||||
|
8.height,
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: children,
|
||||||
|
).toShadowContainer(context, padding: 12),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPreviewRow(BuildContext context, String label, String value) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: '$label:'.bodyText2(context).custom(color: AppColor.neutral120),
|
||||||
|
),
|
||||||
|
8.width,
|
||||||
|
Expanded(
|
||||||
|
flex: 3,
|
||||||
|
child: value.bodyText2(context).custom(color: AppColor.neutral120),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildWorkingTimeTile(BuildContext context) {
|
||||||
|
// Create timer list from model data, same as in RecurrentTaskInfoWidget
|
||||||
|
final timerList = model.planRecurrentTaskTimers?.map((e) {
|
||||||
|
return TimerHistoryModel(
|
||||||
|
id: e.id,
|
||||||
|
startTime: e.startTime,
|
||||||
|
endTime: e.endTime,
|
||||||
|
workingHours: e.workingHours,
|
||||||
|
);
|
||||||
|
}).toList() ?? [];
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
height: 56.toScreenHeight,
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16.toScreenWidth),
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColor.fieldBgColor(context),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Total Working Time",
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: context.isDark ? null : AppColor.neutral20,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
" ${CMRequestUtils.formatTotalWorkingHours(totalWorkingHours)}",
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).expanded,
|
||||||
|
8.width,
|
||||||
|
const Icon(Icons.info_outline_rounded, color: AppColor.primary10),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).onPress(() async {
|
||||||
|
await showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isDismissible: true,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||||
|
builder: (BuildContext context) => TotalWorkingTimeDetailBottomSheet(timerList: timerList),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildImagePreview(BuildContext context, String imageString) {
|
||||||
|
Widget imageWidget;
|
||||||
|
|
||||||
|
// First check if it's base64 encoded (contains | separator)
|
||||||
|
if (imageString.contains('|')) {
|
||||||
|
// Format: "filename|base64data"
|
||||||
|
try {
|
||||||
|
final base64Data = imageString.split('|').last;
|
||||||
|
imageWidget = Image.memory(base64Decode(base64Data), fit: BoxFit.cover);
|
||||||
|
return Container(
|
||||||
|
height: 200,
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColor.fieldBgColor(context),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: imageWidget,
|
||||||
|
).onPress(() {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => Scaffold(
|
||||||
|
body: SafeArea(
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
InteractiveViewer(child: imageWidget).center,
|
||||||
|
const ABackButton(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error decoding base64 image: $e');
|
||||||
|
return _buildCameraIcon(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a local file path
|
||||||
|
if (_isLocalUrl(imageString)) {
|
||||||
|
try {
|
||||||
|
imageWidget = Image.file(File(imageString), fit: BoxFit.cover);
|
||||||
|
return Container(
|
||||||
|
height: 200,
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColor.fieldBgColor(context),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: imageWidget,
|
||||||
|
).onPress(() {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => Scaffold(
|
||||||
|
body: SafeArea(
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
InteractiveViewer(child: imageWidget).center,
|
||||||
|
const ABackButton(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error loading local file: $e');
|
||||||
|
return _buildCameraIcon(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not local and not base64, it's a server filename - use NetworkImage with loading indicator
|
||||||
|
final imageUrl = URLs.getFileUrl(imageString);
|
||||||
|
if (imageUrl != null) {
|
||||||
|
imageWidget = Image.network(
|
||||||
|
imageUrl,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
loadingBuilder: (context, child, loadingProgress) {
|
||||||
|
if (loadingProgress == null) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
return Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: AppColor.primary10,
|
||||||
|
value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
debugPrint('Error loading network image: $error');
|
||||||
|
return _buildCameraIcon(context);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return Container(
|
||||||
|
height: 200,
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColor.fieldBgColor(context),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: imageWidget,
|
||||||
|
).onPress(() {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => Scaffold(
|
||||||
|
body: SafeArea(
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
InteractiveViewer(child: imageWidget).center,
|
||||||
|
const ABackButton(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return _buildCameraIcon(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build camera icon widget for fallback
|
||||||
|
Widget _buildCameraIcon(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
height: 200,
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColor.fieldBgColor(context),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.camera_alt,
|
||||||
|
size: 48,
|
||||||
|
color: context.isDark ? AppColor.white10 : AppColor.black10,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the URL is a local file path
|
||||||
|
bool _isLocalUrl(String url) {
|
||||||
|
return url.startsWith('/') || url.startsWith('file://') || (url.length > 1 && url[1] == ':');
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue