355 lines
12 KiB
Dart
355 lines
12 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:marco/controller/project/create_project_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/my_snackbar.dart';
|
|
|
|
class CreateProjectBottomSheet extends StatefulWidget {
|
|
const CreateProjectBottomSheet({Key? key}) : super(key: key);
|
|
|
|
@override
|
|
State<CreateProjectBottomSheet> createState() =>
|
|
_CreateProjectBottomSheetState();
|
|
}
|
|
|
|
class _CreateProjectBottomSheetState extends State<CreateProjectBottomSheet> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
final CreateProjectController controller = Get.put(CreateProjectController());
|
|
|
|
DateTime? _startDate;
|
|
DateTime? _endDate;
|
|
|
|
Future<void> _pickDate({required bool isStart}) async {
|
|
final picked = await showDatePicker(
|
|
context: context,
|
|
initialDate: DateTime.now(),
|
|
firstDate: DateTime(2000),
|
|
lastDate: DateTime(2100),
|
|
);
|
|
if (picked != null) {
|
|
setState(() {
|
|
if (isStart) {
|
|
_startDate = picked;
|
|
controller.startDateCtrl.text =
|
|
DateFormat('yyyy-MM-dd').format(picked);
|
|
} else {
|
|
_endDate = picked;
|
|
controller.endDateCtrl.text = DateFormat('yyyy-MM-dd').format(picked);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _handleSubmit() async {
|
|
if (!(_formKey.currentState?.validate() ?? false)) return;
|
|
|
|
if (_startDate == null || _endDate == null) {
|
|
showAppSnackbar(
|
|
title: "Error",
|
|
message: "Please select both start and end dates",
|
|
type: SnackbarType.error,
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (controller.selectedStatus == null) {
|
|
showAppSnackbar(
|
|
title: "Error",
|
|
message: "Please select project status",
|
|
type: SnackbarType.error,
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Call API
|
|
final success = await controller.createProject(
|
|
name: controller.nameCtrl.text.trim(),
|
|
shortName: controller.shortNameCtrl.text.trim(),
|
|
projectAddress: controller.addressCtrl.text.trim(),
|
|
contactPerson: controller.contactCtrl.text.trim(),
|
|
startDate: _startDate!,
|
|
endDate: _endDate!,
|
|
projectStatusId: controller.selectedStatus!.id,
|
|
);
|
|
|
|
if (success) Navigator.pop(context);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return BaseBottomSheet(
|
|
title: "Create Project",
|
|
onCancel: () => Navigator.pop(context),
|
|
onSubmit: _handleSubmit,
|
|
child: Form(
|
|
key: _formKey,
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
MySpacing.height(16),
|
|
|
|
/// Project Name
|
|
LabeledInput(
|
|
label: "Project Name",
|
|
hint: "Enter project name",
|
|
controller: controller.nameCtrl,
|
|
validator: (value) =>
|
|
value == null || value.trim().isEmpty ? "Required" : null,
|
|
isRequired: true,
|
|
),
|
|
MySpacing.height(16),
|
|
|
|
/// Short Name
|
|
LabeledInput(
|
|
label: "Short Name",
|
|
hint: "Enter short name",
|
|
controller: controller.shortNameCtrl,
|
|
validator: (value) =>
|
|
value == null || value.trim().isEmpty ? "Required" : null,
|
|
isRequired: true,
|
|
),
|
|
MySpacing.height(16),
|
|
|
|
/// Project Address
|
|
LabeledInput(
|
|
label: "Project Address",
|
|
hint: "Enter project address",
|
|
controller: controller.addressCtrl,
|
|
validator: (value) =>
|
|
value == null || value.trim().isEmpty ? "Required" : null,
|
|
isRequired: true,
|
|
),
|
|
MySpacing.height(16),
|
|
|
|
/// Contact Person
|
|
LabeledInput(
|
|
label: "Contact Person",
|
|
hint: "Enter contact person",
|
|
controller: controller.contactCtrl,
|
|
validator: (value) =>
|
|
value == null || value.trim().isEmpty ? "Required" : null,
|
|
isRequired: true,
|
|
),
|
|
MySpacing.height(16),
|
|
|
|
/// Start Date
|
|
GestureDetector(
|
|
onTap: () => _pickDate(isStart: true),
|
|
child: AbsorbPointer(
|
|
child: LabeledInput(
|
|
label: "Start Date",
|
|
hint: "Select start date",
|
|
controller: controller.startDateCtrl,
|
|
validator: (value) =>
|
|
_startDate == null ? "Required" : null,
|
|
isRequired: true,
|
|
),
|
|
),
|
|
),
|
|
MySpacing.height(16),
|
|
|
|
/// End Date
|
|
GestureDetector(
|
|
onTap: () => _pickDate(isStart: false),
|
|
child: AbsorbPointer(
|
|
child: LabeledInput(
|
|
label: "End Date",
|
|
hint: "Select end date",
|
|
controller: controller.endDateCtrl,
|
|
validator: (value) => _endDate == null ? "Required" : null,
|
|
isRequired: true,
|
|
),
|
|
),
|
|
),
|
|
MySpacing.height(16),
|
|
|
|
/// Project Status using PopupMenuButton
|
|
Obx(() {
|
|
if (controller.statusList.isEmpty) {
|
|
return const Center(child: CircularProgressIndicator());
|
|
}
|
|
return LabeledDropdownPopup(
|
|
label: "Project Status",
|
|
hint: "Select status",
|
|
value: controller.selectedStatus?.name,
|
|
items: controller.statusList.map((e) => e.name).toList(),
|
|
onChanged: (selected) {
|
|
final status = controller.statusList
|
|
.firstWhere((s) => s.name == selected);
|
|
setState(() => controller.selectedStatus = status);
|
|
},
|
|
isRequired: true,
|
|
);
|
|
}),
|
|
MySpacing.height(16),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// ----------------- LabeledInput -----------------
|
|
class LabeledInput extends StatelessWidget {
|
|
final String label;
|
|
final String hint;
|
|
final TextEditingController controller;
|
|
final String? Function(String?) validator;
|
|
final bool isRequired;
|
|
|
|
const LabeledInput({
|
|
Key? key,
|
|
required this.label,
|
|
required this.hint,
|
|
required this.controller,
|
|
required this.validator,
|
|
this.isRequired = 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,
|
|
decoration: 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: EdgeInsets.all(16),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// ----------------- LabeledDropdownPopup -----------------
|
|
class LabeledDropdownPopup extends StatelessWidget {
|
|
final String label;
|
|
final String hint;
|
|
final String? value;
|
|
final List<String> items;
|
|
final ValueChanged<String> onChanged;
|
|
final bool isRequired;
|
|
|
|
LabeledDropdownPopup({
|
|
Key? key,
|
|
required this.label,
|
|
required this.hint,
|
|
required this.value,
|
|
required this.items,
|
|
required this.onChanged,
|
|
this.isRequired = false,
|
|
}) : super(key: key);
|
|
|
|
final GlobalKey _fieldKey = GlobalKey();
|
|
|
|
@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),
|
|
GestureDetector(
|
|
key: _fieldKey,
|
|
onTap: () async {
|
|
// Get the position of the widget
|
|
final RenderBox box =
|
|
_fieldKey.currentContext!.findRenderObject() as RenderBox;
|
|
final Offset offset = box.localToGlobal(Offset.zero);
|
|
final RelativeRect position = RelativeRect.fromLTRB(
|
|
offset.dx,
|
|
offset.dy + box.size.height,
|
|
offset.dx + box.size.width,
|
|
offset.dy,
|
|
);
|
|
|
|
final selected = await showMenu<String>(
|
|
context: context,
|
|
position: position,
|
|
items: items
|
|
.map((item) => PopupMenuItem<String>(
|
|
value: item,
|
|
child: Text(item),
|
|
))
|
|
.toList(),
|
|
);
|
|
if (selected != null) onChanged(selected);
|
|
},
|
|
child: AbsorbPointer(
|
|
child: TextFormField(
|
|
readOnly: true,
|
|
controller: TextEditingController(text: value ?? ""),
|
|
validator: (val) => isRequired && (val == null || val.isEmpty)
|
|
? "Required"
|
|
: null,
|
|
decoration: 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: const EdgeInsets.all(16),
|
|
suffixIcon: const Icon(Icons.expand_more),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|