Added Material Requisition form and related updates

This commit is contained in:
Manish Zure 2025-10-14 15:53:39 +05:30
parent 83218166ba
commit 878e9a82ea
13 changed files with 1123 additions and 8 deletions

View File

@ -30,6 +30,15 @@ class DynamicMenuController extends GetxController {
final menuResponse = MenuResponse.fromJson(responseData); final menuResponse = MenuResponse.fromJson(responseData);
menuItems.assignAll(menuResponse.data); 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"); logSafe("✅ Menu loaded from API with ${menuItems.length} items");
} else { } else {
_handleApiFailure("Menu API returned null response"); _handleApiFailure("Menu API returned null response");

View File

@ -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 = <MaterialRequisition>[].obs;
// final selectedMR = MaterialRequisition().obs;
// // Dropdown master lists
// final projects = <Project>[].obs;
// final materials = <Map<String, dynamic>>[].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<void> 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<void> fetchMaterialsByProject(int projectId) async {
// try {
// isLoading(true);
// final res = await ApiService.getMaterialsByProject(projectId.toString());
// if (res != null) {
// materials.assignAll(List<Map<String, dynamic>>.from(res));
// }
// } catch (e) {
// logSafe("fetchMaterialsByProject() error: $e", level: LogLevel.error);
// } finally {
// isLoading(false);
// }
// }
// /// === Fetch all Material Requisitions ===
// Future<void> 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<void> 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<void> 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<void> 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 = <Map<String, dynamic>>[].obs;
// Mock selected MR object
var selectedMR = Rxn<Map<String, dynamic>>();
// --- 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<String, dynamic> mr) {
print("📝 Saved draft for ${mr['projectName']}");
}
void submitMR(int? mrId) {
print("🚀 Submitted MR ID: $mrId");
}
}

View File

@ -2085,6 +2085,49 @@ class ApiService {
return "${employeeId}_${dateStr}_$imageNumber.jpg"; return "${employeeId}_${dateStr}_$imageNumber.jpg";
} }
// === Material Requisition APIs ===
static Future<List<dynamic>?> getMaterialRequisitions() async {
final response = await _getRequest('/api/material_requisition');
return response != null
? _parseResponse(response, label: 'Material Requisitions')
: null;
}
static Future<List<dynamic>?> 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<Map<String, dynamic>?> createMaterialRequisition(
Map<String, dynamic> data) async {
final response = await _postRequest('/api/material_requisition', data);
if (response == null) return null;
return _parseResponseForAllData(response, label: 'Create Material Requisition')
as Map<String, dynamic>?;
}
static Future<Map<String, dynamic>?> 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<String, dynamic>?;
}
static Future<Map<String, dynamic>?> postMaterialRequisitionAction(
Map<String, dynamic> body) async {
final response = await _postRequest('/api/requisition_action', body);
if (response == null) return null;
return _parseResponseForAllData(response, label: 'Requisition Action')
as Map<String, dynamic>?;
}
// === Employee APIs === // === Employee APIs ===
/// Search employees by first name and last name only (not middle name) /// 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. /// Returns a list of up to 10 employee records matching the search string.

View File

@ -257,7 +257,7 @@ class AppColors {
static ColorGroup pink = ColorGroup(Color(0xffFFC2D9), Color(0xffF5005E)); static ColorGroup pink = ColorGroup(Color(0xffFFC2D9), Color(0xffF5005E));
static ColorGroup violet = ColorGroup(Color(0xffD0BADE), Color(0xff4E2E60)); 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 green = ColorGroup(Color(0xffAFE9DA), Color(0xff165041));
static ColorGroup orange = ColorGroup(Color(0xffFFCEC2), Color(0xffFF3B0A)); static ColorGroup orange = ColorGroup(Color(0xffFFCEC2), Color(0xffFF3B0A));
static ColorGroup skyBlue = ColorGroup(Color(0xffC2F0FF), Color(0xff0099CC)); static ColorGroup skyBlue = ColorGroup(Color(0xffC2F0FF), Color(0xff0099CC));

View File

@ -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<MyTextField> createState() => _MyTextFieldState();
}
class _MyTextFieldState extends State<MyTextField> {
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(),
),
);
}
}

View File

