754 lines
25 KiB
Dart
754 lines
25 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'package:file_picker/file_picker.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:marco/controller/document/document_upload_controller.dart';
|
|
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
|
|
import 'package:marco/helpers/widgets/my_spacing.dart';
|
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
|
import 'package:marco/helpers/widgets/my_text.dart';
|
|
import 'package:marco/helpers/widgets/image_viewer_dialog.dart';
|
|
import 'package:marco/model/document/master_document_type_model.dart';
|
|
import 'package:marco/helpers/widgets/my_text_style.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
|
|
class DocumentEditBottomSheet extends StatefulWidget {
|
|
final Map<String, dynamic> documentData;
|
|
final Function(Map<String, dynamic>) onSubmit;
|
|
|
|
const DocumentEditBottomSheet({
|
|
Key? key,
|
|
required this.documentData,
|
|
required this.onSubmit,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
State<DocumentEditBottomSheet> createState() =>
|
|
_DocumentEditBottomSheetState();
|
|
}
|
|
|
|
class _DocumentEditBottomSheetState extends State<DocumentEditBottomSheet> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
final controller = Get.put(DocumentUploadController());
|
|
|
|
final TextEditingController _docIdController = TextEditingController();
|
|
final TextEditingController _docNameController = TextEditingController();
|
|
final TextEditingController _descriptionController = TextEditingController();
|
|
String? latestVersionUrl;
|
|
|
|
File? selectedFile;
|
|
bool fileChanged = false;
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
_docIdController.text = widget.documentData["documentId"] ?? "";
|
|
_docNameController.text = widget.documentData["name"] ?? "";
|
|
_descriptionController.text = widget.documentData["description"] ?? "";
|
|
|
|
// Tags
|
|
if (widget.documentData["tags"] != null) {
|
|
controller.enteredTags.assignAll(
|
|
List<String>.from(
|
|
(widget.documentData["tags"] as List)
|
|
.map((t) => t is String ? t : t["name"]),
|
|
),
|
|
);
|
|
}
|
|
|
|
// --- Convert category map to DocumentType ---
|
|
if (widget.documentData["category"] != null) {
|
|
controller.selectedCategory =
|
|
DocumentType.fromJson(widget.documentData["category"]);
|
|
}
|
|
|
|
// Type (if separate)
|
|
if (widget.documentData["type"] != null) {
|
|
controller.selectedType =
|
|
DocumentType.fromJson(widget.documentData["type"]);
|
|
}
|
|
// Fetch latest version URL if attachment exists
|
|
final latestVersion = widget.documentData["attachment"];
|
|
if (latestVersion != null) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
|
final url = await controller.fetchPresignedUrl(latestVersion["id"]);
|
|
if (url != null) {
|
|
setState(() {
|
|
latestVersionUrl = url;
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_docIdController.dispose();
|
|
_docNameController.dispose();
|
|
_descriptionController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _handleSubmit() async {
|
|
if (!(_formKey.currentState?.validate() ?? false)) return;
|
|
|
|
// ✅ Validate only if user picked a new file
|
|
if (fileChanged && selectedFile != null) {
|
|
final maxSizeMB = controller.selectedType?.maxSizeAllowedInMB;
|
|
if (maxSizeMB != null && controller.selectedFileSize != null) {
|
|
final fileSizeMB = controller.selectedFileSize! / (1024 * 1024);
|
|
if (fileSizeMB > maxSizeMB) {
|
|
showAppSnackbar(
|
|
title: "Error",
|
|
message: "File size exceeds $maxSizeMB MB limit",
|
|
type: SnackbarType.error,
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
final payload = {
|
|
"id": widget.documentData["id"],
|
|
"documentId": _docIdController.text.trim(),
|
|
"name": _docNameController.text.trim(),
|
|
"description": _descriptionController.text.trim(),
|
|
"documentTypeId": controller.selectedType?.id,
|
|
"tags": controller.enteredTags
|
|
.map((t) => {"name": t, "isActive": true})
|
|
.toList(),
|
|
};
|
|
|
|
// ✅ Always include attachment logic
|
|
if (fileChanged) {
|
|
if (selectedFile != null) {
|
|
// User picked new file
|
|
payload["attachment"] = {
|
|
"fileName": controller.selectedFileName,
|
|
"base64Data": controller.selectedFileBase64,
|
|
"contentType": controller.selectedFileContentType,
|
|
"fileSize": controller.selectedFileSize,
|
|
"isActive": true,
|
|
};
|
|
} else {
|
|
// User explicitly removed file
|
|
payload["attachment"] = null;
|
|
}
|
|
} else {
|
|
// ✅ User did NOT touch the attachment → send null explicitly
|
|
payload["attachment"] = null;
|
|
}
|
|
|
|
// else: do nothing → existing attachment remains as is
|
|
|
|
final success = await controller.editDocument(payload);
|
|
if (success) {
|
|
widget.onSubmit(payload);
|
|
Navigator.pop(context);
|
|
}
|
|
}
|
|
|
|
Future<void> _pickFile() async {
|
|
final result = await FilePicker.platform.pickFiles(
|
|
type: FileType.custom,
|
|
allowedExtensions: ['pdf', 'jpg', 'png', 'jpeg'],
|
|
);
|
|
|
|
if (result != null && result.files.single.path != null) {
|
|
final file = File(result.files.single.path!);
|
|
final fileName = result.files.single.name;
|
|
final fileBytes = await file.readAsBytes();
|
|
final base64Data = base64Encode(fileBytes);
|
|
|
|
setState(() {
|
|
selectedFile = file;
|
|
fileChanged = true;
|
|
controller.selectedFileName = fileName;
|
|
controller.selectedFileBase64 = base64Data;
|
|
controller.selectedFileContentType =
|
|
result.files.single.extension?.toLowerCase() == "pdf"
|
|
? "application/pdf"
|
|
: "image/${result.files.single.extension?.toLowerCase()}";
|
|
controller.selectedFileSize = (fileBytes.length / 1024).round();
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return BaseBottomSheet(
|
|
title: "Edit Document",
|
|
onCancel: () => Navigator.pop(context),
|
|
onSubmit: _handleSubmit,
|
|
child: Form(
|
|
key: _formKey,
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
MySpacing.height(16),
|
|
|
|
/// Document ID
|
|
LabeledInput(
|
|
label: "Document ID",
|
|
hint: "Enter Document ID",
|
|
controller: _docIdController,
|
|
validator: (v) =>
|
|
v == null || v.trim().isEmpty ? "Required" : null,
|
|
isRequired: true,
|
|
),
|
|
MySpacing.height(16),
|
|
|
|
/// Document Name
|
|
LabeledInput(
|
|
label: "Document Name",
|
|
hint: "e.g., PAN Card",
|
|
controller: _docNameController,
|
|
validator: (v) =>
|
|
v == null || v.trim().isEmpty ? "Required" : null,
|
|
isRequired: true,
|
|
),
|
|
MySpacing.height(16),
|
|
|
|
/// Document Category (Read-only, non-editable)
|
|
LabeledInput(
|
|
label: "Document Category",
|
|
hint: "",
|
|
controller: TextEditingController(
|
|
text: controller.selectedCategory?.name ?? ""),
|
|
validator: (_) => null,
|
|
isRequired: false,
|
|
// Disable interaction
|
|
readOnly: true,
|
|
),
|
|
|
|
MySpacing.height(16),
|
|
|
|
/// Document Type (Read-only, non-editable)
|
|
LabeledInput(
|
|
label: "Document Type",
|
|
hint: "",
|
|
controller: TextEditingController(
|
|
text: controller.selectedType?.name ?? ""),
|
|
validator: (_) => null,
|
|
isRequired: false,
|
|
readOnly: true,
|
|
),
|
|
|
|
MySpacing.height(24),
|
|
|
|
/// Attachment Section
|
|
AttachmentSectionSingle(
|
|
attachmentFile: selectedFile,
|
|
attachmentUrl: latestVersionUrl,
|
|
onPick: _pickFile,
|
|
onRemove: () => setState(() {
|
|
selectedFile = null;
|
|
fileChanged = true;
|
|
controller.selectedFileName = null;
|
|
controller.selectedFileBase64 = null;
|
|
controller.selectedFileContentType = null;
|
|
controller.selectedFileSize = null;
|
|
latestVersionUrl = null;
|
|
}),
|
|
),
|
|
|
|
if (controller.selectedType?.maxSizeAllowedInMB != null)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 8),
|
|
child: Text(
|
|
"Max file size: ${controller.selectedType!.maxSizeAllowedInMB} MB",
|
|
style: const TextStyle(color: Colors.grey, fontSize: 12),
|
|
),
|
|
),
|
|
MySpacing.height(16),
|
|
|
|
/// Tags Section
|
|
MyText.labelMedium("Tags"),
|
|
MySpacing.height(8),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
SizedBox(
|
|
height: 56,
|
|
child: TextFormField(
|
|
controller: controller.tagCtrl,
|
|
onChanged: controller.filterSuggestions,
|
|
onFieldSubmitted: (v) {
|
|
controller.addEnteredTag(v);
|
|
controller.tagCtrl.clear();
|
|
controller.clearSuggestions();
|
|
},
|
|
decoration: InputDecoration(
|
|
hintText: "Start typing to add tags",
|
|
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
|
filled: true,
|
|
fillColor: Colors.grey.shade100,
|
|
border: _inputBorder(),
|
|
enabledBorder: _inputBorder(),
|
|
focusedBorder: _inputFocusedBorder(),
|
|
contentPadding: MySpacing.all(16),
|
|
),
|
|
),
|
|
),
|
|
Obx(() => controller.filteredSuggestions.isEmpty
|
|
? const SizedBox.shrink()
|
|
: Container(
|
|
margin: const EdgeInsets.only(top: 4),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
border: Border.all(color: Colors.grey.shade300),
|
|
borderRadius: BorderRadius.circular(8),
|
|
boxShadow: const [
|
|
BoxShadow(color: Colors.black12, blurRadius: 4),
|
|
],
|
|
),
|
|
child: ListView.builder(
|
|
shrinkWrap: true,
|
|
itemCount: controller.filteredSuggestions.length,
|
|
itemBuilder: (_, i) {
|
|
final suggestion =
|
|
controller.filteredSuggestions[i];
|
|
return ListTile(
|
|
dense: true,
|
|
title: Text(suggestion),
|
|
onTap: () {
|
|
controller.addEnteredTag(suggestion);
|
|
controller.tagCtrl.clear();
|
|
controller.clearSuggestions();
|
|
},
|
|
);
|
|
},
|
|
),
|
|
)),
|
|
MySpacing.height(8),
|
|
Obx(() => Wrap(
|
|
spacing: 8,
|
|
children: controller.enteredTags
|
|
.map((tag) => Chip(
|
|
label: Text(tag),
|
|
onDeleted: () =>
|
|
controller.removeEnteredTag(tag),
|
|
))
|
|
.toList(),
|
|
)),
|
|
],
|
|
),
|
|
MySpacing.height(16),
|
|
|
|
/// Description
|
|
LabeledInput(
|
|
label: "Description",
|
|
hint: "Enter short description",
|
|
controller: _descriptionController,
|
|
validator: (v) =>
|
|
v == null || v.trim().isEmpty ? "Required" : null,
|
|
isRequired: true,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
OutlineInputBorder _inputBorder() => OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide(color: Colors.grey.shade300),
|
|
);
|
|
|
|
OutlineInputBorder _inputFocusedBorder() => const OutlineInputBorder(
|
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
|
borderSide: BorderSide(color: Colors.blueAccent, width: 1.5),
|
|
);
|
|
|
|
/// ---------------- Single Attachment Widget (Rewritten) ----------------
|
|
class AttachmentSectionSingle extends StatelessWidget {
|
|
final File? attachmentFile; // Local file
|
|
final String? attachmentUrl; // Online latest version URL
|
|
final VoidCallback onPick;
|
|
final VoidCallback? onRemove;
|
|
|
|
const AttachmentSectionSingle({
|
|
Key? key,
|
|
this.attachmentFile,
|
|
this.attachmentUrl,
|
|
required this.onPick,
|
|
this.onRemove,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final allowedImageExtensions = ['jpg', 'jpeg', 'png'];
|
|
|
|
Widget buildTile({File? file, String? url}) {
|
|
final isImage = file != null
|
|
? allowedImageExtensions
|
|
.contains(file.path.split('.').last.toLowerCase())
|
|
: url != null
|
|
? allowedImageExtensions
|
|
.contains(url.split('.').last.toLowerCase())
|
|
: false;
|
|
|
|
final fileName = file != null
|
|
? file.path.split('/').last
|
|
: url != null
|
|
? url.split('/').last
|
|
: '';
|
|
|
|
IconData fileIcon = Icons.insert_drive_file;
|
|
Color iconColor = Colors.blueGrey;
|
|
|
|
if (!isImage) {
|
|
final ext = fileName.split('.').last.toLowerCase();
|
|
switch (ext) {
|
|
case 'pdf':
|
|
fileIcon = Icons.picture_as_pdf;
|
|
iconColor = Colors.redAccent;
|
|
break;
|
|
case 'doc':
|
|
case 'docx':
|
|
fileIcon = Icons.description;
|
|
iconColor = Colors.blueAccent;
|
|
break;
|
|
case 'xls':
|
|
case 'xlsx':
|
|
fileIcon = Icons.table_chart;
|
|
iconColor = Colors.green;
|
|
break;
|
|
case 'txt':
|
|
fileIcon = Icons.article;
|
|
iconColor = Colors.grey;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return Stack(
|
|
clipBehavior: Clip.none,
|
|
children: [
|
|
GestureDetector(
|
|
onTap: () async {
|
|
if (isImage && file != null) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (_) => ImageViewerDialog(
|
|
imageSources: [file],
|
|
initialIndex: 0,
|
|
),
|
|
);
|
|
} else if (url != null) {
|
|
final uri = Uri.parse(url);
|
|
if (await canLaunchUrl(uri)) {
|
|
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
|
} else {
|
|
showAppSnackbar(
|
|
title: "Error",
|
|
message: "Could not open document",
|
|
type: SnackbarType.error,
|
|
);
|
|
}
|
|
}
|
|
},
|
|
child: Container(
|
|
width: 100,
|
|
height: 100,
|
|
margin: const EdgeInsets.only(right: 8),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: Colors.grey.shade300),
|
|
borderRadius: BorderRadius.circular(8),
|
|
color: Colors.grey.shade100,
|
|
),
|
|
child: isImage && file != null
|
|
? ClipRRect(
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Image.file(file, fit: BoxFit.cover),
|
|
)
|
|
: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(fileIcon, color: iconColor, size: 30),
|
|
const SizedBox(height: 4),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
if (onRemove != null)
|
|
Positioned(
|
|
top: -6,
|
|
right: -6,
|
|
child: IconButton(
|
|
icon: const Icon(Icons.close, color: Colors.red, size: 18),
|
|
onPressed: onRemove,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min, // prevent overflow
|
|
children: [
|
|
Row(
|
|
children: const [
|
|
Text("Attachment", style: TextStyle(fontWeight: FontWeight.w600)),
|
|
Text(" *",
|
|
style:
|
|
TextStyle(color: Colors.red, fontWeight: FontWeight.bold))
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: Row(
|
|
children: [
|
|
if (attachmentFile != null)
|
|
buildTile(file: attachmentFile)
|
|
else if (attachmentUrl != null)
|
|
buildTile(url: attachmentUrl)
|
|
else
|
|
GestureDetector(
|
|
onTap: onPick,
|
|
child: Container(
|
|
width: 100,
|
|
height: 100,
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: Colors.grey.shade300),
|
|
borderRadius: BorderRadius.circular(8),
|
|
color: Colors.grey.shade100,
|
|
),
|
|
child: const Icon(Icons.add, size: 40, color: Colors.grey),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
// ---- Reusable Widgets ----
|
|
|
|
class LabeledInput extends StatelessWidget {
|
|
final String label;
|
|
final String hint;
|
|
final TextEditingController controller;
|
|
final String? Function(String?) validator;
|
|
final bool isRequired;
|
|
final bool readOnly; // <-- Add this
|
|
|
|
const LabeledInput({
|
|
Key? key,
|
|
required this.label,
|
|
required this.hint,
|
|
required this.controller,
|
|
required this.validator,
|
|
this.isRequired = false,
|
|
this.readOnly = false, // default false
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) => Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
MyText.labelMedium(label),
|
|
if (isRequired)
|
|
const Text(
|
|
" *",
|
|
style:
|
|
TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
|
|
),
|
|
],
|
|
),
|
|
MySpacing.height(8),
|
|
TextFormField(
|
|
controller: controller,
|
|
validator: validator,
|
|
readOnly: readOnly, // <-- Use the new property here
|
|
decoration: _inputDecoration(context, hint),
|
|
),
|
|
],
|
|
);
|
|
|
|
InputDecoration _inputDecoration(BuildContext context, String hint) =>
|
|
InputDecoration(
|
|
hintText: hint,
|
|
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
|
filled: true,
|
|
fillColor: Colors.grey.shade100,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide(color: Colors.grey.shade300),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide(color: Colors.grey.shade300),
|
|
),
|
|
focusedBorder: const OutlineInputBorder(
|
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
|
borderSide: BorderSide(color: Colors.blueAccent, width: 1.5),
|
|
),
|
|
contentPadding: MySpacing.all(16),
|
|
);
|
|
}
|
|
|
|
class LabeledDropdown extends StatefulWidget {
|
|
final String label;
|
|
final String hint;
|
|
final String? value;
|
|
final List<String> items;
|
|
final ValueChanged<String> onChanged;
|
|
final bool isRequired;
|
|
|
|
const LabeledDropdown({
|
|
Key? key,
|
|
required this.label,
|
|
required this.hint,
|
|
required this.value,
|
|
required this.items,
|
|
required this.onChanged,
|
|
this.isRequired = false,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
State<LabeledDropdown> createState() => _LabeledDropdownState();
|
|
}
|
|
|
|
class _LabeledDropdownState extends State<LabeledDropdown> {
|
|
final GlobalKey _dropdownKey = GlobalKey();
|
|
|
|
@override
|
|
Widget build(BuildContext context) => Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
MyText.labelMedium(widget.label),
|
|
if (widget.isRequired)
|
|
const Text(
|
|
" *",
|
|
style:
|
|
TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
|
|
),
|
|
],
|
|
),
|
|
MySpacing.height(8),
|
|
GestureDetector(
|
|
key: _dropdownKey,
|
|
onTap: () async {
|
|
final RenderBox renderBox =
|
|
_dropdownKey.currentContext!.findRenderObject() as RenderBox;
|
|
final Offset offset = renderBox.localToGlobal(Offset.zero);
|
|
final Size size = renderBox.size;
|
|
final RelativeRect position = RelativeRect.fromLTRB(
|
|
offset.dx,
|
|
offset.dy + size.height,
|
|
offset.dx + size.width,
|
|
offset.dy,
|
|
);
|
|
final selected = await showMenu<String>(
|
|
context: context,
|
|
position: position,
|
|
items: widget.items
|
|
.map((item) => PopupMenuItem<String>(
|
|
value: item,
|
|
child: Text(item),
|
|
))
|
|
.toList(),
|
|
);
|
|
if (selected != null) widget.onChanged(selected);
|
|
},
|
|
child: AbsorbPointer(
|
|
child: TextFormField(
|
|
readOnly: true,
|
|
controller: TextEditingController(text: widget.value ?? ""),
|
|
validator: (value) =>
|
|
widget.isRequired && (value == null || value.isEmpty)
|
|
? "Required"
|
|
: null,
|
|
decoration: _inputDecoration(context, widget.hint).copyWith(
|
|
suffixIcon: const Icon(Icons.expand_more),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
|
|
InputDecoration _inputDecoration(BuildContext context, String hint) =>
|
|
InputDecoration(
|
|
hintText: hint,
|
|
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
|
filled: true,
|
|
fillColor: Colors.grey.shade100,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide(color: Colors.grey.shade300),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide(color: Colors.grey.shade300),
|
|
),
|
|
focusedBorder: const OutlineInputBorder(
|
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
|
borderSide: BorderSide(color: Colors.blueAccent, width: 1.5),
|
|
),
|
|
contentPadding: MySpacing.all(16),
|
|
);
|
|
}
|
|
|
|
class FilePickerTile extends StatelessWidget {
|
|
final String? pickedFile;
|
|
final VoidCallback onTap;
|
|
final bool isRequired;
|
|
|
|
const FilePickerTile({
|
|
Key? key,
|
|
required this.pickedFile,
|
|
required this.onTap,
|
|
this.isRequired = false,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) => Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
MyText.labelMedium("Attachments"),
|
|
if (isRequired)
|
|
const Text(
|
|
" *",
|
|
style:
|
|
TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
|
|
),
|
|
],
|
|
),
|
|
MySpacing.height(8),
|
|
GestureDetector(
|
|
onTap: onTap,
|
|
child: Container(
|
|
padding: MySpacing.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade100,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: Colors.grey.shade300),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
const Icon(Icons.upload_file, color: Colors.blueAccent),
|
|
const SizedBox(width: 12),
|
|
Text(pickedFile ?? "Choose File"),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|