import 'package:flutter/material.dart'; enum MasterFieldType { text, number, boolean, dropdown } const categoryTypeOptions = ['STOCK', 'ASSET']; const termsNotesTypeOptions = ['PO', 'INVOICE']; class MasterFieldDef { const MasterFieldDef({ required this.key, required this.label, this.type = MasterFieldType.text, this.required = false, this.showInList = false, this.showInForm = true, this.readOnly = false, this.optionsMasterKey, this.optionsQueryParams, this.staticOptions, this.multiline = false, this.filterByFieldKey, this.filterByOptionKey, this.visibleWhenFieldKey, this.visibleWhenValue, this.listNestedKey, }); final String key; final String label; final MasterFieldType type; final bool required; final bool showInList; /// When false, field is list/display-only and excluded from create/update payloads. final bool showInForm; /// When true, shown in the form but not editable (value set by other fields). final bool readOnly; /// Master key used to populate dropdown options (e.g. `locations` for FKs). final String? optionsMasterKey; /// Extra query parameters when loading [optionsMasterKey] options. final Map? optionsQueryParams; /// Fixed dropdown choices (e.g. category type) — no API lookup. final List? staticOptions; final bool multiline; /// Form field whose value filters this dropdown (e.g. `item_category_id`). final String? filterByFieldKey; /// Option-row key matched against [filterByFieldKey] (defaults to same key). final String? filterByOptionKey; /// Show this field only when [visibleWhenFieldKey] equals [visibleWhenValue]. final String? visibleWhenFieldKey; final String? visibleWhenValue; /// Nested object key on list rows for display (e.g. `gst_rate` for `gst_rate_id`). final String? listNestedKey; /// Cache key for dropdown option rows (includes query params when set). String get dropdownLookupKey { final masterKey = optionsMasterKey; if (masterKey == null) return key; final params = optionsQueryParams; if (params == null || params.isEmpty) return masterKey; final parts = params.entries.toList() ..sort((a, b) => a.key.compareTo(b.key)); final query = parts.map((e) => '${e.key}=${e.value}').join('&'); return '$masterKey?$query'; } bool isVisibleInForm(Map values) { final whenKey = visibleWhenFieldKey; if (whenKey == null) return true; return values[whenKey]?.toString() == visibleWhenValue; } } class MasterDefinition { const MasterDefinition({ required this.id, required this.title, required this.subtitle, required this.category, required this.routeKey, required this.apiPath, required this.module, required this.icon, required this.fields, }); final String id; final String title; final String subtitle; final String category; final String routeKey; final String apiPath; final String module; final IconData icon; final List fields; List get listFields => fields.where((field) => field.showInList).toList(); List get formFields => fields.where((field) => field.showInForm).toList(); String listRoute(String base) => '$base/$routeKey'; String addRoute(String base) => '$base/$routeKey/add'; String editRoute(String base, String id) => '$base/$routeKey/$id/edit'; } const _activeField = MasterFieldDef( key: 'is_active', label: 'Active', type: MasterFieldType.boolean, ); MasterDefinition? masterDefinitionById(String id) { for (final def in masterDefinitions) { if (def.id == id) return def; } return null; } MasterDefinition? masterDefinitionByRouteKey(String routeKey) { for (final def in masterDefinitions) { if (def.routeKey == routeKey) return def; } return null; } const masterDefinitions = [ MasterDefinition( id: 'uom', title: 'UOM', subtitle: 'Units of measure', category: 'Inventory & Items', routeKey: 'uom', apiPath: '/masters/uom', module: 'uom', icon: Icons.straighten_outlined, fields: [ MasterFieldDef(key: 'code', label: 'Code', required: true, showInList: true), MasterFieldDef(key: 'name', label: 'Name', required: true, showInList: true), _activeField, ], ), MasterDefinition( id: 'item_categories', title: 'Item Categories', subtitle: 'Top-level item grouping', category: 'Inventory & Items', routeKey: 'item-categories', apiPath: '/masters/item-categories', module: 'item_categories', icon: Icons.category_outlined, fields: [ MasterFieldDef(key: 'code', label: 'Code', required: true, showInList: true), MasterFieldDef(key: 'name', label: 'Name', required: true, showInList: true), MasterFieldDef( key: 'category_type', label: 'Category Type', type: MasterFieldType.dropdown, required: true, showInList: true, staticOptions: categoryTypeOptions, ), MasterFieldDef( key: 'code_prefix', label: 'Code Prefix', showInList: true, visibleWhenFieldKey: 'category_type', visibleWhenValue: 'ASSET', ), MasterFieldDef( key: 'default_useful_life_years', label: 'Useful Life (Years)', type: MasterFieldType.number, visibleWhenFieldKey: 'category_type', visibleWhenValue: 'ASSET', ), MasterFieldDef( key: 'default_depreciation_method', label: 'Depreciation Method', type: MasterFieldType.dropdown, showInList: true, optionsMasterKey: 'asset_depreciation_methods', visibleWhenFieldKey: 'category_type', visibleWhenValue: 'ASSET', ), _activeField, ], ), MasterDefinition( id: 'item_subcategories', title: 'Item Sub Categories', subtitle: 'Sub-grouping under item categories', category: 'Inventory & Items', routeKey: 'item-subcategories', apiPath: '/masters/item-subcategories', module: 'item_subcategories', icon: Icons.subdirectory_arrow_right, fields: [ MasterFieldDef( key: 'item_category_id', label: 'Item Category', type: MasterFieldType.dropdown, required: true, showInList: true, optionsMasterKey: 'item_categories', ), MasterFieldDef(key: 'code', label: 'Code', required: true, showInList: true), MasterFieldDef(key: 'name', label: 'Name', required: true, showInList: true), _activeField, ], ), MasterDefinition( id: 'items', title: 'Items', subtitle: 'Material and product master', category: 'Inventory & Items', routeKey: 'items', apiPath: '/masters/items', module: 'items', icon: Icons.inventory_outlined, fields: [ // Auto-generated by backend — list only; never sent on create/update. MasterFieldDef( key: 'item_code', label: 'Item Code', showInList: true, showInForm: false, ), MasterFieldDef(key: 'item_name', label: 'Item Name', required: true, showInList: true), MasterFieldDef( key: 'is_asset_item', label: 'Asset Item', type: MasterFieldType.boolean, required: true, showInList: true, ), MasterFieldDef( key: 'item_category_id', label: 'Category', type: MasterFieldType.dropdown, required: true, optionsMasterKey: 'item_categories', // Resolved at runtime from is_asset_item (STOCK / ASSET). ), MasterFieldDef( key: 'item_subcategory_id', label: 'Sub Category', type: MasterFieldType.dropdown, optionsMasterKey: 'item_subcategories', filterByFieldKey: 'item_category_id', ), MasterFieldDef( key: 'uom_id', label: 'UOM', type: MasterFieldType.dropdown, required: true, optionsMasterKey: 'uom', ), MasterFieldDef( key: 'hsn_code_id', label: 'HSN Code', type: MasterFieldType.dropdown, showInList: true, optionsMasterKey: 'hsn_codes', ), MasterFieldDef( key: 'gst_rate_id', label: 'GST Rate', type: MasterFieldType.dropdown, showInList: true, readOnly: true, // Filled from selected HSN's nested gst_rate — no gst-rates API. ), MasterFieldDef( key: 'min_order_qty', label: 'Min Order Qty', type: MasterFieldType.number, required: true, // Stock items only — hidden when Asset Item is checked. visibleWhenFieldKey: 'is_asset_item', visibleWhenValue: 'false', ), MasterFieldDef( key: 'reorder_level', label: 'Reorder Level', type: MasterFieldType.number, required: true, visibleWhenFieldKey: 'is_asset_item', visibleWhenValue: 'false', ), MasterFieldDef( key: 'tags', label: 'Tags', multiline: true, showInList: true, ), MasterFieldDef(key: 'description', label: 'Description', multiline: true), MasterFieldDef(key: 'specification', label: 'Specification', multiline: true), _activeField, ], ), MasterDefinition( id: 'hsn_codes', title: 'HSN Codes', subtitle: 'HSN/SAC tax classification codes', category: 'Finance & Terms', routeKey: 'hsn-codes', apiPath: '/masters/hsn-codes', module: 'hsn_codes', icon: Icons.qr_code_outlined, fields: [ MasterFieldDef(key: 'code', label: 'HSN/SAC Code', required: true, showInList: true), MasterFieldDef( key: 'description', label: 'Description', required: true, showInList: true, multiline: true, ), MasterFieldDef( key: 'gst_rate_id', label: 'GST Rate', type: MasterFieldType.dropdown, showInList: true, optionsMasterKey: 'gst_rates', ), _activeField, ], ), MasterDefinition( id: 'document_series', title: 'Document Series', subtitle: 'Numbering series for documents', category: 'Finance & Terms', routeKey: 'document-series', apiPath: '/masters/document-series', module: 'document_series', icon: Icons.numbers_outlined, fields: [ MasterFieldDef(key: 'code', label: 'Code', required: true, showInList: true), MasterFieldDef(key: 'prefix', label: 'Prefix', required: true, showInList: true), MasterFieldDef( key: 'current_number', label: 'Current Number', type: MasterFieldType.number, required: true, showInList: true, ), MasterFieldDef( key: 'padding', label: 'Padding', type: MasterFieldType.number, required: true, ), MasterFieldDef(key: 'description', label: 'Description', multiline: true), _activeField, ], ), MasterDefinition( id: 'locations', title: 'Locations', subtitle: 'Plants and warehouses', category: 'Organization', routeKey: 'locations', apiPath: '/masters/locations', module: 'locations', icon: Icons.place_outlined, fields: [ MasterFieldDef( key: 'type', label: 'Type', type: MasterFieldType.dropdown, required: true, showInList: true, staticOptions: const ['plant', 'warehouse'], ), MasterFieldDef(key: 'code', label: 'Code', required: true, showInList: true), MasterFieldDef(key: 'name', label: 'Name', required: true, showInList: true), MasterFieldDef( key: 'gstin', label: 'GSTIN', showInList: true, visibleWhenFieldKey: 'type', visibleWhenValue: 'plant', ), MasterFieldDef( key: 'city', label: 'City', showInList: true, visibleWhenFieldKey: 'type', visibleWhenValue: 'plant', ), MasterFieldDef( key: 'state', label: 'State', type: MasterFieldType.dropdown, optionsMasterKey: 'location_states', showInList: true, visibleWhenFieldKey: 'type', visibleWhenValue: 'plant', ), MasterFieldDef( key: 'address', label: 'Address', multiline: true, visibleWhenFieldKey: 'type', visibleWhenValue: 'plant', ), MasterFieldDef( key: 'pincode', label: 'Pincode', visibleWhenFieldKey: 'type', visibleWhenValue: 'plant', ), MasterFieldDef( key: 'phone', label: 'Phone', visibleWhenFieldKey: 'type', visibleWhenValue: 'plant', ), MasterFieldDef( key: 'location', label: 'Location Detail', showInList: true, visibleWhenFieldKey: 'type', visibleWhenValue: 'warehouse', ), _activeField, ], ), MasterDefinition( id: 'designations', title: 'Designations', subtitle: 'Employee designations', category: 'Organization', routeKey: 'designations', apiPath: '/masters/designations', module: 'designations', icon: Icons.badge_outlined, fields: [ MasterFieldDef(key: 'name', label: 'Name', required: true, showInList: true), _activeField, ], ), MasterDefinition( id: 'departments', title: 'Departments', subtitle: 'Organisation departments', category: 'Organization', routeKey: 'departments', apiPath: '/masters/departments', module: 'departments', icon: Icons.apartment_outlined, fields: [ MasterFieldDef(key: 'name', label: 'Name', required: true, showInList: true), _activeField, ], ), MasterDefinition( id: 'delivery_terms', title: 'Delivery Terms', subtitle: 'Delivery incoterms and terms', category: 'Finance & Terms', routeKey: 'delivery-terms', apiPath: '/masters/delivery-terms', module: 'delivery_terms', icon: Icons.local_shipping_outlined, fields: [ MasterFieldDef(key: 'code', label: 'Code', required: true, showInList: true), MasterFieldDef(key: 'name', label: 'Name', required: true, showInList: true), MasterFieldDef(key: 'description', label: 'Description', multiline: true), _activeField, ], ), MasterDefinition( id: 'payment_terms', title: 'Payment Terms', subtitle: 'Vendor payment terms', category: 'Finance & Terms', routeKey: 'payment-terms', apiPath: '/masters/payment-terms', module: 'payment_terms', icon: Icons.payments_outlined, fields: [ MasterFieldDef(key: 'code', label: 'Code', required: true, showInList: true), MasterFieldDef(key: 'name', label: 'Name', required: true, showInList: true), MasterFieldDef( key: 'credit_days', label: 'Credit Days', type: MasterFieldType.number, required: true, showInList: true, ), MasterFieldDef(key: 'description', label: 'Description', multiline: true), _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', subtitle: 'Tax rate master', category: 'Finance & Terms', routeKey: 'gst-rates', apiPath: '/masters/gst-rates', module: 'gst_rates', icon: Icons.percent_outlined, fields: [ MasterFieldDef( key: 'rate_pct', label: 'Rate (%)', type: MasterFieldType.number, required: true, showInList: true, ), MasterFieldDef(key: 'description', label: 'Description', showInList: true), _activeField, ], ), ]; List get masterCategories => masterDefinitions.map((def) => def.category).toSet().toList(); String masterRecordLabel(Map row) { final ratePct = row['rate_pct']; if (ratePct != null) { final rate = ratePct is num ? ratePct.toDouble() : double.tryParse(ratePct.toString()); if (rate != null) { final rateLabel = rate % 1 == 0 ? '${rate.toInt()}%' : '$rate%'; final desc = row['description']; if (desc != null && desc.toString().trim().isNotEmpty) { return '$rateLabel — ${desc.toString().trim()}'; } return rateLabel; } } for (final key in ['name', 'item_name', 'item_code']) { final value = row[key]; if (value != null && value.toString().trim().isNotEmpty) { return value.toString(); } } final code = row['code']; if (code != null && code.toString().trim().isNotEmpty) { final desc = row['description']; if (desc != null && desc.toString().trim().isNotEmpty) { return '${code.toString().trim()} — ${desc.toString().trim()}'; } return code.toString().trim(); } final description = row['description']; if (description != null && description.toString().trim().isNotEmpty) { return description.toString().trim(); } return row['id']?.toString() ?? 'Record'; } String masterCellValue(Map row, MasterFieldDef field) { final value = row[field.key]; if (value == null || value == '') return '—'; if (field.key == 'is_asset_item') { return masterIsAssetItem(value) ? 'Asset' : 'Stock'; } if (field.key == 'tags') { if (value is List) { final tags = value .map((e) => e.toString().trim()) .where((e) => e.isNotEmpty) .toList(); return tags.isEmpty ? '—' : tags.join(', '); } final text = value.toString().trim(); return text.isEmpty ? '—' : text; } if (field.type == MasterFieldType.boolean) { return value == true ? 'Yes' : 'No'; } if (field.type == MasterFieldType.dropdown) { final label = row['${field.key}_label']; if (label != null && label.toString().isNotEmpty) return label.toString(); if (field.key == 'gst_rate_id') { final nested = row['gst_rate']; if (nested is Map) { final pct = nested['rate_pct']; if (pct != null) { final rate = pct is num ? pct.toDouble() : double.tryParse(pct.toString()); if (rate != null) { return rate % 1 == 0 ? '${rate.toInt()}%' : '$rate%'; } } } } // Prefer explicit "_name" or nested ".name" / flat code from API // (e.g. asset_category_id -> asset_category_name; hsn_code_id -> hsn_code) if (field.key.endsWith('_id')) { final baseKey = field.key.substring(0, field.key.length - 3); final explicitName = row['${baseKey}_name']; if (explicitName != null && explicitName.toString().trim().isNotEmpty) { return explicitName.toString().trim(); } final nestedKeys = [ if (field.listNestedKey != null) field.listNestedKey!, baseKey, ]; for (final nestedKey in nestedKeys) { final nested = row[nestedKey]; if (nested is Map) { for (final nameKey in ['name', 'code', 'description']) { final nestedValue = nested[nameKey]; if (nestedValue != null && nestedValue.toString().trim().isNotEmpty) { return nestedValue.toString().trim(); } } } else if (nested != null && nested.toString().trim().isNotEmpty) { // Flat denormalized value (e.g. items.hsn_code string) return nested.toString().trim(); } } } if (field.staticOptions != null) { return value.toString().replaceAll('_', ' '); } } return value.toString(); } String masterStatusValue(Map row) { final active = row['is_active']; if (active == false) return 'inactive'; return 'active'; } bool masterIsAssetItem(Object? value) { if (value == true || value == 1) return true; if (value == false || value == 0) return false; final text = value?.toString().trim().toLowerCase() ?? ''; return text == 'true' || text == '1' || text == 'yes'; } /// Category list filter for Items form: ASSET when Asset Item is checked. String itemCategoryTypeForValues(Map values) => values['is_asset_item'] == true ? 'ASSET' : 'STOCK'; /// Effective options query for a form field (may depend on other values). Map? masterFieldOptionsQuery({ required String masterId, required MasterFieldDef field, required Map values, }) { if (masterId == 'items' && field.key == 'item_category_id') { return {'category_type': itemCategoryTypeForValues(values)}; } return field.optionsQueryParams; } String masterFieldDropdownLookupKey({ required String masterId, required MasterFieldDef field, required Map values, }) { final masterKey = field.optionsMasterKey; if (masterKey == null) return field.key; final params = masterFieldOptionsQuery( masterId: masterId, field: field, values: values, ); if (params == null || params.isEmpty) return masterKey; final parts = params.entries.toList() ..sort((a, b) => a.key.compareTo(b.key)); final query = parts.map((e) => '${e.key}=${e.value}').join('&'); return '$masterKey?$query'; } /// Placeholder / hint casing for master field labels (keeps UOM, HSN, GST, etc.). String masterFieldHintLabel(String label) { const acronyms = { 'uom': 'UOM', 'hsn': 'HSN', 'gst': 'GST', 'sac': 'SAC', 'po': 'PO', 'grn': 'Purchase Receipt', 'amc': 'AMC', }; return label .trim() .split(RegExp(r'\s+')) .where((part) => part.isNotEmpty) .map((part) { final lower = part.toLowerCase(); return acronyms[lower] ?? lower; }) .join(' '); } /// Display label for GST filled from HSN nested `gst_rate.description`. String? gstRateDisplayFromValues(Map values) { final nested = values['gst_rate']; if (nested is Map) { final desc = nested['description']; if (desc != null && desc.toString().trim().isNotEmpty) { return desc.toString().trim(); } final pct = nested['rate_pct']; if (pct != null) { final rate = pct is num ? pct.toDouble() : double.tryParse(pct.toString()); if (rate != null) { return rate % 1 == 0 ? '${rate.toInt()}%' : '$rate%'; } } } final label = values['gst_rate_label']; if (label != null && label.toString().trim().isNotEmpty) { return label.toString().trim(); } return null; }