marco.pms.mobileapp/lib/view/project/create_project_bottom_sheet.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),
),
),
),
),
],
);
}