po changes done

This commit is contained in:
Surendiran 2026-07-17 11:07:08 +05:30
parent eb94390961
commit 6721b308a9
31 changed files with 918 additions and 400 deletions

View File

@ -65,6 +65,11 @@ class ApiEndpoints {
static String deliveryTermById(String id) => '/masters/delivery-terms/$id'; static String deliveryTermById(String id) => '/masters/delivery-terms/$id';
static const String paymentTerms = '/masters/payment-terms'; static const String paymentTerms = '/masters/payment-terms';
static String paymentTermById(String id) => '/masters/payment-terms/$id'; static String paymentTermById(String id) => '/masters/payment-terms/$id';
static const String termsNotes = '/masters/terms-notes';
static String termsNoteById(String id) => '/masters/terms-notes/$id';
static const String termsNotesTypes = '/masters/terms-notes/types';
static String termsNotesByType(String type) =>
'/masters/terms-notes/by-type/$type';
static const String gstRates = '/masters/gst-rates'; static const String gstRates = '/masters/gst-rates';
static String gstRateById(String id) => '/masters/gst-rates/$id'; static String gstRateById(String id) => '/masters/gst-rates/$id';
static const String hsnCodes = '/masters/hsn-codes'; static const String hsnCodes = '/masters/hsn-codes';
@ -95,6 +100,8 @@ class ApiEndpoints {
// Purchase Orders // Purchase Orders
static const String purchaseOrders = '/purchase-orders'; static const String purchaseOrders = '/purchase-orders';
static const String purchaseOrdersPendingApproval =
'/purchase-orders/pending-approval';
static const String purchaseOrdersExport = '/purchase-orders/export'; static const String purchaseOrdersExport = '/purchase-orders/export';
static String purchaseOrderById(String id) => '/purchase-orders/$id'; static String purchaseOrderById(String id) => '/purchase-orders/$id';
static String purchaseOrderSubmit(String id) => '/purchase-orders/$id/submit'; static String purchaseOrderSubmit(String id) => '/purchase-orders/$id/submit';

View File

@ -47,6 +47,8 @@ class RouteConstants {
// Purchase Orders // Purchase Orders
static const String purchaseOrders = '/purchase-orders'; static const String purchaseOrders = '/purchase-orders';
static const String purchaseOrdersPendingApproval =
'/purchase-orders/pending-approval';
static const String purchaseOrderAdd = '/purchase-orders/add'; static const String purchaseOrderAdd = '/purchase-orders/add';
static const String purchaseOrderEdit = '/purchase-orders/:id/edit'; static const String purchaseOrderEdit = '/purchase-orders/:id/edit';
static const String purchaseOrderDetail = '/purchase-orders/:id'; static const String purchaseOrderDetail = '/purchase-orders/:id';

View File

@ -444,7 +444,6 @@ class LogServiceVisitPanel extends ConsumerStatefulWidget {
class _LogServiceVisitPanelState extends ConsumerState<LogServiceVisitPanel> { class _LogServiceVisitPanelState extends ConsumerState<LogServiceVisitPanel> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final _visitNumberController = TextEditingController();
final _complaintNoController = TextEditingController(); final _complaintNoController = TextEditingController();
final _complaintDescController = TextEditingController(); final _complaintDescController = TextEditingController();
final _engineerNameController = TextEditingController(); final _engineerNameController = TextEditingController();
@ -468,7 +467,6 @@ class _LogServiceVisitPanelState extends ConsumerState<LogServiceVisitPanel> {
@override @override
void dispose() { void dispose() {
_visitNumberController.dispose();
_complaintNoController.dispose(); _complaintNoController.dispose();
_complaintDescController.dispose(); _complaintDescController.dispose();
_engineerNameController.dispose(); _engineerNameController.dispose();
@ -488,9 +486,6 @@ class _LogServiceVisitPanelState extends ConsumerState<LogServiceVisitPanel> {
_visitType = visit.visitType; _visitType = visit.visitType;
_visitDate = visit.visitDate; _visitDate = visit.visitDate;
_amcContractId = visit.amcContractId; _amcContractId = visit.amcContractId;
if (visit.visitNumber != null) {
_visitNumberController.text = visit.visitNumber.toString();
}
_complaintNoController.text = visit.complaintNo ?? ''; _complaintNoController.text = visit.complaintNo ?? '';
_complaintDate = visit.complaintDate; _complaintDate = visit.complaintDate;
_complaintDescController.text = visit.complaintDesc ?? ''; _complaintDescController.text = visit.complaintDesc ?? '';
@ -529,14 +524,12 @@ class _LogServiceVisitPanelState extends ConsumerState<LogServiceVisitPanel> {
} }
Map<String, dynamic> _buildPayload() { Map<String, dynamic> _buildPayload() {
final visitNumber = int.tryParse(_visitNumberController.text.trim());
final downtimeHours = double.tryParse(_downtimeHoursController.text.trim()); final downtimeHours = double.tryParse(_downtimeHoursController.text.trim());
final serviceCost = double.tryParse(_serviceCostController.text.trim()); final serviceCost = double.tryParse(_serviceCostController.text.trim());
return { return {
if (_visitType != null && _visitType!.trim().isNotEmpty) 'visit_type': _visitType, if (_visitType != null && _visitType!.trim().isNotEmpty) 'visit_type': _visitType,
'visit_date': DateFormatter.toApiDate(_visitDate!), 'visit_date': DateFormatter.toApiDate(_visitDate!),
if (_amcContractId != null) 'amc_contract_id': _amcContractId, if (_amcContractId != null) 'amc_contract_id': _amcContractId,
if (visitNumber != null) 'visit_number': visitNumber,
if (_complaintNoController.text.trim().isNotEmpty) if (_complaintNoController.text.trim().isNotEmpty)
'complaint_no': _complaintNoController.text.trim(), 'complaint_no': _complaintNoController.text.trim(),
if (_complaintDate != null) if (_complaintDate != null)
@ -654,8 +647,7 @@ class _LogServiceVisitPanelState extends ConsumerState<LogServiceVisitPanel> {
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
SidePanelFormRow( _SidePanelDateField(
left: _SidePanelDateField(
label: 'Visit Date', label: 'Visit Date',
isRequired: true, isRequired: true,
value: _visitDate, value: _visitDate,
@ -664,16 +656,6 @@ class _LogServiceVisitPanelState extends ConsumerState<LogServiceVisitPanel> {
onPicked: (date) => setState(() => _visitDate = date), onPicked: (date) => setState(() => _visitDate = date),
), ),
), ),
right: AppTextField(
controller: _visitNumberController,
label: 'Visit Number',
keyboardType: TextInputType.number,
validator: (v) => Validators.optionalPositiveInt(
v,
fieldName: 'Visit Number',
),
),
),
const SizedBox(height: 12), const SizedBox(height: 12),
AppSearchableDropdown<int>( AppSearchableDropdown<int>(
label: 'AMC Contract', label: 'AMC Contract',

View File

@ -150,7 +150,7 @@ class GrnRemoteDataSource {
if (query.status != null) 'status': query.status, if (query.status != null) 'status': query.status,
if (query.poId != null) 'po_id': query.poId, if (query.poId != null) 'po_id': query.poId,
if (query.vendorId != null) 'vendor_id': query.vendorId, if (query.vendorId != null) 'vendor_id': query.vendorId,
if (query.warehouseId != null) 'warehouse_id': query.warehouseId, if (query.locationId != null) 'location_id': query.locationId,
if (query.dateFrom != null) 'date_from': query.dateFrom, if (query.dateFrom != null) 'date_from': query.dateFrom,
if (query.dateTo != null) 'date_to': query.dateTo, if (query.dateTo != null) 'date_to': query.dateTo,
}; };

View File

@ -9,12 +9,13 @@ import '../../../users/data/repositories/user_repository_impl.dart';
class GrnLookups { class GrnLookups {
const GrnLookups({ const GrnLookups({
this.warehouses = const [], this.locations = const [],
this.receivablePurchaseOrders = const [], this.receivablePurchaseOrders = const [],
this.users = const [], this.users = const [],
}); });
final List<FilterOptionModel> warehouses; /// Plants and warehouses (any location type) for list filters.
final List<FilterOptionModel> locations;
final List<PurchaseOrderModel> receivablePurchaseOrders; final List<PurchaseOrderModel> receivablePurchaseOrders;
final List<FilterOptionModel> users; final List<FilterOptionModel> users;
} }
@ -23,12 +24,7 @@ final grnLookupsProvider = FutureProvider.autoDispose<GrnLookups>((ref) async {
final master = ref.watch(masterRemoteDataSourceProvider); final master = ref.watch(masterRemoteDataSourceProvider);
final poRepo = ref.watch(purchaseOrderRepositoryProvider); final poRepo = ref.watch(purchaseOrderRepositoryProvider);
final warehouses = await _safeOptions(() async { final locations = await _safeOptions(master.listLocations);
final locations = await master.listLocations();
return locations
.where((location) => location.slug?.toLowerCase() == 'warehouse')
.toList();
});
final users = await _safeUserOptions(ref); final users = await _safeUserOptions(ref);
final receivablePos = <PurchaseOrderModel>[]; final receivablePos = <PurchaseOrderModel>[];
@ -40,7 +36,7 @@ final grnLookupsProvider = FutureProvider.autoDispose<GrnLookups>((ref) async {
} }
return GrnLookups( return GrnLookups(
warehouses: warehouses, locations: locations,
receivablePurchaseOrders: receivablePos, receivablePurchaseOrders: receivablePos,
users: users, users: users,
); );

View File

@ -109,6 +109,12 @@ class GrnListNotifier extends AutoDisposeAsyncNotifier<GrnListState> {
applyQuery(current.query.copyWith(status: status, page: 1)); applyQuery(current.query.copyWith(status: status, page: 1));
} }
void setLocationFilter(int? locationId) {
final current = state.valueOrNull;
if (current == null) return;
applyQuery(current.query.copyWith(locationId: locationId, page: 1));
}
void setPage(int page) { void setPage(int page) {
final current = state.valueOrNull; final current = state.valueOrNull;
if (current == null) return; if (current == null) return;

View File

@ -240,8 +240,8 @@ class _DetailHeader extends StatelessWidget {
final subtitleParts = [ final subtitleParts = [
if (grn.poNumber?.trim().isNotEmpty == true) 'PO ${grn.poNumber!.trim()}', if (grn.poNumber?.trim().isNotEmpty == true) 'PO ${grn.poNumber!.trim()}',
if (grn.vendorName?.trim().isNotEmpty == true) grn.vendorName!.trim(), if (grn.vendorName?.trim().isNotEmpty == true) grn.vendorName!.trim(),
if (grn.warehouseName?.trim().isNotEmpty == true) if (grn.locationName?.trim().isNotEmpty == true)
grn.warehouseName!.trim(), grn.locationName!.trim(),
]; ];
final actions = Wrap( final actions = Wrap(
@ -533,8 +533,15 @@ class _ReceiptDetailsCard extends StatelessWidget {
value: _displayOrDash(grn.vendorName), value: _displayOrDash(grn.vendorName),
), ),
_DetailField( _DetailField(
label: 'Warehouse', label: 'Location',
value: _displayOrDash(grn.warehouseName), value: _displayOrDash(
[
if (grn.locationName?.trim().isNotEmpty == true)
grn.locationName!.trim(),
if (grn.locationType?.trim().isNotEmpty == true)
'(${grn.locationType!.trim()})',
].join(' ').trim(),
),
), ),
_DetailField( _DetailField(
label: 'Vendor Invoice No', label: 'Vendor Invoice No',

View File

@ -20,7 +20,6 @@ import '../../../../shared/widgets/app_searchable_dropdown.dart';
import '../../../../shared/widgets/app_side_panel.dart'; import '../../../../shared/widgets/app_side_panel.dart';
import '../../../../shared/widgets/app_text_field.dart'; import '../../../../shared/widgets/app_text_field.dart';
import '../../../../shared/widgets/error_view.dart'; import '../../../../shared/widgets/error_view.dart';
import '../../../master_data/presentation/widgets/master_quick_add.dart';
import '../providers/grn_lookups_provider.dart'; import '../providers/grn_lookups_provider.dart';
import '../providers/grn_provider.dart'; import '../providers/grn_provider.dart';
import '../widgets/grn_line_items_editor.dart'; import '../widgets/grn_line_items_editor.dart';
@ -51,7 +50,7 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
DateTime? _vendorInvoiceDate; DateTime? _vendorInvoiceDate;
DateTime? _lrDate; DateTime? _lrDate;
String? _selectedPoId; String? _selectedPoId;
int? _warehouseId; String? _locationLabel;
int? _receivedById; int? _receivedById;
int? _qualityCheckedById; int? _qualityCheckedById;
final List<GrnLineItemDraft> _lines = []; final List<GrnLineItemDraft> _lines = [];
@ -90,7 +89,10 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
setState(() { setState(() {
_grnDate = grn.grnDate ?? DateTime.now(); _grnDate = grn.grnDate ?? DateTime.now();
_selectedPoId = grn.poId?.toString(); _selectedPoId = grn.poId?.toString();
_warehouseId = grn.warehouseId; _locationLabel = _formatLocationLabel(
name: grn.locationName,
type: grn.locationType,
);
_vendorInvoiceNoController.text = grn.vendorInvoiceNo ?? ''; _vendorInvoiceNoController.text = grn.vendorInvoiceNo ?? '';
_vendorInvoiceDate = grn.vendorInvoiceDate; _vendorInvoiceDate = grn.vendorInvoiceDate;
_vendorInvoiceAmountController.text = _vendorInvoiceAmountController.text =
@ -112,12 +114,23 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
_lines _lines
..clear() ..clear()
..addAll(draftsFromPurchaseOrder(po)); ..addAll(draftsFromPurchaseOrder(po));
if (_warehouseId == null && po.shippingId != null) { _locationLabel = _formatLocationLabel(
_warehouseId = po.shippingId; name: po.shippingName,
} type: null,
);
}); });
} }
String? _formatLocationLabel({String? name, String? type}) {
final trimmedName = name?.trim();
if (trimmedName == null || trimmedName.isEmpty) return null;
final trimmedType = type?.trim();
if (trimmedType == null || trimmedType.isEmpty) return trimmedName;
final typeLabel = trimmedType[0].toUpperCase() +
trimmedType.substring(1).toLowerCase();
return '$trimmedName ($typeLabel)';
}
int? _parseId(String value) => int.tryParse(value.trim()); int? _parseId(String value) => int.tryParse(value.trim());
int? _normalizeUserId(int? id) => id != null && id > 0 ? id : null; int? _normalizeUserId(int? id) => id != null && id > 0 ? id : null;
@ -150,7 +163,6 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
final payload = <String, dynamic>{ final payload = <String, dynamic>{
'grn_date': DateFormatter.toApiDate(_grnDate ?? DateTime.now()), 'grn_date': DateFormatter.toApiDate(_grnDate ?? DateTime.now()),
'po_id': poId, 'po_id': poId,
'warehouse_id': _warehouseId,
if (_vendorInvoiceNoController.text.trim().isNotEmpty) if (_vendorInvoiceNoController.text.trim().isNotEmpty)
'vendor_invoice_no': _vendorInvoiceNoController.text.trim(), 'vendor_invoice_no': _vendorInvoiceNoController.text.trim(),
if (_vendorInvoiceDate != null) if (_vendorInvoiceDate != null)
@ -236,9 +248,9 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
} }
if (!widget.isEditing) { if (!widget.isEditing) {
if (_selectedPoId == null || _warehouseId == null) { if (_selectedPoId == null) {
showAppToastFromSnackBar(context, showAppToastFromSnackBar(context,
const SnackBar(content: Text('Please select PO and warehouse')), const SnackBar(content: Text('Please select a purchase order')),
); );
return; return;
} }
@ -371,8 +383,6 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
); );
} }
final warehouseIds =
lookups.warehouses.map((e) => _parseId(e.id)).whereType<int>();
final userIds = final userIds =
lookups.users.map((e) => _parseId(e.id)).whereType<int>(); lookups.users.map((e) => _parseId(e.id)).whereType<int>();
final poOptions = lookups.receivablePurchaseOrders final poOptions = lookups.receivablePurchaseOrders
@ -421,7 +431,10 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
searchHint: 'Search PO...', searchHint: 'Search PO...',
options: poOptions, options: poOptions,
onChanged: (v) async { onChanged: (v) async {
setState(() => _selectedPoId = v); setState(() {
_selectedPoId = v;
if (v == null) _locationLabel = null;
});
if (v == null) { if (v == null) {
for (final line in _lines) { for (final line in _lines) {
line.dispose(); line.dispose();
@ -452,25 +465,17 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
label: 'Purchase Order', label: 'Purchase Order',
value: existing?.poNumber ?? '', value: existing?.poNumber ?? '',
), ),
MasterQuickAddDropdown<int>( _ReadOnlyField(
masterId: 'locations', label: 'Location',
label: 'Warehouse *', value: _locationLabel ??
value: _dropdownValue(_warehouseId, warehouseIds), _formatLocationLabel(
hint: 'Select warehouse', name: existing?.locationName,
searchHint: 'Search warehouse...', type: existing?.locationType,
options: _intOptions(lookups.warehouses), ) ??
initialValues: const {'type': 'warehouse'}, '',
refreshLookups: () => hint: widget.isEditing
ref.invalidate(grnLookupsProvider),
parseCreatedId: int.tryParse,
onChanged: widget.isEditing
? (_) {}
: (v) => setState(() => _warehouseId = v),
validator: widget.isEditing
? null ? null
: (v) => : 'From PO shipping location',
v == null ? 'Warehouse is required' : null,
enabled: !widget.isEditing,
), ),
AppTextField( AppTextField(
label: 'Vendor Invoice No', label: 'Vendor Invoice No',
@ -768,21 +773,32 @@ class _ReadOnlyField extends StatelessWidget {
const _ReadOnlyField({ const _ReadOnlyField({
required this.label, required this.label,
required this.value, required this.value,
this.hint,
}); });
final String label; final String label;
final String value; final String value;
final String? hint;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context);
return Padding( return Padding(
padding: const EdgeInsets.only(top: 8), padding: const EdgeInsets.only(top: 8),
child: InputDecorator( child: InputDecorator(
decoration: InputDecoration( decoration: InputDecoration(
labelText: label, labelText: label,
floatingLabelBehavior: FloatingLabelBehavior.always, floatingLabelBehavior: FloatingLabelBehavior.always,
helperText: hint,
),
child: Text(
value,
style: value == ''
? theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
)
: null,
), ),
child: Text(value),
), ),
); );
} }

View File

@ -8,6 +8,7 @@ import '../../../../core/errors/failure.dart';
import '../../../../core/utils/formatters.dart'; import '../../../../core/utils/formatters.dart';
import '../../../../core/utils/responsive_utils.dart'; import '../../../../core/utils/responsive_utils.dart';
import '../../../../shared/models/grn_model.dart'; import '../../../../shared/models/grn_model.dart';
import '../../../../shared/models/user_management_models.dart';
import '../../../../shared/providers/permissions_provider.dart'; import '../../../../shared/providers/permissions_provider.dart';
import '../../../../shared/widgets/app_card.dart'; import '../../../../shared/widgets/app_card.dart';
import '../../../../shared/widgets/app_data_table.dart'; import '../../../../shared/widgets/app_data_table.dart';
@ -23,6 +24,7 @@ import '../../../../shared/widgets/app_table_shell.dart';
import '../../../../shared/widgets/error_view.dart'; import '../../../../shared/widgets/error_view.dart';
import '../../../../shared/widgets/page_header.dart'; import '../../../../shared/widgets/page_header.dart';
import '../../../../shared/utils/file_download_helper.dart'; import '../../../../shared/utils/file_download_helper.dart';
import '../providers/grn_lookups_provider.dart';
import '../providers/grn_provider.dart'; import '../providers/grn_provider.dart';
import '../widgets/grn_status_chip.dart'; import '../widgets/grn_status_chip.dart';
import '../../../../shared/widgets/app_toast.dart'; import '../../../../shared/widgets/app_toast.dart';
@ -46,6 +48,7 @@ class _GrnListScreenState extends ConsumerState<GrnListScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final listAsync = ref.watch(grnListProvider); final listAsync = ref.watch(grnListProvider);
final lookupsAsync = ref.watch(grnLookupsProvider);
final canEdit = ref.can('grn', PermissionAction.update); final canEdit = ref.can('grn', PermissionAction.update);
final canExport = ref.can('grn', PermissionAction.export); final canExport = ref.can('grn', PermissionAction.export);
@ -87,12 +90,15 @@ class _GrnListScreenState extends ConsumerState<GrnListScreen> {
toolbar: _FiltersBar( toolbar: _FiltersBar(
searchController: _searchController, searchController: _searchController,
query: state.query, query: state.query,
locationOptions: lookupsAsync.valueOrNull?.locations ?? const [],
showExport: canExport, showExport: canExport,
isExporting: state.isExporting, isExporting: state.isExporting,
onExport: _exportGrns, onExport: _exportGrns,
onSearch: ref.read(grnListProvider.notifier).setSearch, onSearch: ref.read(grnListProvider.notifier).setSearch,
onStatusChanged: onStatusChanged:
ref.read(grnListProvider.notifier).setStatusFilter, ref.read(grnListProvider.notifier).setStatusFilter,
onLocationChanged:
ref.read(grnListProvider.notifier).setLocationFilter,
), ),
footer: AppPagination( footer: AppPagination(
currentPage: state.query.page, currentPage: state.query.page,
@ -187,8 +193,10 @@ class _FiltersBar extends StatelessWidget {
const _FiltersBar({ const _FiltersBar({
required this.searchController, required this.searchController,
required this.query, required this.query,
required this.locationOptions,
required this.onSearch, required this.onSearch,
required this.onStatusChanged, required this.onStatusChanged,
required this.onLocationChanged,
this.showExport = false, this.showExport = false,
this.isExporting = false, this.isExporting = false,
this.onExport, this.onExport,
@ -196,8 +204,10 @@ class _FiltersBar extends StatelessWidget {
final TextEditingController searchController; final TextEditingController searchController;
final GrnListQuery query; final GrnListQuery query;
final List<FilterOptionModel> locationOptions;
final ValueChanged<String> onSearch; final ValueChanged<String> onSearch;
final ValueChanged<String?> onStatusChanged; final ValueChanged<String?> onStatusChanged;
final ValueChanged<int?> onLocationChanged;
final bool showExport; final bool showExport;
final bool isExporting; final bool isExporting;
final VoidCallback? onExport; final VoidCallback? onExport;
@ -233,6 +243,21 @@ class _FiltersBar extends StatelessWidget {
], ],
onChanged: onStatusChanged, onChanged: onStatusChanged,
), ),
AppSearchableDropdown<int?>(
label: 'Location',
value: query.locationId,
searchHint: 'Search location...',
isDense: true,
options: [
const AppDropdownOption(value: null, label: 'All Locations'),
...locationOptions.map((location) {
final id = int.tryParse(location.id);
if (id == null) return null;
return AppDropdownOption(value: id, label: location.name);
}).whereType<AppDropdownOption<int?>>(),
],
onChanged: onLocationChanged,
),
]; ];
return AppResponsiveFilterBar( return AppResponsiveFilterBar(
@ -295,6 +320,12 @@ class _GrnDataTable extends StatelessWidget {
searchText: (grn) => grn.vendorName ?? '', searchText: (grn) => grn.vendorName ?? '',
cellBuilder: (_, grn) => Text(grn.vendorName ?? ''), cellBuilder: (_, grn) => Text(grn.vendorName ?? ''),
), ),
AppDataColumn(
label: 'Location',
flex: 2,
searchText: (grn) => grn.locationName ?? '',
cellBuilder: (_, grn) => Text(grn.locationName ?? ''),
),
AppDataColumn( AppDataColumn(
label: 'Status', label: 'Status',
flex: 1, flex: 1,
@ -371,6 +402,7 @@ class _GrnCardList extends StatelessWidget {
const SizedBox(height: 8), const SizedBox(height: 8),
Text('PO: ${grn.poNumber ?? ''}'), Text('PO: ${grn.poNumber ?? ''}'),
Text('Vendor: ${grn.vendorName ?? ''}'), Text('Vendor: ${grn.vendorName ?? ''}'),
Text('Location: ${grn.locationName ?? ''}'),
Text('Date: ${DateFormatter.displayDate(grn.grnDate)}'), Text('Date: ${DateFormatter.displayDate(grn.grnDate)}'),
const SizedBox(height: 8), const SizedBox(height: 8),
Row( Row(

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
enum MasterFieldType { text, number, boolean, dropdown } enum MasterFieldType { text, number, boolean, dropdown }
const categoryTypeOptions = ['STOCK', 'ASSET']; const categoryTypeOptions = ['STOCK', 'ASSET'];
const termsNotesTypeOptions = ['PO', 'INVOICE'];
class MasterFieldDef { class MasterFieldDef {
const MasterFieldDef({ const MasterFieldDef({
@ -465,6 +466,34 @@ const masterDefinitions = <MasterDefinition>[
_activeField, _activeField,
], ],
), ),
MasterDefinition(
id: 'terms_notes',
title: 'Terms & Notes',
subtitle: 'Default terms by document type (PO, Invoice)',
category: 'Finance & Terms',
routeKey: 'terms-notes',
apiPath: '/masters/terms-notes',
module: 'terms_notes',
icon: Icons.notes_outlined,
fields: [
MasterFieldDef(
key: 'type',
label: 'Document Type',
type: MasterFieldType.dropdown,
required: true,
showInList: true,
staticOptions: termsNotesTypeOptions,
),
MasterFieldDef(
key: 'notes',
label: 'Notes',
required: true,
showInList: true,
multiline: true,
),
_activeField,
],
),
MasterDefinition( MasterDefinition(
id: 'gst_rates', id: 'gst_rates',
title: 'GST Rates', title: 'GST Rates',

View File

@ -231,6 +231,7 @@ String masterQuickAddNoun(String masterId) {
'departments' => 'department', 'departments' => 'department',
'delivery_terms' => 'delivery term', 'delivery_terms' => 'delivery term',
'payment_terms' => 'payment term', 'payment_terms' => 'payment term',
'terms_notes' => 'terms note',
'gst_rates' => 'GST rate', 'gst_rates' => 'GST rate',
'document_series' => 'document series', 'document_series' => 'document series',
_ => masterId.replaceAll('_', ' '), _ => masterId.replaceAll('_', ' '),

View File

@ -103,6 +103,40 @@ class MasterRemoteDataSource {
Future<List<FilterOptionModel>> listDeliveryTerms() => Future<List<FilterOptionModel>> listDeliveryTerms() =>
_listOptions(ApiEndpoints.deliveryTerms); _listOptions(ApiEndpoints.deliveryTerms);
/// Active terms/notes text for a document type (`PO` | `INVOICE`).
Future<String?> getTermsNotesByType(String type) async {
final response = await dio.get(ApiEndpoints.termsNotesByType(type));
final body = response.data;
final raw = body is Map ? body['data'] : null;
if (raw is! Map) return null;
final map = Map<String, dynamic>.from(raw);
if (!isActiveOptionRow(map)) return null;
final notes = map['notes']?.toString().trim();
if (notes == null || notes.isEmpty) return null;
return notes;
}
/// Document types supported by terms-notes master.
Future<List<String>> listTermsNoteTypes() async {
final response = await dio.get(ApiEndpoints.termsNotesTypes);
final body = response.data;
final raw = body is Map ? body['data'] : null;
if (raw is! List) return const [];
return raw
.map((item) {
if (item is String) return item.trim();
if (item is Map) {
final map = Map<String, dynamic>.from(item);
return (map['value'] ?? map['type'] ?? map['id'] ?? '')
.toString()
.trim();
}
return '';
})
.where((value) => value.isNotEmpty)
.toList();
}
Future<List<FilterOptionModel>> listWarehouses() => Future<List<FilterOptionModel>> listWarehouses() =>
_listOptions(ApiEndpoints.warehouses); _listOptions(ApiEndpoints.warehouses);

View File

@ -22,6 +22,17 @@ class PurchaseOrderRemoteDataSource {
return _parsePaginated(response.data, PurchaseOrderModel.fromJson); return _parsePaginated(response.data, PurchaseOrderModel.fromJson);
} }
/// Approver-only list of POs with status PENDING_APPROVAL.
Future<PaginatedResponse<PurchaseOrderModel>> getPendingApprovalPurchaseOrders(
PurchaseOrderListQuery query,
) async {
final response = await dio.get(
ApiEndpoints.purchaseOrdersPendingApproval,
queryParameters: _queryToMap(query),
);
return _parsePaginated(response.data, PurchaseOrderModel.fromJson);
}
/// Form-dropdown loader (`dropdown_call=true`). Optional status filter. /// Form-dropdown loader (`dropdown_call=true`). Optional status filter.
Future<List<PurchaseOrderModel>> listPurchaseOrderOptions({ Future<List<PurchaseOrderModel>> listPurchaseOrderOptions({
String? status, String? status,

View File

@ -32,6 +32,14 @@ class PurchaseOrderRepositoryImpl implements PurchaseOrderRepository {
return safeApiCall(() => dataSource.getPurchaseOrders(query)); return safeApiCall(() => dataSource.getPurchaseOrders(query));
} }
@override
Future<Result<PaginatedResponse<PurchaseOrderModel>>>
getPendingApprovalPurchaseOrders(PurchaseOrderListQuery query) {
return safeApiCall(
() => dataSource.getPendingApprovalPurchaseOrders(query),
);
}
@override @override
Future<Result<List<PurchaseOrderModel>>> listPurchaseOrderOptions({ Future<Result<List<PurchaseOrderModel>>> listPurchaseOrderOptions({
String? status, String? status,

View File

@ -8,6 +8,8 @@ abstract class PurchaseOrderRepository {
Future<Result<PaginatedResponse<PurchaseOrderModel>>> getPurchaseOrders( Future<Result<PaginatedResponse<PurchaseOrderModel>>> getPurchaseOrders(
PurchaseOrderListQuery query, PurchaseOrderListQuery query,
); );
Future<Result<PaginatedResponse<PurchaseOrderModel>>>
getPendingApprovalPurchaseOrders(PurchaseOrderListQuery query);
Future<Result<List<PurchaseOrderModel>>> listPurchaseOrderOptions({ Future<Result<List<PurchaseOrderModel>>> listPurchaseOrderOptions({
String? status, String? status,
}); });

View File

@ -6,6 +6,17 @@ import '../../../masters/data/datasources/master_remote_data_source.dart';
import '../../../vendors/data/repositories/vendor_repository_impl.dart'; import '../../../vendors/data/repositories/vendor_repository_impl.dart';
import '../../../vendors/domain/repositories/vendor_repository.dart'; import '../../../vendors/domain/repositories/vendor_repository.dart';
/// Default terms text from active `terms_notes` where type = PO.
final poDefaultTermsNotesProvider =
FutureProvider.autoDispose<String?>((ref) async {
final master = ref.watch(masterRemoteDataSourceProvider);
try {
return await master.getTermsNotesByType('PO');
} catch (_) {
return null;
}
});
class PurchaseOrderLookups { class PurchaseOrderLookups {
const PurchaseOrderLookups({ const PurchaseOrderLookups({
this.vendors = const [], this.vendors = const [],

View File

@ -58,6 +58,12 @@ final purchaseOrdersListProvider = AsyncNotifierProvider.autoDispose<
PurchaseOrdersListNotifier.new, PurchaseOrdersListNotifier.new,
); );
final pendingApprovalPurchaseOrdersListProvider =
AsyncNotifierProvider.autoDispose<PendingApprovalPurchaseOrdersListNotifier,
PurchaseOrdersListState>(
PendingApprovalPurchaseOrdersListNotifier.new,
);
class PurchaseOrdersListNotifier class PurchaseOrdersListNotifier
extends AutoDisposeAsyncNotifier<PurchaseOrdersListState> { extends AutoDisposeAsyncNotifier<PurchaseOrdersListState> {
@override @override
@ -169,6 +175,89 @@ class PurchaseOrdersListNotifier
} }
} }
class PendingApprovalPurchaseOrdersListNotifier
extends AutoDisposeAsyncNotifier<PurchaseOrdersListState> {
@override
Future<PurchaseOrdersListState> build() async {
return _load(const PurchaseOrderListQuery(limit: 20));
}
Future<PurchaseOrdersListState> _load(PurchaseOrderListQuery query) async {
final repository = ref.read(purchaseOrderRepositoryProvider);
final result = await repository.getPendingApprovalPurchaseOrders(query);
if (result.failure != null) throw result.failure!;
final page = result.data!;
return PurchaseOrdersListState(
orders: page.items,
query: query,
total: page.total,
totalPages: page.totalPages,
);
}
Future<void> refresh() async {
final current = state.valueOrNull ?? const PurchaseOrdersListState();
state = AsyncData(current.copyWith(isRefreshing: true, clearMessages: true));
try {
state = AsyncData(await _load(current.query));
} catch (e, st) {
state = AsyncError(e, st);
}
}
Future<void> applyQuery(PurchaseOrderListQuery query) async {
final previous = state.valueOrNull;
if (previous == null) {
state = const AsyncLoading();
}
try {
state = AsyncData(await _load(query));
} catch (e, st) {
state = AsyncError(e, st);
}
}
void setSearch(String search) {
final current = state.valueOrNull;
if (current == null) return;
applyQuery(current.query.copyWith(search: TableSearch.normalize(search), page: 1));
}
void setPage(int page) {
final current = state.valueOrNull;
if (current == null) return;
applyQuery(current.query.copyWith(page: page));
}
void setPageSize(int limit) {
final current = state.valueOrNull;
if (current == null) return;
applyQuery(current.query.copyWith(limit: limit, page: 1));
}
Future<bool> approvePurchaseOrder(String id, {String? remarks}) async {
final repository = ref.read(purchaseOrderRepositoryProvider);
final result = await repository.approvePurchaseOrder(id, remarks: remarks);
if (result.failure != null) {
final current = state.valueOrNull;
if (current != null) {
state = AsyncData(current.copyWith(actionError: result.failure!.message));
}
return false;
}
ref.invalidate(purchaseOrdersListProvider);
ref.invalidate(grnLookupsProvider);
await refresh();
final current = state.valueOrNull;
if (current != null) {
state = AsyncData(
current.copyWith(actionSuccess: 'Purchase order approved'),
);
}
return true;
}
}
final purchaseOrderDetailProvider = AsyncNotifierProvider.family< final purchaseOrderDetailProvider = AsyncNotifierProvider.family<
PurchaseOrderDetailNotifier, PurchaseOrderModel, String>( PurchaseOrderDetailNotifier, PurchaseOrderModel, String>(
PurchaseOrderDetailNotifier.new, PurchaseOrderDetailNotifier.new,
@ -195,6 +284,7 @@ class PurchaseOrderDetailNotifier
if (result.failure != null) throw result.failure!; if (result.failure != null) throw result.failure!;
state = AsyncData(result.data!); state = AsyncData(result.data!);
ref.invalidate(purchaseOrdersListProvider); ref.invalidate(purchaseOrdersListProvider);
ref.invalidate(pendingApprovalPurchaseOrdersListProvider);
ref.invalidate(grnLookupsProvider); ref.invalidate(grnLookupsProvider);
return result.data!; return result.data!;
} }
@ -205,6 +295,7 @@ class PurchaseOrderDetailNotifier
if (result.failure != null) throw result.failure!; if (result.failure != null) throw result.failure!;
state = AsyncData(result.data!); state = AsyncData(result.data!);
ref.invalidate(purchaseOrdersListProvider); ref.invalidate(purchaseOrdersListProvider);
ref.invalidate(pendingApprovalPurchaseOrdersListProvider);
ref.invalidate(grnLookupsProvider); ref.invalidate(grnLookupsProvider);
return result.data!; return result.data!;
} }
@ -215,6 +306,7 @@ class PurchaseOrderDetailNotifier
if (result.failure != null) throw result.failure!; if (result.failure != null) throw result.failure!;
state = AsyncData(result.data!); state = AsyncData(result.data!);
ref.invalidate(purchaseOrdersListProvider); ref.invalidate(purchaseOrdersListProvider);
ref.invalidate(pendingApprovalPurchaseOrdersListProvider);
ref.invalidate(grnLookupsProvider); ref.invalidate(grnLookupsProvider);
return result.data!; return result.data!;
} }
@ -224,6 +316,7 @@ class PurchaseOrderDetailNotifier
final result = await repository.amendPurchaseOrder(arg); final result = await repository.amendPurchaseOrder(arg);
if (result.failure != null) throw result.failure!; if (result.failure != null) throw result.failure!;
ref.invalidate(purchaseOrdersListProvider); ref.invalidate(purchaseOrdersListProvider);
ref.invalidate(pendingApprovalPurchaseOrdersListProvider);
ref.invalidate(grnLookupsProvider); ref.invalidate(grnLookupsProvider);
return result.data!; return result.data!;
} }
@ -234,6 +327,7 @@ class PurchaseOrderDetailNotifier
if (result.failure != null) throw result.failure!; if (result.failure != null) throw result.failure!;
state = AsyncData(result.data!); state = AsyncData(result.data!);
ref.invalidate(purchaseOrdersListProvider); ref.invalidate(purchaseOrdersListProvider);
ref.invalidate(pendingApprovalPurchaseOrdersListProvider);
ref.invalidate(grnLookupsProvider); ref.invalidate(grnLookupsProvider);
return result.data!; return result.data!;
} }
@ -315,6 +409,7 @@ class PurchaseOrderFormNotifier extends FamilyAsyncNotifier<PurchaseOrderModel?,
final result = await repository.createPurchaseOrder(data); final result = await repository.createPurchaseOrder(data);
if (result.failure != null) throw result.failure!; if (result.failure != null) throw result.failure!;
ref.invalidate(purchaseOrdersListProvider); ref.invalidate(purchaseOrdersListProvider);
ref.invalidate(pendingApprovalPurchaseOrdersListProvider);
ref.invalidate(grnLookupsProvider); ref.invalidate(grnLookupsProvider);
return result.data!; return result.data!;
} }
@ -324,6 +419,7 @@ class PurchaseOrderFormNotifier extends FamilyAsyncNotifier<PurchaseOrderModel?,
final result = await repository.updatePurchaseOrder(id, data); final result = await repository.updatePurchaseOrder(id, data);
if (result.failure != null) throw result.failure!; if (result.failure != null) throw result.failure!;
ref.invalidate(purchaseOrdersListProvider); ref.invalidate(purchaseOrdersListProvider);
ref.invalidate(pendingApprovalPurchaseOrdersListProvider);
ref.invalidate(purchaseOrderDetailProvider(id)); ref.invalidate(purchaseOrderDetailProvider(id));
ref.invalidate(grnLookupsProvider); ref.invalidate(grnLookupsProvider);
return result.data!; return result.data!;

View File

@ -65,9 +65,6 @@ class _PurchaseOrderDetailScreenState
final lookups = lookupsAsync.asData?.value; final lookups = lookupsAsync.asData?.value;
return SingleChildScrollView( return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 32), padding: const EdgeInsets.fromLTRB(24, 16, 24, 32),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 1200),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
@ -131,8 +128,6 @@ class _PurchaseOrderDetailScreenState
_DetailFooter(order: order), _DetailFooter(order: order),
], ],
), ),
),
),
); );
}, },
), ),

View File

@ -58,6 +58,7 @@ class _PurchaseOrderFormScreenState extends ConsumerState<PurchaseOrderFormScree
final List<PoLineItemDraft> _lines = []; final List<PoLineItemDraft> _lines = [];
bool _isSubmitting = false; bool _isSubmitting = false;
String? _populatedSignature; String? _populatedSignature;
bool _defaultTermsApplied = false;
@override @override
void initState() { void initState() {
@ -506,8 +507,9 @@ class _PurchaseOrderFormScreenState extends ConsumerState<PurchaseOrderFormScree
AppTextField( AppTextField(
controller: _termsController, controller: _termsController,
label: 'Terms & Conditions', label: 'Terms & Conditions',
hint: hint: widget.isEditing
'Payment terms, inspection conditions, etc.', ? 'Payment terms, inspection conditions, etc.'
: 'Defaults from Terms & Notes master when empty',
maxLines: 5, maxLines: 5,
), ),
AppTextField( AppTextField(
@ -685,6 +687,17 @@ class _PurchaseOrderFormScreenState extends ConsumerState<PurchaseOrderFormScree
Widget build(BuildContext context) { Widget build(BuildContext context) {
final lookupsAsync = ref.watch(purchaseOrderLookupsProvider); final lookupsAsync = ref.watch(purchaseOrderLookupsProvider);
if (!widget.isEditing) {
ref.listen(poDefaultTermsNotesProvider, (prev, next) {
if (_defaultTermsApplied) return;
final notes = next.asData?.value;
if (notes == null || notes.isEmpty) return;
if (_termsController.text.trim().isNotEmpty) return;
_defaultTermsApplied = true;
_termsController.text = notes;
});
}
return Scaffold( return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
body: lookupsAsync.when( body: lookupsAsync.when(

View File

@ -31,7 +31,12 @@ import '../widgets/po_status_chip.dart';
import '../../../../shared/widgets/app_toast.dart'; import '../../../../shared/widgets/app_toast.dart';
class PurchaseOrderListScreen extends ConsumerStatefulWidget { class PurchaseOrderListScreen extends ConsumerStatefulWidget {
const PurchaseOrderListScreen({super.key}); const PurchaseOrderListScreen({
super.key,
this.pendingApprovalOnly = false,
});
final bool pendingApprovalOnly;
@override @override
ConsumerState<PurchaseOrderListScreen> createState() => ConsumerState<PurchaseOrderListScreen> createState() =>
@ -59,12 +64,23 @@ class _PurchaseOrderListScreenState extends ConsumerState<PurchaseOrderListScree
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ordersAsync = ref.watch(purchaseOrdersListProvider); final pendingOnly = widget.pendingApprovalOnly;
final canEdit = ref.can('purchase_orders', PermissionAction.update); final ordersAsync = pendingOnly
final canDelete = ref.can('purchase_orders', PermissionAction.delete); ? ref.watch(pendingApprovalPurchaseOrdersListProvider)
final canExport = ref.can('purchase_orders', PermissionAction.export); : ref.watch(purchaseOrdersListProvider);
final canEdit =
!pendingOnly && ref.can('purchase_orders', PermissionAction.update);
final canDelete =
!pendingOnly && ref.can('purchase_orders', PermissionAction.delete);
final canExport =
!pendingOnly && ref.can('purchase_orders', PermissionAction.export);
final canApprove =
pendingOnly && ref.can('purchase_orders', PermissionAction.approve);
ref.listen(purchaseOrdersListProvider, (prev, next) { void listenListMessages(
AsyncValue<PurchaseOrdersListState>? prev,
AsyncValue<PurchaseOrdersListState> next,
) {
final error = next.valueOrNull?.actionError; final error = next.valueOrNull?.actionError;
final success = next.valueOrNull?.actionSuccess; final success = next.valueOrNull?.actionSuccess;
if (error != null && error != prev?.valueOrNull?.actionError) { if (error != null && error != prev?.valueOrNull?.actionError) {
@ -73,15 +89,27 @@ class _PurchaseOrderListScreenState extends ConsumerState<PurchaseOrderListScree
if (success != null && success != prev?.valueOrNull?.actionSuccess) { if (success != null && success != prev?.valueOrNull?.actionSuccess) {
showAppToastFromSnackBar(context, SnackBar(content: Text(success))); showAppToastFromSnackBar(context, SnackBar(content: Text(success)));
} }
}); }
if (pendingOnly) {
ref.listen(pendingApprovalPurchaseOrdersListProvider, listenListMessages);
} else {
ref.listen(purchaseOrdersListProvider, listenListMessages);
}
return Padding( return Padding(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
child: ordersAsync.when( child: ordersAsync.when(
loading: () => const AppLoadingView(message: 'Loading purchase orders...'), loading: () => AppLoadingView(
message: pendingOnly
? 'Loading pending approvals...'
: 'Loading purchase orders...',
),
error: (error, _) => ErrorView.fromFailure( error: (error, _) => ErrorView.fromFailure(
error is Failure ? error : Failure.unknown(message: error.toString()), error is Failure ? error : Failure.unknown(message: error.toString()),
onRetry: () => ref.invalidate(purchaseOrdersListProvider), onRetry: () => pendingOnly
? ref.invalidate(pendingApprovalPurchaseOrdersListProvider)
: ref.invalidate(purchaseOrdersListProvider),
), ),
data: (state) { data: (state) {
_rememberStatuses(state.orders); _rememberStatuses(state.orders);
@ -89,14 +117,18 @@ class _PurchaseOrderListScreenState extends ConsumerState<PurchaseOrderListScree
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
PageHeader( PageHeader(
title: 'Purchase Orders', title: pendingOnly ? 'Pending Approval' : 'Purchase Orders',
subtitle: 'Create, approve and track procurement orders', subtitle: pendingOnly
? 'Purchase orders awaiting approval'
: 'Create, approve and track procurement orders',
actions: [ actions: [
if (!pendingOnly)
CanPermission( CanPermission(
module: 'purchase_orders', module: 'purchase_orders',
action: PermissionAction.create, action: PermissionAction.create,
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: () => context.go(RouteConstants.purchaseOrderAdd), onPressed: () =>
context.go(RouteConstants.purchaseOrderAdd),
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
label: const Text('Create PO'), label: const Text('Create PO'),
), ),
@ -109,38 +141,72 @@ class _PurchaseOrderListScreenState extends ConsumerState<PurchaseOrderListScree
searchController: _searchController, searchController: _searchController,
query: state.query, query: state.query,
statusOptions: _statusFilterOptions(state.orders), statusOptions: _statusFilterOptions(state.orders),
showStatusFilter: !pendingOnly,
showExport: canExport, showExport: canExport,
isExporting: state.isExporting, isExporting: state.isExporting,
onExport: _exportPurchaseOrders, onExport: _exportPurchaseOrders,
onSearch: ref.read(purchaseOrdersListProvider.notifier).setSearch, onSearch: pendingOnly
onStatusChanged: ? ref
ref.read(purchaseOrdersListProvider.notifier).setStatusFilter, .read(
pendingApprovalPurchaseOrdersListProvider.notifier,
)
.setSearch
: ref.read(purchaseOrdersListProvider.notifier).setSearch,
onStatusChanged: pendingOnly
? (_) {}
: ref
.read(purchaseOrdersListProvider.notifier)
.setStatusFilter,
), ),
footer: AppPagination( footer: AppPagination(
currentPage: state.query.page, currentPage: state.query.page,
totalPages: state.totalPages, totalPages: state.totalPages,
totalItems: state.total, totalItems: state.total,
pageSize: state.query.limit, pageSize: state.query.limit,
itemLabel: 'purchase orders', itemLabel: pendingOnly
onPageChanged: ? 'pending approvals'
ref.read(purchaseOrdersListProvider.notifier).setPage, : 'purchase orders',
onPageSizeChanged: onPageChanged: pendingOnly
ref.read(purchaseOrdersListProvider.notifier).setPageSize, ? ref
.read(
pendingApprovalPurchaseOrdersListProvider.notifier,
)
.setPage
: ref.read(purchaseOrdersListProvider.notifier).setPage,
onPageSizeChanged: pendingOnly
? ref
.read(
pendingApprovalPurchaseOrdersListProvider.notifier,
)
.setPageSize
: ref
.read(purchaseOrdersListProvider.notifier)
.setPageSize,
), ),
child: RefreshIndicator( child: RefreshIndicator(
onRefresh: () => onRefresh: () => pendingOnly
ref.read(purchaseOrdersListProvider.notifier).refresh(), ? ref
.read(
pendingApprovalPurchaseOrdersListProvider.notifier,
)
.refresh()
: ref.read(purchaseOrdersListProvider.notifier).refresh(),
child: state.orders.isEmpty child: state.orders.isEmpty
? ListView( ? ListView(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
children: const [ children: [
SizedBox( SizedBox(
height: 240, height: 240,
child: AppEmptyState( child: AppEmptyState(
title: 'No purchase orders found', title: pendingOnly
description: ? 'No pending approvals'
'Try adjusting filters or create a new purchase order.', : 'No purchase orders found',
icon: Icons.receipt_long_outlined, description: pendingOnly
? 'There are no purchase orders waiting for approval.'
: 'Try adjusting filters or create a new purchase order.',
icon: pendingOnly
? Icons.pending_actions_outlined
: Icons.receipt_long_outlined,
), ),
), ),
], ],
@ -151,12 +217,14 @@ class _PurchaseOrderListScreenState extends ConsumerState<PurchaseOrderListScree
onView: _viewOrder, onView: _viewOrder,
onEdit: canEdit ? _editOrder : null, onEdit: canEdit ? _editOrder : null,
onDelete: canDelete ? _deleteOrder : null, onDelete: canDelete ? _deleteOrder : null,
onApprove: canApprove ? _approveOrder : null,
) )
: _PoDataTable( : _PoDataTable(
orders: state.orders, orders: state.orders,
onView: _viewOrder, onView: _viewOrder,
onEdit: canEdit ? _editOrder : null, onEdit: canEdit ? _editOrder : null,
onDelete: canDelete ? _deleteOrder : null, onDelete: canDelete ? _deleteOrder : null,
onApprove: canApprove ? _approveOrder : null,
), ),
), ),
), ),
@ -216,6 +284,26 @@ class _PurchaseOrderListScreenState extends ConsumerState<PurchaseOrderListScree
); );
} }
Future<void> _approveOrder(PurchaseOrderModel order) async {
if (!order.canApprove) {
showAppToastFromSnackBar(
context,
const SnackBar(content: Text('This order cannot be approved')),
);
return;
}
final confirmed = await showAppConfirmationDialog(
context: context,
title: 'Approve Purchase Order',
message: 'Approve ${order.poNo ?? order.id}?',
confirmLabel: 'Approve',
);
if (confirmed != true || !mounted) return;
await ref
.read(pendingApprovalPurchaseOrdersListProvider.notifier)
.approvePurchaseOrder(order.id);
}
Future<void> _deleteOrder(PurchaseOrderModel order) async { Future<void> _deleteOrder(PurchaseOrderModel order) async {
if (!order.canDelete) { if (!order.canDelete) {
showAppToastFromSnackBar(context, showAppToastFromSnackBar(context,
@ -272,6 +360,7 @@ class _FiltersBar extends StatelessWidget {
required this.statusOptions, required this.statusOptions,
required this.onSearch, required this.onSearch,
required this.onStatusChanged, required this.onStatusChanged,
this.showStatusFilter = true,
this.showExport = false, this.showExport = false,
this.isExporting = false, this.isExporting = false,
this.onExport, this.onExport,
@ -282,6 +371,7 @@ class _FiltersBar extends StatelessWidget {
final List<AppDropdownOption<String?>> statusOptions; final List<AppDropdownOption<String?>> statusOptions;
final ValueChanged<String> onSearch; final ValueChanged<String> onSearch;
final ValueChanged<String?> onStatusChanged; final ValueChanged<String?> onStatusChanged;
final bool showStatusFilter;
final bool showExport; final bool showExport;
final bool isExporting; final bool isExporting;
final VoidCallback? onExport; final VoidCallback? onExport;
@ -294,7 +384,8 @@ class _FiltersBar extends StatelessWidget {
onChanged: onSearch, onChanged: onSearch,
); );
final filters = [ final filters = <Widget>[
if (showStatusFilter)
AppSearchableDropdown<String?>( AppSearchableDropdown<String?>(
label: 'Status', label: 'Status',
value: query.status, value: query.status,
@ -331,15 +422,18 @@ class _PoDataTable extends StatelessWidget {
required this.onView, required this.onView,
this.onEdit, this.onEdit,
this.onDelete, this.onDelete,
this.onApprove,
}); });
final List<PurchaseOrderModel> orders; final List<PurchaseOrderModel> orders;
final ValueChanged<PurchaseOrderModel> onView; final ValueChanged<PurchaseOrderModel> onView;
final ValueChanged<PurchaseOrderModel>? onEdit; final ValueChanged<PurchaseOrderModel>? onEdit;
final ValueChanged<PurchaseOrderModel>? onDelete; final ValueChanged<PurchaseOrderModel>? onDelete;
final ValueChanged<PurchaseOrderModel>? onApprove;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context);
return AppDataTable<PurchaseOrderModel>( return AppDataTable<PurchaseOrderModel>(
wrapInCard: false, wrapInCard: false,
columns: [ columns: [
@ -385,7 +479,7 @@ class _PoDataTable extends StatelessWidget {
), ),
AppDataColumn( AppDataColumn(
label: 'Actions', label: 'Actions',
flex: 1, flex: onApprove != null ? 2 : 1,
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
enableSearch: false, enableSearch: false,
cellBuilder: (_, order) => AppTableActions( cellBuilder: (_, order) => AppTableActions(
@ -395,6 +489,13 @@ class _PoDataTable extends StatelessWidget {
icon: Icons.visibility_outlined, icon: Icons.visibility_outlined,
onPressed: () => onView(order), onPressed: () => onView(order),
), ),
if (onApprove != null && order.canApprove)
AppTableActionIcon(
tooltip: 'Approve',
icon: Icons.check_circle_outline,
color: theme.colorScheme.primary,
onPressed: () => onApprove!(order),
),
if (onEdit != null && order.canEdit) if (onEdit != null && order.canEdit)
AppTableActionIcon( AppTableActionIcon(
tooltip: 'Edit', tooltip: 'Edit',
@ -422,12 +523,14 @@ class _PoCardList extends StatelessWidget {
required this.onView, required this.onView,
this.onEdit, this.onEdit,
this.onDelete, this.onDelete,
this.onApprove,
}); });
final List<PurchaseOrderModel> orders; final List<PurchaseOrderModel> orders;
final ValueChanged<PurchaseOrderModel> onView; final ValueChanged<PurchaseOrderModel> onView;
final ValueChanged<PurchaseOrderModel>? onEdit; final ValueChanged<PurchaseOrderModel>? onEdit;
final ValueChanged<PurchaseOrderModel>? onDelete; final ValueChanged<PurchaseOrderModel>? onDelete;
final ValueChanged<PurchaseOrderModel>? onApprove;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -442,7 +545,23 @@ class _PoCardList extends StatelessWidget {
subtitle: Text( subtitle: Text(
'${order.vendorName ?? ''} · ${vendorTypeLabel(order.vendorType)}', '${order.vendorName ?? ''} · ${vendorTypeLabel(order.vendorType)}',
), ),
trailing: PoStatusChip(status: order.status, compact: true), trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
PoStatusChip(status: order.status, compact: true),
if (onApprove != null && order.canApprove) ...[
const SizedBox(width: 4),
IconButton(
tooltip: 'Approve',
icon: Icon(
Icons.check_circle_outline,
color: Theme.of(context).colorScheme.primary,
),
onPressed: () => onApprove!(order),
),
],
],
),
onTap: () => onView(order), onTap: () => onView(order),
), ),
); );

View File

@ -578,9 +578,11 @@ class _BankDetailsTab extends ConsumerWidget {
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: () async { onPressed: () async {
final hasPrimary = bankDetails.any((b) => b.isPrimary && b.isActive);
final saved = await openVendorBankDetailPanel( final saved = await openVendorBankDetailPanel(
context, context,
vendorId: vendorId, vendorId: vendorId,
hasPrimaryAccount: hasPrimary,
); );
if (saved == true && context.mounted) { if (saved == true && context.mounted) {
showAppToastFromSnackBar(context, showAppToastFromSnackBar(context,
@ -600,11 +602,27 @@ class _BankDetailsTab extends ConsumerWidget {
description: 'Add bank accounts for payments.', description: 'Add bank accounts for payments.',
icon: Icons.account_balance_outlined, icon: Icons.account_balance_outlined,
) )
: GridView.builder( : RadioGroup<String>(
groupValue: bankDetails
.where((b) => b.isPrimary && b.isActive)
.map((b) => b.id)
.firstOrNull,
onChanged: (selectedId) {
if (selectedId == null || !canEdit) return;
final detail = bankDetails
.where((b) => b.id == selectedId)
.firstOrNull;
if (detail == null || !detail.isActive || detail.isPrimary) {
return;
}
_setPrimaryBankDetail(context, ref, vendorId, detail);
},
child: GridView.builder(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( gridDelegate:
const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 320, maxCrossAxisExtent: 320,
mainAxisExtent: 175, mainAxisExtent: 190,
crossAxisSpacing: 16, crossAxisSpacing: 16,
mainAxisSpacing: 16, mainAxisSpacing: 16,
), ),
@ -618,7 +636,22 @@ class _BankDetailsTab extends ConsumerWidget {
context, context,
vendorId: vendorId, vendorId: vendorId,
bankDetail: detail, bankDetail: detail,
hasPrimaryAccount: bankDetails.any(
(b) =>
b.isPrimary &&
b.isActive &&
b.id != detail.id,
), ),
),
showPrimaryRadio: canEdit && detail.isActive,
onSetPrimary: canEdit && detail.isActive
? () => _setPrimaryBankDetail(
context,
ref,
vendorId,
detail,
)
: null,
onDelete: () => _deleteBankDetail( onDelete: () => _deleteBankDetail(
context, context,
ref, ref,
@ -629,10 +662,43 @@ class _BankDetailsTab extends ConsumerWidget {
}, },
), ),
), ),
),
], ],
); );
} }
Future<void> _setPrimaryBankDetail(
BuildContext context,
WidgetRef ref,
String vendorId,
VendorBankDetailModel detail,
) async {
if (detail.isPrimary) return;
try {
await ref.read(vendorDetailProvider(vendorId).notifier).updateBankDetail(
detail.id,
{
'bank_name': detail.bankName,
if (detail.branch != null && detail.branch!.trim().isNotEmpty)
'branch': detail.branch,
if (detail.accountNumber != null &&
detail.accountNumber!.trim().isNotEmpty)
'account_number': detail.accountNumber,
if (detail.ifsc != null) 'ifsc': detail.ifsc,
if (detail.accountHolderName != null)
'account_holder_name': detail.accountHolderName,
if (detail.accountType != null) 'account_type': detail.accountType,
'is_primary': true,
'is_active': detail.isActive,
},
);
} catch (e) {
if (context.mounted) {
showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString())));
}
}
}
Future<void> _deleteBankDetail( Future<void> _deleteBankDetail(
BuildContext context, BuildContext context,
WidgetRef ref, WidgetRef ref,
@ -999,12 +1065,16 @@ class _VendorBankDetailCard extends StatelessWidget {
required this.canEdit, required this.canEdit,
required this.onEdit, required this.onEdit,
required this.onDelete, required this.onDelete,
this.showPrimaryRadio = false,
this.onSetPrimary,
}); });
final VendorBankDetailModel bankDetail; final VendorBankDetailModel bankDetail;
final bool canEdit; final bool canEdit;
final VoidCallback onEdit; final VoidCallback onEdit;
final VoidCallback onDelete; final VoidCallback onDelete;
final bool showPrimaryRadio;
final VoidCallback? onSetPrimary;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -1070,13 +1140,13 @@ class _VendorBankDetailCard extends StatelessWidget {
], ],
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 10),
Text( Text(
bankDetail.bankName?.trim().isNotEmpty == true bankDetail.bankName ?? 'Bank account',
? bankDetail.bankName! maxLines: 1,
: 'Bank Account', overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleSmall?.copyWith( style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700, fontWeight: FontWeight.w600,
), ),
), ),
if (subtitle.isNotEmpty) ...[ if (subtitle.isNotEmpty) ...[
@ -1090,69 +1160,33 @@ class _VendorBankDetailCard extends StatelessWidget {
), ),
), ),
], ],
if (bankDetail.ifsc?.trim().isNotEmpty == true) ...[
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.tag_outlined,
size: 14,
color: theme.colorScheme.onSurfaceVariant,
),
const SizedBox(width: 4),
Expanded(
child: Text(
bankDetail.ifsc!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
),
],
),
],
const Spacer(), const Spacer(),
Row( Row(
children: [ children: [
if (bankDetail.accountNumber?.trim().isNotEmpty == true) if (showPrimaryRadio)
Flexible( SizedBox(
child: Row( height: 32,
mainAxisSize: MainAxisSize.min, child: Radio<String>(
children: [ value: bankDetail.id,
Icon( visualDensity: VisualDensity.compact,
Icons.numbers_outlined, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
size: 14,
color: theme.colorScheme.onSurfaceVariant,
), ),
const SizedBox(width: 4), ),
Flexible( if (onSetPrimary != null)
GestureDetector(
onTap: onSetPrimary,
child: Text( child: Text(
'A/C: ${bankDetail.accountNumber!}', isPrimary ? 'Primary' : 'Set primary',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant, color: primaryColor,
fontWeight: FontWeight.w600,
), ),
), ),
), ),
],
),
),
const Spacer(), const Spacer(),
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: statusColor,
shape: BoxShape.circle,
),
),
const SizedBox(width: 6),
Text( Text(
bankDetail.isActive ? 'Active' : 'Inactive', bankDetail.isActive ? 'Active' : 'Inactive',
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.labelSmall?.copyWith(
color: statusColor, color: statusColor,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),

View File

@ -39,10 +39,15 @@ Future<bool?> openVendorBankDetailPanel(
BuildContext context, { BuildContext context, {
required String vendorId, required String vendorId,
VendorBankDetailModel? bankDetail, VendorBankDetailModel? bankDetail,
bool hasPrimaryAccount = false,
}) { }) {
return showSidePanel<bool>( return showSidePanel<bool>(
context, context,
VendorBankDetailPanel(vendorId: vendorId, bankDetail: bankDetail), VendorBankDetailPanel(
vendorId: vendorId,
bankDetail: bankDetail,
hasPrimaryAccount: hasPrimaryAccount,
),
width: 520, width: 520,
); );
} }
@ -347,10 +352,12 @@ class VendorBankDetailPanel extends ConsumerStatefulWidget {
super.key, super.key,
required this.vendorId, required this.vendorId,
this.bankDetail, this.bankDetail,
this.hasPrimaryAccount = false,
}); });
final String vendorId; final String vendorId;
final VendorBankDetailModel? bankDetail; final VendorBankDetailModel? bankDetail;
final bool hasPrimaryAccount;
bool get isEditing => bankDetail != null; bool get isEditing => bankDetail != null;
@ -386,6 +393,8 @@ class _VendorBankDetailPanelState extends ConsumerState<VendorBankDetailPanel> {
_isActive = detail.isActive; _isActive = detail.isActive;
} else { } else {
_accountType = 'CURRENT'; _accountType = 'CURRENT';
// Only one active primary per vendor default first account to primary.
_isPrimary = !widget.hasPrimaryAccount;
} }
} }
@ -423,6 +432,7 @@ class _VendorBankDetailPanelState extends ConsumerState<VendorBankDetailPanel> {
} else { } else {
await notifier.createBankDetail(payload); await notifier.createBankDetail(payload);
} }
// Provider reload refreshes bank list so other primaries show false.
if (mounted) Navigator.of(context, rootNavigator: true).pop(true); if (mounted) Navigator.of(context, rootNavigator: true).pop(true);
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
@ -435,6 +445,7 @@ class _VendorBankDetailPanelState extends ConsumerState<VendorBankDetailPanel> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context);
return SidePanelScaffold( return SidePanelScaffold(
title: widget.isEditing ? 'Edit Bank Detail' : 'Add Bank Detail', title: widget.isEditing ? 'Edit Bank Detail' : 'Add Bank Detail',
footer: _panelFooter( footer: _panelFooter(
@ -446,6 +457,7 @@ class _VendorBankDetailPanelState extends ConsumerState<VendorBankDetailPanel> {
child: Form( child: Form(
key: _formKey, key: _formKey,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
AppTextField( AppTextField(
controller: _bankNameController, controller: _bankNameController,
@ -488,11 +500,39 @@ class _VendorBankDetailPanelState extends ConsumerState<VendorBankDetailPanel> {
.toList(), .toList(),
onChanged: (v) => setState(() => _accountType = v), onChanged: (v) => setState(() => _accountType = v),
), ),
const SizedBox(height: 12), const SizedBox(height: 16),
AppFormToggleField( Text(
label: 'Primary Account', 'Primary account',
value: _isPrimary, style: theme.textTheme.bodyLarge,
onChanged: (v) => setState(() => _isPrimary = v), ),
const SizedBox(height: 4),
Text(
'Only one active primary account is allowed per vendor.',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
RadioGroup<bool>(
groupValue: _isPrimary,
onChanged: (v) {
if (v != null) setState(() => _isPrimary = v);
},
child: Column(
children: const [
RadioListTile<bool>(
contentPadding: EdgeInsets.zero,
dense: true,
title: Text('Primary'),
value: true,
),
RadioListTile<bool>(
contentPadding: EdgeInsets.zero,
dense: true,
title: Text('Secondary'),
value: false,
),
],
),
), ),
AppFormToggleField( AppFormToggleField(
label: 'Active', label: 'Active',

View File

@ -514,7 +514,6 @@ class ServiceVisitModel with _$ServiceVisitModel {
@JsonKey(name: 'visit_type') required String visitType, @JsonKey(name: 'visit_type') required String visitType,
@JsonKey(name: 'visit_date', fromJson: _dateFromJson) required DateTime visitDate, @JsonKey(name: 'visit_date', fromJson: _dateFromJson) required DateTime visitDate,
@JsonKey(name: 'amc_contract_id', fromJson: _intFromJsonNullable) int? amcContractId, @JsonKey(name: 'amc_contract_id', fromJson: _intFromJsonNullable) int? amcContractId,
@JsonKey(name: 'visit_number', fromJson: _intFromJsonNullable) int? visitNumber,
@JsonKey(name: 'complaint_no') String? complaintNo, @JsonKey(name: 'complaint_no') String? complaintNo,
@JsonKey(name: 'complaint_date', fromJson: _dateFromJsonNullable) DateTime? complaintDate, @JsonKey(name: 'complaint_date', fromJson: _dateFromJsonNullable) DateTime? complaintDate,
@JsonKey(name: 'complaint_desc') String? complaintDesc, @JsonKey(name: 'complaint_desc') String? complaintDesc,

View File

@ -2660,8 +2660,6 @@ mixin _$ServiceVisitModel {
DateTime get visitDate => throw _privateConstructorUsedError; DateTime get visitDate => throw _privateConstructorUsedError;
@JsonKey(name: 'amc_contract_id', fromJson: _intFromJsonNullable) @JsonKey(name: 'amc_contract_id', fromJson: _intFromJsonNullable)
int? get amcContractId => throw _privateConstructorUsedError; int? get amcContractId => throw _privateConstructorUsedError;
@JsonKey(name: 'visit_number', fromJson: _intFromJsonNullable)
int? get visitNumber => throw _privateConstructorUsedError;
@JsonKey(name: 'complaint_no') @JsonKey(name: 'complaint_no')
String? get complaintNo => throw _privateConstructorUsedError; String? get complaintNo => throw _privateConstructorUsedError;
@JsonKey(name: 'complaint_date', fromJson: _dateFromJsonNullable) @JsonKey(name: 'complaint_date', fromJson: _dateFromJsonNullable)
@ -2719,8 +2717,6 @@ abstract class $ServiceVisitModelCopyWith<$Res> {
@JsonKey(name: 'visit_date', fromJson: _dateFromJson) DateTime visitDate, @JsonKey(name: 'visit_date', fromJson: _dateFromJson) DateTime visitDate,
@JsonKey(name: 'amc_contract_id', fromJson: _intFromJsonNullable) @JsonKey(name: 'amc_contract_id', fromJson: _intFromJsonNullable)
int? amcContractId, int? amcContractId,
@JsonKey(name: 'visit_number', fromJson: _intFromJsonNullable)
int? visitNumber,
@JsonKey(name: 'complaint_no') String? complaintNo, @JsonKey(name: 'complaint_no') String? complaintNo,
@JsonKey(name: 'complaint_date', fromJson: _dateFromJsonNullable) @JsonKey(name: 'complaint_date', fromJson: _dateFromJsonNullable)
DateTime? complaintDate, DateTime? complaintDate,
@ -2767,7 +2763,6 @@ class _$ServiceVisitModelCopyWithImpl<$Res, $Val extends ServiceVisitModel>
Object? visitType = null, Object? visitType = null,
Object? visitDate = null, Object? visitDate = null,
Object? amcContractId = freezed, Object? amcContractId = freezed,
Object? visitNumber = freezed,
Object? complaintNo = freezed, Object? complaintNo = freezed,
Object? complaintDate = freezed, Object? complaintDate = freezed,
Object? complaintDesc = freezed, Object? complaintDesc = freezed,
@ -2808,10 +2803,6 @@ class _$ServiceVisitModelCopyWithImpl<$Res, $Val extends ServiceVisitModel>
? _value.amcContractId ? _value.amcContractId
: amcContractId // ignore: cast_nullable_to_non_nullable : amcContractId // ignore: cast_nullable_to_non_nullable
as int?, as int?,
visitNumber: freezed == visitNumber
? _value.visitNumber
: visitNumber // ignore: cast_nullable_to_non_nullable
as int?,
complaintNo: freezed == complaintNo complaintNo: freezed == complaintNo
? _value.complaintNo ? _value.complaintNo
: complaintNo // ignore: cast_nullable_to_non_nullable : complaintNo // ignore: cast_nullable_to_non_nullable
@ -2902,8 +2893,6 @@ abstract class _$$ServiceVisitModelImplCopyWith<$Res>
@JsonKey(name: 'visit_date', fromJson: _dateFromJson) DateTime visitDate, @JsonKey(name: 'visit_date', fromJson: _dateFromJson) DateTime visitDate,
@JsonKey(name: 'amc_contract_id', fromJson: _intFromJsonNullable) @JsonKey(name: 'amc_contract_id', fromJson: _intFromJsonNullable)
int? amcContractId, int? amcContractId,
@JsonKey(name: 'visit_number', fromJson: _intFromJsonNullable)
int? visitNumber,
@JsonKey(name: 'complaint_no') String? complaintNo, @JsonKey(name: 'complaint_no') String? complaintNo,
@JsonKey(name: 'complaint_date', fromJson: _dateFromJsonNullable) @JsonKey(name: 'complaint_date', fromJson: _dateFromJsonNullable)
DateTime? complaintDate, DateTime? complaintDate,
@ -2949,7 +2938,6 @@ class __$$ServiceVisitModelImplCopyWithImpl<$Res>
Object? visitType = null, Object? visitType = null,
Object? visitDate = null, Object? visitDate = null,
Object? amcContractId = freezed, Object? amcContractId = freezed,
Object? visitNumber = freezed,
Object? complaintNo = freezed, Object? complaintNo = freezed,
Object? complaintDate = freezed, Object? complaintDate = freezed,
Object? complaintDesc = freezed, Object? complaintDesc = freezed,
@ -2990,10 +2978,6 @@ class __$$ServiceVisitModelImplCopyWithImpl<$Res>
? _value.amcContractId ? _value.amcContractId
: amcContractId // ignore: cast_nullable_to_non_nullable : amcContractId // ignore: cast_nullable_to_non_nullable
as int?, as int?,
visitNumber: freezed == visitNumber
? _value.visitNumber
: visitNumber // ignore: cast_nullable_to_non_nullable
as int?,
complaintNo: freezed == complaintNo complaintNo: freezed == complaintNo
? _value.complaintNo ? _value.complaintNo
: complaintNo // ignore: cast_nullable_to_non_nullable : complaintNo // ignore: cast_nullable_to_non_nullable
@ -3078,8 +3062,6 @@ class _$ServiceVisitModelImpl implements _ServiceVisitModel {
required this.visitDate, required this.visitDate,
@JsonKey(name: 'amc_contract_id', fromJson: _intFromJsonNullable) @JsonKey(name: 'amc_contract_id', fromJson: _intFromJsonNullable)
this.amcContractId, this.amcContractId,
@JsonKey(name: 'visit_number', fromJson: _intFromJsonNullable)
this.visitNumber,
@JsonKey(name: 'complaint_no') this.complaintNo, @JsonKey(name: 'complaint_no') this.complaintNo,
@JsonKey(name: 'complaint_date', fromJson: _dateFromJsonNullable) @JsonKey(name: 'complaint_date', fromJson: _dateFromJsonNullable)
this.complaintDate, this.complaintDate,
@ -3124,9 +3106,6 @@ class _$ServiceVisitModelImpl implements _ServiceVisitModel {
@JsonKey(name: 'amc_contract_id', fromJson: _intFromJsonNullable) @JsonKey(name: 'amc_contract_id', fromJson: _intFromJsonNullable)
final int? amcContractId; final int? amcContractId;
@override @override
@JsonKey(name: 'visit_number', fromJson: _intFromJsonNullable)
final int? visitNumber;
@override
@JsonKey(name: 'complaint_no') @JsonKey(name: 'complaint_no')
final String? complaintNo; final String? complaintNo;
@override @override
@ -3179,7 +3158,7 @@ class _$ServiceVisitModelImpl implements _ServiceVisitModel {
@override @override
String toString() { String toString() {
return 'ServiceVisitModel(id: $id, assetId: $assetId, visitType: $visitType, visitDate: $visitDate, amcContractId: $amcContractId, visitNumber: $visitNumber, complaintNo: $complaintNo, complaintDate: $complaintDate, complaintDesc: $complaintDesc, engineerName: $engineerName, engineerPhone: $engineerPhone, vendorId: $vendorId, vendorName: $vendorName, workDone: $workDone, partsReplaced: $partsReplaced, nextServiceDate: $nextServiceDate, status: $status, downtimeHours: $downtimeHours, serviceCost: $serviceCost, isUnderAmc: $isUnderAmc, assetConditionAfter: $assetConditionAfter, remarks: $remarks, createdAt: $createdAt)'; return 'ServiceVisitModel(id: $id, assetId: $assetId, visitType: $visitType, visitDate: $visitDate, amcContractId: $amcContractId, complaintNo: $complaintNo, complaintDate: $complaintDate, complaintDesc: $complaintDesc, engineerName: $engineerName, engineerPhone: $engineerPhone, vendorId: $vendorId, vendorName: $vendorName, workDone: $workDone, partsReplaced: $partsReplaced, nextServiceDate: $nextServiceDate, status: $status, downtimeHours: $downtimeHours, serviceCost: $serviceCost, isUnderAmc: $isUnderAmc, assetConditionAfter: $assetConditionAfter, remarks: $remarks, createdAt: $createdAt)';
} }
@override @override
@ -3195,8 +3174,6 @@ class _$ServiceVisitModelImpl implements _ServiceVisitModel {
other.visitDate == visitDate) && other.visitDate == visitDate) &&
(identical(other.amcContractId, amcContractId) || (identical(other.amcContractId, amcContractId) ||
other.amcContractId == amcContractId) && other.amcContractId == amcContractId) &&
(identical(other.visitNumber, visitNumber) ||
other.visitNumber == visitNumber) &&
(identical(other.complaintNo, complaintNo) || (identical(other.complaintNo, complaintNo) ||
other.complaintNo == complaintNo) && other.complaintNo == complaintNo) &&
(identical(other.complaintDate, complaintDate) || (identical(other.complaintDate, complaintDate) ||
@ -3240,7 +3217,6 @@ class _$ServiceVisitModelImpl implements _ServiceVisitModel {
visitType, visitType,
visitDate, visitDate,
amcContractId, amcContractId,
visitNumber,
complaintNo, complaintNo,
complaintDate, complaintDate,
complaintDesc, complaintDesc,
@ -3286,8 +3262,6 @@ abstract class _ServiceVisitModel implements ServiceVisitModel {
required final DateTime visitDate, required final DateTime visitDate,
@JsonKey(name: 'amc_contract_id', fromJson: _intFromJsonNullable) @JsonKey(name: 'amc_contract_id', fromJson: _intFromJsonNullable)
final int? amcContractId, final int? amcContractId,
@JsonKey(name: 'visit_number', fromJson: _intFromJsonNullable)
final int? visitNumber,
@JsonKey(name: 'complaint_no') final String? complaintNo, @JsonKey(name: 'complaint_no') final String? complaintNo,
@JsonKey(name: 'complaint_date', fromJson: _dateFromJsonNullable) @JsonKey(name: 'complaint_date', fromJson: _dateFromJsonNullable)
final DateTime? complaintDate, final DateTime? complaintDate,
@ -3333,9 +3307,6 @@ abstract class _ServiceVisitModel implements ServiceVisitModel {
@JsonKey(name: 'amc_contract_id', fromJson: _intFromJsonNullable) @JsonKey(name: 'amc_contract_id', fromJson: _intFromJsonNullable)
int? get amcContractId; int? get amcContractId;
@override @override
@JsonKey(name: 'visit_number', fromJson: _intFromJsonNullable)
int? get visitNumber;
@override
@JsonKey(name: 'complaint_no') @JsonKey(name: 'complaint_no')
String? get complaintNo; String? get complaintNo;
@override @override

View File

@ -217,7 +217,6 @@ _$ServiceVisitModelImpl _$$ServiceVisitModelImplFromJson(
visitType: json['visit_type'] as String, visitType: json['visit_type'] as String,
visitDate: _dateFromJson(json['visit_date']), visitDate: _dateFromJson(json['visit_date']),
amcContractId: _intFromJsonNullable(json['amc_contract_id']), amcContractId: _intFromJsonNullable(json['amc_contract_id']),
visitNumber: _intFromJsonNullable(json['visit_number']),
complaintNo: json['complaint_no'] as String?, complaintNo: json['complaint_no'] as String?,
complaintDate: _dateFromJsonNullable(json['complaint_date']), complaintDate: _dateFromJsonNullable(json['complaint_date']),
complaintDesc: json['complaint_desc'] as String?, complaintDesc: json['complaint_desc'] as String?,
@ -245,7 +244,6 @@ Map<String, dynamic> _$$ServiceVisitModelImplToJson(
'visit_type': instance.visitType, 'visit_type': instance.visitType,
'visit_date': instance.visitDate.toIso8601String(), 'visit_date': instance.visitDate.toIso8601String(),
'amc_contract_id': instance.amcContractId, 'amc_contract_id': instance.amcContractId,
'visit_number': instance.visitNumber,
'complaint_no': instance.complaintNo, 'complaint_no': instance.complaintNo,
'complaint_date': instance.complaintDate?.toIso8601String(), 'complaint_date': instance.complaintDate?.toIso8601String(),
'complaint_desc': instance.complaintDesc, 'complaint_desc': instance.complaintDesc,

View File

@ -38,9 +38,23 @@ Object? _readNestedName(Map<dynamic, dynamic> json, String flatKey, String neste
Object? _readVendorName(Map<dynamic, dynamic> json, String key) => Object? _readVendorName(Map<dynamic, dynamic> json, String key) =>
_readNestedName(json, 'vendor_name', 'vendor'); _readNestedName(json, 'vendor_name', 'vendor');
Object? _readWarehouseName(Map<dynamic, dynamic> json, String key) => Object? _readLocationName(Map<dynamic, dynamic> json, String key) {
_readNestedName(json, 'warehouse_name', 'warehouse'); final flat = json['location_name'];
if (flat is String && flat.isNotEmpty) return flat;
final nested = json['location'];
if (nested is Map) {
return nested['name'] ?? nested['code'];
}
return null;
}
Object? _readLocationType(Map<dynamic, dynamic> json, String key) {
final flat = json['location_type'];
if (flat is String && flat.isNotEmpty) return flat;
final nested = json['location'];
if (nested is Map) return nested['type'];
return null;
}
Object? _readPoNumber(Map<dynamic, dynamic> json, String key) { Object? _readPoNumber(Map<dynamic, dynamic> json, String key) {
final grnNumber = json['grn_number']; final grnNumber = json['grn_number'];
if (grnNumber != null && grnNumber.toString().trim().isNotEmpty) { if (grnNumber != null && grnNumber.toString().trim().isNotEmpty) {
@ -130,8 +144,11 @@ class GrnModel with _$GrnModel {
@JsonKey(name: 'po_number', readValue: _readPoRefNumber) String? poNumber, @JsonKey(name: 'po_number', readValue: _readPoRefNumber) String? poNumber,
@JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? vendorId, @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? vendorId,
@JsonKey(name: 'vendor_name', readValue: _readVendorName) String? vendorName, @JsonKey(name: 'vendor_name', readValue: _readVendorName) String? vendorName,
@JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable) int? warehouseId, @JsonKey(name: 'location_id', fromJson: _intFromJsonNullable) int? locationId,
@JsonKey(name: 'warehouse_name', readValue: _readWarehouseName) String? warehouseName, @JsonKey(name: 'location_name', readValue: _readLocationName)
String? locationName,
@JsonKey(name: 'location_type', readValue: _readLocationType)
String? locationType,
@JsonKey(name: 'vendor_invoice_no') String? vendorInvoiceNo, @JsonKey(name: 'vendor_invoice_no') String? vendorInvoiceNo,
@JsonKey(name: 'vendor_invoice_date', fromJson: _dateFromJsonNullable) @JsonKey(name: 'vendor_invoice_date', fromJson: _dateFromJsonNullable)
DateTime? vendorInvoiceDate, DateTime? vendorInvoiceDate,
@ -246,7 +263,7 @@ class GrnListQuery with _$GrnListQuery {
String? status, String? status,
int? poId, int? poId,
int? vendorId, int? vendorId,
int? warehouseId, int? locationId,
String? dateFrom, String? dateFrom,
String? dateTo, String? dateTo,
}) = _GrnListQuery; }) = _GrnListQuery;

View File

@ -36,10 +36,12 @@ mixin _$GrnModel {
int? get vendorId => throw _privateConstructorUsedError; int? get vendorId => throw _privateConstructorUsedError;
@JsonKey(name: 'vendor_name', readValue: _readVendorName) @JsonKey(name: 'vendor_name', readValue: _readVendorName)
String? get vendorName => throw _privateConstructorUsedError; String? get vendorName => throw _privateConstructorUsedError;
@JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable) @JsonKey(name: 'location_id', fromJson: _intFromJsonNullable)
int? get warehouseId => throw _privateConstructorUsedError; int? get locationId => throw _privateConstructorUsedError;
@JsonKey(name: 'warehouse_name', readValue: _readWarehouseName) @JsonKey(name: 'location_name', readValue: _readLocationName)
String? get warehouseName => throw _privateConstructorUsedError; String? get locationName => throw _privateConstructorUsedError;
@JsonKey(name: 'location_type', readValue: _readLocationType)
String? get locationType => throw _privateConstructorUsedError;
@JsonKey(name: 'vendor_invoice_no') @JsonKey(name: 'vendor_invoice_no')
String? get vendorInvoiceNo => throw _privateConstructorUsedError; String? get vendorInvoiceNo => throw _privateConstructorUsedError;
@JsonKey(name: 'vendor_invoice_date', fromJson: _dateFromJsonNullable) @JsonKey(name: 'vendor_invoice_date', fromJson: _dateFromJsonNullable)
@ -93,10 +95,12 @@ abstract class $GrnModelCopyWith<$Res> {
@JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? vendorId, @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? vendorId,
@JsonKey(name: 'vendor_name', readValue: _readVendorName) @JsonKey(name: 'vendor_name', readValue: _readVendorName)
String? vendorName, String? vendorName,
@JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable) @JsonKey(name: 'location_id', fromJson: _intFromJsonNullable)
int? warehouseId, int? locationId,
@JsonKey(name: 'warehouse_name', readValue: _readWarehouseName) @JsonKey(name: 'location_name', readValue: _readLocationName)
String? warehouseName, String? locationName,
@JsonKey(name: 'location_type', readValue: _readLocationType)
String? locationType,
@JsonKey(name: 'vendor_invoice_no') String? vendorInvoiceNo, @JsonKey(name: 'vendor_invoice_no') String? vendorInvoiceNo,
@JsonKey(name: 'vendor_invoice_date', fromJson: _dateFromJsonNullable) @JsonKey(name: 'vendor_invoice_date', fromJson: _dateFromJsonNullable)
DateTime? vendorInvoiceDate, DateTime? vendorInvoiceDate,
@ -143,8 +147,9 @@ class _$GrnModelCopyWithImpl<$Res, $Val extends GrnModel>
Object? poNumber = freezed, Object? poNumber = freezed,
Object? vendorId = freezed, Object? vendorId = freezed,
Object? vendorName = freezed, Object? vendorName = freezed,
Object? warehouseId = freezed, Object? locationId = freezed,
Object? warehouseName = freezed, Object? locationName = freezed,
Object? locationType = freezed,
Object? vendorInvoiceNo = freezed, Object? vendorInvoiceNo = freezed,
Object? vendorInvoiceDate = freezed, Object? vendorInvoiceDate = freezed,
Object? vendorInvoiceAmount = freezed, Object? vendorInvoiceAmount = freezed,
@ -194,13 +199,17 @@ class _$GrnModelCopyWithImpl<$Res, $Val extends GrnModel>
? _value.vendorName ? _value.vendorName
: vendorName // ignore: cast_nullable_to_non_nullable : vendorName // ignore: cast_nullable_to_non_nullable
as String?, as String?,
warehouseId: freezed == warehouseId locationId: freezed == locationId
? _value.warehouseId ? _value.locationId
: warehouseId // ignore: cast_nullable_to_non_nullable : locationId // ignore: cast_nullable_to_non_nullable
as int?, as int?,
warehouseName: freezed == warehouseName locationName: freezed == locationName
? _value.warehouseName ? _value.locationName
: warehouseName // ignore: cast_nullable_to_non_nullable : locationName // ignore: cast_nullable_to_non_nullable
as String?,
locationType: freezed == locationType
? _value.locationType
: locationType // ignore: cast_nullable_to_non_nullable
as String?, as String?,
vendorInvoiceNo: freezed == vendorInvoiceNo vendorInvoiceNo: freezed == vendorInvoiceNo
? _value.vendorInvoiceNo ? _value.vendorInvoiceNo
@ -284,10 +293,12 @@ abstract class _$$GrnModelImplCopyWith<$Res>
@JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? vendorId, @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? vendorId,
@JsonKey(name: 'vendor_name', readValue: _readVendorName) @JsonKey(name: 'vendor_name', readValue: _readVendorName)
String? vendorName, String? vendorName,
@JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable) @JsonKey(name: 'location_id', fromJson: _intFromJsonNullable)
int? warehouseId, int? locationId,
@JsonKey(name: 'warehouse_name', readValue: _readWarehouseName) @JsonKey(name: 'location_name', readValue: _readLocationName)
String? warehouseName, String? locationName,
@JsonKey(name: 'location_type', readValue: _readLocationType)
String? locationType,
@JsonKey(name: 'vendor_invoice_no') String? vendorInvoiceNo, @JsonKey(name: 'vendor_invoice_no') String? vendorInvoiceNo,
@JsonKey(name: 'vendor_invoice_date', fromJson: _dateFromJsonNullable) @JsonKey(name: 'vendor_invoice_date', fromJson: _dateFromJsonNullable)
DateTime? vendorInvoiceDate, DateTime? vendorInvoiceDate,
@ -333,8 +344,9 @@ class __$$GrnModelImplCopyWithImpl<$Res>
Object? poNumber = freezed, Object? poNumber = freezed,
Object? vendorId = freezed, Object? vendorId = freezed,
Object? vendorName = freezed, Object? vendorName = freezed,
Object? warehouseId = freezed, Object? locationId = freezed,
Object? warehouseName = freezed, Object? locationName = freezed,
Object? locationType = freezed,
Object? vendorInvoiceNo = freezed, Object? vendorInvoiceNo = freezed,
Object? vendorInvoiceDate = freezed, Object? vendorInvoiceDate = freezed,
Object? vendorInvoiceAmount = freezed, Object? vendorInvoiceAmount = freezed,
@ -384,13 +396,17 @@ class __$$GrnModelImplCopyWithImpl<$Res>
? _value.vendorName ? _value.vendorName
: vendorName // ignore: cast_nullable_to_non_nullable : vendorName // ignore: cast_nullable_to_non_nullable
as String?, as String?,
warehouseId: freezed == warehouseId locationId: freezed == locationId
? _value.warehouseId ? _value.locationId
: warehouseId // ignore: cast_nullable_to_non_nullable : locationId // ignore: cast_nullable_to_non_nullable
as int?, as int?,
warehouseName: freezed == warehouseName locationName: freezed == locationName
? _value.warehouseName ? _value.locationName
: warehouseName // ignore: cast_nullable_to_non_nullable : locationName // ignore: cast_nullable_to_non_nullable
as String?,
locationType: freezed == locationType
? _value.locationType
: locationType // ignore: cast_nullable_to_non_nullable
as String?, as String?,
vendorInvoiceNo: freezed == vendorInvoiceNo vendorInvoiceNo: freezed == vendorInvoiceNo
? _value.vendorInvoiceNo ? _value.vendorInvoiceNo
@ -465,10 +481,12 @@ class _$GrnModelImpl extends _GrnModel {
@JsonKey(name: 'po_number', readValue: _readPoRefNumber) this.poNumber, @JsonKey(name: 'po_number', readValue: _readPoRefNumber) this.poNumber,
@JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) this.vendorId, @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) this.vendorId,
@JsonKey(name: 'vendor_name', readValue: _readVendorName) this.vendorName, @JsonKey(name: 'vendor_name', readValue: _readVendorName) this.vendorName,
@JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable) @JsonKey(name: 'location_id', fromJson: _intFromJsonNullable)
this.warehouseId, this.locationId,
@JsonKey(name: 'warehouse_name', readValue: _readWarehouseName) @JsonKey(name: 'location_name', readValue: _readLocationName)
this.warehouseName, this.locationName,
@JsonKey(name: 'location_type', readValue: _readLocationType)
this.locationType,
@JsonKey(name: 'vendor_invoice_no') this.vendorInvoiceNo, @JsonKey(name: 'vendor_invoice_no') this.vendorInvoiceNo,
@JsonKey(name: 'vendor_invoice_date', fromJson: _dateFromJsonNullable) @JsonKey(name: 'vendor_invoice_date', fromJson: _dateFromJsonNullable)
this.vendorInvoiceDate, this.vendorInvoiceDate,
@ -521,11 +539,14 @@ class _$GrnModelImpl extends _GrnModel {
@JsonKey(name: 'vendor_name', readValue: _readVendorName) @JsonKey(name: 'vendor_name', readValue: _readVendorName)
final String? vendorName; final String? vendorName;
@override @override
@JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable) @JsonKey(name: 'location_id', fromJson: _intFromJsonNullable)
final int? warehouseId; final int? locationId;
@override @override
@JsonKey(name: 'warehouse_name', readValue: _readWarehouseName) @JsonKey(name: 'location_name', readValue: _readLocationName)
final String? warehouseName; final String? locationName;
@override
@JsonKey(name: 'location_type', readValue: _readLocationType)
final String? locationType;
@override @override
@JsonKey(name: 'vendor_invoice_no') @JsonKey(name: 'vendor_invoice_no')
final String? vendorInvoiceNo; final String? vendorInvoiceNo;
@ -581,7 +602,7 @@ class _$GrnModelImpl extends _GrnModel {
@override @override
String toString() { String toString() {
return 'GrnModel(id: $id, grnNumber: $grnNumber, grnDate: $grnDate, status: $status, poId: $poId, poNumber: $poNumber, vendorId: $vendorId, vendorName: $vendorName, warehouseId: $warehouseId, warehouseName: $warehouseName, vendorInvoiceNo: $vendorInvoiceNo, vendorInvoiceDate: $vendorInvoiceDate, vendorInvoiceAmount: $vendorInvoiceAmount, vehicleNo: $vehicleNo, lrNo: $lrNo, lrDate: $lrDate, receivedBy: $receivedBy, qualityCheckedBy: $qualityCheckedBy, remarks: $remarks, cancellationReason: $cancellationReason, createdAt: $createdAt, updatedAt: $updatedAt, items: $items, attachments: $attachments)'; return 'GrnModel(id: $id, grnNumber: $grnNumber, grnDate: $grnDate, status: $status, poId: $poId, poNumber: $poNumber, vendorId: $vendorId, vendorName: $vendorName, locationId: $locationId, locationName: $locationName, locationType: $locationType, vendorInvoiceNo: $vendorInvoiceNo, vendorInvoiceDate: $vendorInvoiceDate, vendorInvoiceAmount: $vendorInvoiceAmount, vehicleNo: $vehicleNo, lrNo: $lrNo, lrDate: $lrDate, receivedBy: $receivedBy, qualityCheckedBy: $qualityCheckedBy, remarks: $remarks, cancellationReason: $cancellationReason, createdAt: $createdAt, updatedAt: $updatedAt, items: $items, attachments: $attachments)';
} }
@override @override
@ -601,10 +622,12 @@ class _$GrnModelImpl extends _GrnModel {
other.vendorId == vendorId) && other.vendorId == vendorId) &&
(identical(other.vendorName, vendorName) || (identical(other.vendorName, vendorName) ||
other.vendorName == vendorName) && other.vendorName == vendorName) &&
(identical(other.warehouseId, warehouseId) || (identical(other.locationId, locationId) ||
other.warehouseId == warehouseId) && other.locationId == locationId) &&
(identical(other.warehouseName, warehouseName) || (identical(other.locationName, locationName) ||
other.warehouseName == warehouseName) && other.locationName == locationName) &&
(identical(other.locationType, locationType) ||
other.locationType == locationType) &&
(identical(other.vendorInvoiceNo, vendorInvoiceNo) || (identical(other.vendorInvoiceNo, vendorInvoiceNo) ||
other.vendorInvoiceNo == vendorInvoiceNo) && other.vendorInvoiceNo == vendorInvoiceNo) &&
(identical(other.vendorInvoiceDate, vendorInvoiceDate) || (identical(other.vendorInvoiceDate, vendorInvoiceDate) ||
@ -645,8 +668,9 @@ class _$GrnModelImpl extends _GrnModel {
poNumber, poNumber,
vendorId, vendorId,
vendorName, vendorName,
warehouseId, locationId,
warehouseName, locationName,
locationType,
vendorInvoiceNo, vendorInvoiceNo,
vendorInvoiceDate, vendorInvoiceDate,
vendorInvoiceAmount, vendorInvoiceAmount,
@ -692,10 +716,12 @@ abstract class _GrnModel extends GrnModel {
final int? vendorId, final int? vendorId,
@JsonKey(name: 'vendor_name', readValue: _readVendorName) @JsonKey(name: 'vendor_name', readValue: _readVendorName)
final String? vendorName, final String? vendorName,
@JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable) @JsonKey(name: 'location_id', fromJson: _intFromJsonNullable)
final int? warehouseId, final int? locationId,
@JsonKey(name: 'warehouse_name', readValue: _readWarehouseName) @JsonKey(name: 'location_name', readValue: _readLocationName)
final String? warehouseName, final String? locationName,
@JsonKey(name: 'location_type', readValue: _readLocationType)
final String? locationType,
@JsonKey(name: 'vendor_invoice_no') final String? vendorInvoiceNo, @JsonKey(name: 'vendor_invoice_no') final String? vendorInvoiceNo,
@JsonKey(name: 'vendor_invoice_date', fromJson: _dateFromJsonNullable) @JsonKey(name: 'vendor_invoice_date', fromJson: _dateFromJsonNullable)
final DateTime? vendorInvoiceDate, final DateTime? vendorInvoiceDate,
@ -747,11 +773,14 @@ abstract class _GrnModel extends GrnModel {
@JsonKey(name: 'vendor_name', readValue: _readVendorName) @JsonKey(name: 'vendor_name', readValue: _readVendorName)
String? get vendorName; String? get vendorName;
@override @override
@JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable) @JsonKey(name: 'location_id', fromJson: _intFromJsonNullable)
int? get warehouseId; int? get locationId;
@override @override
@JsonKey(name: 'warehouse_name', readValue: _readWarehouseName) @JsonKey(name: 'location_name', readValue: _readLocationName)
String? get warehouseName; String? get locationName;
@override
@JsonKey(name: 'location_type', readValue: _readLocationType)
String? get locationType;
@override @override
@JsonKey(name: 'vendor_invoice_no') @JsonKey(name: 'vendor_invoice_no')
String? get vendorInvoiceNo; String? get vendorInvoiceNo;
@ -1969,7 +1998,7 @@ mixin _$GrnListQuery {
String? get status => throw _privateConstructorUsedError; String? get status => throw _privateConstructorUsedError;
int? get poId => throw _privateConstructorUsedError; int? get poId => throw _privateConstructorUsedError;
int? get vendorId => throw _privateConstructorUsedError; int? get vendorId => throw _privateConstructorUsedError;
int? get warehouseId => throw _privateConstructorUsedError; int? get locationId => throw _privateConstructorUsedError;
String? get dateFrom => throw _privateConstructorUsedError; String? get dateFrom => throw _privateConstructorUsedError;
String? get dateTo => throw _privateConstructorUsedError; String? get dateTo => throw _privateConstructorUsedError;
@ -1994,7 +2023,7 @@ abstract class $GrnListQueryCopyWith<$Res> {
String? status, String? status,
int? poId, int? poId,
int? vendorId, int? vendorId,
int? warehouseId, int? locationId,
String? dateFrom, String? dateFrom,
String? dateTo, String? dateTo,
}); });
@ -2021,7 +2050,7 @@ class _$GrnListQueryCopyWithImpl<$Res, $Val extends GrnListQuery>
Object? status = freezed, Object? status = freezed,
Object? poId = freezed, Object? poId = freezed,
Object? vendorId = freezed, Object? vendorId = freezed,
Object? warehouseId = freezed, Object? locationId = freezed,
Object? dateFrom = freezed, Object? dateFrom = freezed,
Object? dateTo = freezed, Object? dateTo = freezed,
}) { }) {
@ -2051,9 +2080,9 @@ class _$GrnListQueryCopyWithImpl<$Res, $Val extends GrnListQuery>
? _value.vendorId ? _value.vendorId
: vendorId // ignore: cast_nullable_to_non_nullable : vendorId // ignore: cast_nullable_to_non_nullable
as int?, as int?,
warehouseId: freezed == warehouseId locationId: freezed == locationId
? _value.warehouseId ? _value.locationId
: warehouseId // ignore: cast_nullable_to_non_nullable : locationId // ignore: cast_nullable_to_non_nullable
as int?, as int?,
dateFrom: freezed == dateFrom dateFrom: freezed == dateFrom
? _value.dateFrom ? _value.dateFrom
@ -2085,7 +2114,7 @@ abstract class _$$GrnListQueryImplCopyWith<$Res>
String? status, String? status,
int? poId, int? poId,
int? vendorId, int? vendorId,
int? warehouseId, int? locationId,
String? dateFrom, String? dateFrom,
String? dateTo, String? dateTo,
}); });
@ -2111,7 +2140,7 @@ class __$$GrnListQueryImplCopyWithImpl<$Res>
Object? status = freezed, Object? status = freezed,
Object? poId = freezed, Object? poId = freezed,
Object? vendorId = freezed, Object? vendorId = freezed,
Object? warehouseId = freezed, Object? locationId = freezed,
Object? dateFrom = freezed, Object? dateFrom = freezed,
Object? dateTo = freezed, Object? dateTo = freezed,
}) { }) {
@ -2141,9 +2170,9 @@ class __$$GrnListQueryImplCopyWithImpl<$Res>
? _value.vendorId ? _value.vendorId
: vendorId // ignore: cast_nullable_to_non_nullable : vendorId // ignore: cast_nullable_to_non_nullable
as int?, as int?,
warehouseId: freezed == warehouseId locationId: freezed == locationId
? _value.warehouseId ? _value.locationId
: warehouseId // ignore: cast_nullable_to_non_nullable : locationId // ignore: cast_nullable_to_non_nullable
as int?, as int?,
dateFrom: freezed == dateFrom dateFrom: freezed == dateFrom
? _value.dateFrom ? _value.dateFrom
@ -2168,7 +2197,7 @@ class _$GrnListQueryImpl implements _GrnListQuery {
this.status, this.status,
this.poId, this.poId,
this.vendorId, this.vendorId,
this.warehouseId, this.locationId,
this.dateFrom, this.dateFrom,
this.dateTo, this.dateTo,
}); });
@ -2188,7 +2217,7 @@ class _$GrnListQueryImpl implements _GrnListQuery {
@override @override
final int? vendorId; final int? vendorId;
@override @override
final int? warehouseId; final int? locationId;
@override @override
final String? dateFrom; final String? dateFrom;
@override @override
@ -2196,7 +2225,7 @@ class _$GrnListQueryImpl implements _GrnListQuery {
@override @override
String toString() { String toString() {
return 'GrnListQuery(page: $page, limit: $limit, search: $search, status: $status, poId: $poId, vendorId: $vendorId, warehouseId: $warehouseId, dateFrom: $dateFrom, dateTo: $dateTo)'; return 'GrnListQuery(page: $page, limit: $limit, search: $search, status: $status, poId: $poId, vendorId: $vendorId, locationId: $locationId, dateFrom: $dateFrom, dateTo: $dateTo)';
} }
@override @override
@ -2211,8 +2240,8 @@ class _$GrnListQueryImpl implements _GrnListQuery {
(identical(other.poId, poId) || other.poId == poId) && (identical(other.poId, poId) || other.poId == poId) &&
(identical(other.vendorId, vendorId) || (identical(other.vendorId, vendorId) ||
other.vendorId == vendorId) && other.vendorId == vendorId) &&
(identical(other.warehouseId, warehouseId) || (identical(other.locationId, locationId) ||
other.warehouseId == warehouseId) && other.locationId == locationId) &&
(identical(other.dateFrom, dateFrom) || (identical(other.dateFrom, dateFrom) ||
other.dateFrom == dateFrom) && other.dateFrom == dateFrom) &&
(identical(other.dateTo, dateTo) || other.dateTo == dateTo)); (identical(other.dateTo, dateTo) || other.dateTo == dateTo));
@ -2227,7 +2256,7 @@ class _$GrnListQueryImpl implements _GrnListQuery {
status, status,
poId, poId,
vendorId, vendorId,
warehouseId, locationId,
dateFrom, dateFrom,
dateTo, dateTo,
); );
@ -2249,7 +2278,7 @@ abstract class _GrnListQuery implements GrnListQuery {
final String? status, final String? status,
final int? poId, final int? poId,
final int? vendorId, final int? vendorId,
final int? warehouseId, final int? locationId,
final String? dateFrom, final String? dateFrom,
final String? dateTo, final String? dateTo,
}) = _$GrnListQueryImpl; }) = _$GrnListQueryImpl;
@ -2267,7 +2296,7 @@ abstract class _GrnListQuery implements GrnListQuery {
@override @override
int? get vendorId; int? get vendorId;
@override @override
int? get warehouseId; int? get locationId;
@override @override
String? get dateFrom; String? get dateFrom;
@override @override

View File

@ -17,8 +17,9 @@ _$GrnModelImpl _$$GrnModelImplFromJson(
poNumber: _readPoRefNumber(json, 'po_number') as String?, poNumber: _readPoRefNumber(json, 'po_number') as String?,
vendorId: _intFromJsonNullable(json['vendor_id']), vendorId: _intFromJsonNullable(json['vendor_id']),
vendorName: _readVendorName(json, 'vendor_name') as String?, vendorName: _readVendorName(json, 'vendor_name') as String?,
warehouseId: _intFromJsonNullable(json['warehouse_id']), locationId: _intFromJsonNullable(json['location_id']),
warehouseName: _readWarehouseName(json, 'warehouse_name') as String?, locationName: _readLocationName(json, 'location_name') as String?,
locationType: _readLocationType(json, 'location_type') as String?,
vendorInvoiceNo: json['vendor_invoice_no'] as String?, vendorInvoiceNo: json['vendor_invoice_no'] as String?,
vendorInvoiceDate: _dateFromJsonNullable(json['vendor_invoice_date']), vendorInvoiceDate: _dateFromJsonNullable(json['vendor_invoice_date']),
vendorInvoiceAmount: _doubleFromJsonNullable(json['vendor_invoice_amount']), vendorInvoiceAmount: _doubleFromJsonNullable(json['vendor_invoice_amount']),
@ -53,8 +54,9 @@ Map<String, dynamic> _$$GrnModelImplToJson(_$GrnModelImpl instance) =>
'po_number': instance.poNumber, 'po_number': instance.poNumber,
'vendor_id': instance.vendorId, 'vendor_id': instance.vendorId,
'vendor_name': instance.vendorName, 'vendor_name': instance.vendorName,
'warehouse_id': instance.warehouseId, 'location_id': instance.locationId,
'warehouse_name': instance.warehouseName, 'location_name': instance.locationName,
'location_type': instance.locationType,
'vendor_invoice_no': instance.vendorInvoiceNo, 'vendor_invoice_no': instance.vendorInvoiceNo,
'vendor_invoice_date': instance.vendorInvoiceDate?.toIso8601String(), 'vendor_invoice_date': instance.vendorInvoiceDate?.toIso8601String(),
'vendor_invoice_amount': instance.vendorInvoiceAmount, 'vendor_invoice_amount': instance.vendorInvoiceAmount,

View File

@ -227,6 +227,13 @@ final routerProvider = Provider<GoRouter>((ref) {
pageBuilder: (context, state) => pageBuilder: (context, state) =>
shellPage(state, const PurchaseOrderListScreen()), shellPage(state, const PurchaseOrderListScreen()),
routes: [ routes: [
GoRoute(
path: 'pending-approval',
pageBuilder: (context, state) => shellPage(
state,
const PurchaseOrderListScreen(pendingApprovalOnly: true),
),
),
GoRoute( GoRoute(
path: 'add', path: 'add',
pageBuilder: (context, state) => shellPage( pageBuilder: (context, state) => shellPage(

View File

@ -12,6 +12,7 @@ class MenuItem {
required this.module, required this.module,
this.children = const [], this.children = const [],
this.requiredRole, this.requiredRole,
this.requiredAction,
}); });
final String label; final String label;
@ -20,6 +21,9 @@ class MenuItem {
final String module; final String module;
final List<MenuItem> children; final List<MenuItem> children;
final UserRole? requiredRole; final UserRole? requiredRole;
/// When set, menu visibility requires this action instead of default view.
final PermissionAction? requiredAction;
} }
const List<MenuItem> appMenuItems = [ const List<MenuItem> appMenuItems = [
@ -79,6 +83,21 @@ const List<MenuItem> appMenuItems = [
icon: Icons.receipt_long_outlined, icon: Icons.receipt_long_outlined,
route: RouteConstants.purchaseOrders, route: RouteConstants.purchaseOrders,
module: 'purchase_orders', module: 'purchase_orders',
children: [
MenuItem(
label: 'All Orders',
icon: Icons.receipt_long_outlined,
route: RouteConstants.purchaseOrders,
module: 'purchase_orders',
),
MenuItem(
label: 'Pending Approval',
icon: Icons.pending_actions_outlined,
route: RouteConstants.purchaseOrdersPendingApproval,
module: 'purchase_orders',
requiredAction: PermissionAction.approve,
),
],
), ),
MenuItem( MenuItem(
label: 'GRN', label: 'GRN',
@ -145,9 +164,9 @@ List<MenuItem> getVisibleMenuItems({
if (item.children.isNotEmpty) { if (item.children.isNotEmpty) {
final visibleChildren = item.children final visibleChildren = item.children
.where( .where(
(child) => canSeeMenuModule( (child) => _canSeeMenuItem(
permissions: permissions, permissions: permissions,
module: child.module, item: child,
role: role, role: role,
), ),
) )
@ -161,16 +180,13 @@ List<MenuItem> getVisibleMenuItems({
module: item.module, module: item.module,
children: visibleChildren, children: visibleChildren,
requiredRole: item.requiredRole, requiredRole: item.requiredRole,
requiredAction: item.requiredAction,
), ),
); );
continue; continue;
} }
if (!canSeeMenuModule( if (!_canSeeMenuItem(permissions: permissions, item: item, role: role)) {
permissions: permissions,
module: item.module,
role: role,
)) {
continue; continue;
} }
@ -179,3 +195,24 @@ List<MenuItem> getVisibleMenuItems({
return visible; return visible;
} }
bool _canSeeMenuItem({
required List<String> permissions,
required MenuItem item,
required UserRole role,
}) {
if (item.requiredAction != null) {
if (permissions.contains('*')) return true;
return hasPermission(
userPermissions: permissions,
module: item.module,
action: item.requiredAction!,
);
}
return canSeeMenuModule(
permissions: permissions,
module: item.module,
role: role,
);
}

View File

@ -96,14 +96,31 @@ class _AppSidebarState extends ConsumerState<AppSidebar> {
return _expandedMenus.contains(item.route) || _isGroupActive(item); return _expandedMenus.contains(item.route) || _isGroupActive(item);
} }
Iterable<menu.MenuItem> get _allGroupItems =>
[..._primaryMenuItems, ..._supportMenuItems]
.where((item) => item.children.isNotEmpty);
/// Accordion: keep only [keepRoute] expanded; collapse every other group.
void _expandOnly(String keepRoute) {
_expandedMenus
..clear()
..add(keepRoute);
_manuallyCollapsedMenus
..clear()
..addAll(
_allGroupItems
.map((item) => item.route)
.where((route) => route != keepRoute),
);
}
void _toggleGroup(menu.MenuItem item) { void _toggleGroup(menu.MenuItem item) {
setState(() { setState(() {
if (_isGroupExpanded(item)) { if (_isGroupExpanded(item)) {
_expandedMenus.remove(item.route); _expandedMenus.remove(item.route);
_manuallyCollapsedMenus.add(item.route); _manuallyCollapsedMenus.add(item.route);
} else { } else {
_manuallyCollapsedMenus.remove(item.route); _expandOnly(item.route);
_expandedMenus.add(item.route);
} }
}); });
} }
@ -112,10 +129,10 @@ class _AppSidebarState extends ConsumerState<AppSidebar> {
void didUpdateWidget(covariant AppSidebar oldWidget) { void didUpdateWidget(covariant AppSidebar oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (oldWidget.currentRoute != widget.currentRoute) { if (oldWidget.currentRoute != widget.currentRoute) {
for (final item in [..._primaryMenuItems, ..._supportMenuItems]) { for (final item in _allGroupItems) {
if (item.children.isNotEmpty && _isGroupActive(item)) { if (_isGroupActive(item)) {
_expandedMenus.add(item.route); _expandOnly(item.route);
_manuallyCollapsedMenus.remove(item.route); break;
} }
} }
} }