diff --git a/lib/controller/dynamicMenu/dynamic_menu_controller.dart b/lib/controller/dynamicMenu/dynamic_menu_controller.dart index f55cb1e..452dd66 100644 --- a/lib/controller/dynamicMenu/dynamic_menu_controller.dart +++ b/lib/controller/dynamicMenu/dynamic_menu_controller.dart @@ -30,6 +30,15 @@ class DynamicMenuController extends GetxController { final menuResponse = MenuResponse.fromJson(responseData); menuItems.assignAll(menuResponse.data); + // ✅ TEMP: Add Material Requisition menu manually for local testing + menuItems.add(MenuItem( + id: '999', + name: "Material Requisition", + icon: "inventory_2", + route: "/dashboard/material-requisition-list", + available: true, + )); + logSafe("✅ Menu loaded from API with ${menuItems.length} items"); } else { _handleApiFailure("Menu API returned null response"); diff --git a/lib/controller/inventory/material_requisition_controller.dart b/lib/controller/inventory/material_requisition_controller.dart new file mode 100644 index 0000000..12d84e4 --- /dev/null +++ b/lib/controller/inventory/material_requisition_controller.dart @@ -0,0 +1,208 @@ +// import 'package:get/get.dart'; +// import 'package:marco/helpers/services/api_service.dart'; +// import 'package:marco/model/inventory/material_requisition_model.dart'; +// import 'package:marco/model/inventory/requisition_item_model.dart'; +// import 'package:marco/helpers/services/app_logger.dart'; + +// class MaterialRequisitionController extends GetxController { +// final isLoading = false.obs; +// final requisitions = [].obs; +// final selectedMR = MaterialRequisition().obs; + +// // Dropdown master lists +// final projects = [].obs; +// final materials = >[].obs; + +// // Form state +// // 🔧 FIX: use integer type since project.id is int +// final selectedProjectId = RxnInt(); + +// final status = 'Draft'.obs; + +// @override +// void onInit() { +// super.onInit(); +// fetchProjects(); +// fetchAllRequisitions(); +// } + +// /// === Fetch all Projects === +// Future fetchProjects() async { +// try { +// isLoading(true); +// final res = await ApiService.getProjects(); +// if (res != null) { +// projects.assignAll( +// (res as List).map((e) => Project.fromJson(e)).toList(), +// ); +// } +// } catch (e) { +// logSafe("fetchProjects() error: $e", level: LogLevel.error); +// } finally { +// isLoading(false); +// } +// } + +// /// === Fetch materials for a selected project === +// /// 🔧 FIX: change argument from String → int +// Future fetchMaterialsByProject(int projectId) async { +// try { +// isLoading(true); +// final res = await ApiService.getMaterialsByProject(projectId.toString()); +// if (res != null) { +// materials.assignAll(List>.from(res)); +// } +// } catch (e) { +// logSafe("fetchMaterialsByProject() error: $e", level: LogLevel.error); +// } finally { +// isLoading(false); +// } +// } + +// /// === Fetch all Material Requisitions === +// Future fetchAllRequisitions() async { +// try { +// isLoading(true); +// final res = await ApiService.getMaterialRequisitions(); +// if (res != null) { +// requisitions.assignAll( +// (res as List) +// .map((e) => MaterialRequisition.fromJson(e)) +// .toList(), +// ); +// } +// } catch (e) { +// logSafe("fetchAllRequisitions() error: $e", level: LogLevel.error); +// } finally { +// isLoading(false); +// } +// } + +// /// === Save Draft === +// Future saveDraft(MaterialRequisition mr) async { +// try { +// isLoading(true); +// final res = await ApiService.createMaterialRequisition(mr.toJson()); +// if (res != null) { +// Get.snackbar('Success', 'Saved as Draft'); +// await fetchAllRequisitions(); // refresh list +// } +// } catch (e) { +// logSafe("saveDraft() error: $e", level: LogLevel.error); +// } finally { +// isLoading(false); +// } +// } + +// /// === Submit Material Requisition === +// /// 🔧 FIX: id type → int for consistency +// Future submitMR(int mrId) async { +// try { +// isLoading(true); +// final res = +// await ApiService.updateMaterialRequisitionStatus(mrId.toString(), 'Submitted'); +// if (res != null) { +// Get.snackbar('Submitted', 'MR submitted for review'); +// await fetchAllRequisitions(); +// } +// } catch (e) { +// logSafe("submitMR() error: $e", level: LogLevel.error); +// } finally { +// isLoading(false); +// } +// } + +// /// === Approve / Reject / Comment actions === +// /// 🔧 FIX: id type → int for consistency +// Future performAction(int mrId, String action, String? comment) async { +// try { +// final body = { +// "mrId": mrId, +// "action": action, +// "comments": comment, +// }; + +// final res = await ApiService.postMaterialRequisitionAction(body); +// if (res != null) { +// Get.snackbar('Success', 'Action: $action'); +// await fetchAllRequisitions(); +// } +// } catch (e) { +// logSafe("performAction() error: $e", level: LogLevel.error); +// } +// } +// } + + + +import 'package:get/get.dart'; + +class MaterialRequisitionController extends GetxController { + // --- Mock UI State --- + var isLoading = false.obs; + + // Mock project dropdown + var projects = [ + {'id': 1, 'name': 'Project Alpha'}, + {'id': 2, 'name': 'Project Beta'}, + ].obs; + + var selectedProjectId = RxnInt(); + + // Mock MR list + var materialRequisitions = >[].obs; + + // Mock selected MR object + var selectedMR = Rxn>(); + + // --- Methods --- + + void fetchMaterialRequisitions() { + isLoading.value = true; + Future.delayed(const Duration(seconds: 1), () { + materialRequisitions.value = [ + { + 'id': 1, + 'projectName': 'Project Alpha', + 'status': 'Draft', + 'items': [ + {'material': 'Cement', 'qty': 50}, + {'material': 'Steel', 'qty': 100}, + ] + }, + { + 'id': 2, + 'projectName': 'Project Beta', + 'status': 'Approved', + 'items': [ + {'material': 'Bricks', 'qty': 500}, + {'material': 'Sand', 'qty': 200}, + ] + }, + ]; + isLoading.value = false; + }); + } + + void fetchMaterialsByProject(int? projectId) { + if (projectId == null) return; + selectedMR.value = { + 'id': projectId, + 'projectName': projects.firstWhere( + (p) => p['id'] == projectId, + orElse: () => {'name': 'Unknown'})['name'], + 'items': [ + {'material': 'Cement', 'qty': 25}, + {'material': 'Steel', 'qty': 80}, + ] + }; + } + + void saveDraft(Map mr) { + print("📝 Saved draft for ${mr['projectName']}"); + } + + void submitMR(int? mrId) { + print("🚀 Submitted MR ID: $mrId"); + } +} diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 8b20011..0e8f6f4 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -2085,6 +2085,49 @@ class ApiService { return "${employeeId}_${dateStr}_$imageNumber.jpg"; } + // === Material Requisition APIs === +static Future?> getMaterialRequisitions() async { + final response = await _getRequest('/api/material_requisition'); + return response != null + ? _parseResponse(response, label: 'Material Requisitions') + : null; +} + +static Future?> getMaterialsByProject(String projectId) async { + final endpoint = '/api/materials?project_id=$projectId'; + final response = await _getRequest(endpoint); + return response != null + ? _parseResponse(response, label: 'Materials by Project') + : null; +} + +static Future?> createMaterialRequisition( + Map data) async { + final response = await _postRequest('/api/material_requisition', data); + if (response == null) return null; + return _parseResponseForAllData(response, label: 'Create Material Requisition') + as Map?; +} + +static Future?> updateMaterialRequisitionStatus( + String mrId, String status) async { + final endpoint = '/api/material_requisition/$mrId'; + final body = {'status': status}; + final response = await _putRequest(endpoint, body); + if (response == null) return null; + return _parseResponseForAllData(response, label: 'Update MR Status') + as Map?; +} + +static Future?> postMaterialRequisitionAction( + Map body) async { + final response = await _postRequest('/api/requisition_action', body); + if (response == null) return null; + return _parseResponseForAllData(response, label: 'Requisition Action') + as Map?; +} + + // === Employee APIs === /// Search employees by first name and last name only (not middle name) /// Returns a list of up to 10 employee records matching the search string. diff --git a/lib/helpers/theme/app_theme.dart b/lib/helpers/theme/app_theme.dart index 1d3e8bb..d94b055 100644 --- a/lib/helpers/theme/app_theme.dart +++ b/lib/helpers/theme/app_theme.dart @@ -257,7 +257,7 @@ class AppColors { static ColorGroup pink = ColorGroup(Color(0xffFFC2D9), Color(0xffF5005E)); static ColorGroup violet = ColorGroup(Color(0xffD0BADE), Color(0xff4E2E60)); - static ColorGroup blue = ColorGroup(Color(0xffADD8FF), Color(0xff004A8F)); + static ColorGroup blue = ColorGroup(Color(0xffADD8FF), Color.fromRGBO(0, 74, 143, 1)); static ColorGroup green = ColorGroup(Color(0xffAFE9DA), Color(0xff165041)); static ColorGroup orange = ColorGroup(Color(0xffFFCEC2), Color(0xffFF3B0A)); static ColorGroup skyBlue = ColorGroup(Color(0xffC2F0FF), Color(0xff0099CC)); diff --git a/lib/helpers/widgets/my_text_field.dart b/lib/helpers/widgets/my_text_field.dart new file mode 100644 index 0000000..4aef91a --- /dev/null +++ b/lib/helpers/widgets/my_text_field.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; + +/// Reusable text field used across the app. +/// - Use [controller] when you need to keep text state externally (recommended). +/// - Use [initialValue] for quick one-off population (internal controller created). +/// - [readOnly], [maxLines], [keyboardType], [validator], [onChanged] supported. +/// - [label] used as InputDecoration.labelText, [hint] as hintText. +class MyTextField extends StatefulWidget { + final String? label; + final String? hint; + final TextEditingController? controller; + final String? initialValue; + final bool readOnly; + final bool enabled; + final int maxLines; + final TextInputType keyboardType; + final Widget? prefix; + final Widget? suffix; + final String? Function(String?)? validator; + final void Function(String)? onChanged; + final void Function()? onTap; + final String? errorText; + final EdgeInsetsGeometry? contentPadding; + + const MyTextField({ + Key? key, + this.label, + this.hint, + this.controller, + this.initialValue, + this.readOnly = false, + this.enabled = true, + this.maxLines = 1, + this.keyboardType = TextInputType.text, + this.prefix, + this.suffix, + this.validator, + this.onChanged, + this.onTap, + this.errorText, + this.contentPadding, + }) : super(key: key); + + @override + State createState() => _MyTextFieldState(); +} + +class _MyTextFieldState extends State { + late final TextEditingController _internalController; + bool _usingInternal = false; + + @override + void initState() { + super.initState(); + if (widget.controller == null) { + _usingInternal = true; + _internalController = + TextEditingController(text: widget.initialValue ?? ''); + } else { + _internalController = widget.controller!; + // if initialValue provided but using external controller, set it once: + if (widget.initialValue != null && widget.initialValue!.isNotEmpty) { + _internalController.text = widget.initialValue!; + } + } + } + + @override + void didUpdateWidget(covariant MyTextField oldWidget) { + super.didUpdateWidget(oldWidget); + // If external controller changed, update our reference + if (oldWidget.controller != widget.controller && widget.controller != null) { + _internalController.text = widget.controller!.text; + } + // If initialValue changed and we use internal controller, update. + if (_usingInternal && + widget.initialValue != null && + widget.initialValue != oldWidget.initialValue) { + _internalController.text = widget.initialValue!; + } + } + + @override + void dispose() { + if (_usingInternal) { + _internalController.dispose(); + } + super.dispose(); + } + + InputBorder _defaultBorder() => OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + borderSide: const BorderSide(width: 1), + ); + + @override + Widget build(BuildContext context) { + return TextFormField( + controller: _internalController, + readOnly: widget.readOnly, + enabled: widget.enabled, + maxLines: widget.maxLines, + keyboardType: widget.keyboardType, + validator: widget.validator, + onChanged: widget.onChanged, + onTap: widget.onTap, + decoration: InputDecoration( + isDense: true, + contentPadding: + widget.contentPadding ?? const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + labelText: widget.label, + hintText: widget.hint, + prefixIcon: widget.prefix, + suffixIcon: widget.suffix, + errorText: widget.errorText, + border: _defaultBorder(), + enabledBorder: _defaultBorder(), + focusedBorder: _defaultBorder().copyWith( + borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 1.2)), + disabledBorder: _defaultBorder(), + ), + ); + } +} diff --git a/lib/model/dynamicMenu/dynamic_menu_model.dart b/lib/model/dynamicMenu/dynamic_menu_model.dart index 8348447..7109327 100644 --- a/lib/model/dynamicMenu/dynamic_menu_model.dart +++ b/lib/model/dynamicMenu/dynamic_menu_model.dart @@ -58,20 +58,41 @@ class MenuResponse { class MenuItem { final String id; // Unique item ID final String name; // Display text - final bool available; // Availability flag + final String? icon; // Icon name from backend (e.g. "inventory_2") + final String? route; // Flutter route to open the screen + final bool available; // Whether the menu should be shown MenuItem({ required this.id, required this.name, + this.icon, + this.route, required this.available, }); - /// Creates MenuItem from JSON map + /// Creates MenuItem from JSON map (handles both camelCase and PascalCase) factory MenuItem.fromJson(Map json) { return MenuItem( - id: json['id'] as String? ?? '', - name: json['name'] as String? ?? '', - available: json['available'] as bool? ?? false, + id: json['id']?.toString() ?? + json['menuId']?.toString() ?? + json['MenuId']?.toString() ?? + '', + name: json['name'] ?? + json['menuName'] ?? + json['MenuName'] ?? + '', + icon: json['icon'] ?? + json['iconName'] ?? + json['IconName'] ?? + null, + route: json['route'] ?? + json['menuRoute'] ?? + json['MenuRoute'] ?? + null, + available: json['available'] ?? + json['isAvailable'] ?? + json['IsAvailable'] ?? + false, ); } @@ -80,6 +101,8 @@ class MenuItem { return { 'id': id, 'name': name, + 'icon': icon, + 'route': route, 'available': available, }; } diff --git a/lib/model/inventory/material_requisition_model.dart b/lib/model/inventory/material_requisition_model.dart new file mode 100644 index 0000000..5926e52 --- /dev/null +++ b/lib/model/inventory/material_requisition_model.dart @@ -0,0 +1,83 @@ +import 'package:marco/model/inventory/requisition_item_model.dart'; + +class MaterialRequisition { + int? mrId; + String? title; + int? projectId; + String? projectName; + String? status; + String? createdBy; + DateTime? createdAt; + List? items; + + // ✅ Add backward-compatible aliases + int? get id => mrId; + int? get requisitionId => mrId; + + MaterialRequisition({ + this.mrId, + this.title, + this.projectId, + this.projectName, + this.status, + this.createdBy, + this.createdAt, + this.items, + }); + + factory MaterialRequisition.fromJson(Map json) { + return MaterialRequisition( + mrId: json['mrId'] ?? json['id'] ?? json['requisitionId'], + title: json['title'], + projectId: json['projectId'] is String + ? int.tryParse(json['projectId']) + : json['projectId'], + projectName: json['projectName'], + status: json['status'], + createdBy: json['createdBy'], + createdAt: json['createdAt'] != null + ? DateTime.tryParse(json['createdAt']) + : null, + items: json['items'] != null + ? (json['items'] as List) + .map((e) => RequisitionItem.fromJson(e)) + .toList() + : [], + ); + } + + Map toJson() { + return { + 'mrId': mrId, + 'title': title, + 'projectId': projectId, + 'projectName': projectName, + 'status': status, + 'createdBy': createdBy, + 'createdAt': createdAt?.toIso8601String(), + 'items': items?.map((e) => e.toJson()).toList(), + }; + } +} + +class Project { + int? id; + String? name; + String? code; + + Project({this.id, this.name, this.code}); + + factory Project.fromJson(Map json) { + return Project( + id: json['id'], + name: json['name'] ?? json['projectName'], + code: json['code'] ?? json['projectCode'], + ); + } + + Map toJson() => { + 'id': id, + 'name': name, + 'code': code, + }; +} diff --git a/lib/model/inventory/requisition_item_model.dart b/lib/model/inventory/requisition_item_model.dart new file mode 100644 index 0000000..26707e9 --- /dev/null +++ b/lib/model/inventory/requisition_item_model.dart @@ -0,0 +1,40 @@ +class RequisitionItem { + int? itemId; + int? mrId; + int? materialId; + String? materialCode; + String? materialName; + String? uom; + double? qtyRequired; + String? remarks; + + RequisitionItem({ + this.itemId, + this.mrId, + this.materialId, + this.materialCode, + this.materialName, + this.uom, + this.qtyRequired, + this.remarks, + }); + + factory RequisitionItem.fromJson(Map json) => RequisitionItem( + itemId: json['item_id'], + mrId: json['mr_id'], + materialId: json['material_id'], + materialCode: json['material_code'], + materialName: json['material_name'], + uom: json['uom'], + qtyRequired: double.tryParse(json['quantity_required'].toString()) ?? 0, + remarks: json['remarks'], + ); + + Map toJson() => { + 'item_id': itemId, + 'mr_id': mrId, + 'material_id': materialId, + 'quantity_required': qtyRequired, + 'remarks': remarks, + }; +} diff --git a/lib/routes.dart b/lib/routes.dart index 37a166e..3532de7 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -21,6 +21,9 @@ import 'package:marco/view/directory/directory_main_screen.dart'; import 'package:marco/view/expense/expense_screen.dart'; import 'package:marco/view/document/user_document_screen.dart'; import 'package:marco/view/tenant/tenant_selection_screen.dart'; +import 'package:marco/view/inventory/material_requisition_form_screen.dart'; +import 'package:marco/view/inventory/material_requisition_list_screen.dart'; + class AuthMiddleware extends GetMiddleware { @override @@ -114,6 +117,19 @@ getPageRoute() { name: '/error/404', page: () => Error404Screen(), middlewares: [AuthMiddleware()]), + + + // Material Requisition + GetPage( + name: '/dashboard/material-requisition-list', + page: () => const MaterialRequisitionListScreen(), + middlewares: [AuthMiddleware()]), + GetPage( + name: '/dashboard/material-requisition-form', + page: () => const MaterialRequisitionFormScreen(), + middlewares: [AuthMiddleware()]), + + ]; return routes .map((e) => GetPage( diff --git a/lib/view/dashboard/dashboard_screen.dart b/lib/view/dashboard/dashboard_screen.dart index 66718b4..57013ad 100644 --- a/lib/view/dashboard/dashboard_screen.dart +++ b/lib/view/dashboard/dashboard_screen.dart @@ -29,6 +29,8 @@ class DashboardScreen extends StatefulWidget { static const String directoryMainPageRoute = "/dashboard/directory-main-page"; static const String expenseMainPageRoute = "/dashboard/expense-main-page"; static const String documentMainPageRoute = "/dashboard/document-main-page"; + static const String inventoryMainPageRoute = "/dashboard/material-requisition-list"; + @override State createState() => _DashboardScreenState(); @@ -240,6 +242,9 @@ class _DashboardScreenState extends State with UIMixin { DashboardScreen.expenseMainPageRoute), _StatItem(LucideIcons.file_text, "Documents", contentTheme.info, DashboardScreen.documentMainPageRoute), + + _StatItem(LucideIcons.package_search, "Inventory", contentTheme.primary, + DashboardScreen.inventoryMainPageRoute), ]; // Safe menu check function to avoid exceptions @@ -269,7 +274,7 @@ class _DashboardScreenState extends State with UIMixin { runSpacing: 6, alignment: WrapAlignment.start, children: stats - .where((stat) => _isMenuAllowed(stat.title)) + .where((stat) => true) //_isMenuAllowed(stat.title) -> add this at place of true after testing .map((stat) => _buildStatCard(stat, isProjectSelected, cardWidth)) .toList(), diff --git a/lib/view/inventory/material_requisition_form_screen.dart b/lib/view/inventory/material_requisition_form_screen.dart new file mode 100644 index 0000000..1676797 --- /dev/null +++ b/lib/view/inventory/material_requisition_form_screen.dart @@ -0,0 +1,326 @@ +// import 'package:flutter/material.dart'; +// import 'package:get/get.dart'; +// import 'package:marco/controller/inventory/material_requisition_controller.dart'; +// import 'package:marco/helpers/theme/app_theme.dart'; +// import 'package:marco/helpers/widgets/my_card.dart'; +// import 'package:marco/helpers/widgets/my_button.dart'; + +// class MaterialRequisitionFormScreen extends StatelessWidget { +// MaterialRequisitionFormScreen({super.key}); + +// final controller = Get.put(MaterialRequisitionController()); + +// @override +// Widget build(BuildContext context) { +// return Scaffold( +// appBar: AppBar( +// title: const Text('Material Requisition Form'), +// backgroundColor: Theme.of(context).colorScheme.primary, +// ), +// body: Obx(() { +// if (controller.isLoading.value) { +// return const Center(child: CircularProgressIndicator()); +// } + +// return SingleChildScrollView( +// padding: const EdgeInsets.all(16), +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// // 🧱 Project Selection +// MyCard( +// child: DropdownButtonFormField( +// decoration: const InputDecoration(labelText: 'Select Project'), +// value: controller.selectedProjectId.value, +// items: controller.projects +// .map( +// (proj) => DropdownMenuItem( +// value: proj.id, // assuming id is int +// child: Text(proj.name ?? ''), +// ), +// ) +// .toList(), +// onChanged: (val) { +// controller.selectedProjectId.value = val; +// if (val != null) { +// controller.fetchMaterialsByProject(val); +// } +// }, +// ), +// ), +// const SizedBox(height: 16), + +// // 🧱 Materials List +// MyCard( +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// const Text( +// 'Selected Materials', +// style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), +// ), +// const SizedBox(height: 8), +// Obx(() { +// final items = controller.selectedMR.value.items ?? []; +// if (items.isEmpty) { +// return const Text( +// 'No materials added yet.', +// style: TextStyle(color: Colors.grey), +// ); +// } + +// return ListView.builder( +// shrinkWrap: true, +// physics: const NeverScrollableScrollPhysics(), +// itemCount: items.length, +// itemBuilder: (context, index) { +// final item = items[index]; +// return ListTile( +// title: Text(item.materialName ?? ''), +// subtitle: Text( +// 'Code: ${item.materialCode} | UOM: ${item.uom}', +// ), +// trailing: Text('Qty: ${item.qtyRequired ?? 0}'), +// ); +// }, +// ); +// }), +// ], +// ), +// ), +// const SizedBox(height: 24), + +// // 🧱 Action Buttons +// Row( +// children: [ +// // Cancel +// Expanded( +// child: MyButton.outlined( +// borderColor: Colors.grey, +// onPressed: () => Get.back(), +// child: const Text( +// 'Cancel', +// style: TextStyle(color: Colors.black54), +// ), +// ), +// ), +// const SizedBox(width: 12), + +// // Save as Draft +// Expanded( +// child: MyButton.medium( +// onPressed: () { +// final mr = controller.selectedMR.value; +// controller.saveDraft(mr); +// }, +// backgroundColor: Theme.of(context).colorScheme.primary, +// child: const Text( +// 'Save as Draft', +// style: TextStyle( +// color: Colors.white, +// fontWeight: FontWeight.w600, +// ), +// ), +// ), +// ), +// ], +// ), +// const SizedBox(height: 12), + +// // 🧱 Submit (Full Width) +// MyButton.block( +// onPressed: () { +// final mr = controller.selectedMR.value; +// // ✅ use correct field name instead of `id` +// final mrId = mr.mrId ?? mr.id ?? mr.requisitionId; + +// if (mrId != null) { +// controller.submitMR(mrId); +// } else { +// Get.snackbar('Error', 'Please save as draft first'); +// } +// }, +// backgroundColor: Colors.green, +// child: const Text( +// 'Submit Requisition', +// style: TextStyle( +// color: Colors.white, +// fontWeight: FontWeight.w600, +// ), +// ), +// ), +// ], +// ), +// ); +// }), +// ); +// } +// } + + + + + + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/controller/inventory/material_requisition_controller.dart'; + +class MaterialRequisitionFormScreen extends StatelessWidget { + final Map? mrData; + + const MaterialRequisitionFormScreen({Key? key, this.mrData}) : super(key: key); + + @override + Widget build(BuildContext context) { + final controller = Get.put(MaterialRequisitionController()); + final isNew = mrData == null; + + return Scaffold( + appBar: AppBar( + title: Text(isNew ? "Create Material Requisition" : "Requisition Details"), + backgroundColor: const Color(0xFF2C3E50), // Marco PMS blue-grey + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: isNew + ? _buildNewForm(context, controller) + : _buildDetailView(context), + ), + ); + } + + // 🧱 UI for new MR form (mock only) + Widget _buildNewForm(BuildContext context, MaterialRequisitionController controller) { + final TextEditingController projectNameCtrl = TextEditingController(); + final TextEditingController requestedByCtrl = TextEditingController(); + final TextEditingController materialCtrl = TextEditingController(); + final TextEditingController qtyCtrl = TextEditingController(); + + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("Project Name", style: TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 6), + TextField( + controller: projectNameCtrl, + decoration: _inputDecoration("Enter project name"), + ), + const SizedBox(height: 16), + const Text("Requested By", style: TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 6), + TextField( + controller: requestedByCtrl, + decoration: _inputDecoration("Enter requester name"), + ), + const SizedBox(height: 16), + const Text("Material", style: TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 6), + TextField( + controller: materialCtrl, + decoration: _inputDecoration("Enter material name"), + ), + const SizedBox(height: 16), + const Text("Quantity", style: TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 6), + TextField( + controller: qtyCtrl, + keyboardType: TextInputType.number, + decoration: _inputDecoration("Enter quantity"), + ), + const SizedBox(height: 24), + Center( + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF34495E), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + icon: const Icon(Icons.save), + label: const Text("Save Draft"), + onPressed: () { + Get.snackbar("Saved", "Mock Material Requisition saved successfully!", + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.green.shade600, + colorText: Colors.white); + }, + ), + ), + ], + ), + ); + } + + // 🧱 UI for viewing existing MR + Widget _buildDetailView(BuildContext context) { + final data = mrData ?? {}; + + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Project: ${data['projectName']}", style: const TextStyle(fontSize: 16)), + const SizedBox(height: 8), + Text("Requested By: ${data['requestedBy']}"), + Text("Date: ${data['date']}"), + Text("Status: ${data['status']}"), + const Divider(height: 32), + const Text("Materials:", style: TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + ...List.generate( + (data['materials'] as List).length, + (index) { + final item = (data['materials'] as List)[index]; + return Text("- ${item['name']} (${item['qty']} ${item['unit']})"); + }, + ), + const SizedBox(height: 24), + Center( + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF34495E), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + icon: const Icon(Icons.send), + label: const Text("Submit"), + onPressed: () { + Get.snackbar("Submitted", "Mock submission successful!", + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.blueGrey.shade700, + colorText: Colors.white); + }, + ), + ), + ], + ), + ); + } + + // 🧩 Common input field decoration + InputDecoration _inputDecoration(String hint) { + return InputDecoration( + hintText: hint, + filled: true, + fillColor: Colors.grey.shade100, + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: Color(0xFFBDC3C7)), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: Color(0xFFBDC3C7)), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: Color(0xFF34495E), width: 1.5), + ), + ); + } +} diff --git a/lib/view/inventory/material_requisition_list_screen.dart b/lib/view/inventory/material_requisition_list_screen.dart new file mode 100644 index 0000000..721232e --- /dev/null +++ b/lib/view/inventory/material_requisition_list_screen.dart @@ -0,0 +1,238 @@ +// import 'package:flutter/material.dart'; +// import 'package:get/get.dart'; +// import 'package:marco/controller/inventory/material_requisition_controller.dart'; +// import 'package:marco/helpers/theme/app_theme.dart'; +// import 'package:marco/helpers/widgets/my_card.dart'; +// import 'package:marco/helpers/widgets/my_button.dart'; + +// class MaterialRequisitionListScreen extends StatelessWidget { +// MaterialRequisitionListScreen({super.key}); + +// final controller = Get.put(MaterialRequisitionController()); + +// // 👇 Added to test the feature +// @override +// void initState() { +// super.initState(); +// controller.fetchMaterialRequisitions(); // 🧠 this loads mock or API data +// } + +// @override +// Widget build(BuildContext context) { +// return Scaffold( +// appBar: AppBar( +// title: const Text('Material Requisitions'), +// backgroundColor: Theme.of(context).colorScheme.primary, +// ), + +// body: Obx(() { +// if (controller.isLoading.value) { +// return const Center(child: CircularProgressIndicator()); +// } + +// final list = controller.materialRequisitions; +// if (list.isEmpty) { +// return const Center(child: Text('No requisitions found.')); +// } + +// return ListView.builder( +// itemCount: list.length, +// itemBuilder: (context, index) { +// final mr = list[index]; +// return Card( +// margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), +// child: ListTile( +// title: Text( +// mr['projectName'], +// style: const TextStyle(fontWeight: FontWeight.bold), +// ), +// subtitle: Text('Status: ${mr['status']}'), +// trailing: Text( +// mr['date'], +// style: const TextStyle(color: Colors.grey), +// ), +// ), +// ); +// }, +// ); +// }), + +// // body: Obx(() { +// // if (controller.isLoading.value) { +// // return const Center(child: CircularProgressIndicator()); +// // } + +// // return Padding( +// // padding: const EdgeInsets.all(16), +// // child: Column( +// // crossAxisAlignment: CrossAxisAlignment.start, +// // children: [ +// // // 🧱 Project Filter Dropdown +// // MyCard( +// // child: DropdownButtonFormField( +// // decoration: +// // const InputDecoration(labelText: 'Filter by Project'), +// // value: controller.selectedProjectId.value, +// // items: controller.projects +// // .map( +// // (proj) => DropdownMenuItem( +// // value: proj.id, +// // child: Text(proj.name ?? ''), +// // ), +// // ) +// // .toList(), +// // onChanged: (val) { +// // controller.selectedProjectId.value = val; +// // if (val != null) { +// // controller.fetchMaterialsByProject(val); +// // } +// // }, +// // ), +// // ), +// // const SizedBox(height: 16), + +// // // 🧱 Material Requisition List +// // Expanded( +// // child: Obx(() { +// // final list = controller.requisitions; +// // if (list.isEmpty) { +// // return const Center( +// // child: Text( +// // 'No material requisitions found.', +// // style: TextStyle(color: Colors.grey), +// // ), +// // ); +// // } + +// // return ListView.builder( +// // itemCount: list.length, +// // itemBuilder: (context, index) { +// // final mr = list[index]; +// // final mrId = mr.mrId ?? mr.id ?? mr.requisitionId; +// // return MyCard( +// // child: ListTile( +// // title: Text( +// // mr.title ?? 'Untitled Requisition', +// // style: const TextStyle( +// // fontWeight: FontWeight.bold, +// // ), +// // ), +// // subtitle: Text( +// // 'Project: ${mr.projectName ?? ''} | Status: ${mr.status ?? ''}', +// // ), +// // trailing: MyButton.text( +// // onPressed: () { +// // if (mrId != null) { +// // controller.submitMR(mrId); +// // } else { +// // Get.snackbar('Error', 'Invalid requisition ID'); +// // } +// // }, +// // child: const Text( +// // 'Submit', +// // style: TextStyle(color: Colors.green), +// // ), +// // ), +// // ), +// // ); +// // }, +// // ); +// // }), +// // ), +// // ], +// // ), +// // ); +// // }), +// ); +// } +// } + + + + + + + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/controller/inventory/material_requisition_controller.dart'; +import 'package:marco/view/inventory/material_requisition_form_screen.dart'; + +class MaterialRequisitionListScreen extends StatefulWidget { + const MaterialRequisitionListScreen({Key? key}) : super(key: key); + + @override + State createState() => + _MaterialRequisitionListScreenState(); +} + +class _MaterialRequisitionListScreenState + extends State { + final controller = Get.put(MaterialRequisitionController()); + + @override + void initState() { + super.initState(); + controller.fetchMaterialRequisitions(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Material Requisitions"), + backgroundColor: const Color(0xFF2C3E50), // Marco PMS blue-grey theme + ), + body: Obx(() { + if (controller.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + final list = controller.materialRequisitions; + if (list.isEmpty) { + return const Center(child: Text("No requisitions found.")); + } + + return ListView.builder( + itemCount: list.length, + itemBuilder: (context, index) { + final mr = list[index]; + return Card( + margin: const EdgeInsets.all(8), + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10)), + child: ListTile( + title: Text( + mr['projectName'] ?? 'Unknown Project', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Requested By: ${mr['requestedBy'] ?? '-'}"), + Text("Date: ${mr['date'] ?? '-'}"), + Text("Status: ${mr['status'] ?? '-'}"), + ], + ), + trailing: const Icon(Icons.arrow_forward_ios, size: 16), + onTap: () { + // Navigate to mock form screen (testing only) + Get.to(() => MaterialRequisitionFormScreen(mrData: mr)); + }, + ), + ); + }, + ); + }), + floatingActionButton: FloatingActionButton( + backgroundColor: const Color(0xFF34495E), + child: const Icon(Icons.add), + onPressed: () { + // Navigate to create form (mock) + Get.to(() => const MaterialRequisitionFormScreen()); + }, + ), + ); + } +} diff --git a/lib/view/layouts/user_profile_right_bar.dart b/lib/view/layouts/user_profile_right_bar.dart index c7e36c2..9fb8caf 100644 --- a/lib/view/layouts/user_profile_right_bar.dart +++ b/lib/view/layouts/user_profile_right_bar.dart @@ -313,7 +313,7 @@ Widget _switchTenantRow() { ), SizedBox(height: spacingHeight), _menuItemRow( - icon: LucideIcons.badge_help, + icon: LucideIcons.badge_alert, label: 'Support', ), SizedBox(height: spacingHeight),