erp_data_table 0.0.2
erp_data_table: ^0.0.2 copied to clipboard
A modern ERP-style reusable Flutter data table with grouping and totals.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:erp_data_table/erp_data_table.dart';
void main() {
runApp(const DemoApp());
}
class DemoApp extends StatelessWidget {
const DemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'ERP Data Table Example',
theme: ThemeData.dark(),
home: const ErpTableExampleScreen(),
);
}
}
class ErpTableExampleScreen extends StatefulWidget {
const ErpTableExampleScreen({super.key});
@override
State<ErpTableExampleScreen> createState() => _ErpTableExampleScreenState();
}
class _ErpTableExampleScreenState extends State<ErpTableExampleScreen> {
ErpThemeVariant _themeVariant = ErpThemeVariant.ocean;
// Selected row is now identified by MAP REFERENCE (identity), not index
Map<String, dynamic>? _selectedRow;
bool _isEditMode = false;
Map<String, String> _formValues = {};
ErpTheme get _theme => ErpTheme(_themeVariant);
// ── Form Config ─────────────────────────────────────────────────────────────
List<List<ErpFieldConfig>> get _formRows => [
[
ErpFieldConfig(
key: 'invoiceNo',
label: 'INVOICE NO',
type: ErpFieldType.dropdown,
hint: 'Select Invoice',
required: true,
flex: 2,
dropdownItems: [
'INV-001',
'INV-002',
'INV-003',
'INV-004',
'INV-005',
'INV-006',
'INV-007',
'INV-008',
'INV-009',
'INV-010',
],
),
ErpFieldConfig(
key: 'cut',
label: 'CUT',
type: ErpFieldType.dropdown,
hint: 'Select Cut',
required: true,
flex: 2,
dropdownItems: ['62.1', '63.2', '64.0', '65.5', '66.1', '67.3'],
),
ErpFieldConfig(
key: 'date',
label: 'DATE',
type: ErpFieldType.date,
hint: 'dd-mm-yyyy',
flex: 2,
),
],
[
ErpFieldConfig(
key: 'remarks',
label: 'REMARKS',
hint: 'Enter remarks...',
flex: 1,
maxLines: 1,
),
],
[
ErpFieldConfig(
key: 'totalWt',
label: 'TOTAL WT',
type: ErpFieldType.number,
hint: '0.00',
flex: 1,
),
ErpFieldConfig(
key: 'assortWt',
label: 'ASSORT WT',
type: ErpFieldType.number,
hint: '0.00',
flex: 1,
),
ErpFieldConfig(
key: 'pendingWt',
label: 'PENDING WT',
type: ErpFieldType.number,
hint: '0.00',
readOnly: true,
flex: 1,
),
ErpFieldConfig(
key: 'pendingAmt',
label: 'PENDING AMT',
type: ErpFieldType.number,
hint: '0.00',
readOnly: true,
flex: 1,
),
],
[
ErpFieldConfig(
key: 'assort',
label: 'ASSORT',
type: ErpFieldType.dropdown,
hint: 'Select',
flex: 2,
dropdownItems: ['GHAT', 'PALSA', 'BOT', 'LB', 'ROUGH', 'MIXED'],
),
ErpFieldConfig(
key: 'wt',
label: 'WT',
type: ErpFieldType.number,
hint: '0.00',
flex: 1,
),
ErpFieldConfig(
key: 'dollar',
label: '\$ DOLLAR',
type: ErpFieldType.number,
hint: '0.00',
flex: 1,
),
ErpFieldConfig(
key: 'rate',
label: 'RATE',
type: ErpFieldType.number,
hint: '0.00',
flex: 1,
),
ErpFieldConfig(
key: 'less',
label: 'LESS',
type: ErpFieldType.number,
hint: '0',
flex: 1,
),
ErpFieldConfig(
key: 'amt',
label: 'AMT',
type: ErpFieldType.number,
hint: '0.00',
readOnly: true,
flex: 2,
),
],
];
// ── Table Columns ─────────────────────────────────────────────────────────
List<ErpColumnConfig> get _tableColumns => [
const ErpColumnConfig(
key: 'invoiceNo',
label: 'INVOICE NO',
flex: 0.8,
required: true, // cannot be removed
),
const ErpColumnConfig(
key: 'date',
label: 'DATE',
flex: 1.2,
isDate: true,
required: true, // cannot be removed
),
ErpColumnConfig(
key: 'assortWt',
label: 'ASSORT WT',
flex: 0.9,
align: ColumnAlign.right,
formatter: (v) =>
double.tryParse(v.toString())?.toStringAsFixed(2) ?? v.toString(),
),
ErpColumnConfig(
key: 'amount',
label: 'AMOUNT',
flex: 1.1,
align: ColumnAlign.right,
formatter: (v) {
final n = double.tryParse(v.toString());
if (n == null) return v.toString();
return n >= 1000000
? '${(n / 1000000).toStringAsFixed(2)}M'
: n >= 1000
? '${(n / 1000).toStringAsFixed(1)}K'
: n.toStringAsFixed(2);
},
),
];
// ── Extra available columns (shown in pool, can be dragged into table) ───
List<ErpColumnConfig> get _extraColumns => [
const ErpColumnConfig(key: 'cut', label: 'CUT', flex: 0.7),
const ErpColumnConfig(key: 'assort', label: 'ASSORT', flex: 1.0),
ErpColumnConfig(
key: 'rate',
label: 'RATE',
flex: 0.8,
align: ColumnAlign.right,
formatter: (v) =>
double.tryParse(v.toString())?.toStringAsFixed(2) ?? v.toString(),
),
];
// ── Sample Data ───────────────────────────────────────────────────────────
final List<Map<String, dynamic>> _tableData = [
{
'date': '23/02/2026',
'invoiceNo': 't1',
'assortWt': 79.03,
'amount': 4676940.08,
},
{
'date': '23/02/2026',
'invoiceNo': 't1',
'assortWt': 259.00,
'amount': 15327438.70,
},
{
'date': '23/02/2026',
'invoiceNo': 't1',
'assortWt': 150.56,
'amount': 9191404.95,
},
{
'date': '21/02/2026',
'invoiceNo': 'testing',
'assortWt': 12.70,
'amount': 222308.93,
},
{
'date': '21/02/2026',
'invoiceNo': 'testing',
'assortWt': 23.81,
'amount': 358719.16,
},
{
'date': '20/02/2026',
'invoiceNo': 'tt',
'assortWt': 35.00,
'amount': 931862.40,
},
{
'date': '20/02/2026',
'invoiceNo': 'tt',
'assortWt': 125.00,
'amount': 3294120.00,
},
{
'date': '20/02/2026',
'invoiceNo': 'tt',
'assortWt': 160.00,
'amount': 3277478.40,
},
{
'date': '18/02/2026',
'invoiceNo': '65',
'assortWt': 2674.64,
'amount': 4111736.40,
},
{
'date': '18/02/2026',
'invoiceNo': '64',
'assortWt': 848.34,
'amount': 1839953.84,
},
{
'date': '18/02/2026',
'invoiceNo': '63',
'assortWt': 751.64,
'amount': 1553157.07,
},
{
'date': '18/02/2026',
'invoiceNo': '62',
'assortWt': 3386.43,
'amount': 2145082.59,
},
{
'date': '18/02/2026',
'invoiceNo': '61',
'assortWt': 4442.87,
'amount': 3037428.54,
},
{
'date': '18/02/2026',
'invoiceNo': '60',
'assortWt': 3806.23,
'amount': 2279026.44,
},
{
'date': '20/01/2026',
'invoiceNo': '59',
'assortWt': 134.83,
'amount': 269660.00,
},
{
'date': '12/01/2026',
'invoiceNo': '58',
'assortWt': 3293.62,
'amount': 2443540.50,
},
{
'date': '13/02/2026',
'invoiceNo': '57',
'assortWt': 4643.46,
'amount': 2619453.03,
},
{
'date': '13/02/2026',
'invoiceNo': '56',
'assortWt': 4643.47,
'amount': 2619453.03,
},
{
'date': '01/01/2026',
'invoiceNo': '55',
'assortWt': 4643.47,
'amount': 2619453.03,
},
{
'date': '01/01/2026',
'invoiceNo': '54',
'assortWt': 961.59,
'amount': 447570.07,
},
{
'date': '01/01/2026',
'invoiceNo': '53',
'assortWt': 3384.75,
'amount': 1181709.96,
},
{
'date': '11/12/2025',
'invoiceNo': '52',
'assortWt': 2189.51,
'amount': 390145.66,
},
{
'date': '11/12/2025',
'invoiceNo': '51',
'assortWt': 2189.52,
'amount': 390124.47,
},
{
'date': '01/01/2026',
'invoiceNo': '50',
'assortWt': 4930.29,
'amount': 2562719.40,
},
{
'date': '11/12/2025',
'invoiceNo': '49',
'assortWt': 3856.10,
'amount': 1106603.52,
},
{
'date': '11/12/2025',
'invoiceNo': '48',
'assortWt': 90.56,
'amount': 181120.00,
},
{
'date': '11/12/2025',
'invoiceNo': '47',
'assortWt': 2547.85,
'amount': 783822.38,
},
{
'date': '03/12/2025',
'invoiceNo': '46',
'assortWt': 123.51,
'amount': 988080.00,
},
];
Map<String, double> get _totals {
double totalWt = 0, totalAmt = 0;
for (final row in _tableData) {
totalWt += (row['assortWt'] as num).toDouble();
totalAmt += (row['amount'] as num).toDouble();
}
return {'assortWt': totalWt, 'amount': totalAmt};
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: _theme.bg,
body: Column(
children: [
_buildTopBar(),
Expanded(
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 3,
child: SizedBox(
width: 900,
child: ErpForm(
title: 'ROUGH ASSORT',
subtitle: 'Diamond Inventory Management',
rows: _formRows,
theme: _theme,
initialValues: _formValues,
isEditMode: _isEditMode,
onSave: (values) {
setState(() {
_formValues = {};
_isEditMode = false;
_selectedRow = null;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Record saved successfully'),
backgroundColor: _theme.primary,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
margin: const EdgeInsets.all(12),
),
);
},
onCancel: () => setState(() {
_formValues = {};
_isEditMode = false;
_selectedRow = null;
}),
onDelete: _isEditMode
? () {
setState(() {
_isEditMode = false;
_selectedRow = null;
_formValues = {};
});
}
: null,
),
),
),
const SizedBox(width: 12),
// ── Right: Table Panel
Expanded(
flex: 2,
child: ErpDataTable(
title: 'ASSORT RECORDS',
columns: _tableColumns,
availableExtraColumns: _extraColumns,
data: _tableData,
theme: _theme,
showSearch: true,
showFooterTotals: true,
totals: _totals,
// Pass the actual map reference for identity-based selection
selectedRow: _selectedRow,
onRowTap: (row) {
setState(() {
_selectedRow = row;
_isEditMode = true;
_formValues = {
'invoiceNo': row['invoiceNo'].toString(),
'date': row['date'].toString(),
'assortWt': row['assortWt'].toString(),
'amt': row['amount'].toString(),
};
});
},
),
),
],
),
),
),
],
),
);
}
Widget _buildTopBar() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.06),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
// App icon
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
gradient: LinearGradient(colors: _theme.primaryGradient),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.diamond_outlined,
color: Colors.white,
size: 18,
),
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Jay Export',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w800,
color: _theme.text,
),
),
Text(
'Diamond ERP System',
style: TextStyle(fontSize: 10, color: _theme.textLight),
),
],
),
const SizedBox(width: 20),
// Tabs
...[
('Tender', Icons.receipt_outlined),
('Purchase', Icons.shopping_bag_outlined),
('Rough Assort', Icons.grid_view_outlined),
].map((tab) {
final isActive = tab.$1 == 'Rough Assort';
return Padding(
padding: const EdgeInsets.only(right: 4),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
gradient: isActive
? LinearGradient(colors: _theme.primaryGradient)
: null,
color: isActive ? null : Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
tab.$2,
size: 13,
color: isActive ? Colors.white : _theme.textLight,
),
const SizedBox(width: 5),
Text(
tab.$1,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: isActive ? Colors.white : _theme.textLight,
),
),
if (isActive) ...[
const SizedBox(width: 5),
GestureDetector(
child: Icon(
Icons.close,
size: 12,
color: Colors.white.withOpacity(0.8),
),
),
],
],
),
),
);
}),
const Spacer(),
ErpThemeSwitcher(
current: _themeVariant,
onChanged: (v) => setState(() => _themeVariant = v),
),
const SizedBox(width: 16),
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: _theme.bg,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: _theme.border),
),
child: Icon(
Icons.notifications_outlined,
size: 18,
color: _theme.textLight,
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
gradient: LinearGradient(colors: _theme.primaryGradient),
borderRadius: BorderRadius.circular(8),
),
child: const Row(
children: [
Icon(Icons.person_outline, size: 14, color: Colors.white),
SizedBox(width: 5),
Text(
'Admin',
style: TextStyle(
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
);
}
}