781 lines
26 KiB
Dart
781 lines
26 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_text.dart';
|
||
import 'package:marco/helpers/widgets/my_text_style.dart';
|
||
import 'package:marco/helpers/widgets/image_viewer_dialog.dart';
|
||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||
|
||
class DocumentUploadBottomSheet extends StatefulWidget {
|
||
final Function(Map<String, dynamic>) onSubmit;
|
||
final bool isEmployee;
|
||
const DocumentUploadBottomSheet({
|
||
Key? key,
|
||
required this.onSubmit,
|
||
this.isEmployee = false,
|
||
}) : super(key: key);
|
||
|
||
@override
|
||
State<DocumentUploadBottomSheet> createState() =>
|
||
_DocumentUploadBottomSheetState();
|
||
}
|
||
|
||
class _DocumentUploadBottomSheetState extends State<DocumentUploadBottomSheet> {
|
||
final _formKey = GlobalKey<FormState>();
|
||
final controller = Get.put(DocumentUploadController());
|
||
|
||
final TextEditingController _docIdController = TextEditingController();
|
||
final TextEditingController _docNameController = TextEditingController();
|
||
final TextEditingController _descriptionController = TextEditingController();
|
||
|
||
File? selectedFile;
|
||
|
||
@override
|
||
void dispose() {
|
||
_docIdController.dispose();
|
||
_docNameController.dispose();
|
||
_descriptionController.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
void _handleSubmit() {
|
||
final formState = _formKey.currentState;
|
||
|
||
// 1️⃣ Validate form fields
|
||
if (!(formState?.validate() ?? false)) {
|
||
// Collect first validation error
|
||
final errorFields = [
|
||
{"label": "Document ID", "value": _docIdController.text.trim()},
|
||
{"label": "Document Name", "value": _docNameController.text.trim()},
|
||
{"label": "Description", "value": _descriptionController.text.trim()},
|
||
];
|
||
|
||
for (var field in errorFields) {
|
||
if (field["value"] == null || (field["value"] as String).isEmpty) {
|
||
showAppSnackbar(
|
||
title: "Error",
|
||
message: "${field["label"]} is required",
|
||
type: SnackbarType.error,
|
||
);
|
||
return;
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 2️⃣ Validate file attachment
|
||
if (selectedFile == null) {
|
||
showAppSnackbar(
|
||
title: "Error",
|
||
message: "Please attach a document",
|
||
type: SnackbarType.error,
|
||
);
|
||
return;
|
||
}
|
||
|
||
// 3️⃣ Validate document category based on employee/project
|
||
if (controller.selectedCategory != null) {
|
||
final selectedCategoryName = controller.selectedCategory!.name;
|
||
|
||
if (widget.isEmployee && selectedCategoryName != 'Employee Documents') {
|
||
showAppSnackbar(
|
||
title: "Error",
|
||
message:
|
||
"Only 'Employee Documents' can be uploaded from the Employee screen. Please select the correct document type.",
|
||
type: SnackbarType.error,
|
||
);
|
||
return;
|
||
} else if (!widget.isEmployee &&
|
||
selectedCategoryName != 'Project Documents') {
|
||
showAppSnackbar(
|
||
title: "Error",
|
||
message:
|
||
"Only 'Project Documents' can be uploaded from the Project screen. Please select the correct document type.",
|
||
type: SnackbarType.error,
|
||
);
|
||
return;
|
||
}
|
||
} else {
|
||
showAppSnackbar(
|
||
title: "Error",
|
||
message: "Please select a Document Category before uploading.",
|
||
type: SnackbarType.error,
|
||
);
|
||
return;
|
||
}
|
||
|
||
// 4️⃣ Validate file size
|
||
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;
|
||
}
|
||
}
|
||
|
||
// 5️⃣ Validate file type
|
||
final allowedType = controller.selectedType?.allowedContentType;
|
||
if (allowedType != null && controller.selectedFileContentType != null) {
|
||
if (!allowedType
|
||
.toLowerCase()
|
||
.contains(controller.selectedFileContentType!.toLowerCase())) {
|
||
showAppSnackbar(
|
||
title: "Error",
|
||
message: "Only $allowedType files are allowed for this type",
|
||
type: SnackbarType.error,
|
||
);
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 6️⃣ Prepare payload
|
||
final payload = {
|
||
"documentId": _docIdController.text.trim(),
|
||
"name": _docNameController.text.trim(),
|
||
"description": _descriptionController.text.trim(),
|
||
"documentTypeId": controller.selectedType?.id,
|
||
"attachment": {
|
||
"fileName": controller.selectedFileName,
|
||
"base64Data": controller.selectedFileBase64,
|
||
"contentType": controller.selectedFileContentType,
|
||
"fileSize": controller.selectedFileSize,
|
||
"isActive": true,
|
||
},
|
||
"tags": controller.enteredTags
|
||
.map((t) => {"name": t, "isActive": true})
|
||
.toList(),
|
||
};
|
||
|
||
// 7️⃣ Submit
|
||
widget.onSubmit(payload);
|
||
|
||
// 8️⃣ Show success message
|
||
showAppSnackbar(
|
||
title: "Success",
|
||
message: "Document submitted successfully",
|
||
type: SnackbarType.success,
|
||
);
|
||
}
|
||
|
||
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;
|
||
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: "Upload Document",
|
||
onCancel: () => Navigator.pop(context),
|
||
onSubmit: _handleSubmit,
|
||
child: Form(
|
||
key: _formKey,
|
||
child: SingleChildScrollView(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
MySpacing.height(16),
|
||
|
||
/// Document Category
|
||
Obx(() {
|
||
if (controller.isLoading.value &&
|
||
controller.categories.isEmpty) {
|
||
return const Center(child: CircularProgressIndicator());
|
||
}
|
||
|
||
return LabeledDropdown(
|
||
label: "Document Category",
|
||
hint: "Select Category",
|
||
value: controller.selectedCategory?.name,
|
||
items: controller.categories.map((c) => c.name).toList(),
|
||
onChanged: (selected) async {
|
||
final category = controller.categories
|
||
.firstWhere((c) => c.name == selected);
|
||
setState(() => controller.selectedCategory = category);
|
||
await controller.fetchDocumentTypes(category.id);
|
||
},
|
||
isRequired: true,
|
||
);
|
||
}),
|
||
MySpacing.height(16),
|
||
|
||
/// Document Type
|
||
Obx(() {
|
||
if (controller.documentTypes.isEmpty) {
|
||
return const SizedBox.shrink();
|
||
}
|
||
return LabeledDropdown(
|
||
label: "Document Type",
|
||
hint: "Select Type",
|
||
value: controller.selectedType?.name,
|
||
items: controller.documentTypes.map((t) => t.name).toList(),
|
||
onChanged: (selected) {
|
||
final type = controller.documentTypes
|
||
.firstWhere((t) => t.name == selected);
|
||
setState(() => controller.selectedType = type);
|
||
},
|
||
isRequired: true,
|
||
);
|
||
}),
|
||
MySpacing.height(12),
|
||
|
||
/// Document ID
|
||
LabeledInput(
|
||
label: "Document ID",
|
||
hint: "Enter Document ID",
|
||
controller: _docIdController,
|
||
validator: (value) {
|
||
if (value == null || value.trim().isEmpty) {
|
||
return "Required";
|
||
}
|
||
|
||
// ✅ Regex validation if enabled
|
||
final selectedType = controller.selectedType;
|
||
if (selectedType != null &&
|
||
selectedType.isValidationRequired &&
|
||
selectedType.regexExpression != null &&
|
||
selectedType.regexExpression!.isNotEmpty) {
|
||
final regExp = RegExp(selectedType.regexExpression!);
|
||
if (!regExp.hasMatch(value.trim())) {
|
||
return "Invalid ${selectedType.name} format";
|
||
}
|
||
}
|
||
|
||
return null;
|
||
},
|
||
isRequired: true,
|
||
),
|
||
|
||
MySpacing.height(16),
|
||
|
||
/// Document Name
|
||
LabeledInput(
|
||
label: "Document Name",
|
||
hint: "e.g., PAN Card",
|
||
controller: _docNameController,
|
||
validator: (value) =>
|
||
value == null || value.trim().isEmpty ? "Required" : null,
|
||
isRequired: true,
|
||
),
|
||
MySpacing.height(16),
|
||
|
||
/// Single Attachment Section
|
||
AttachmentSectionSingle(
|
||
attachment: selectedFile,
|
||
onPick: _pickFile,
|
||
onRemove: () => setState(() {
|
||
selectedFile = null;
|
||
controller.selectedFileName = null;
|
||
controller.selectedFileBase64 = null;
|
||
controller.selectedFileContentType = null;
|
||
controller.selectedFileSize = 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
|
||
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: (value) =>
|
||
value == null || value.trim().isEmpty ? "Required" : null,
|
||
isRequired: true,
|
||
maxLines: 3,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
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 ----------------
|
||
class AttachmentSectionSingle extends StatelessWidget {
|
||
final File? attachment;
|
||
final VoidCallback onPick;
|
||
final VoidCallback? onRemove;
|
||
|
||
const AttachmentSectionSingle({
|
||
Key? key,
|
||
this.attachment,
|
||
required this.onPick,
|
||
this.onRemove,
|
||
}) : super(key: key);
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final allowedImageExtensions = ['jpg', 'jpeg', 'png'];
|
||
|
||
Widget buildTile(File file) {
|
||
final isImage = allowedImageExtensions
|
||
.contains(file.path.split('.').last.toLowerCase());
|
||
|
||
final fileName = file.path.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: () {
|
||
if (isImage) {
|
||
showDialog(
|
||
context: context,
|
||
builder: (_) => ImageViewerDialog(
|
||
imageSources: [file],
|
||
initialIndex: 0,
|
||
),
|
||
);
|
||
}
|
||
},
|
||
child: Container(
|
||
width: 100,
|
||
height: 100,
|
||
decoration: BoxDecoration(
|
||
border: Border.all(color: Colors.grey.shade300),
|
||
borderRadius: BorderRadius.circular(8),
|
||
color: Colors.grey.shade100,
|
||
),
|
||
child: isImage
|
||
? 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),
|
||
Text(
|
||
fileName.split('.').last.toUpperCase(),
|
||
style: TextStyle(
|
||
fontSize: 10,
|
||
fontWeight: FontWeight.bold,
|
||
color: iconColor),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
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,
|
||
children: [
|
||
Row(
|
||
children: const [
|
||
Text("Attachment", style: TextStyle(fontWeight: FontWeight.w600)),
|
||
Text(" *",
|
||
style:
|
||
TextStyle(color: Colors.red, fontWeight: FontWeight.bold))
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
Row(
|
||
children: [
|
||
if (attachment != null)
|
||
buildTile(attachment!)
|
||
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 int maxLines;
|
||
|
||
const LabeledInput({
|
||
Key? key,
|
||
required this.label,
|
||
required this.hint,
|
||
required this.controller,
|
||
required this.validator,
|
||
this.isRequired = false,
|
||
this.maxLines = 1,
|
||
}) : 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,
|
||
decoration: _inputDecoration(context, hint),
|
||
maxLines: maxLines,
|
||
),
|
||
],
|
||
);
|
||
|
||
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"),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|