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 getDashboardProjects = "/dashboard/projects";
|
||||
|
||||
///// Projects Module API Endpoints
|
||||
static const String createProject = "/project";
|
||||
|
||||
|
||||
|
||||
// Attendance Module API Endpoints
|
||||
static const String getProjects = "/project/list";
|
||||
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
|
||||
static Future<OrganizationListResponse?> getAssignedOrganizations(
|
||||
String projectId) async {
|
||||
@ -1730,19 +1784,18 @@ class ApiService {
|
||||
return false;
|
||||
}
|
||||
|
||||
static Future<List<dynamic>?> getDirectoryComments(
|
||||
String contactId, {
|
||||
bool active = true,
|
||||
}) async {
|
||||
final url = "${ApiEndpoints.getDirectoryNotes}/$contactId?active=$active";
|
||||
final response = await _getRequest(url);
|
||||
final data = response != null
|
||||
? _parseResponse(response, label: 'Directory Comments')
|
||||
: null;
|
||||
|
||||
return data is List ? data : null;
|
||||
}
|
||||
static Future<List<dynamic>?> getDirectoryComments(
|
||||
String contactId, {
|
||||
bool active = true,
|
||||
}) async {
|
||||
final url = "${ApiEndpoints.getDirectoryNotes}/$contactId?active=$active";
|
||||
final response = await _getRequest(url);
|
||||
final data = response != null
|
||||
? _parseResponse(response, label: 'Directory Comments')
|
||||
: null;
|
||||
|
||||
return data is List ? data : null;
|
||||
}
|
||||
|
||||
static Future<bool> updateContact(
|
||||
String contactId, Map<String, dynamic> payload) async {
|
||||
|
@ -71,7 +71,15 @@ class BaseBottomSheet extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
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
|
||||
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_confirmation_dialog.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
|
||||
Future<T?> showAddExpenseBottomSheet<T>({
|
||||
@ -157,6 +158,31 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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>(
|
||||
icon: Icons.work_outline,
|
||||
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