|
|
|
|
@ -12,20 +12,16 @@ 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_text_form_field.dart';
|
|
|
|
|
import 'package:test_sa/views/widgets/buttons/app_back_button.dart';
|
|
|
|
|
|
|
|
|
|
class CleanerChecklistWidget extends StatefulWidget {
|
|
|
|
|
final List<ChecklistItem> checklistItems;
|
|
|
|
|
final Function(List<ChecklistItem>)? onChecklistChanged;
|
|
|
|
|
final String? capturedImageBase64;
|
|
|
|
|
final Function(String?)? onImageCaptured;
|
|
|
|
|
bool isClosed;
|
|
|
|
|
|
|
|
|
|
const CleanerChecklistWidget({
|
|
|
|
|
Key? key,
|
|
|
|
|
required this.checklistItems,
|
|
|
|
|
this.onChecklistChanged,
|
|
|
|
|
this.capturedImageBase64,
|
|
|
|
|
this.onImageCaptured,
|
|
|
|
|
}) : super(key: key);
|
|
|
|
|
CleanerChecklistWidget({Key? key, required this.checklistItems, this.onChecklistChanged, this.capturedImageBase64, this.onImageCaptured, this.isClosed = false}) : super(key: key);
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
State<CleanerChecklistWidget> createState() => _CleanerChecklistWidgetState();
|
|
|
|
|
@ -50,82 +46,88 @@ class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
|
|
|
|
|
),
|
|
|
|
|
16.height,
|
|
|
|
|
...widget.checklistItems.map((item) => _buildChecklistItem(context, item)).toList(),
|
|
|
|
|
16.height,
|
|
|
|
|
// 16.height,
|
|
|
|
|
_buildImageCaptureWidget(context),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildChecklistItem(BuildContext context, ChecklistItem item) {
|
|
|
|
|
return Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
// Checkbox with title
|
|
|
|
|
Row(
|
|
|
|
|
children: [
|
|
|
|
|
Checkbox(
|
|
|
|
|
value: item.isChecked,
|
|
|
|
|
activeColor: AppColor.primary10,
|
|
|
|
|
onChanged: (bool? value) {
|
|
|
|
|
setState(() {
|
|
|
|
|
item.isChecked = value ?? false;
|
|
|
|
|
if (!item.isChecked) {
|
|
|
|
|
item.commentController.clear();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
// Notify parent of changes
|
|
|
|
|
widget.onChecklistChanged?.call(widget.checklistItems);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
8.width,
|
|
|
|
|
Expanded(
|
|
|
|
|
child: RichText(
|
|
|
|
|
text: TextSpan(
|
|
|
|
|
children: [
|
|
|
|
|
TextSpan(
|
|
|
|
|
text: item.title,
|
|
|
|
|
style: AppTextStyles.bodyText.copyWith(
|
|
|
|
|
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
|
|
|
|
|
fontWeight: FontWeight.w600,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
if (item.isMandatory)
|
|
|
|
|
return IgnorePointer(
|
|
|
|
|
ignoring: widget.isClosed,
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
|
|
|
|
// Checkbox with title
|
|
|
|
|
Row(
|
|
|
|
|
children: [
|
|
|
|
|
Checkbox(
|
|
|
|
|
// visualDensity: const VisualDensity(horizontal: -4.0, vertical: -4.0),
|
|
|
|
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
|
|
|
value: item.isChecked,
|
|
|
|
|
activeColor: AppColor.primary10,
|
|
|
|
|
onChanged: (bool? value) {
|
|
|
|
|
setState(() {
|
|
|
|
|
item.isChecked = value ?? false;
|
|
|
|
|
if (!item.isChecked) {
|
|
|
|
|
item.commentController.clear();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
// Notify parent of changes
|
|
|
|
|
widget.onChecklistChanged?.call(widget.checklistItems);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
8.width,
|
|
|
|
|
Expanded(
|
|
|
|
|
child: RichText(
|
|
|
|
|
text: TextSpan(
|
|
|
|
|
children: [
|
|
|
|
|
TextSpan(
|
|
|
|
|
text: ' *',
|
|
|
|
|
text: item.title,
|
|
|
|
|
style: AppTextStyles.bodyText.copyWith(
|
|
|
|
|
color: Colors.red,
|
|
|
|
|
fontWeight: FontWeight.w600,
|
|
|
|
|
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
|
|
|
|
|
fontWeight: FontWeight.w500,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
if (item.isMandatory)
|
|
|
|
|
TextSpan(
|
|
|
|
|
text: ' *',
|
|
|
|
|
style: AppTextStyles.bodyText.copyWith(
|
|
|
|
|
color: Colors.red,
|
|
|
|
|
fontWeight: FontWeight.w500,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// Comment box - only visible when checked
|
|
|
|
|
if (item.isChecked) ...[
|
|
|
|
|
16.height,
|
|
|
|
|
AppTextFormField(
|
|
|
|
|
controller: item.commentController,
|
|
|
|
|
labelText: context.translation.comment,
|
|
|
|
|
backgroundColor: AppColor.fieldBgColor(context),
|
|
|
|
|
showShadow: false,
|
|
|
|
|
labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(
|
|
|
|
|
color: context.isDark ? AppColor.white10 : AppColor.black10,
|
|
|
|
|
// Comment box - only visible when checked
|
|
|
|
|
if (item.isChecked) ...[
|
|
|
|
|
16.height,
|
|
|
|
|
AppTextFormField(
|
|
|
|
|
controller: item.commentController,
|
|
|
|
|
labelText: context.translation.comment,
|
|
|
|
|
backgroundColor: AppColor.fieldBgColor(context),
|
|
|
|
|
showShadow: false,
|
|
|
|
|
labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(
|
|
|
|
|
color: context.isDark ? AppColor.white10 : AppColor.black10,
|
|
|
|
|
),
|
|
|
|
|
alignLabelWithHint: true,
|
|
|
|
|
textInputType: TextInputType.multiline,
|
|
|
|
|
onChange: (value) {
|
|
|
|
|
item.comment = value;
|
|
|
|
|
// Notify parent of changes
|
|
|
|
|
widget.onChecklistChanged?.call(widget.checklistItems);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
alignLabelWithHint: true,
|
|
|
|
|
textInputType: TextInputType.multiline,
|
|
|
|
|
onChange: (value) {
|
|
|
|
|
item.comment = value;
|
|
|
|
|
// Notify parent of changes
|
|
|
|
|
widget.onChecklistChanged?.call(widget.checklistItems);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
).toShadowContainer(context).paddingOnly(bottom: 16);
|
|
|
|
|
).toShadowContainer(context).paddingOnly(bottom: 16),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildImageCaptureWidget(BuildContext context) {
|
|
|
|
|
@ -140,26 +142,7 @@ class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
8.height,
|
|
|
|
|
GestureDetector(
|
|
|
|
|
onTap: () async {
|
|
|
|
|
// Image capture logic
|
|
|
|
|
final pickedFile = await ImagePicker().pickImage(source: ImageSource.camera);
|
|
|
|
|
if (pickedFile != null) {
|
|
|
|
|
// Convert image to Base64 with filename prefix (same format as CM request)
|
|
|
|
|
final bytes = await pickedFile.readAsBytes();
|
|
|
|
|
final base64Image = base64Encode(bytes);
|
|
|
|
|
final fileName = pickedFile.path.split("/").last;
|
|
|
|
|
final formattedImage = "$fileName|$base64Image";
|
|
|
|
|
|
|
|
|
|
// Update parent with captured image
|
|
|
|
|
widget.onImageCaptured?.call(formattedImage);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
child: ClipRRect(
|
|
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
|
|
child: _buildImageWidget(context),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
_buildImageWidget(context),
|
|
|
|
|
],
|
|
|
|
|
).toShadowContainer(context);
|
|
|
|
|
}
|
|
|
|
|
@ -180,9 +163,23 @@ class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
|
|
|
|
|
size: 48,
|
|
|
|
|
color: context.isDark ? AppColor.white10 : AppColor.black10,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
).onPress(() async {
|
|
|
|
|
final pickedFile = await ImagePicker().pickImage(source: ImageSource.camera, imageQuality: 70, maxWidth: 800, maxHeight: 800);
|
|
|
|
|
if (pickedFile != null) {
|
|
|
|
|
// Convert image to Base64 with filename prefix (same format as CM request)
|
|
|
|
|
final bytes = await pickedFile.readAsBytes();
|
|
|
|
|
final base64Image = base64Encode(bytes);
|
|
|
|
|
final fileName = pickedFile.path.split("/").last;
|
|
|
|
|
final formattedImage = "$fileName|$base64Image";
|
|
|
|
|
|
|
|
|
|
// Update parent with captured image
|
|
|
|
|
widget.onImageCaptured?.call(formattedImage);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget imageWidget;
|
|
|
|
|
|
|
|
|
|
final imageString = widget.capturedImageBase64!;
|
|
|
|
|
|
|
|
|
|
// First check if it's base64 encoded (contains | separator)
|
|
|
|
|
@ -190,17 +187,37 @@ class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
|
|
|
|
|
// Format: "filename|base64data"
|
|
|
|
|
try {
|
|
|
|
|
final base64Data = imageString.split('|').last;
|
|
|
|
|
return Container(
|
|
|
|
|
height: 200,
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: AppColor.fieldBgColor(context),
|
|
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
|
|
),
|
|
|
|
|
child: Image.memory(
|
|
|
|
|
base64Decode(base64Data),
|
|
|
|
|
fit: BoxFit.cover,
|
|
|
|
|
),
|
|
|
|
|
imageWidget = Image.memory(base64Decode(base64Data), fit: BoxFit.cover);
|
|
|
|
|
return Stack(
|
|
|
|
|
alignment: AlignmentDirectional.topEnd,
|
|
|
|
|
children: [
|
|
|
|
|
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: Image(image: getImageObject(itemIndex))).center,
|
|
|
|
|
InteractiveViewer(child: imageWidget).center,
|
|
|
|
|
const ABackButton(),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}),
|
|
|
|
|
_delete(),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
debugPrint('Error decoding base64 image: $e');
|
|
|
|
|
@ -211,17 +228,37 @@ class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
|
|
|
|
|
// Check if it's a local file path
|
|
|
|
|
if (_isLocalUrl(imageString)) {
|
|
|
|
|
try {
|
|
|
|
|
return Container(
|
|
|
|
|
height: 200,
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: AppColor.fieldBgColor(context),
|
|
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
|
|
),
|
|
|
|
|
child: Image.file(
|
|
|
|
|
File(imageString),
|
|
|
|
|
fit: BoxFit.cover,
|
|
|
|
|
),
|
|
|
|
|
imageWidget = Image.file(File(imageString), fit: BoxFit.cover);
|
|
|
|
|
return Stack(
|
|
|
|
|
alignment: AlignmentDirectional.topEnd,
|
|
|
|
|
children: [
|
|
|
|
|
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: Image(image: getImageObject(itemIndex))).center,
|
|
|
|
|
InteractiveViewer(child: imageWidget).center,
|
|
|
|
|
const ABackButton(),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}),
|
|
|
|
|
_delete(),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
debugPrint('Error loading local file: $e');
|
|
|
|
|
@ -232,34 +269,55 @@ class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
|
|
|
|
|
// If not local and not base64, it's a server filename - use NetworkImage with loading indicator
|
|
|
|
|
final imageUrl = URLs.getFileUrl(imageString);
|
|
|
|
|
if (imageUrl != null) {
|
|
|
|
|
return Container(
|
|
|
|
|
height: 200,
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: AppColor.fieldBgColor(context),
|
|
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
|
|
),
|
|
|
|
|
child: 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,
|
|
|
|
|
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 Stack(
|
|
|
|
|
alignment: AlignmentDirectional.topEnd,
|
|
|
|
|
children: [
|
|
|
|
|
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: Image(image: getImageObject(itemIndex))).center,
|
|
|
|
|
InteractiveViewer(child: imageWidget).center,
|
|
|
|
|
const ABackButton(),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
errorBuilder: (context, error, stackTrace) {
|
|
|
|
|
debugPrint('Error loading network image: $error');
|
|
|
|
|
return _buildCameraIcon(context);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
}),
|
|
|
|
|
_delete(),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -295,6 +353,26 @@ class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
|
|
|
|
|
// Note: Don't dispose controllers here as they're managed by ChecklistItem
|
|
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _delete() {
|
|
|
|
|
if (widget.isClosed) return const SizedBox();
|
|
|
|
|
return Container(
|
|
|
|
|
height: 45.toScreenHeight,
|
|
|
|
|
width: 45.toScreenHeight,
|
|
|
|
|
margin: const EdgeInsets.only(top: 4, right: 4),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: (context.isDark ? AppColor.neutral20 : AppColor.neutral40).withValues(alpha: 0.4),
|
|
|
|
|
border: Border.all(
|
|
|
|
|
color: (context.isDark ? AppColor.neutral20 : AppColor.neutral40).withValues(alpha: 0.7),
|
|
|
|
|
),
|
|
|
|
|
borderRadius: BorderRadius.circular(30),
|
|
|
|
|
//boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10)],
|
|
|
|
|
),
|
|
|
|
|
child: const Icon(Icons.delete_rounded, color: Color(0xffF63939)))
|
|
|
|
|
.onPress(() {
|
|
|
|
|
widget.onImageCaptured?.call(null);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Model class for checklist items
|
|
|
|
|
|