added api and craeted add project bottomsheet
This commit is contained in:
parent
2b8196b216
commit
c9e6840161
113
lib/controller/project/create_project_controller.dart
Normal file
113
lib/controller/project/create_project_controller.dart
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
|
|
||||||
|
class ProjectStatus {
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
ProjectStatus({required this.id, required this.name});
|
||||||
|
}
|
||||||
|
|
||||||
|
class CreateProjectController extends GetxController {
|
||||||
|
// Observables
|
||||||
|
var isSubmitting = false.obs;
|
||||||
|
var statusList = <ProjectStatus>[].obs;
|
||||||
|
ProjectStatus? selectedStatus;
|
||||||
|
|
||||||
|
/// Text controllers for form fields
|
||||||
|
final nameCtrl = TextEditingController();
|
||||||
|
final shortNameCtrl = TextEditingController();
|
||||||
|
final addressCtrl = TextEditingController();
|
||||||
|
final contactCtrl = TextEditingController();
|
||||||
|
final startDateCtrl = TextEditingController();
|
||||||
|
final endDateCtrl = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
loadHardcodedStatuses();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hardcoded project statuses
|
||||||
|
void loadHardcodedStatuses() {
|
||||||
|
final List<ProjectStatus> statuses = [
|
||||||
|
ProjectStatus(
|
||||||
|
id: "b74da4c2-d07e-46f2-9919-e75e49b12731", name: "Active"),
|
||||||
|
ProjectStatus(
|
||||||
|
id: "cdad86aa-8a56-4ff4-b633-9c629057dfef", name: "In Progress"),
|
||||||
|
ProjectStatus(
|
||||||
|
id: "603e994b-a27f-4e5d-a251-f3d69b0498ba", name: "On Hold"),
|
||||||
|
ProjectStatus(
|
||||||
|
id: "ef1c356e-0fe0-42df-a5d3-8daee355492d", name: "In Active"),
|
||||||
|
ProjectStatus(
|
||||||
|
id: "33deaef9-9af1-4f2a-b443-681ea0d04f81", name: "Completed"),
|
||||||
|
];
|
||||||
|
statusList.assignAll(statuses);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create project API call using ApiService
|
||||||
|
Future<bool> createProject({
|
||||||
|
required String name,
|
||||||
|
required String projectAddress,
|
||||||
|
required String shortName,
|
||||||
|
required String contactPerson,
|
||||||
|
required DateTime startDate,
|
||||||
|
required DateTime endDate,
|
||||||
|
required String projectStatusId,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
isSubmitting.value = true;
|
||||||
|
|
||||||
|
final success = await ApiService.createProjectApi(
|
||||||
|
name: name,
|
||||||
|
projectAddress: projectAddress,
|
||||||
|
shortName: shortName,
|
||||||
|
contactPerson: contactPerson,
|
||||||
|
startDate: startDate,
|
||||||
|
endDate: endDate,
|
||||||
|
projectStatusId: projectStatusId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
showAppSnackbar(
|
||||||
|
title: "Success",
|
||||||
|
message: "Project created successfully",
|
||||||
|
type: SnackbarType.success,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showAppSnackbar(
|
||||||
|
title: "Error",
|
||||||
|
message: "Failed to create project",
|
||||||
|
type: SnackbarType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
} catch (e, stack) {
|
||||||
|
logSafe("Create project error: $e", level: LogLevel.error);
|
||||||
|
logSafe("Stacktrace: $stack", level: LogLevel.debug);
|
||||||
|
showAppSnackbar(
|
||||||
|
title: "Error",
|
||||||
|
message: "An unexpected error occurred",
|
||||||
|
type: SnackbarType.error,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose() {
|
||||||
|
nameCtrl.dispose();
|
||||||
|
shortNameCtrl.dispose();
|
||||||
|
addressCtrl.dispose();
|
||||||
|
contactCtrl.dispose();
|
||||||
|
startDateCtrl.dispose();
|
||||||
|
endDateCtrl.dispose();
|
||||||
|
super.onClose();
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,11 @@ class ApiEndpoints {
|
|||||||
static const String getDashboardTeams = "/dashboard/teams";
|
static const String getDashboardTeams = "/dashboard/teams";
|
||||||
static const String getDashboardProjects = "/dashboard/projects";
|
static const String getDashboardProjects = "/dashboard/projects";
|
||||||
|
|
||||||
|
///// Projects Module API Endpoints
|
||||||
|
static const String createProject = "/project";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Attendance Module API Endpoints
|
// Attendance Module API Endpoints
|
||||||
static const String getProjects = "/project/list";
|
static const String getProjects = "/project/list";
|
||||||
static const String getGlobalProjects = "/project/list/basic";
|
static const String getGlobalProjects = "/project/list/basic";
|
||||||
|
@ -288,6 +288,60 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Create Project API
|
||||||
|
static Future<bool> createProjectApi({
|
||||||
|
required String name,
|
||||||
|
required String projectAddress,
|
||||||
|
required String shortName,
|
||||||
|
required String contactPerson,
|
||||||
|
required DateTime startDate,
|
||||||
|
required DateTime endDate,
|
||||||
|
required String projectStatusId,
|
||||||
|
}) async {
|
||||||
|
const endpoint = ApiEndpoints.createProject;
|
||||||
|
logSafe("Creating project: $name");
|
||||||
|
|
||||||
|
final Map<String, dynamic> payload = {
|
||||||
|
"name": name,
|
||||||
|
"projectAddress": projectAddress,
|
||||||
|
"shortName": shortName,
|
||||||
|
"contactPerson": contactPerson,
|
||||||
|
"startDate": startDate.toIso8601String(),
|
||||||
|
"endDate": endDate.toIso8601String(),
|
||||||
|
"projectStatusId": projectStatusId,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response =
|
||||||
|
await _postRequest(endpoint, payload, customTimeout: extendedTimeout);
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
logSafe("Create project failed: null response", level: LogLevel.error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
logSafe("Create project response status: ${response.statusCode}");
|
||||||
|
logSafe("Create project response body: ${response.body}");
|
||||||
|
|
||||||
|
final json = jsonDecode(response.body);
|
||||||
|
if (json['success'] == true) {
|
||||||
|
logSafe("Project created successfully: ${json['data']}");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
logSafe(
|
||||||
|
"Failed to create project: ${json['message'] ?? 'Unknown error'}",
|
||||||
|
level: LogLevel.warning,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e, stack) {
|
||||||
|
logSafe("Exception during createProjectApi: $e", level: LogLevel.error);
|
||||||
|
logSafe("StackTrace: $stack", level: LogLevel.debug);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/// Get Organizations assigned to a Project
|
/// Get Organizations assigned to a Project
|
||||||
static Future<OrganizationListResponse?> getAssignedOrganizations(
|
static Future<OrganizationListResponse?> getAssignedOrganizations(
|
||||||
String projectId) async {
|
String projectId) async {
|
||||||
@ -1730,19 +1784,18 @@ class ApiService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<List<dynamic>?> getDirectoryComments(
|
static Future<List<dynamic>?> getDirectoryComments(
|
||||||
String contactId, {
|
String contactId, {
|
||||||
bool active = true,
|
bool active = true,
|
||||||
}) async {
|
}) async {
|
||||||
final url = "${ApiEndpoints.getDirectoryNotes}/$contactId?active=$active";
|
final url = "${ApiEndpoints.getDirectoryNotes}/$contactId?active=$active";
|
||||||
final response = await _getRequest(url);
|
final response = await _getRequest(url);
|
||||||
final data = response != null
|
final data = response != null
|
||||||
? _parseResponse(response, label: 'Directory Comments')
|
? _parseResponse(response, label: 'Directory Comments')
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return data is List ? data : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
return data is List ? data : null;
|
||||||
|
}
|
||||||
|
|
||||||
static Future<bool> updateContact(
|
static Future<bool> updateContact(
|
||||||
String contactId, Map<String, dynamic> payload) async {
|
String contactId, Map<String, dynamic> payload) async {
|
||||||
|
@ -71,7 +71,15 @@ class BaseBottomSheet extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
MySpacing.height(12),
|
MySpacing.height(12),
|
||||||
MyText.titleLarge(title, fontWeight: 700),
|
|
||||||
|
// ✅ Centered title
|
||||||
|
Center(
|
||||||
|
child: MyText.titleLarge(
|
||||||
|
title,
|
||||||
|
fontWeight: 700,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
// ✅ Subtitle shown just below header if provided
|
// ✅ Subtitle shown just below header if provided
|
||||||
if (subtitle != null && subtitle!.isNotEmpty) ...[
|
if (subtitle != null && subtitle!.isNotEmpty) ...[
|
||||||
|
@ -11,6 +11,7 @@ import 'package:marco/helpers/widgets/my_spacing.dart';
|
|||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:marco/helpers/widgets/my_confirmation_dialog.dart';
|
import 'package:marco/helpers/widgets/my_confirmation_dialog.dart';
|
||||||
import 'package:marco/helpers/widgets/expense/expense_form_widgets.dart';
|
import 'package:marco/helpers/widgets/expense/expense_form_widgets.dart';
|
||||||
|
import 'package:marco/view/project/create_project_bottom_sheet.dart';
|
||||||
|
|
||||||
/// Show bottom sheet wrapper
|
/// Show bottom sheet wrapper
|
||||||
Future<T?> showAddExpenseBottomSheet<T>({
|
Future<T?> showAddExpenseBottomSheet<T>({
|
||||||
@ -157,6 +158,31 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
// 👇 Add New Project Button
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: TextButton.icon(
|
||||||
|
onPressed: () async {
|
||||||
|
await Get.bottomSheet(
|
||||||
|
const CreateProjectBottomSheet(),
|
||||||
|
isScrollControlled: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 🔄 Refresh project list after adding new project (optional)
|
||||||
|
await controller.fetchGlobalProjects();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.add, color: Colors.blue),
|
||||||
|
label: const Text(
|
||||||
|
"Add Project",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.blue,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_gap(),
|
||||||
|
|
||||||
_buildDropdownField<String>(
|
_buildDropdownField<String>(
|
||||||
icon: Icons.work_outline,
|
icon: Icons.work_outline,
|
||||||
title: "Project",
|
title: "Project",
|
||||||
|
354
lib/view/project/create_project_bottom_sheet.dart
Normal file
354
lib/view/project/create_project_bottom_sheet.dart
Normal file
@ -0,0 +1,354 @@
|
|||||||
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user