bharat_erp/lib/modules/master_data/domain/entities/master_definition.dart

764 lines
23 KiB
Dart

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<String, dynamic>? optionsQueryParams;
/// Fixed dropdown choices (e.g. category type) — no API lookup.
final List<String>? 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<String, dynamic> 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<MasterFieldDef> fields;
List<MasterFieldDef> get listFields =>
fields.where((field) => field.showInList).toList();
List<MasterFieldDef> 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>[
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<String> get masterCategories =>
masterDefinitions.map((def) => def.category).toSet().toList();
String masterRecordLabel(Map<String, dynamic> 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<String, dynamic> 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 "<base>_name" or nested "<base>.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 = <String>[
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<String, dynamic> 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<String, dynamic> values) =>
values['is_asset_item'] == true ? 'ASSET' : 'STOCK';
/// Effective options query for a form field (may depend on other values).
Map<String, dynamic>? masterFieldOptionsQuery({
required String masterId,
required MasterFieldDef field,
required Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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;
}