Compare commits
No commits in common. "feature/inventory" and "main" have entirely different histories.
feature/in
...
main
@ -30,15 +30,6 @@ 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");
|
||||
|
@ -1,208 +0,0 @@
|
||||
// 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");
|
||||
}
|
||||
}
|
@ -2085,49 +2085,6 @@ class ApiService {
|
||||
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 ===
|
||||
/// 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.
|
||||
|
@ -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.fromRGBO(0, 74, 143, 1));
|
||||
static ColorGroup blue = ColorGroup(Color(0xffADD8FF), Color(0xff004A8F));
|
||||
static ColorGroup green = ColorGroup(Color(0xffAFE9DA), Color(0xff165041));
|
||||
static ColorGroup orange = ColorGroup(Color(0xffFFCEC2), Color(0xffFF3B0A));
|
||||
static ColorGroup skyBlue = ColorGroup(Color(0xffC2F0FF), Color(0xff0099CC));
|
||||
|
@ -1,124 +0,0 @@
|
||||
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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -58,41 +58,20 @@ class MenuResponse {
|
||||
class MenuItem {
|
||||
final String id; // Unique item ID
|
||||
final String name; // Display text
|
||||
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
|
||||
final bool available; // Availability flag
|
||||
|
||||
MenuItem({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.icon,
|
||||
this.route,
|
||||
required this.available,
|
||||
});
|
||||
|
||||
/// Creates MenuItem from JSON map (handles both camelCase and PascalCase)
|
||||
/// Creates MenuItem from JSON map
|
||||
factory MenuItem.fromJson(Map<String, dynamic> json) {
|
||||
return MenuItem(
|
||||
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,
|
||||
id: json['id'] as String? ?? '',
|
||||
name: json['name'] as String? ?? '',
|
||||
available: json['available'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
@ -101,8 +80,6 @@ class MenuItem {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'icon': icon,
|
||||
'route': route,
|
||||
'available': available,
|
||||
};
|
||||
}
|
||||
|
@ -1,83 +0,0 @@
|
||||
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,
|
||||
};
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
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,
|
||||
};
|
||||
}
|
@ -21,9 +21,6 @@ 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
|
||||
@ -117,19 +114,6 @@ 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(
|
||||
|
@ -29,8 +29,6 @@ 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<DashboardScreen> createState() => _DashboardScreenState();
|
||||
@ -242,9 +240,6 @@ class _DashboardScreenState extends State<DashboardScreen> 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
|
||||
@ -274,7 +269,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
runSpacing: 6,
|
||||
alignment: WrapAlignment.start,
|
||||
children: stats
|
||||
.where((stat) => true) //_isMenuAllowed(stat.title) -> add this at place of true after testing
|
||||
.where((stat) => _isMenuAllowed(stat.title))
|
||||
.map((stat) =>
|
||||
_buildStatCard(stat, isProjectSelected, cardWidth))
|
||||
.toList(),
|
||||
|
@ -1,326 +0,0 @@
|
||||
// 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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,238 +0,0 @@
|
||||
// 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());
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -313,7 +313,7 @@ Widget _switchTenantRow() {
|
||||
),
|
||||
SizedBox(height: spacingHeight),
|
||||
_menuItemRow(
|
||||
icon: LucideIcons.badge_alert,
|
||||
icon: LucideIcons.badge_help,
|
||||
label: 'Support',
|
||||
),
|
||||
SizedBox(height: spacingHeight),
|
||||
|
Loading…
x
Reference in New Issue
Block a user