po changes done
This commit is contained in:
parent
eb94390961
commit
6721b308a9
@ -65,6 +65,11 @@ class ApiEndpoints {
|
||||
static String deliveryTermById(String id) => '/masters/delivery-terms/$id';
|
||||
static const String paymentTerms = '/masters/payment-terms';
|
||||
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 String gstRateById(String id) => '/masters/gst-rates/$id';
|
||||
static const String hsnCodes = '/masters/hsn-codes';
|
||||
@ -95,6 +100,8 @@ class ApiEndpoints {
|
||||
|
||||
// Purchase Orders
|
||||
static const String purchaseOrders = '/purchase-orders';
|
||||
static const String purchaseOrdersPendingApproval =
|
||||
'/purchase-orders/pending-approval';
|
||||
static const String purchaseOrdersExport = '/purchase-orders/export';
|
||||
static String purchaseOrderById(String id) => '/purchase-orders/$id';
|
||||
static String purchaseOrderSubmit(String id) => '/purchase-orders/$id/submit';
|
||||
|
||||
@ -47,6 +47,8 @@ class RouteConstants {
|
||||
|
||||
// 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 purchaseOrderEdit = '/purchase-orders/:id/edit';
|
||||
static const String purchaseOrderDetail = '/purchase-orders/:id';
|
||||
|
||||
@ -444,7 +444,6 @@ class LogServiceVisitPanel extends ConsumerStatefulWidget {
|
||||
|
||||
class _LogServiceVisitPanelState extends ConsumerState<LogServiceVisitPanel> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _visitNumberController = TextEditingController();
|
||||
final _complaintNoController = TextEditingController();
|
||||
final _complaintDescController = TextEditingController();
|
||||
final _engineerNameController = TextEditingController();
|
||||
@ -468,7 +467,6 @@ class _LogServiceVisitPanelState extends ConsumerState<LogServiceVisitPanel> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_visitNumberController.dispose();
|
||||
_complaintNoController.dispose();
|
||||
_complaintDescController.dispose();
|
||||
_engineerNameController.dispose();
|
||||
@ -488,9 +486,6 @@ class _LogServiceVisitPanelState extends ConsumerState<LogServiceVisitPanel> {
|
||||
_visitType = visit.visitType;
|
||||
_visitDate = visit.visitDate;
|
||||
_amcContractId = visit.amcContractId;
|
||||
if (visit.visitNumber != null) {
|
||||
_visitNumberController.text = visit.visitNumber.toString();
|
||||
}
|
||||
_complaintNoController.text = visit.complaintNo ?? '';
|
||||
_complaintDate = visit.complaintDate;
|
||||
_complaintDescController.text = visit.complaintDesc ?? '';
|
||||
@ -529,14 +524,12 @@ class _LogServiceVisitPanelState extends ConsumerState<LogServiceVisitPanel> {
|
||||
}
|
||||
|
||||
Map<String, dynamic> _buildPayload() {
|
||||
final visitNumber = int.tryParse(_visitNumberController.text.trim());
|
||||
final downtimeHours = double.tryParse(_downtimeHoursController.text.trim());
|
||||
final serviceCost = double.tryParse(_serviceCostController.text.trim());
|
||||
return {
|
||||
if (_visitType != null && _visitType!.trim().isNotEmpty) 'visit_type': _visitType,
|
||||
'visit_date': DateFormatter.toApiDate(_visitDate!),
|
||||
if (_amcContractId != null) 'amc_contract_id': _amcContractId,
|
||||
if (visitNumber != null) 'visit_number': visitNumber,
|
||||
if (_complaintNoController.text.trim().isNotEmpty)
|
||||
'complaint_no': _complaintNoController.text.trim(),
|
||||
if (_complaintDate != null)
|
||||
@ -654,24 +647,13 @@ class _LogServiceVisitPanelState extends ConsumerState<LogServiceVisitPanel> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SidePanelFormRow(
|
||||
left: _SidePanelDateField(
|
||||
label: 'Visit Date',
|
||||
isRequired: true,
|
||||
value: _visitDate,
|
||||
onPick: () => _pickDate(
|
||||
current: _visitDate,
|
||||
onPicked: (date) => setState(() => _visitDate = date),
|
||||
),
|
||||
),
|
||||
right: AppTextField(
|
||||
controller: _visitNumberController,
|
||||
label: 'Visit Number',
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (v) => Validators.optionalPositiveInt(
|
||||
v,
|
||||
fieldName: 'Visit Number',
|
||||
),
|
||||
_SidePanelDateField(
|
||||
label: 'Visit Date',
|
||||
isRequired: true,
|
||||
value: _visitDate,
|
||||
onPick: () => _pickDate(
|
||||
current: _visitDate,
|
||||
onPicked: (date) => setState(() => _visitDate = date),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
@ -150,7 +150,7 @@ class GrnRemoteDataSource {
|
||||
if (query.status != null) 'status': query.status,
|
||||
if (query.poId != null) 'po_id': query.poId,
|
||||
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.dateTo != null) 'date_to': query.dateTo,
|
||||
};
|
||||
|
||||
@ -9,12 +9,13 @@ import '../../../users/data/repositories/user_repository_impl.dart';
|
||||
|
||||
class GrnLookups {
|
||||
const GrnLookups({
|
||||
this.warehouses = const [],
|
||||
this.locations = const [],
|
||||
this.receivablePurchaseOrders = 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<FilterOptionModel> users;
|
||||
}
|
||||
@ -23,12 +24,7 @@ final grnLookupsProvider = FutureProvider.autoDispose<GrnLookups>((ref) async {
|
||||
final master = ref.watch(masterRemoteDataSourceProvider);
|
||||
final poRepo = ref.watch(purchaseOrderRepositoryProvider);
|
||||
|
||||
final warehouses = await _safeOptions(() async {
|
||||
final locations = await master.listLocations();
|
||||
return locations
|
||||
.where((location) => location.slug?.toLowerCase() == 'warehouse')
|
||||
.toList();
|
||||
});
|
||||
final locations = await _safeOptions(master.listLocations);
|
||||
final users = await _safeUserOptions(ref);
|
||||
|
||||
final receivablePos = <PurchaseOrderModel>[];
|
||||
@ -40,7 +36,7 @@ final grnLookupsProvider = FutureProvider.autoDispose<GrnLookups>((ref) async {
|
||||
}
|
||||
|
||||
return GrnLookups(
|
||||
warehouses: warehouses,
|
||||
locations: locations,
|
||||
receivablePurchaseOrders: receivablePos,
|
||||
users: users,
|
||||
);
|
||||
|
||||
@ -109,6 +109,12 @@ class GrnListNotifier extends AutoDisposeAsyncNotifier<GrnListState> {
|
||||
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) {
|
||||
final current = state.valueOrNull;
|
||||
if (current == null) return;
|
||||
|
||||
@ -240,8 +240,8 @@ class _DetailHeader extends StatelessWidget {
|
||||
final subtitleParts = [
|
||||
if (grn.poNumber?.trim().isNotEmpty == true) 'PO ${grn.poNumber!.trim()}',
|
||||
if (grn.vendorName?.trim().isNotEmpty == true) grn.vendorName!.trim(),
|
||||
if (grn.warehouseName?.trim().isNotEmpty == true)
|
||||
grn.warehouseName!.trim(),
|
||||
if (grn.locationName?.trim().isNotEmpty == true)
|
||||
grn.locationName!.trim(),
|
||||
];
|
||||
|
||||
final actions = Wrap(
|
||||
@ -533,8 +533,15 @@ class _ReceiptDetailsCard extends StatelessWidget {
|
||||
value: _displayOrDash(grn.vendorName),
|
||||
),
|
||||
_DetailField(
|
||||
label: 'Warehouse',
|
||||
value: _displayOrDash(grn.warehouseName),
|
||||
label: 'Location',
|
||||
value: _displayOrDash(
|
||||
[
|
||||
if (grn.locationName?.trim().isNotEmpty == true)
|
||||
grn.locationName!.trim(),
|
||||
if (grn.locationType?.trim().isNotEmpty == true)
|
||||
'(${grn.locationType!.trim()})',
|
||||
].join(' ').trim(),
|
||||
),
|
||||
),
|
||||
_DetailField(
|
||||
label: 'Vendor Invoice No',
|
||||
|
||||
@ -20,7 +20,6 @@ import '../../../../shared/widgets/app_searchable_dropdown.dart';
|
||||
import '../../../../shared/widgets/app_side_panel.dart';
|
||||
import '../../../../shared/widgets/app_text_field.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_provider.dart';
|
||||
import '../widgets/grn_line_items_editor.dart';
|
||||
@ -51,7 +50,7 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
||||
DateTime? _vendorInvoiceDate;
|
||||
DateTime? _lrDate;
|
||||
String? _selectedPoId;
|
||||
int? _warehouseId;
|
||||
String? _locationLabel;
|
||||
int? _receivedById;
|
||||
int? _qualityCheckedById;
|
||||
final List<GrnLineItemDraft> _lines = [];
|
||||
@ -90,7 +89,10 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
||||
setState(() {
|
||||
_grnDate = grn.grnDate ?? DateTime.now();
|
||||
_selectedPoId = grn.poId?.toString();
|
||||
_warehouseId = grn.warehouseId;
|
||||
_locationLabel = _formatLocationLabel(
|
||||
name: grn.locationName,
|
||||
type: grn.locationType,
|
||||
);
|
||||
_vendorInvoiceNoController.text = grn.vendorInvoiceNo ?? '';
|
||||
_vendorInvoiceDate = grn.vendorInvoiceDate;
|
||||
_vendorInvoiceAmountController.text =
|
||||
@ -112,12 +114,23 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
||||
_lines
|
||||
..clear()
|
||||
..addAll(draftsFromPurchaseOrder(po));
|
||||
if (_warehouseId == null && po.shippingId != null) {
|
||||
_warehouseId = po.shippingId;
|
||||
}
|
||||
_locationLabel = _formatLocationLabel(
|
||||
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? _normalizeUserId(int? id) => id != null && id > 0 ? id : null;
|
||||
@ -150,7 +163,6 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
||||
final payload = <String, dynamic>{
|
||||
'grn_date': DateFormatter.toApiDate(_grnDate ?? DateTime.now()),
|
||||
'po_id': poId,
|
||||
'warehouse_id': _warehouseId,
|
||||
if (_vendorInvoiceNoController.text.trim().isNotEmpty)
|
||||
'vendor_invoice_no': _vendorInvoiceNoController.text.trim(),
|
||||
if (_vendorInvoiceDate != null)
|
||||
@ -236,9 +248,9 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
||||
}
|
||||
|
||||
if (!widget.isEditing) {
|
||||
if (_selectedPoId == null || _warehouseId == null) {
|
||||
if (_selectedPoId == null) {
|
||||
showAppToastFromSnackBar(context,
|
||||
const SnackBar(content: Text('Please select PO and warehouse')),
|
||||
const SnackBar(content: Text('Please select a purchase order')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@ -371,8 +383,6 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
final warehouseIds =
|
||||
lookups.warehouses.map((e) => _parseId(e.id)).whereType<int>();
|
||||
final userIds =
|
||||
lookups.users.map((e) => _parseId(e.id)).whereType<int>();
|
||||
final poOptions = lookups.receivablePurchaseOrders
|
||||
@ -421,7 +431,10 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
||||
searchHint: 'Search PO...',
|
||||
options: poOptions,
|
||||
onChanged: (v) async {
|
||||
setState(() => _selectedPoId = v);
|
||||
setState(() {
|
||||
_selectedPoId = v;
|
||||
if (v == null) _locationLabel = null;
|
||||
});
|
||||
if (v == null) {
|
||||
for (final line in _lines) {
|
||||
line.dispose();
|
||||
@ -452,25 +465,17 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
||||
label: 'Purchase Order',
|
||||
value: existing?.poNumber ?? '—',
|
||||
),
|
||||
MasterQuickAddDropdown<int>(
|
||||
masterId: 'locations',
|
||||
label: 'Warehouse *',
|
||||
value: _dropdownValue(_warehouseId, warehouseIds),
|
||||
hint: 'Select warehouse',
|
||||
searchHint: 'Search warehouse...',
|
||||
options: _intOptions(lookups.warehouses),
|
||||
initialValues: const {'type': 'warehouse'},
|
||||
refreshLookups: () =>
|
||||
ref.invalidate(grnLookupsProvider),
|
||||
parseCreatedId: int.tryParse,
|
||||
onChanged: widget.isEditing
|
||||
? (_) {}
|
||||
: (v) => setState(() => _warehouseId = v),
|
||||
validator: widget.isEditing
|
||||
_ReadOnlyField(
|
||||
label: 'Location',
|
||||
value: _locationLabel ??
|
||||
_formatLocationLabel(
|
||||
name: existing?.locationName,
|
||||
type: existing?.locationType,
|
||||
) ??
|
||||
'—',
|
||||
hint: widget.isEditing
|
||||
? null
|
||||
: (v) =>
|
||||
v == null ? 'Warehouse is required' : null,
|
||||
enabled: !widget.isEditing,
|
||||
: 'From PO shipping location',
|
||||
),
|
||||
AppTextField(
|
||||
label: 'Vendor Invoice No',
|
||||
@ -768,21 +773,32 @@ class _ReadOnlyField extends StatelessWidget {
|
||||
const _ReadOnlyField({
|
||||
required this.label,
|
||||
required this.value,
|
||||
this.hint,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final String value;
|
||||
final String? hint;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: InputDecorator(
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
helperText: hint,
|
||||
),
|
||||
child: Text(
|
||||
value,
|
||||
style: value == '—'
|
||||
? theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: Text(value),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import '../../../../core/errors/failure.dart';
|
||||
import '../../../../core/utils/formatters.dart';
|
||||
import '../../../../core/utils/responsive_utils.dart';
|
||||
import '../../../../shared/models/grn_model.dart';
|
||||
import '../../../../shared/models/user_management_models.dart';
|
||||
import '../../../../shared/providers/permissions_provider.dart';
|
||||
import '../../../../shared/widgets/app_card.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/page_header.dart';
|
||||
import '../../../../shared/utils/file_download_helper.dart';
|
||||
import '../providers/grn_lookups_provider.dart';
|
||||
import '../providers/grn_provider.dart';
|
||||
import '../widgets/grn_status_chip.dart';
|
||||
import '../../../../shared/widgets/app_toast.dart';
|
||||
@ -46,6 +48,7 @@ class _GrnListScreenState extends ConsumerState<GrnListScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final listAsync = ref.watch(grnListProvider);
|
||||
final lookupsAsync = ref.watch(grnLookupsProvider);
|
||||
final canEdit = ref.can('grn', PermissionAction.update);
|
||||
final canExport = ref.can('grn', PermissionAction.export);
|
||||
|
||||
@ -87,12 +90,15 @@ class _GrnListScreenState extends ConsumerState<GrnListScreen> {
|
||||
toolbar: _FiltersBar(
|
||||
searchController: _searchController,
|
||||
query: state.query,
|
||||
locationOptions: lookupsAsync.valueOrNull?.locations ?? const [],
|
||||
showExport: canExport,
|
||||
isExporting: state.isExporting,
|
||||
onExport: _exportGrns,
|
||||
onSearch: ref.read(grnListProvider.notifier).setSearch,
|
||||
onStatusChanged:
|
||||
ref.read(grnListProvider.notifier).setStatusFilter,
|
||||
onLocationChanged:
|
||||
ref.read(grnListProvider.notifier).setLocationFilter,
|
||||
),
|
||||
footer: AppPagination(
|
||||
currentPage: state.query.page,
|
||||
@ -187,8 +193,10 @@ class _FiltersBar extends StatelessWidget {
|
||||
const _FiltersBar({
|
||||
required this.searchController,
|
||||
required this.query,
|
||||
required this.locationOptions,
|
||||
required this.onSearch,
|
||||
required this.onStatusChanged,
|
||||
required this.onLocationChanged,
|
||||
this.showExport = false,
|
||||
this.isExporting = false,
|
||||
this.onExport,
|
||||
@ -196,8 +204,10 @@ class _FiltersBar extends StatelessWidget {
|
||||
|
||||
final TextEditingController searchController;
|
||||
final GrnListQuery query;
|
||||
final List<FilterOptionModel> locationOptions;
|
||||
final ValueChanged<String> onSearch;
|
||||
final ValueChanged<String?> onStatusChanged;
|
||||
final ValueChanged<int?> onLocationChanged;
|
||||
final bool showExport;
|
||||
final bool isExporting;
|
||||
final VoidCallback? onExport;
|
||||
@ -233,6 +243,21 @@ class _FiltersBar extends StatelessWidget {
|
||||
],
|
||||
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(
|
||||
@ -295,6 +320,12 @@ class _GrnDataTable extends StatelessWidget {
|
||||
searchText: (grn) => grn.vendorName ?? '',
|
||||
cellBuilder: (_, grn) => Text(grn.vendorName ?? '—'),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Location',
|
||||
flex: 2,
|
||||
searchText: (grn) => grn.locationName ?? '',
|
||||
cellBuilder: (_, grn) => Text(grn.locationName ?? '—'),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Status',
|
||||
flex: 1,
|
||||
@ -371,6 +402,7 @@ class _GrnCardList extends StatelessWidget {
|
||||
const SizedBox(height: 8),
|
||||
Text('PO: ${grn.poNumber ?? '—'}'),
|
||||
Text('Vendor: ${grn.vendorName ?? '—'}'),
|
||||
Text('Location: ${grn.locationName ?? '—'}'),
|
||||
Text('Date: ${DateFormatter.displayDate(grn.grnDate)}'),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
|
||||
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
enum MasterFieldType { text, number, boolean, dropdown }
|
||||
|
||||
const categoryTypeOptions = ['STOCK', 'ASSET'];
|
||||
const termsNotesTypeOptions = ['PO', 'INVOICE'];
|
||||
|
||||
class MasterFieldDef {
|
||||
const MasterFieldDef({
|
||||
@ -465,6 +466,34 @@ const masterDefinitions = <MasterDefinition>[
|
||||
_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(
|
||||
id: 'gst_rates',
|
||||
title: 'GST Rates',
|
||||
|
||||
@ -231,6 +231,7 @@ String masterQuickAddNoun(String masterId) {
|
||||
'departments' => 'department',
|
||||
'delivery_terms' => 'delivery term',
|
||||
'payment_terms' => 'payment term',
|
||||
'terms_notes' => 'terms note',
|
||||
'gst_rates' => 'GST rate',
|
||||
'document_series' => 'document series',
|
||||
_ => masterId.replaceAll('_', ' '),
|
||||
|
||||
@ -103,6 +103,40 @@ class MasterRemoteDataSource {
|
||||
Future<List<FilterOptionModel>> listDeliveryTerms() =>
|
||||
_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() =>
|
||||
_listOptions(ApiEndpoints.warehouses);
|
||||
|
||||
|
||||
@ -22,6 +22,17 @@ class PurchaseOrderRemoteDataSource {
|
||||
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.
|
||||
Future<List<PurchaseOrderModel>> listPurchaseOrderOptions({
|
||||
String? status,
|
||||
|
||||
@ -32,6 +32,14 @@ class PurchaseOrderRepositoryImpl implements PurchaseOrderRepository {
|
||||
return safeApiCall(() => dataSource.getPurchaseOrders(query));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<PaginatedResponse<PurchaseOrderModel>>>
|
||||
getPendingApprovalPurchaseOrders(PurchaseOrderListQuery query) {
|
||||
return safeApiCall(
|
||||
() => dataSource.getPendingApprovalPurchaseOrders(query),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<List<PurchaseOrderModel>>> listPurchaseOrderOptions({
|
||||
String? status,
|
||||
|
||||
@ -8,6 +8,8 @@ abstract class PurchaseOrderRepository {
|
||||
Future<Result<PaginatedResponse<PurchaseOrderModel>>> getPurchaseOrders(
|
||||
PurchaseOrderListQuery query,
|
||||
);
|
||||
Future<Result<PaginatedResponse<PurchaseOrderModel>>>
|
||||
getPendingApprovalPurchaseOrders(PurchaseOrderListQuery query);
|
||||
Future<Result<List<PurchaseOrderModel>>> listPurchaseOrderOptions({
|
||||
String? status,
|
||||
});
|
||||
|
||||
@ -6,6 +6,17 @@ import '../../../masters/data/datasources/master_remote_data_source.dart';
|
||||
import '../../../vendors/data/repositories/vendor_repository_impl.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 {
|
||||
const PurchaseOrderLookups({
|
||||
this.vendors = const [],
|
||||
|
||||
@ -58,6 +58,12 @@ final purchaseOrdersListProvider = AsyncNotifierProvider.autoDispose<
|
||||
PurchaseOrdersListNotifier.new,
|
||||
);
|
||||
|
||||
final pendingApprovalPurchaseOrdersListProvider =
|
||||
AsyncNotifierProvider.autoDispose<PendingApprovalPurchaseOrdersListNotifier,
|
||||
PurchaseOrdersListState>(
|
||||
PendingApprovalPurchaseOrdersListNotifier.new,
|
||||
);
|
||||
|
||||
class PurchaseOrdersListNotifier
|
||||
extends AutoDisposeAsyncNotifier<PurchaseOrdersListState> {
|
||||
@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<
|
||||
PurchaseOrderDetailNotifier, PurchaseOrderModel, String>(
|
||||
PurchaseOrderDetailNotifier.new,
|
||||
@ -195,6 +284,7 @@ class PurchaseOrderDetailNotifier
|
||||
if (result.failure != null) throw result.failure!;
|
||||
state = AsyncData(result.data!);
|
||||
ref.invalidate(purchaseOrdersListProvider);
|
||||
ref.invalidate(pendingApprovalPurchaseOrdersListProvider);
|
||||
ref.invalidate(grnLookupsProvider);
|
||||
return result.data!;
|
||||
}
|
||||
@ -205,6 +295,7 @@ class PurchaseOrderDetailNotifier
|
||||
if (result.failure != null) throw result.failure!;
|
||||
state = AsyncData(result.data!);
|
||||
ref.invalidate(purchaseOrdersListProvider);
|
||||
ref.invalidate(pendingApprovalPurchaseOrdersListProvider);
|
||||
ref.invalidate(grnLookupsProvider);
|
||||
return result.data!;
|
||||
}
|
||||
@ -215,6 +306,7 @@ class PurchaseOrderDetailNotifier
|
||||
if (result.failure != null) throw result.failure!;
|
||||
state = AsyncData(result.data!);
|
||||
ref.invalidate(purchaseOrdersListProvider);
|
||||
ref.invalidate(pendingApprovalPurchaseOrdersListProvider);
|
||||
ref.invalidate(grnLookupsProvider);
|
||||
return result.data!;
|
||||
}
|
||||
@ -224,6 +316,7 @@ class PurchaseOrderDetailNotifier
|
||||
final result = await repository.amendPurchaseOrder(arg);
|
||||
if (result.failure != null) throw result.failure!;
|
||||
ref.invalidate(purchaseOrdersListProvider);
|
||||
ref.invalidate(pendingApprovalPurchaseOrdersListProvider);
|
||||
ref.invalidate(grnLookupsProvider);
|
||||
return result.data!;
|
||||
}
|
||||
@ -234,6 +327,7 @@ class PurchaseOrderDetailNotifier
|
||||
if (result.failure != null) throw result.failure!;
|
||||
state = AsyncData(result.data!);
|
||||
ref.invalidate(purchaseOrdersListProvider);
|
||||
ref.invalidate(pendingApprovalPurchaseOrdersListProvider);
|
||||
ref.invalidate(grnLookupsProvider);
|
||||
return result.data!;
|
||||
}
|
||||
@ -315,6 +409,7 @@ class PurchaseOrderFormNotifier extends FamilyAsyncNotifier<PurchaseOrderModel?,
|
||||
final result = await repository.createPurchaseOrder(data);
|
||||
if (result.failure != null) throw result.failure!;
|
||||
ref.invalidate(purchaseOrdersListProvider);
|
||||
ref.invalidate(pendingApprovalPurchaseOrdersListProvider);
|
||||
ref.invalidate(grnLookupsProvider);
|
||||
return result.data!;
|
||||
}
|
||||
@ -324,6 +419,7 @@ class PurchaseOrderFormNotifier extends FamilyAsyncNotifier<PurchaseOrderModel?,
|
||||
final result = await repository.updatePurchaseOrder(id, data);
|
||||
if (result.failure != null) throw result.failure!;
|
||||
ref.invalidate(purchaseOrdersListProvider);
|
||||
ref.invalidate(pendingApprovalPurchaseOrdersListProvider);
|
||||
ref.invalidate(purchaseOrderDetailProvider(id));
|
||||
ref.invalidate(grnLookupsProvider);
|
||||
return result.data!;
|
||||
|
||||
@ -65,73 +65,68 @@ class _PurchaseOrderDetailScreenState
|
||||
final lookups = lookupsAsync.asData?.value;
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(24, 16, 24, 32),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 1200),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_DetailHeader(
|
||||
order: order,
|
||||
isWorking: _isWorking,
|
||||
isDownloadingPdf: _isDownloadingPdf,
|
||||
canEdit: canEdit,
|
||||
canDelete: canDelete,
|
||||
canApprove: canApprove,
|
||||
canExport: canExport,
|
||||
onBack: () => context.go(RouteConstants.purchaseOrders),
|
||||
onPdf: () => _downloadPdf(order),
|
||||
onEdit: () => context.push(
|
||||
'${RouteConstants.purchaseOrders}/${order.id}/edit',
|
||||
),
|
||||
onSubmit: () => _submit(order),
|
||||
onApprove: () => _approve(order),
|
||||
onReject: () => _reject(order),
|
||||
onAmend: () => _amend(order),
|
||||
onCancel: () => _cancel(order),
|
||||
onDelete: _delete,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_OrderDetailsCard(order: order, lookups: lookups),
|
||||
const SizedBox(height: 16),
|
||||
_LineItemsCard(order: order, lookups: lookups),
|
||||
const SizedBox(height: 16),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final stack = constraints.maxWidth < 900;
|
||||
final terms = _TermsRemarksColumn(order: order);
|
||||
final summary = _AmountSummaryCard(order: order);
|
||||
if (stack) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
terms,
|
||||
const SizedBox(height: 16),
|
||||
summary,
|
||||
],
|
||||
);
|
||||
}
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(flex: 3, child: terms),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(flex: 2, child: summary),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_PoAttachmentsSection(
|
||||
poId: order.id,
|
||||
canUpload: canEdit,
|
||||
canDelete: canDelete,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_DetailFooter(order: order),
|
||||
],
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_DetailHeader(
|
||||
order: order,
|
||||
isWorking: _isWorking,
|
||||
isDownloadingPdf: _isDownloadingPdf,
|
||||
canEdit: canEdit,
|
||||
canDelete: canDelete,
|
||||
canApprove: canApprove,
|
||||
canExport: canExport,
|
||||
onBack: () => context.go(RouteConstants.purchaseOrders),
|
||||
onPdf: () => _downloadPdf(order),
|
||||
onEdit: () => context.push(
|
||||
'${RouteConstants.purchaseOrders}/${order.id}/edit',
|
||||
),
|
||||
onSubmit: () => _submit(order),
|
||||
onApprove: () => _approve(order),
|
||||
onReject: () => _reject(order),
|
||||
onAmend: () => _amend(order),
|
||||
onCancel: () => _cancel(order),
|
||||
onDelete: _delete,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_OrderDetailsCard(order: order, lookups: lookups),
|
||||
const SizedBox(height: 16),
|
||||
_LineItemsCard(order: order, lookups: lookups),
|
||||
const SizedBox(height: 16),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final stack = constraints.maxWidth < 900;
|
||||
final terms = _TermsRemarksColumn(order: order);
|
||||
final summary = _AmountSummaryCard(order: order);
|
||||
if (stack) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
terms,
|
||||
const SizedBox(height: 16),
|
||||
summary,
|
||||
],
|
||||
);
|
||||
}
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(flex: 3, child: terms),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(flex: 2, child: summary),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_PoAttachmentsSection(
|
||||
poId: order.id,
|
||||
canUpload: canEdit,
|
||||
canDelete: canDelete,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_DetailFooter(order: order),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@ -58,6 +58,7 @@ class _PurchaseOrderFormScreenState extends ConsumerState<PurchaseOrderFormScree
|
||||
final List<PoLineItemDraft> _lines = [];
|
||||
bool _isSubmitting = false;
|
||||
String? _populatedSignature;
|
||||
bool _defaultTermsApplied = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -506,8 +507,9 @@ class _PurchaseOrderFormScreenState extends ConsumerState<PurchaseOrderFormScree
|
||||
AppTextField(
|
||||
controller: _termsController,
|
||||
label: 'Terms & Conditions',
|
||||
hint:
|
||||
'Payment terms, inspection conditions, etc.',
|
||||
hint: widget.isEditing
|
||||
? 'Payment terms, inspection conditions, etc.'
|
||||
: 'Defaults from Terms & Notes master when empty',
|
||||
maxLines: 5,
|
||||
),
|
||||
AppTextField(
|
||||
@ -685,6 +687,17 @@ class _PurchaseOrderFormScreenState extends ConsumerState<PurchaseOrderFormScree
|
||||
Widget build(BuildContext context) {
|
||||
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(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
body: lookupsAsync.when(
|
||||
|
||||
@ -31,7 +31,12 @@ import '../widgets/po_status_chip.dart';
|
||||
import '../../../../shared/widgets/app_toast.dart';
|
||||
|
||||
class PurchaseOrderListScreen extends ConsumerStatefulWidget {
|
||||
const PurchaseOrderListScreen({super.key});
|
||||
const PurchaseOrderListScreen({
|
||||
super.key,
|
||||
this.pendingApprovalOnly = false,
|
||||
});
|
||||
|
||||
final bool pendingApprovalOnly;
|
||||
|
||||
@override
|
||||
ConsumerState<PurchaseOrderListScreen> createState() =>
|
||||
@ -59,12 +64,23 @@ class _PurchaseOrderListScreenState extends ConsumerState<PurchaseOrderListScree
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ordersAsync = ref.watch(purchaseOrdersListProvider);
|
||||
final canEdit = ref.can('purchase_orders', PermissionAction.update);
|
||||
final canDelete = ref.can('purchase_orders', PermissionAction.delete);
|
||||
final canExport = ref.can('purchase_orders', PermissionAction.export);
|
||||
final pendingOnly = widget.pendingApprovalOnly;
|
||||
final ordersAsync = pendingOnly
|
||||
? ref.watch(pendingApprovalPurchaseOrdersListProvider)
|
||||
: 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 success = next.valueOrNull?.actionSuccess;
|
||||
if (error != null && error != prev?.valueOrNull?.actionError) {
|
||||
@ -73,15 +89,27 @@ class _PurchaseOrderListScreenState extends ConsumerState<PurchaseOrderListScree
|
||||
if (success != null && success != prev?.valueOrNull?.actionSuccess) {
|
||||
showAppToastFromSnackBar(context, SnackBar(content: Text(success)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (pendingOnly) {
|
||||
ref.listen(pendingApprovalPurchaseOrdersListProvider, listenListMessages);
|
||||
} else {
|
||||
ref.listen(purchaseOrdersListProvider, listenListMessages);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
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 is Failure ? error : Failure.unknown(message: error.toString()),
|
||||
onRetry: () => ref.invalidate(purchaseOrdersListProvider),
|
||||
onRetry: () => pendingOnly
|
||||
? ref.invalidate(pendingApprovalPurchaseOrdersListProvider)
|
||||
: ref.invalidate(purchaseOrdersListProvider),
|
||||
),
|
||||
data: (state) {
|
||||
_rememberStatuses(state.orders);
|
||||
@ -89,18 +117,22 @@ class _PurchaseOrderListScreenState extends ConsumerState<PurchaseOrderListScree
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
PageHeader(
|
||||
title: 'Purchase Orders',
|
||||
subtitle: 'Create, approve and track procurement orders',
|
||||
title: pendingOnly ? 'Pending Approval' : 'Purchase Orders',
|
||||
subtitle: pendingOnly
|
||||
? 'Purchase orders awaiting approval'
|
||||
: 'Create, approve and track procurement orders',
|
||||
actions: [
|
||||
CanPermission(
|
||||
module: 'purchase_orders',
|
||||
action: PermissionAction.create,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => context.go(RouteConstants.purchaseOrderAdd),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Create PO'),
|
||||
if (!pendingOnly)
|
||||
CanPermission(
|
||||
module: 'purchase_orders',
|
||||
action: PermissionAction.create,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () =>
|
||||
context.go(RouteConstants.purchaseOrderAdd),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Create PO'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
@ -109,38 +141,72 @@ class _PurchaseOrderListScreenState extends ConsumerState<PurchaseOrderListScree
|
||||
searchController: _searchController,
|
||||
query: state.query,
|
||||
statusOptions: _statusFilterOptions(state.orders),
|
||||
showStatusFilter: !pendingOnly,
|
||||
showExport: canExport,
|
||||
isExporting: state.isExporting,
|
||||
onExport: _exportPurchaseOrders,
|
||||
onSearch: ref.read(purchaseOrdersListProvider.notifier).setSearch,
|
||||
onStatusChanged:
|
||||
ref.read(purchaseOrdersListProvider.notifier).setStatusFilter,
|
||||
onSearch: pendingOnly
|
||||
? ref
|
||||
.read(
|
||||
pendingApprovalPurchaseOrdersListProvider.notifier,
|
||||
)
|
||||
.setSearch
|
||||
: ref.read(purchaseOrdersListProvider.notifier).setSearch,
|
||||
onStatusChanged: pendingOnly
|
||||
? (_) {}
|
||||
: ref
|
||||
.read(purchaseOrdersListProvider.notifier)
|
||||
.setStatusFilter,
|
||||
),
|
||||
footer: AppPagination(
|
||||
currentPage: state.query.page,
|
||||
totalPages: state.totalPages,
|
||||
totalItems: state.total,
|
||||
pageSize: state.query.limit,
|
||||
itemLabel: 'purchase orders',
|
||||
onPageChanged:
|
||||
ref.read(purchaseOrdersListProvider.notifier).setPage,
|
||||
onPageSizeChanged:
|
||||
ref.read(purchaseOrdersListProvider.notifier).setPageSize,
|
||||
itemLabel: pendingOnly
|
||||
? 'pending approvals'
|
||||
: 'purchase orders',
|
||||
onPageChanged: pendingOnly
|
||||
? ref
|
||||
.read(
|
||||
pendingApprovalPurchaseOrdersListProvider.notifier,
|
||||
)
|
||||
.setPage
|
||||
: ref.read(purchaseOrdersListProvider.notifier).setPage,
|
||||
onPageSizeChanged: pendingOnly
|
||||
? ref
|
||||
.read(
|
||||
pendingApprovalPurchaseOrdersListProvider.notifier,
|
||||
)
|
||||
.setPageSize
|
||||
: ref
|
||||
.read(purchaseOrdersListProvider.notifier)
|
||||
.setPageSize,
|
||||
),
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () =>
|
||||
ref.read(purchaseOrdersListProvider.notifier).refresh(),
|
||||
onRefresh: () => pendingOnly
|
||||
? ref
|
||||
.read(
|
||||
pendingApprovalPurchaseOrdersListProvider.notifier,
|
||||
)
|
||||
.refresh()
|
||||
: ref.read(purchaseOrdersListProvider.notifier).refresh(),
|
||||
child: state.orders.isEmpty
|
||||
? ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: const [
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 240,
|
||||
child: AppEmptyState(
|
||||
title: 'No purchase orders found',
|
||||
description:
|
||||
'Try adjusting filters or create a new purchase order.',
|
||||
icon: Icons.receipt_long_outlined,
|
||||
title: pendingOnly
|
||||
? 'No pending approvals'
|
||||
: 'No purchase orders found',
|
||||
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,
|
||||
onEdit: canEdit ? _editOrder : null,
|
||||
onDelete: canDelete ? _deleteOrder : null,
|
||||
onApprove: canApprove ? _approveOrder : null,
|
||||
)
|
||||
: _PoDataTable(
|
||||
orders: state.orders,
|
||||
onView: _viewOrder,
|
||||
onEdit: canEdit ? _editOrder : 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 {
|
||||
if (!order.canDelete) {
|
||||
showAppToastFromSnackBar(context,
|
||||
@ -272,6 +360,7 @@ class _FiltersBar extends StatelessWidget {
|
||||
required this.statusOptions,
|
||||
required this.onSearch,
|
||||
required this.onStatusChanged,
|
||||
this.showStatusFilter = true,
|
||||
this.showExport = false,
|
||||
this.isExporting = false,
|
||||
this.onExport,
|
||||
@ -282,6 +371,7 @@ class _FiltersBar extends StatelessWidget {
|
||||
final List<AppDropdownOption<String?>> statusOptions;
|
||||
final ValueChanged<String> onSearch;
|
||||
final ValueChanged<String?> onStatusChanged;
|
||||
final bool showStatusFilter;
|
||||
final bool showExport;
|
||||
final bool isExporting;
|
||||
final VoidCallback? onExport;
|
||||
@ -294,15 +384,16 @@ class _FiltersBar extends StatelessWidget {
|
||||
onChanged: onSearch,
|
||||
);
|
||||
|
||||
final filters = [
|
||||
AppSearchableDropdown<String?>(
|
||||
label: 'Status',
|
||||
value: query.status,
|
||||
searchHint: 'Search status...',
|
||||
isDense: true,
|
||||
options: statusOptions,
|
||||
onChanged: onStatusChanged,
|
||||
),
|
||||
final filters = <Widget>[
|
||||
if (showStatusFilter)
|
||||
AppSearchableDropdown<String?>(
|
||||
label: 'Status',
|
||||
value: query.status,
|
||||
searchHint: 'Search status...',
|
||||
isDense: true,
|
||||
options: statusOptions,
|
||||
onChanged: onStatusChanged,
|
||||
),
|
||||
];
|
||||
|
||||
return AppResponsiveFilterBar(
|
||||
@ -331,15 +422,18 @@ class _PoDataTable extends StatelessWidget {
|
||||
required this.onView,
|
||||
this.onEdit,
|
||||
this.onDelete,
|
||||
this.onApprove,
|
||||
});
|
||||
|
||||
final List<PurchaseOrderModel> orders;
|
||||
final ValueChanged<PurchaseOrderModel> onView;
|
||||
final ValueChanged<PurchaseOrderModel>? onEdit;
|
||||
final ValueChanged<PurchaseOrderModel>? onDelete;
|
||||
final ValueChanged<PurchaseOrderModel>? onApprove;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return AppDataTable<PurchaseOrderModel>(
|
||||
wrapInCard: false,
|
||||
columns: [
|
||||
@ -385,7 +479,7 @@ class _PoDataTable extends StatelessWidget {
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Actions',
|
||||
flex: 1,
|
||||
flex: onApprove != null ? 2 : 1,
|
||||
alignment: Alignment.centerRight,
|
||||
enableSearch: false,
|
||||
cellBuilder: (_, order) => AppTableActions(
|
||||
@ -395,6 +489,13 @@ class _PoDataTable extends StatelessWidget {
|
||||
icon: Icons.visibility_outlined,
|
||||
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)
|
||||
AppTableActionIcon(
|
||||
tooltip: 'Edit',
|
||||
@ -422,12 +523,14 @@ class _PoCardList extends StatelessWidget {
|
||||
required this.onView,
|
||||
this.onEdit,
|
||||
this.onDelete,
|
||||
this.onApprove,
|
||||
});
|
||||
|
||||
final List<PurchaseOrderModel> orders;
|
||||
final ValueChanged<PurchaseOrderModel> onView;
|
||||
final ValueChanged<PurchaseOrderModel>? onEdit;
|
||||
final ValueChanged<PurchaseOrderModel>? onDelete;
|
||||
final ValueChanged<PurchaseOrderModel>? onApprove;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -442,7 +545,23 @@ class _PoCardList extends StatelessWidget {
|
||||
subtitle: Text(
|
||||
'${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),
|
||||
),
|
||||
);
|
||||
|
||||
@ -578,9 +578,11 @@ class _BankDetailsTab extends ConsumerWidget {
|
||||
alignment: Alignment.centerRight,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
final hasPrimary = bankDetails.any((b) => b.isPrimary && b.isActive);
|
||||
final saved = await openVendorBankDetailPanel(
|
||||
context,
|
||||
vendorId: vendorId,
|
||||
hasPrimaryAccount: hasPrimary,
|
||||
);
|
||||
if (saved == true && context.mounted) {
|
||||
showAppToastFromSnackBar(context,
|
||||
@ -600,39 +602,103 @@ class _BankDetailsTab extends ConsumerWidget {
|
||||
description: 'Add bank accounts for payments.',
|
||||
icon: Icons.account_balance_outlined,
|
||||
)
|
||||
: GridView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 320,
|
||||
mainAxisExtent: 175,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
),
|
||||
itemCount: bankDetails.length,
|
||||
itemBuilder: (context, index) {
|
||||
final detail = bankDetails[index];
|
||||
return _VendorBankDetailCard(
|
||||
bankDetail: detail,
|
||||
canEdit: canEdit,
|
||||
onEdit: () => openVendorBankDetailPanel(
|
||||
context,
|
||||
vendorId: vendorId,
|
||||
bankDetail: detail,
|
||||
),
|
||||
onDelete: () => _deleteBankDetail(
|
||||
context,
|
||||
ref,
|
||||
vendorId,
|
||||
detail.id,
|
||||
),
|
||||
);
|
||||
: 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,
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 320,
|
||||
mainAxisExtent: 190,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
),
|
||||
itemCount: bankDetails.length,
|
||||
itemBuilder: (context, index) {
|
||||
final detail = bankDetails[index];
|
||||
return _VendorBankDetailCard(
|
||||
bankDetail: detail,
|
||||
canEdit: canEdit,
|
||||
onEdit: () => openVendorBankDetailPanel(
|
||||
context,
|
||||
vendorId: vendorId,
|
||||
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(
|
||||
context,
|
||||
ref,
|
||||
vendorId,
|
||||
detail.id,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
@ -999,12 +1065,16 @@ class _VendorBankDetailCard extends StatelessWidget {
|
||||
required this.canEdit,
|
||||
required this.onEdit,
|
||||
required this.onDelete,
|
||||
this.showPrimaryRadio = false,
|
||||
this.onSetPrimary,
|
||||
});
|
||||
|
||||
final VendorBankDetailModel bankDetail;
|
||||
final bool canEdit;
|
||||
final VoidCallback onEdit;
|
||||
final VoidCallback onDelete;
|
||||
final bool showPrimaryRadio;
|
||||
final VoidCallback? onSetPrimary;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -1070,13 +1140,13 @@ class _VendorBankDetailCard extends StatelessWidget {
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
bankDetail.bankName?.trim().isNotEmpty == true
|
||||
? bankDetail.bankName!
|
||||
: 'Bank Account',
|
||||
bankDetail.bankName ?? 'Bank account',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
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(),
|
||||
Row(
|
||||
children: [
|
||||
if (bankDetail.accountNumber?.trim().isNotEmpty == true)
|
||||
Flexible(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.numbers_outlined,
|
||||
size: 14,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'A/C: ${bankDetail.accountNumber!}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (showPrimaryRadio)
|
||||
SizedBox(
|
||||
height: 32,
|
||||
child: Radio<String>(
|
||||
value: bankDetail.id,
|
||||
visualDensity: VisualDensity.compact,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
),
|
||||
if (onSetPrimary != null)
|
||||
GestureDetector(
|
||||
onTap: onSetPrimary,
|
||||
child: Text(
|
||||
isPrimary ? 'Primary' : 'Set primary',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: primaryColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
bankDetail.isActive ? 'Active' : 'Inactive',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
|
||||
@ -39,10 +39,15 @@ Future<bool?> openVendorBankDetailPanel(
|
||||
BuildContext context, {
|
||||
required String vendorId,
|
||||
VendorBankDetailModel? bankDetail,
|
||||
bool hasPrimaryAccount = false,
|
||||
}) {
|
||||
return showSidePanel<bool>(
|
||||
context,
|
||||
VendorBankDetailPanel(vendorId: vendorId, bankDetail: bankDetail),
|
||||
VendorBankDetailPanel(
|
||||
vendorId: vendorId,
|
||||
bankDetail: bankDetail,
|
||||
hasPrimaryAccount: hasPrimaryAccount,
|
||||
),
|
||||
width: 520,
|
||||
);
|
||||
}
|
||||
@ -347,10 +352,12 @@ class VendorBankDetailPanel extends ConsumerStatefulWidget {
|
||||
super.key,
|
||||
required this.vendorId,
|
||||
this.bankDetail,
|
||||
this.hasPrimaryAccount = false,
|
||||
});
|
||||
|
||||
final String vendorId;
|
||||
final VendorBankDetailModel? bankDetail;
|
||||
final bool hasPrimaryAccount;
|
||||
|
||||
bool get isEditing => bankDetail != null;
|
||||
|
||||
@ -386,6 +393,8 @@ class _VendorBankDetailPanelState extends ConsumerState<VendorBankDetailPanel> {
|
||||
_isActive = detail.isActive;
|
||||
} else {
|
||||
_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 {
|
||||
await notifier.createBankDetail(payload);
|
||||
}
|
||||
// Provider reload refreshes bank list so other primaries show false.
|
||||
if (mounted) Navigator.of(context, rootNavigator: true).pop(true);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
@ -435,6 +445,7 @@ class _VendorBankDetailPanelState extends ConsumerState<VendorBankDetailPanel> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return SidePanelScaffold(
|
||||
title: widget.isEditing ? 'Edit Bank Detail' : 'Add Bank Detail',
|
||||
footer: _panelFooter(
|
||||
@ -446,6 +457,7 @@ class _VendorBankDetailPanelState extends ConsumerState<VendorBankDetailPanel> {
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AppTextField(
|
||||
controller: _bankNameController,
|
||||
@ -488,11 +500,39 @@ class _VendorBankDetailPanelState extends ConsumerState<VendorBankDetailPanel> {
|
||||
.toList(),
|
||||
onChanged: (v) => setState(() => _accountType = v),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
AppFormToggleField(
|
||||
label: 'Primary Account',
|
||||
value: _isPrimary,
|
||||
onChanged: (v) => setState(() => _isPrimary = v),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Primary account',
|
||||
style: theme.textTheme.bodyLarge,
|
||||
),
|
||||
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(
|
||||
label: 'Active',
|
||||
|
||||
@ -514,7 +514,6 @@ class ServiceVisitModel with _$ServiceVisitModel {
|
||||
@JsonKey(name: 'visit_type') required String visitType,
|
||||
@JsonKey(name: 'visit_date', fromJson: _dateFromJson) required DateTime visitDate,
|
||||
@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_date', fromJson: _dateFromJsonNullable) DateTime? complaintDate,
|
||||
@JsonKey(name: 'complaint_desc') String? complaintDesc,
|
||||
|
||||
@ -2660,8 +2660,6 @@ mixin _$ServiceVisitModel {
|
||||
DateTime get visitDate => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'amc_contract_id', fromJson: _intFromJsonNullable)
|
||||
int? get amcContractId => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'visit_number', fromJson: _intFromJsonNullable)
|
||||
int? get visitNumber => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'complaint_no')
|
||||
String? get complaintNo => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'complaint_date', fromJson: _dateFromJsonNullable)
|
||||
@ -2719,8 +2717,6 @@ abstract class $ServiceVisitModelCopyWith<$Res> {
|
||||
@JsonKey(name: 'visit_date', fromJson: _dateFromJson) DateTime visitDate,
|
||||
@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_date', fromJson: _dateFromJsonNullable)
|
||||
DateTime? complaintDate,
|
||||
@ -2767,7 +2763,6 @@ class _$ServiceVisitModelCopyWithImpl<$Res, $Val extends ServiceVisitModel>
|
||||
Object? visitType = null,
|
||||
Object? visitDate = null,
|
||||
Object? amcContractId = freezed,
|
||||
Object? visitNumber = freezed,
|
||||
Object? complaintNo = freezed,
|
||||
Object? complaintDate = freezed,
|
||||
Object? complaintDesc = freezed,
|
||||
@ -2808,10 +2803,6 @@ class _$ServiceVisitModelCopyWithImpl<$Res, $Val extends ServiceVisitModel>
|
||||
? _value.amcContractId
|
||||
: amcContractId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
visitNumber: freezed == visitNumber
|
||||
? _value.visitNumber
|
||||
: visitNumber // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
complaintNo: freezed == complaintNo
|
||||
? _value.complaintNo
|
||||
: 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: 'amc_contract_id', fromJson: _intFromJsonNullable)
|
||||
int? amcContractId,
|
||||
@JsonKey(name: 'visit_number', fromJson: _intFromJsonNullable)
|
||||
int? visitNumber,
|
||||
@JsonKey(name: 'complaint_no') String? complaintNo,
|
||||
@JsonKey(name: 'complaint_date', fromJson: _dateFromJsonNullable)
|
||||
DateTime? complaintDate,
|
||||
@ -2949,7 +2938,6 @@ class __$$ServiceVisitModelImplCopyWithImpl<$Res>
|
||||
Object? visitType = null,
|
||||
Object? visitDate = null,
|
||||
Object? amcContractId = freezed,
|
||||
Object? visitNumber = freezed,
|
||||
Object? complaintNo = freezed,
|
||||
Object? complaintDate = freezed,
|
||||
Object? complaintDesc = freezed,
|
||||
@ -2990,10 +2978,6 @@ class __$$ServiceVisitModelImplCopyWithImpl<$Res>
|
||||
? _value.amcContractId
|
||||
: amcContractId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
visitNumber: freezed == visitNumber
|
||||
? _value.visitNumber
|
||||
: visitNumber // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
complaintNo: freezed == complaintNo
|
||||
? _value.complaintNo
|
||||
: complaintNo // ignore: cast_nullable_to_non_nullable
|
||||
@ -3078,8 +3062,6 @@ class _$ServiceVisitModelImpl implements _ServiceVisitModel {
|
||||
required this.visitDate,
|
||||
@JsonKey(name: 'amc_contract_id', fromJson: _intFromJsonNullable)
|
||||
this.amcContractId,
|
||||
@JsonKey(name: 'visit_number', fromJson: _intFromJsonNullable)
|
||||
this.visitNumber,
|
||||
@JsonKey(name: 'complaint_no') this.complaintNo,
|
||||
@JsonKey(name: 'complaint_date', fromJson: _dateFromJsonNullable)
|
||||
this.complaintDate,
|
||||
@ -3124,9 +3106,6 @@ class _$ServiceVisitModelImpl implements _ServiceVisitModel {
|
||||
@JsonKey(name: 'amc_contract_id', fromJson: _intFromJsonNullable)
|
||||
final int? amcContractId;
|
||||
@override
|
||||
@JsonKey(name: 'visit_number', fromJson: _intFromJsonNullable)
|
||||
final int? visitNumber;
|
||||
@override
|
||||
@JsonKey(name: 'complaint_no')
|
||||
final String? complaintNo;
|
||||
@override
|
||||
@ -3179,7 +3158,7 @@ class _$ServiceVisitModelImpl implements _ServiceVisitModel {
|
||||
|
||||
@override
|
||||
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
|
||||
@ -3195,8 +3174,6 @@ class _$ServiceVisitModelImpl implements _ServiceVisitModel {
|
||||
other.visitDate == visitDate) &&
|
||||
(identical(other.amcContractId, amcContractId) ||
|
||||
other.amcContractId == amcContractId) &&
|
||||
(identical(other.visitNumber, visitNumber) ||
|
||||
other.visitNumber == visitNumber) &&
|
||||
(identical(other.complaintNo, complaintNo) ||
|
||||
other.complaintNo == complaintNo) &&
|
||||
(identical(other.complaintDate, complaintDate) ||
|
||||
@ -3240,7 +3217,6 @@ class _$ServiceVisitModelImpl implements _ServiceVisitModel {
|
||||
visitType,
|
||||
visitDate,
|
||||
amcContractId,
|
||||
visitNumber,
|
||||
complaintNo,
|
||||
complaintDate,
|
||||
complaintDesc,
|
||||
@ -3286,8 +3262,6 @@ abstract class _ServiceVisitModel implements ServiceVisitModel {
|
||||
required final DateTime visitDate,
|
||||
@JsonKey(name: 'amc_contract_id', fromJson: _intFromJsonNullable)
|
||||
final int? amcContractId,
|
||||
@JsonKey(name: 'visit_number', fromJson: _intFromJsonNullable)
|
||||
final int? visitNumber,
|
||||
@JsonKey(name: 'complaint_no') final String? complaintNo,
|
||||
@JsonKey(name: 'complaint_date', fromJson: _dateFromJsonNullable)
|
||||
final DateTime? complaintDate,
|
||||
@ -3333,9 +3307,6 @@ abstract class _ServiceVisitModel implements ServiceVisitModel {
|
||||
@JsonKey(name: 'amc_contract_id', fromJson: _intFromJsonNullable)
|
||||
int? get amcContractId;
|
||||
@override
|
||||
@JsonKey(name: 'visit_number', fromJson: _intFromJsonNullable)
|
||||
int? get visitNumber;
|
||||
@override
|
||||
@JsonKey(name: 'complaint_no')
|
||||
String? get complaintNo;
|
||||
@override
|
||||
|
||||
@ -217,7 +217,6 @@ _$ServiceVisitModelImpl _$$ServiceVisitModelImplFromJson(
|
||||
visitType: json['visit_type'] as String,
|
||||
visitDate: _dateFromJson(json['visit_date']),
|
||||
amcContractId: _intFromJsonNullable(json['amc_contract_id']),
|
||||
visitNumber: _intFromJsonNullable(json['visit_number']),
|
||||
complaintNo: json['complaint_no'] as String?,
|
||||
complaintDate: _dateFromJsonNullable(json['complaint_date']),
|
||||
complaintDesc: json['complaint_desc'] as String?,
|
||||
@ -245,7 +244,6 @@ Map<String, dynamic> _$$ServiceVisitModelImplToJson(
|
||||
'visit_type': instance.visitType,
|
||||
'visit_date': instance.visitDate.toIso8601String(),
|
||||
'amc_contract_id': instance.amcContractId,
|
||||
'visit_number': instance.visitNumber,
|
||||
'complaint_no': instance.complaintNo,
|
||||
'complaint_date': instance.complaintDate?.toIso8601String(),
|
||||
'complaint_desc': instance.complaintDesc,
|
||||
|
||||
@ -38,9 +38,23 @@ Object? _readNestedName(Map<dynamic, dynamic> json, String flatKey, String neste
|
||||
Object? _readVendorName(Map<dynamic, dynamic> json, String key) =>
|
||||
_readNestedName(json, 'vendor_name', 'vendor');
|
||||
|
||||
Object? _readWarehouseName(Map<dynamic, dynamic> json, String key) =>
|
||||
_readNestedName(json, 'warehouse_name', 'warehouse');
|
||||
Object? _readLocationName(Map<dynamic, dynamic> json, String key) {
|
||||
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) {
|
||||
final grnNumber = json['grn_number'];
|
||||
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: 'vendor_id', fromJson: _intFromJsonNullable) int? vendorId,
|
||||
@JsonKey(name: 'vendor_name', readValue: _readVendorName) String? vendorName,
|
||||
@JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable) int? warehouseId,
|
||||
@JsonKey(name: 'warehouse_name', readValue: _readWarehouseName) String? warehouseName,
|
||||
@JsonKey(name: 'location_id', fromJson: _intFromJsonNullable) int? locationId,
|
||||
@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_date', fromJson: _dateFromJsonNullable)
|
||||
DateTime? vendorInvoiceDate,
|
||||
@ -246,7 +263,7 @@ class GrnListQuery with _$GrnListQuery {
|
||||
String? status,
|
||||
int? poId,
|
||||
int? vendorId,
|
||||
int? warehouseId,
|
||||
int? locationId,
|
||||
String? dateFrom,
|
||||
String? dateTo,
|
||||
}) = _GrnListQuery;
|
||||
|
||||
@ -36,10 +36,12 @@ mixin _$GrnModel {
|
||||
int? get vendorId => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'vendor_name', readValue: _readVendorName)
|
||||
String? get vendorName => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable)
|
||||
int? get warehouseId => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'warehouse_name', readValue: _readWarehouseName)
|
||||
String? get warehouseName => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'location_id', fromJson: _intFromJsonNullable)
|
||||
int? get locationId => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'location_name', readValue: _readLocationName)
|
||||
String? get locationName => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'location_type', readValue: _readLocationType)
|
||||
String? get locationType => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'vendor_invoice_no')
|
||||
String? get vendorInvoiceNo => throw _privateConstructorUsedError;
|
||||
@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_name', readValue: _readVendorName)
|
||||
String? vendorName,
|
||||
@JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable)
|
||||
int? warehouseId,
|
||||
@JsonKey(name: 'warehouse_name', readValue: _readWarehouseName)
|
||||
String? warehouseName,
|
||||
@JsonKey(name: 'location_id', fromJson: _intFromJsonNullable)
|
||||
int? locationId,
|
||||
@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_date', fromJson: _dateFromJsonNullable)
|
||||
DateTime? vendorInvoiceDate,
|
||||
@ -143,8 +147,9 @@ class _$GrnModelCopyWithImpl<$Res, $Val extends GrnModel>
|
||||
Object? poNumber = freezed,
|
||||
Object? vendorId = freezed,
|
||||
Object? vendorName = freezed,
|
||||
Object? warehouseId = freezed,
|
||||
Object? warehouseName = freezed,
|
||||
Object? locationId = freezed,
|
||||
Object? locationName = freezed,
|
||||
Object? locationType = freezed,
|
||||
Object? vendorInvoiceNo = freezed,
|
||||
Object? vendorInvoiceDate = freezed,
|
||||
Object? vendorInvoiceAmount = freezed,
|
||||
@ -194,13 +199,17 @@ class _$GrnModelCopyWithImpl<$Res, $Val extends GrnModel>
|
||||
? _value.vendorName
|
||||
: vendorName // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
warehouseId: freezed == warehouseId
|
||||
? _value.warehouseId
|
||||
: warehouseId // ignore: cast_nullable_to_non_nullable
|
||||
locationId: freezed == locationId
|
||||
? _value.locationId
|
||||
: locationId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
warehouseName: freezed == warehouseName
|
||||
? _value.warehouseName
|
||||
: warehouseName // ignore: cast_nullable_to_non_nullable
|
||||
locationName: freezed == locationName
|
||||
? _value.locationName
|
||||
: locationName // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
locationType: freezed == locationType
|
||||
? _value.locationType
|
||||
: locationType // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
vendorInvoiceNo: freezed == vendorInvoiceNo
|
||||
? _value.vendorInvoiceNo
|
||||
@ -284,10 +293,12 @@ abstract class _$$GrnModelImplCopyWith<$Res>
|
||||
@JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? vendorId,
|
||||
@JsonKey(name: 'vendor_name', readValue: _readVendorName)
|
||||
String? vendorName,
|
||||
@JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable)
|
||||
int? warehouseId,
|
||||
@JsonKey(name: 'warehouse_name', readValue: _readWarehouseName)
|
||||
String? warehouseName,
|
||||
@JsonKey(name: 'location_id', fromJson: _intFromJsonNullable)
|
||||
int? locationId,
|
||||
@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_date', fromJson: _dateFromJsonNullable)
|
||||
DateTime? vendorInvoiceDate,
|
||||
@ -333,8 +344,9 @@ class __$$GrnModelImplCopyWithImpl<$Res>
|
||||
Object? poNumber = freezed,
|
||||
Object? vendorId = freezed,
|
||||
Object? vendorName = freezed,
|
||||
Object? warehouseId = freezed,
|
||||
Object? warehouseName = freezed,
|
||||
Object? locationId = freezed,
|
||||
Object? locationName = freezed,
|
||||
Object? locationType = freezed,
|
||||
Object? vendorInvoiceNo = freezed,
|
||||
Object? vendorInvoiceDate = freezed,
|
||||
Object? vendorInvoiceAmount = freezed,
|
||||
@ -384,13 +396,17 @@ class __$$GrnModelImplCopyWithImpl<$Res>
|
||||
? _value.vendorName
|
||||
: vendorName // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
warehouseId: freezed == warehouseId
|
||||
? _value.warehouseId
|
||||
: warehouseId // ignore: cast_nullable_to_non_nullable
|
||||
locationId: freezed == locationId
|
||||
? _value.locationId
|
||||
: locationId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
warehouseName: freezed == warehouseName
|
||||
? _value.warehouseName
|
||||
: warehouseName // ignore: cast_nullable_to_non_nullable
|
||||
locationName: freezed == locationName
|
||||
? _value.locationName
|
||||
: locationName // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
locationType: freezed == locationType
|
||||
? _value.locationType
|
||||
: locationType // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
vendorInvoiceNo: freezed == vendorInvoiceNo
|
||||
? _value.vendorInvoiceNo
|
||||
@ -465,10 +481,12 @@ class _$GrnModelImpl extends _GrnModel {
|
||||
@JsonKey(name: 'po_number', readValue: _readPoRefNumber) this.poNumber,
|
||||
@JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) this.vendorId,
|
||||
@JsonKey(name: 'vendor_name', readValue: _readVendorName) this.vendorName,
|
||||
@JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable)
|
||||
this.warehouseId,
|
||||
@JsonKey(name: 'warehouse_name', readValue: _readWarehouseName)
|
||||
this.warehouseName,
|
||||
@JsonKey(name: 'location_id', fromJson: _intFromJsonNullable)
|
||||
this.locationId,
|
||||
@JsonKey(name: 'location_name', readValue: _readLocationName)
|
||||
this.locationName,
|
||||
@JsonKey(name: 'location_type', readValue: _readLocationType)
|
||||
this.locationType,
|
||||
@JsonKey(name: 'vendor_invoice_no') this.vendorInvoiceNo,
|
||||
@JsonKey(name: 'vendor_invoice_date', fromJson: _dateFromJsonNullable)
|
||||
this.vendorInvoiceDate,
|
||||
@ -521,11 +539,14 @@ class _$GrnModelImpl extends _GrnModel {
|
||||
@JsonKey(name: 'vendor_name', readValue: _readVendorName)
|
||||
final String? vendorName;
|
||||
@override
|
||||
@JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable)
|
||||
final int? warehouseId;
|
||||
@JsonKey(name: 'location_id', fromJson: _intFromJsonNullable)
|
||||
final int? locationId;
|
||||
@override
|
||||
@JsonKey(name: 'warehouse_name', readValue: _readWarehouseName)
|
||||
final String? warehouseName;
|
||||
@JsonKey(name: 'location_name', readValue: _readLocationName)
|
||||
final String? locationName;
|
||||
@override
|
||||
@JsonKey(name: 'location_type', readValue: _readLocationType)
|
||||
final String? locationType;
|
||||
@override
|
||||
@JsonKey(name: 'vendor_invoice_no')
|
||||
final String? vendorInvoiceNo;
|
||||
@ -581,7 +602,7 @@ class _$GrnModelImpl extends _GrnModel {
|
||||
|
||||
@override
|
||||
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
|
||||
@ -601,10 +622,12 @@ class _$GrnModelImpl extends _GrnModel {
|
||||
other.vendorId == vendorId) &&
|
||||
(identical(other.vendorName, vendorName) ||
|
||||
other.vendorName == vendorName) &&
|
||||
(identical(other.warehouseId, warehouseId) ||
|
||||
other.warehouseId == warehouseId) &&
|
||||
(identical(other.warehouseName, warehouseName) ||
|
||||
other.warehouseName == warehouseName) &&
|
||||
(identical(other.locationId, locationId) ||
|
||||
other.locationId == locationId) &&
|
||||
(identical(other.locationName, locationName) ||
|
||||
other.locationName == locationName) &&
|
||||
(identical(other.locationType, locationType) ||
|
||||
other.locationType == locationType) &&
|
||||
(identical(other.vendorInvoiceNo, vendorInvoiceNo) ||
|
||||
other.vendorInvoiceNo == vendorInvoiceNo) &&
|
||||
(identical(other.vendorInvoiceDate, vendorInvoiceDate) ||
|
||||
@ -645,8 +668,9 @@ class _$GrnModelImpl extends _GrnModel {
|
||||
poNumber,
|
||||
vendorId,
|
||||
vendorName,
|
||||
warehouseId,
|
||||
warehouseName,
|
||||
locationId,
|
||||
locationName,
|
||||
locationType,
|
||||
vendorInvoiceNo,
|
||||
vendorInvoiceDate,
|
||||
vendorInvoiceAmount,
|
||||
@ -692,10 +716,12 @@ abstract class _GrnModel extends GrnModel {
|
||||
final int? vendorId,
|
||||
@JsonKey(name: 'vendor_name', readValue: _readVendorName)
|
||||
final String? vendorName,
|
||||
@JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable)
|
||||
final int? warehouseId,
|
||||
@JsonKey(name: 'warehouse_name', readValue: _readWarehouseName)
|
||||
final String? warehouseName,
|
||||
@JsonKey(name: 'location_id', fromJson: _intFromJsonNullable)
|
||||
final int? locationId,
|
||||
@JsonKey(name: 'location_name', readValue: _readLocationName)
|
||||
final String? locationName,
|
||||
@JsonKey(name: 'location_type', readValue: _readLocationType)
|
||||
final String? locationType,
|
||||
@JsonKey(name: 'vendor_invoice_no') final String? vendorInvoiceNo,
|
||||
@JsonKey(name: 'vendor_invoice_date', fromJson: _dateFromJsonNullable)
|
||||
final DateTime? vendorInvoiceDate,
|
||||
@ -747,11 +773,14 @@ abstract class _GrnModel extends GrnModel {
|
||||
@JsonKey(name: 'vendor_name', readValue: _readVendorName)
|
||||
String? get vendorName;
|
||||
@override
|
||||
@JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable)
|
||||
int? get warehouseId;
|
||||
@JsonKey(name: 'location_id', fromJson: _intFromJsonNullable)
|
||||
int? get locationId;
|
||||
@override
|
||||
@JsonKey(name: 'warehouse_name', readValue: _readWarehouseName)
|
||||
String? get warehouseName;
|
||||
@JsonKey(name: 'location_name', readValue: _readLocationName)
|
||||
String? get locationName;
|
||||
@override
|
||||
@JsonKey(name: 'location_type', readValue: _readLocationType)
|
||||
String? get locationType;
|
||||
@override
|
||||
@JsonKey(name: 'vendor_invoice_no')
|
||||
String? get vendorInvoiceNo;
|
||||
@ -1969,7 +1998,7 @@ mixin _$GrnListQuery {
|
||||
String? get status => throw _privateConstructorUsedError;
|
||||
int? get poId => throw _privateConstructorUsedError;
|
||||
int? get vendorId => throw _privateConstructorUsedError;
|
||||
int? get warehouseId => throw _privateConstructorUsedError;
|
||||
int? get locationId => throw _privateConstructorUsedError;
|
||||
String? get dateFrom => throw _privateConstructorUsedError;
|
||||
String? get dateTo => throw _privateConstructorUsedError;
|
||||
|
||||
@ -1994,7 +2023,7 @@ abstract class $GrnListQueryCopyWith<$Res> {
|
||||
String? status,
|
||||
int? poId,
|
||||
int? vendorId,
|
||||
int? warehouseId,
|
||||
int? locationId,
|
||||
String? dateFrom,
|
||||
String? dateTo,
|
||||
});
|
||||
@ -2021,7 +2050,7 @@ class _$GrnListQueryCopyWithImpl<$Res, $Val extends GrnListQuery>
|
||||
Object? status = freezed,
|
||||
Object? poId = freezed,
|
||||
Object? vendorId = freezed,
|
||||
Object? warehouseId = freezed,
|
||||
Object? locationId = freezed,
|
||||
Object? dateFrom = freezed,
|
||||
Object? dateTo = freezed,
|
||||
}) {
|
||||
@ -2051,9 +2080,9 @@ class _$GrnListQueryCopyWithImpl<$Res, $Val extends GrnListQuery>
|
||||
? _value.vendorId
|
||||
: vendorId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
warehouseId: freezed == warehouseId
|
||||
? _value.warehouseId
|
||||
: warehouseId // ignore: cast_nullable_to_non_nullable
|
||||
locationId: freezed == locationId
|
||||
? _value.locationId
|
||||
: locationId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
dateFrom: freezed == dateFrom
|
||||
? _value.dateFrom
|
||||
@ -2085,7 +2114,7 @@ abstract class _$$GrnListQueryImplCopyWith<$Res>
|
||||
String? status,
|
||||
int? poId,
|
||||
int? vendorId,
|
||||
int? warehouseId,
|
||||
int? locationId,
|
||||
String? dateFrom,
|
||||
String? dateTo,
|
||||
});
|
||||
@ -2111,7 +2140,7 @@ class __$$GrnListQueryImplCopyWithImpl<$Res>
|
||||
Object? status = freezed,
|
||||
Object? poId = freezed,
|
||||
Object? vendorId = freezed,
|
||||
Object? warehouseId = freezed,
|
||||
Object? locationId = freezed,
|
||||
Object? dateFrom = freezed,
|
||||
Object? dateTo = freezed,
|
||||
}) {
|
||||
@ -2141,9 +2170,9 @@ class __$$GrnListQueryImplCopyWithImpl<$Res>
|
||||
? _value.vendorId
|
||||
: vendorId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
warehouseId: freezed == warehouseId
|
||||
? _value.warehouseId
|
||||
: warehouseId // ignore: cast_nullable_to_non_nullable
|
||||
locationId: freezed == locationId
|
||||
? _value.locationId
|
||||
: locationId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
dateFrom: freezed == dateFrom
|
||||
? _value.dateFrom
|
||||
@ -2168,7 +2197,7 @@ class _$GrnListQueryImpl implements _GrnListQuery {
|
||||
this.status,
|
||||
this.poId,
|
||||
this.vendorId,
|
||||
this.warehouseId,
|
||||
this.locationId,
|
||||
this.dateFrom,
|
||||
this.dateTo,
|
||||
});
|
||||
@ -2188,7 +2217,7 @@ class _$GrnListQueryImpl implements _GrnListQuery {
|
||||
@override
|
||||
final int? vendorId;
|
||||
@override
|
||||
final int? warehouseId;
|
||||
final int? locationId;
|
||||
@override
|
||||
final String? dateFrom;
|
||||
@override
|
||||
@ -2196,7 +2225,7 @@ class _$GrnListQueryImpl implements _GrnListQuery {
|
||||
|
||||
@override
|
||||
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
|
||||
@ -2211,8 +2240,8 @@ class _$GrnListQueryImpl implements _GrnListQuery {
|
||||
(identical(other.poId, poId) || other.poId == poId) &&
|
||||
(identical(other.vendorId, vendorId) ||
|
||||
other.vendorId == vendorId) &&
|
||||
(identical(other.warehouseId, warehouseId) ||
|
||||
other.warehouseId == warehouseId) &&
|
||||
(identical(other.locationId, locationId) ||
|
||||
other.locationId == locationId) &&
|
||||
(identical(other.dateFrom, dateFrom) ||
|
||||
other.dateFrom == dateFrom) &&
|
||||
(identical(other.dateTo, dateTo) || other.dateTo == dateTo));
|
||||
@ -2227,7 +2256,7 @@ class _$GrnListQueryImpl implements _GrnListQuery {
|
||||
status,
|
||||
poId,
|
||||
vendorId,
|
||||
warehouseId,
|
||||
locationId,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
);
|
||||
@ -2249,7 +2278,7 @@ abstract class _GrnListQuery implements GrnListQuery {
|
||||
final String? status,
|
||||
final int? poId,
|
||||
final int? vendorId,
|
||||
final int? warehouseId,
|
||||
final int? locationId,
|
||||
final String? dateFrom,
|
||||
final String? dateTo,
|
||||
}) = _$GrnListQueryImpl;
|
||||
@ -2267,7 +2296,7 @@ abstract class _GrnListQuery implements GrnListQuery {
|
||||
@override
|
||||
int? get vendorId;
|
||||
@override
|
||||
int? get warehouseId;
|
||||
int? get locationId;
|
||||
@override
|
||||
String? get dateFrom;
|
||||
@override
|
||||
|
||||
@ -17,8 +17,9 @@ _$GrnModelImpl _$$GrnModelImplFromJson(
|
||||
poNumber: _readPoRefNumber(json, 'po_number') as String?,
|
||||
vendorId: _intFromJsonNullable(json['vendor_id']),
|
||||
vendorName: _readVendorName(json, 'vendor_name') as String?,
|
||||
warehouseId: _intFromJsonNullable(json['warehouse_id']),
|
||||
warehouseName: _readWarehouseName(json, 'warehouse_name') as String?,
|
||||
locationId: _intFromJsonNullable(json['location_id']),
|
||||
locationName: _readLocationName(json, 'location_name') as String?,
|
||||
locationType: _readLocationType(json, 'location_type') as String?,
|
||||
vendorInvoiceNo: json['vendor_invoice_no'] as String?,
|
||||
vendorInvoiceDate: _dateFromJsonNullable(json['vendor_invoice_date']),
|
||||
vendorInvoiceAmount: _doubleFromJsonNullable(json['vendor_invoice_amount']),
|
||||
@ -53,8 +54,9 @@ Map<String, dynamic> _$$GrnModelImplToJson(_$GrnModelImpl instance) =>
|
||||
'po_number': instance.poNumber,
|
||||
'vendor_id': instance.vendorId,
|
||||
'vendor_name': instance.vendorName,
|
||||
'warehouse_id': instance.warehouseId,
|
||||
'warehouse_name': instance.warehouseName,
|
||||
'location_id': instance.locationId,
|
||||
'location_name': instance.locationName,
|
||||
'location_type': instance.locationType,
|
||||
'vendor_invoice_no': instance.vendorInvoiceNo,
|
||||
'vendor_invoice_date': instance.vendorInvoiceDate?.toIso8601String(),
|
||||
'vendor_invoice_amount': instance.vendorInvoiceAmount,
|
||||
|
||||
@ -227,6 +227,13 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
pageBuilder: (context, state) =>
|
||||
shellPage(state, const PurchaseOrderListScreen()),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'pending-approval',
|
||||
pageBuilder: (context, state) => shellPage(
|
||||
state,
|
||||
const PurchaseOrderListScreen(pendingApprovalOnly: true),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'add',
|
||||
pageBuilder: (context, state) => shellPage(
|
||||
|
||||
@ -12,6 +12,7 @@ class MenuItem {
|
||||
required this.module,
|
||||
this.children = const [],
|
||||
this.requiredRole,
|
||||
this.requiredAction,
|
||||
});
|
||||
|
||||
final String label;
|
||||
@ -20,6 +21,9 @@ class MenuItem {
|
||||
final String module;
|
||||
final List<MenuItem> children;
|
||||
final UserRole? requiredRole;
|
||||
|
||||
/// When set, menu visibility requires this action instead of default view.
|
||||
final PermissionAction? requiredAction;
|
||||
}
|
||||
|
||||
const List<MenuItem> appMenuItems = [
|
||||
@ -79,6 +83,21 @@ const List<MenuItem> appMenuItems = [
|
||||
icon: Icons.receipt_long_outlined,
|
||||
route: RouteConstants.purchaseOrders,
|
||||
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(
|
||||
label: 'GRN',
|
||||
@ -145,9 +164,9 @@ List<MenuItem> getVisibleMenuItems({
|
||||
if (item.children.isNotEmpty) {
|
||||
final visibleChildren = item.children
|
||||
.where(
|
||||
(child) => canSeeMenuModule(
|
||||
(child) => _canSeeMenuItem(
|
||||
permissions: permissions,
|
||||
module: child.module,
|
||||
item: child,
|
||||
role: role,
|
||||
),
|
||||
)
|
||||
@ -161,16 +180,13 @@ List<MenuItem> getVisibleMenuItems({
|
||||
module: item.module,
|
||||
children: visibleChildren,
|
||||
requiredRole: item.requiredRole,
|
||||
requiredAction: item.requiredAction,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!canSeeMenuModule(
|
||||
permissions: permissions,
|
||||
module: item.module,
|
||||
role: role,
|
||||
)) {
|
||||
if (!_canSeeMenuItem(permissions: permissions, item: item, role: role)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -179,3 +195,24 @@ List<MenuItem> getVisibleMenuItems({
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
@ -96,14 +96,31 @@ class _AppSidebarState extends ConsumerState<AppSidebar> {
|
||||
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) {
|
||||
setState(() {
|
||||
if (_isGroupExpanded(item)) {
|
||||
_expandedMenus.remove(item.route);
|
||||
_manuallyCollapsedMenus.add(item.route);
|
||||
} else {
|
||||
_manuallyCollapsedMenus.remove(item.route);
|
||||
_expandedMenus.add(item.route);
|
||||
_expandOnly(item.route);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -112,10 +129,10 @@ class _AppSidebarState extends ConsumerState<AppSidebar> {
|
||||
void didUpdateWidget(covariant AppSidebar oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.currentRoute != widget.currentRoute) {
|
||||
for (final item in [..._primaryMenuItems, ..._supportMenuItems]) {
|
||||
if (item.children.isNotEmpty && _isGroupActive(item)) {
|
||||
_expandedMenus.add(item.route);
|
||||
_manuallyCollapsedMenus.remove(item.route);
|
||||
for (final item in _allGroupItems) {
|
||||
if (_isGroupActive(item)) {
|
||||
_expandOnly(item.route);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user