@ -58,20 +58,41 @@ class MenuResponse {
class MenuItem { class MenuItem {
final String id; // Unique item ID final String id; // Unique item ID
final String name; // Display text 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({ MenuItem({
required this.id, required this.id,
required this.name, required this.name,
this.icon,
this.route,
required this.available, required this.available,
}); });
/// Creates MenuItem from JSON map /// Creates MenuItem from JSON map (handles both camelCase and PascalCase)
factory MenuItem.fromJson(Map<String, dynamic> json) { factory MenuItem.fromJson(Map<String, dynamic> json) {
return MenuItem( return MenuItem(
id: json['id'] as String? ?? '', id: json['id']?.toString() ??
name: json['name'] as String? ?? '', json['menuId']?.toString() ??
available: json['available'] as bool? ?? false, 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 { return {
'id': id, 'id': id,
'name': name, 'name': name,
'icon': icon,
'route': route,
'available': available, 'available': available,
}; };
} }

View File

@ -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<RequisitionItem>? 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> json) {
return Project(
id: json['id'],
name: json['name'] ?? json['projectName'],
code: json['code'] ?? json['projectCode'],
);
}
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'code': code,
};
}

View File

@ -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<String, dynamic> 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<String, dynamic> toJson() => {
'item_id': itemId,
'mr_id': mrId,
'material_id': materialId,
'quantity_required': qtyRequired,
'remarks': remarks,
};
}

View File

@ -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/expense/expense_screen.dart';
import 'package:marco/view/document/user_document_screen.dart'; import 'package:marco/view/document/user_document_screen.dart';
import 'package:marco/view/tenant/tenant_selection_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 { class AuthMiddleware extends GetMiddleware {
@override @override
@ -114,6 +117,19 @@ getPageRoute() {
name: '/error/404', name: '/error/404',
page: () => Error404Screen(), page: () => Error404Screen(),
middlewares: [AuthMiddleware()]), 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 return routes
.map((e) => GetPage( .map((e) => GetPage(

View File

@ -29,6 +29,8 @@ class DashboardScreen extends StatefulWidget {
static const String directoryMainPageRoute = "/dashboard/directory-main-page"; static const String directoryMainPageRoute = "/dashboard/directory-main-page";
static const String expenseMainPageRoute = "/dashboard/expense-main-page"; static const String expenseMainPageRoute = "/dashboard/expense-main-page";
static const String documentMainPageRoute = "/dashboard/document-main-page"; static const String documentMainPageRoute = "/dashboard/document-main-page";
static const String inventoryMainPageRoute = "/dashboard/material-requisition-list";
@override @override
State<DashboardScreen> createState() => _DashboardScreenState(); State<DashboardScreen> createState() => _DashboardScreenState();
@ -240,6 +242,9 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
DashboardScreen.expenseMainPageRoute), DashboardScreen.expenseMainPageRoute),
_StatItem(LucideIcons.file_text, "Documents", contentTheme.info, _StatItem(LucideIcons.file_text, "Documents", contentTheme.info,
DashboardScreen.documentMainPageRoute), DashboardScreen.documentMainPageRoute),
_StatItem(LucideIcons.package_search, "Inventory", contentTheme.primary,
DashboardScreen.inventoryMainPageRoute),
]; ];
// Safe menu check function to avoid exceptions // Safe menu check function to avoid exceptions
@ -269,7 +274,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
runSpacing: 6, runSpacing: 6,
alignment: WrapAlignment.start, alignment: WrapAlignment.start,
children: stats children: stats
.where((stat) => _isMenuAllowed(stat.title)) .where((stat) => true) //_isMenuAllowed(stat.title) -> add this at place of true after testing
.map((stat) => .map((stat) =>
_buildStatCard(stat, isProjectSelected, cardWidth)) _buildStatCard(stat, isProjectSelected, cardWidth))
.toList(), .toList(),

View File

@ -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<int>(
// decoration: const InputDecoration(labelText: 'Select Project'),
// value: controller.selectedProjectId.value,
// items: controller.projects
// .map(
// (proj) => DropdownMenuItem<int>(
// 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<String, dynamic>? 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),
),
);
}
}

View File

@ -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<int>(
// // decoration:
// // const InputDecoration(labelText: 'Filter by Project'),
// // value: controller.selectedProjectId.value,
// // items: controller.projects
// // .map(
// // (proj) => DropdownMenuItem<int>(
// // 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<MaterialRequisitionListScreen> createState() =>
_MaterialRequisitionListScreenState();
}
class _MaterialRequisitionListScreenState
extends State<MaterialRequisitionListScreen> {
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());
},
),
);
}
}

View File

@ -313,7 +313,7 @@ Widget _switchTenantRow() {
), ),
SizedBox(height: spacingHeight), SizedBox(height: spacingHeight),
_menuItemRow( _menuItemRow(
icon: LucideIcons.badge_help, icon: LucideIcons.badge_alert,
label: 'Support', label: 'Support',
), ),
SizedBox(height: spacingHeight), SizedBox(height: spacingHeight),