grn changes
This commit is contained in:
parent
fa610b7efd
commit
83c73e31ae
@ -21,4 +21,24 @@ extension ResponsiveContext on BuildContext {
|
|||||||
if (isDesktop) return 1200;
|
if (isDesktop) return 1200;
|
||||||
return double.infinity;
|
return double.infinity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Form grid columns: 4 on medium+, 2 on small screens.
|
||||||
|
int get formGridColumns {
|
||||||
|
final width = MediaQuery.sizeOf(this).width;
|
||||||
|
return formGridColumnsForWidth(width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Responsive form field columns based on viewport width.
|
||||||
|
int formGridColumnsForWidth(
|
||||||
|
double width, {
|
||||||
|
int smallColumns = 2,
|
||||||
|
int mediumColumns = 4,
|
||||||
|
int largeColumns = 4,
|
||||||
|
double mediumBreakpoint = AppBreakpoints.tablet,
|
||||||
|
double largeBreakpoint = AppBreakpoints.desktop,
|
||||||
|
}) {
|
||||||
|
if (width >= largeBreakpoint) return largeColumns;
|
||||||
|
if (width >= mediumBreakpoint) return mediumColumns;
|
||||||
|
return smallColumns;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -138,7 +138,7 @@ class _AssetDetailScreenState extends ConsumerState<AssetDetailScreen>
|
|||||||
final confirmed = await showAppConfirmationDialog(
|
final confirmed = await showAppConfirmationDialog(
|
||||||
context: context,
|
context: context,
|
||||||
title: 'Delete Asset',
|
title: 'Delete Asset',
|
||||||
message: 'Soft delete this asset?',
|
message: 'Do you want to delete this asset?',
|
||||||
confirmLabel: 'Delete',
|
confirmLabel: 'Delete',
|
||||||
isDestructive: true,
|
isDestructive: true,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -5,17 +5,20 @@ import '../../../../shared/models/purchase_order_model.dart';
|
|||||||
import '../../../../shared/models/user_management_models.dart';
|
import '../../../../shared/models/user_management_models.dart';
|
||||||
import '../../../masters/data/datasources/master_remote_data_source.dart';
|
import '../../../masters/data/datasources/master_remote_data_source.dart';
|
||||||
import '../../../purchase_orders/data/repositories/purchase_order_repository_impl.dart';
|
import '../../../purchase_orders/data/repositories/purchase_order_repository_impl.dart';
|
||||||
|
import '../../../users/presentation/providers/users_provider.dart';
|
||||||
|
|
||||||
class GrnLookups {
|
class GrnLookups {
|
||||||
const GrnLookups({
|
const GrnLookups({
|
||||||
this.warehouses = const [],
|
this.warehouses = const [],
|
||||||
this.receivablePurchaseOrders = const [],
|
this.receivablePurchaseOrders = const [],
|
||||||
this.assetCategories = const [],
|
this.assetCategories = const [],
|
||||||
|
this.users = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
final List<FilterOptionModel> warehouses;
|
final List<FilterOptionModel> warehouses;
|
||||||
final List<PurchaseOrderModel> receivablePurchaseOrders;
|
final List<PurchaseOrderModel> receivablePurchaseOrders;
|
||||||
final List<FilterOptionModel> assetCategories;
|
final List<FilterOptionModel> assetCategories;
|
||||||
|
final List<FilterOptionModel> users;
|
||||||
}
|
}
|
||||||
|
|
||||||
final grnLookupsProvider = FutureProvider.autoDispose<GrnLookups>((ref) async {
|
final grnLookupsProvider = FutureProvider.autoDispose<GrnLookups>((ref) async {
|
||||||
@ -24,6 +27,7 @@ final grnLookupsProvider = FutureProvider.autoDispose<GrnLookups>((ref) async {
|
|||||||
|
|
||||||
final warehouses = await _safeOptions(master.listWarehouses);
|
final warehouses = await _safeOptions(master.listWarehouses);
|
||||||
final assetCategories = await _safeOptions(master.listAssetCategories);
|
final assetCategories = await _safeOptions(master.listAssetCategories);
|
||||||
|
final users = await _safeUserOptions(ref);
|
||||||
|
|
||||||
final receivablePos = <PurchaseOrderModel>[];
|
final receivablePos = <PurchaseOrderModel>[];
|
||||||
for (final status in ['APPROVED', 'PARTIALLY_RECEIVED']) {
|
for (final status in ['APPROVED', 'PARTIALLY_RECEIVED']) {
|
||||||
@ -43,6 +47,7 @@ final grnLookupsProvider = FutureProvider.autoDispose<GrnLookups>((ref) async {
|
|||||||
warehouses: warehouses,
|
warehouses: warehouses,
|
||||||
receivablePurchaseOrders: receivablePos,
|
receivablePurchaseOrders: receivablePos,
|
||||||
assetCategories: assetCategories,
|
assetCategories: assetCategories,
|
||||||
|
users: users,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -56,6 +61,45 @@ Future<List<FilterOptionModel>> _safeOptions(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<FilterOptionModel>> _safeUserOptions(Ref ref) async {
|
||||||
|
try {
|
||||||
|
final result = await ref.read(getUsersUseCaseProvider)(
|
||||||
|
const UserListQuery(
|
||||||
|
page: 1,
|
||||||
|
limit: AppConstants.maxPageSize,
|
||||||
|
status: 'active',
|
||||||
|
isActive: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (result.failure != null || result.data == null) return const [];
|
||||||
|
return result.data!.items
|
||||||
|
.where((user) {
|
||||||
|
final status = user.status.trim().toLowerCase();
|
||||||
|
return status == 'active' && user.isActive;
|
||||||
|
})
|
||||||
|
.map(
|
||||||
|
(user) => FilterOptionModel(
|
||||||
|
id: user.id,
|
||||||
|
name: user.fullName,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
} catch (_) {
|
||||||
|
return const [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final grnAssetSubcategoriesProvider =
|
||||||
|
FutureProvider.autoDispose.family<List<FilterOptionModel>, int?>(
|
||||||
|
(ref, categoryId) async {
|
||||||
|
if (categoryId == null) return const [];
|
||||||
|
final master = ref.watch(masterRemoteDataSourceProvider);
|
||||||
|
return _safeOptions(
|
||||||
|
() => master.listAssetSubcategories(assetCategoryId: categoryId),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
final grnPurchaseOrderProvider =
|
final grnPurchaseOrderProvider =
|
||||||
FutureProvider.autoDispose.family<PurchaseOrderModel?, String>((ref, poId) async {
|
FutureProvider.autoDispose.family<PurchaseOrderModel?, String>((ref, poId) async {
|
||||||
final result =
|
final result =
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import '../../../../core/constants/route_constants.dart';
|
|||||||
import '../../../../core/errors/failure.dart';
|
import '../../../../core/errors/failure.dart';
|
||||||
import '../../../../core/utils/formatters.dart';
|
import '../../../../core/utils/formatters.dart';
|
||||||
import '../../../../shared/models/grn_model.dart';
|
import '../../../../shared/models/grn_model.dart';
|
||||||
|
import '../../../../shared/models/user_management_models.dart';
|
||||||
import '../../../../shared/providers/permissions_provider.dart';
|
import '../../../../shared/providers/permissions_provider.dart';
|
||||||
import '../../../../shared/utils/file_download_helper.dart';
|
import '../../../../shared/utils/file_download_helper.dart';
|
||||||
import '../../../../shared/widgets/app_card.dart';
|
import '../../../../shared/widgets/app_card.dart';
|
||||||
@ -14,6 +15,7 @@ import '../../../../shared/widgets/app_loading_view.dart';
|
|||||||
import '../../../../shared/widgets/app_text_field.dart';
|
import '../../../../shared/widgets/app_text_field.dart';
|
||||||
import '../../../../shared/widgets/error_view.dart';
|
import '../../../../shared/widgets/error_view.dart';
|
||||||
import '../../../../shared/widgets/page_header.dart';
|
import '../../../../shared/widgets/page_header.dart';
|
||||||
|
import '../providers/grn_lookups_provider.dart';
|
||||||
import '../providers/grn_provider.dart';
|
import '../providers/grn_provider.dart';
|
||||||
import '../widgets/grn_line_items_editor.dart';
|
import '../widgets/grn_line_items_editor.dart';
|
||||||
import '../widgets/grn_status_chip.dart';
|
import '../widgets/grn_status_chip.dart';
|
||||||
@ -209,15 +211,25 @@ class _GrnDetailScreenState extends ConsumerState<GrnDetailScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _OverviewCard extends StatelessWidget {
|
class _OverviewCard extends ConsumerWidget {
|
||||||
const _OverviewCard({required this.grn});
|
const _OverviewCard({required this.grn});
|
||||||
|
|
||||||
final GrnModel grn;
|
final GrnModel grn;
|
||||||
|
|
||||||
|
String _userLabel(List<FilterOptionModel> users, int? userId) {
|
||||||
|
if (userId == null) return '—';
|
||||||
|
final match = users.where((u) => int.tryParse(u.id) == userId);
|
||||||
|
if (match.isNotEmpty) return match.first.name;
|
||||||
|
return 'User #$userId';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final hasRemarks = grn.remarks?.trim().isNotEmpty == true;
|
final users = ref.watch(grnLookupsProvider).maybeWhen(
|
||||||
|
data: (lookups) => lookups.users,
|
||||||
|
orElse: () => const <FilterOptionModel>[],
|
||||||
|
);
|
||||||
|
|
||||||
return AppCard(
|
return AppCard(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@ -254,18 +266,16 @@ class _OverviewCard extends StatelessWidget {
|
|||||||
_GrnInfo('Vehicle No', grn.vehicleNo ?? '—'),
|
_GrnInfo('Vehicle No', grn.vehicleNo ?? '—'),
|
||||||
_GrnInfo('LR No', grn.lrNo ?? '—'),
|
_GrnInfo('LR No', grn.lrNo ?? '—'),
|
||||||
_GrnInfo('LR Date', DateFormatter.displayDate(grn.lrDate)),
|
_GrnInfo('LR Date', DateFormatter.displayDate(grn.lrDate)),
|
||||||
|
_GrnInfo('Received By', _userLabel(users, grn.receivedBy)),
|
||||||
|
_GrnInfo(
|
||||||
|
'Quality Checked By',
|
||||||
|
_userLabel(users, grn.qualityCheckedBy),
|
||||||
|
),
|
||||||
|
_GrnInfo('Remarks', grn.remarks?.trim().isNotEmpty == true
|
||||||
|
? grn.remarks!
|
||||||
|
: '—'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (hasRemarks) ...[
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 20),
|
|
||||||
child: Divider(height: 1),
|
|
||||||
),
|
|
||||||
_GrnInfoGrid(
|
|
||||||
columns: 1,
|
|
||||||
items: [_GrnInfo('Remarks', grn.remarks!)],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -47,6 +47,8 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
|||||||
DateTime? _lrDate;
|
DateTime? _lrDate;
|
||||||
String? _selectedPoId;
|
String? _selectedPoId;
|
||||||
int? _warehouseId;
|
int? _warehouseId;
|
||||||
|
int? _receivedById;
|
||||||
|
int? _qualityCheckedById;
|
||||||
final List<GrnLineItemDraft> _lines = [];
|
final List<GrnLineItemDraft> _lines = [];
|
||||||
bool _isSubmitting = false;
|
bool _isSubmitting = false;
|
||||||
String? _populatedSignature;
|
String? _populatedSignature;
|
||||||
@ -88,6 +90,8 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
|||||||
_vehicleNoController.text = grn.vehicleNo ?? '';
|
_vehicleNoController.text = grn.vehicleNo ?? '';
|
||||||
_lrNoController.text = grn.lrNo ?? '';
|
_lrNoController.text = grn.lrNo ?? '';
|
||||||
_lrDate = grn.lrDate;
|
_lrDate = grn.lrDate;
|
||||||
|
_receivedById = grn.receivedBy;
|
||||||
|
_qualityCheckedById = grn.qualityCheckedBy;
|
||||||
_remarksController.text = grn.remarks ?? '';
|
_remarksController.text = grn.remarks ?? '';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -124,14 +128,6 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<AppDropdownOption<String>> _assetCategoryOptions(
|
|
||||||
List<FilterOptionModel> categories,
|
|
||||||
) {
|
|
||||||
return categories
|
|
||||||
.map((e) => AppDropdownOption(value: e.id, label: e.name))
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> _buildCreatePayload() {
|
Map<String, dynamic> _buildCreatePayload() {
|
||||||
final poId = int.tryParse(_selectedPoId ?? '');
|
final poId = int.tryParse(_selectedPoId ?? '');
|
||||||
return {
|
return {
|
||||||
@ -149,7 +145,8 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
|||||||
'vehicle_no': _vehicleNoController.text.trim(),
|
'vehicle_no': _vehicleNoController.text.trim(),
|
||||||
if (_lrNoController.text.trim().isNotEmpty) 'lr_no': _lrNoController.text.trim(),
|
if (_lrNoController.text.trim().isNotEmpty) 'lr_no': _lrNoController.text.trim(),
|
||||||
if (_lrDate != null) 'lr_date': DateFormatter.toApiDate(_lrDate!),
|
if (_lrDate != null) 'lr_date': DateFormatter.toApiDate(_lrDate!),
|
||||||
if (_remarksController.text.trim().isNotEmpty)
|
if (_receivedById != null) 'received_by': _receivedById,
|
||||||
|
if (_qualityCheckedById != null) 'quality_checked_by': _qualityCheckedById,
|
||||||
'remarks': _remarksController.text.trim(),
|
'remarks': _remarksController.text.trim(),
|
||||||
'items': _lines.map((line) => line.toPayload()).toList(),
|
'items': _lines.map((line) => line.toPayload()).toList(),
|
||||||
};
|
};
|
||||||
@ -168,7 +165,8 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
|||||||
'vehicle_no': _vehicleNoController.text.trim(),
|
'vehicle_no': _vehicleNoController.text.trim(),
|
||||||
if (_lrNoController.text.trim().isNotEmpty) 'lr_no': _lrNoController.text.trim(),
|
if (_lrNoController.text.trim().isNotEmpty) 'lr_no': _lrNoController.text.trim(),
|
||||||
if (_lrDate != null) 'lr_date': DateFormatter.toApiDate(_lrDate!),
|
if (_lrDate != null) 'lr_date': DateFormatter.toApiDate(_lrDate!),
|
||||||
if (_remarksController.text.trim().isNotEmpty)
|
if (_receivedById != null) 'received_by': _receivedById,
|
||||||
|
if (_qualityCheckedById != null) 'quality_checked_by': _qualityCheckedById,
|
||||||
'remarks': _remarksController.text.trim(),
|
'remarks': _remarksController.text.trim(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -364,7 +362,7 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
FormRowThree(
|
ResponsiveFormGrid(
|
||||||
children: [
|
children: [
|
||||||
_DateField(
|
_DateField(
|
||||||
label: 'GRN Date *',
|
label: 'GRN Date *',
|
||||||
@ -382,6 +380,7 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
|||||||
label: 'Purchase Order *',
|
label: 'Purchase Order *',
|
||||||
value: _selectedPoId,
|
value: _selectedPoId,
|
||||||
searchHint: 'Search PO...',
|
searchHint: 'Search PO...',
|
||||||
|
isDense: true,
|
||||||
options: poOptions,
|
options: poOptions,
|
||||||
onChanged: (v) async {
|
onChanged: (v) async {
|
||||||
setState(() => _selectedPoId = v);
|
setState(() => _selectedPoId = v);
|
||||||
@ -408,16 +407,22 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
|||||||
v == null ? 'Purchase order is required' : null,
|
v == null ? 'Purchase order is required' : null,
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
InputDecorator(
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8),
|
||||||
|
child: InputDecorator(
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Purchase Order',
|
labelText: 'Purchase Order',
|
||||||
|
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||||
|
isDense: true,
|
||||||
),
|
),
|
||||||
child: Text(existing?.poNumber ?? '—'),
|
child: Text(existing?.poNumber ?? '—'),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
AppSearchableDropdown<int>(
|
AppSearchableDropdown<int>(
|
||||||
label: 'Warehouse *',
|
label: 'Warehouse *',
|
||||||
value: _dropdownValue(_warehouseId, warehouseIds),
|
value: _dropdownValue(_warehouseId, warehouseIds),
|
||||||
searchHint: 'Search warehouse...',
|
searchHint: 'Search warehouse...',
|
||||||
|
isDense: true,
|
||||||
options: _intOptions(lookups.warehouses),
|
options: _intOptions(lookups.warehouses),
|
||||||
onChanged: widget.isEditing
|
onChanged: widget.isEditing
|
||||||
? (_) {}
|
? (_) {}
|
||||||
@ -427,13 +432,10 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
|||||||
: (v) => v == null ? 'Warehouse is required' : null,
|
: (v) => v == null ? 'Warehouse is required' : null,
|
||||||
enabled: !widget.isEditing,
|
enabled: !widget.isEditing,
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
FormRowThree(
|
|
||||||
children: [
|
|
||||||
AppTextField(
|
AppTextField(
|
||||||
label: 'Vendor Invoice No',
|
label: 'Vendor Invoice No',
|
||||||
controller: _vendorInvoiceNoController,
|
controller: _vendorInvoiceNoController,
|
||||||
|
isDense: true,
|
||||||
),
|
),
|
||||||
_DateField(
|
_DateField(
|
||||||
label: 'Vendor Invoice Date',
|
label: 'Vendor Invoice Date',
|
||||||
@ -449,18 +451,17 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
|||||||
controller: _vendorInvoiceAmountController,
|
controller: _vendorInvoiceAmountController,
|
||||||
keyboardType:
|
keyboardType:
|
||||||
const TextInputType.numberWithOptions(decimal: true),
|
const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
isDense: true,
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
FormRowThree(
|
|
||||||
children: [
|
|
||||||
AppTextField(
|
AppTextField(
|
||||||
label: 'Vehicle No',
|
label: 'Vehicle No',
|
||||||
controller: _vehicleNoController,
|
controller: _vehicleNoController,
|
||||||
|
isDense: true,
|
||||||
),
|
),
|
||||||
AppTextField(
|
AppTextField(
|
||||||
label: 'LR No',
|
label: 'LR No',
|
||||||
controller: _lrNoController,
|
controller: _lrNoController,
|
||||||
|
isDense: true,
|
||||||
),
|
),
|
||||||
_DateField(
|
_DateField(
|
||||||
label: 'LR Date',
|
label: 'LR Date',
|
||||||
@ -470,12 +471,36 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
|||||||
onPicked: (d) => setState(() => _lrDate = d),
|
onPicked: (d) => setState(() => _lrDate = d),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
AppSearchableDropdown<int>(
|
||||||
|
label: 'Received By',
|
||||||
|
value: _dropdownValue(
|
||||||
|
_receivedById,
|
||||||
|
lookups.users.map((e) => _parseId(e.id)).whereType<int>(),
|
||||||
|
),
|
||||||
|
searchHint: 'Search user...',
|
||||||
|
isDense: true,
|
||||||
|
options: _intOptions(lookups.users),
|
||||||
|
onChanged: (v) => setState(() => _receivedById = v),
|
||||||
|
),
|
||||||
|
AppSearchableDropdown<int>(
|
||||||
|
label: 'Quality Checked By',
|
||||||
|
value: _dropdownValue(
|
||||||
|
_qualityCheckedById,
|
||||||
|
lookups.users.map((e) => _parseId(e.id)).whereType<int>(),
|
||||||
|
),
|
||||||
|
searchHint: 'Search user...',
|
||||||
|
isDense: true,
|
||||||
|
options: _intOptions(lookups.users),
|
||||||
|
onChanged: (v) =>
|
||||||
|
setState(() => _qualityCheckedById = v),
|
||||||
),
|
),
|
||||||
AppTextField(
|
AppTextField(
|
||||||
label: 'Remarks',
|
label: 'Remarks',
|
||||||
controller: _remarksController,
|
controller: _remarksController,
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
|
isDense: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
if (!widget.isEditing) ...[
|
if (!widget.isEditing) ...[
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
@ -488,8 +513,6 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
|||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
GrnLineItemsEditor(
|
GrnLineItemsEditor(
|
||||||
items: _lines,
|
items: _lines,
|
||||||
assetCategoryOptions:
|
|
||||||
_assetCategoryOptions(lookups.assetCategories),
|
|
||||||
onChanged: () => setState(() {}),
|
onChanged: () => setState(() {}),
|
||||||
),
|
),
|
||||||
] else ...[
|
] else ...[
|
||||||
@ -539,12 +562,16 @@ class _DateField extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return InkWell(
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8),
|
||||||
|
child: InkWell(
|
||||||
onTap: enabled ? onTap : null,
|
onTap: enabled ? onTap : null,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: InputDecorator(
|
child: InputDecorator(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: label,
|
labelText: label,
|
||||||
|
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||||
|
isDense: true,
|
||||||
suffixIcon: const Icon(Icons.calendar_today_outlined, size: 18),
|
suffixIcon: const Icon(Icons.calendar_today_outlined, size: 18),
|
||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
),
|
),
|
||||||
@ -557,6 +584,7 @@ class _DateField extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,6 @@ import '../../../../shared/widgets/app_dropdown.dart';
|
|||||||
import '../../../../shared/widgets/app_empty_state.dart';
|
import '../../../../shared/widgets/app_empty_state.dart';
|
||||||
import '../../../../shared/widgets/app_loading_view.dart';
|
import '../../../../shared/widgets/app_loading_view.dart';
|
||||||
import '../../../../shared/widgets/app_pagination.dart';
|
import '../../../../shared/widgets/app_pagination.dart';
|
||||||
import '../../../../shared/widgets/app_search_field.dart';
|
|
||||||
import '../../../../shared/widgets/app_searchable_dropdown.dart';
|
import '../../../../shared/widgets/app_searchable_dropdown.dart';
|
||||||
import '../../../../shared/widgets/can_permission.dart';
|
import '../../../../shared/widgets/can_permission.dart';
|
||||||
import '../../../../shared/widgets/app_table_action_icon.dart';
|
import '../../../../shared/widgets/app_table_action_icon.dart';
|
||||||
@ -165,12 +164,21 @@ class _FiltersBar extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final searchField = SizedBox(
|
final searchField = Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8),
|
||||||
|
child: SizedBox(
|
||||||
width: wrapped ? double.infinity : null,
|
width: wrapped ? double.infinity : null,
|
||||||
child: AppSearchField(
|
child: TextField(
|
||||||
controller: searchController,
|
controller: searchController,
|
||||||
hint: 'Search GRN number, PO, vendor...',
|
|
||||||
onChanged: onSearch,
|
onChanged: onSearch,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Search',
|
||||||
|
hintText: 'Search GRN number, PO, vendor...',
|
||||||
|
prefixIcon: Icon(Icons.search, size: 20),
|
||||||
|
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||||
|
isDense: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -203,6 +211,7 @@ class _FiltersBar extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Expanded(flex: 3, child: searchField),
|
Expanded(flex: 3, child: searchField),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
|
|||||||
@ -1,12 +1,20 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../../../../core/theme/app_colors.dart';
|
import '../../../../core/theme/app_colors.dart';
|
||||||
|
import '../../../../core/utils/formatters.dart';
|
||||||
|
import '../../../../shared/models/asset_model.dart';
|
||||||
import '../../../../shared/models/grn_model.dart';
|
import '../../../../shared/models/grn_model.dart';
|
||||||
import '../../../../shared/models/purchase_order_model.dart';
|
import '../../../../shared/models/purchase_order_model.dart';
|
||||||
|
import '../../../../shared/models/user_management_models.dart';
|
||||||
import '../../../../shared/widgets/app_data_table.dart';
|
import '../../../../shared/widgets/app_data_table.dart';
|
||||||
import '../../../../shared/widgets/app_dropdown.dart';
|
import '../../../../shared/widgets/app_dropdown.dart';
|
||||||
|
import '../../../../shared/widgets/app_searchable_dropdown.dart';
|
||||||
|
import '../../../../shared/widgets/app_side_panel.dart';
|
||||||
import '../../../../shared/widgets/app_text_field.dart';
|
import '../../../../shared/widgets/app_text_field.dart';
|
||||||
|
import '../../../assets/presentation/providers/asset_categories_provider.dart';
|
||||||
|
import '../providers/grn_lookups_provider.dart';
|
||||||
|
|
||||||
String _formatQty(double value) {
|
String _formatQty(double value) {
|
||||||
if (value % 1 == 0) return value.toInt().toString();
|
if (value % 1 == 0) return value.toInt().toString();
|
||||||
@ -22,22 +30,27 @@ class GrnLineItemDraft {
|
|||||||
required this.receivedQty,
|
required this.receivedQty,
|
||||||
required this.remainingQty,
|
required this.remainingQty,
|
||||||
TextEditingController? acceptedQtyController,
|
TextEditingController? acceptedQtyController,
|
||||||
TextEditingController? damagedQtyController,
|
TextEditingController? rejectedQtyController,
|
||||||
TextEditingController? shortQtyController,
|
TextEditingController? rateController,
|
||||||
TextEditingController? excessQtyController,
|
|
||||||
TextEditingController? batchNoController,
|
TextEditingController? batchNoController,
|
||||||
TextEditingController? expiryDateController,
|
TextEditingController? storageLocationController,
|
||||||
|
TextEditingController? rejectionReasonController,
|
||||||
TextEditingController? remarksController,
|
TextEditingController? remarksController,
|
||||||
|
this.mfgDate,
|
||||||
|
this.expiryDate,
|
||||||
this.assetCategoryId,
|
this.assetCategoryId,
|
||||||
|
this.assetSubcategoryId,
|
||||||
}) : acceptedQtyController = acceptedQtyController ??
|
}) : acceptedQtyController = acceptedQtyController ??
|
||||||
TextEditingController(
|
TextEditingController(
|
||||||
text: remainingQty > 0 ? _formatQty(remainingQty) : '',
|
text: remainingQty > 0 ? _formatQty(remainingQty) : '',
|
||||||
),
|
),
|
||||||
damagedQtyController = damagedQtyController ?? TextEditingController(),
|
rejectedQtyController = rejectedQtyController ?? TextEditingController(),
|
||||||
shortQtyController = shortQtyController ?? TextEditingController(),
|
rateController = rateController ?? TextEditingController(),
|
||||||
excessQtyController = excessQtyController ?? TextEditingController(),
|
|
||||||
batchNoController = batchNoController ?? TextEditingController(),
|
batchNoController = batchNoController ?? TextEditingController(),
|
||||||
expiryDateController = expiryDateController ?? TextEditingController(),
|
storageLocationController =
|
||||||
|
storageLocationController ?? TextEditingController(),
|
||||||
|
rejectionReasonController =
|
||||||
|
rejectionReasonController ?? TextEditingController(),
|
||||||
remarksController = remarksController ?? TextEditingController();
|
remarksController = remarksController ?? TextEditingController();
|
||||||
|
|
||||||
final String poItemId;
|
final String poItemId;
|
||||||
@ -47,27 +60,28 @@ class GrnLineItemDraft {
|
|||||||
final double receivedQty;
|
final double receivedQty;
|
||||||
final double remainingQty;
|
final double remainingQty;
|
||||||
final TextEditingController acceptedQtyController;
|
final TextEditingController acceptedQtyController;
|
||||||
final TextEditingController damagedQtyController;
|
final TextEditingController rejectedQtyController;
|
||||||
final TextEditingController shortQtyController;
|
final TextEditingController rateController;
|
||||||
final TextEditingController excessQtyController;
|
|
||||||
final TextEditingController batchNoController;
|
final TextEditingController batchNoController;
|
||||||
final TextEditingController expiryDateController;
|
final TextEditingController storageLocationController;
|
||||||
|
final TextEditingController rejectionReasonController;
|
||||||
final TextEditingController remarksController;
|
final TextEditingController remarksController;
|
||||||
String? assetCategoryId;
|
DateTime? mfgDate;
|
||||||
|
DateTime? expiryDate;
|
||||||
|
int? assetCategoryId;
|
||||||
|
int? assetSubcategoryId;
|
||||||
|
|
||||||
double get acceptedQty => double.tryParse(acceptedQtyController.text.trim()) ?? 0;
|
double get acceptedQty => double.tryParse(acceptedQtyController.text.trim()) ?? 0;
|
||||||
double get damagedQty => double.tryParse(damagedQtyController.text.trim()) ?? 0;
|
double get rejectedQty => double.tryParse(rejectedQtyController.text.trim()) ?? 0;
|
||||||
double get shortQty => double.tryParse(shortQtyController.text.trim()) ?? 0;
|
double get currentQty => acceptedQty + rejectedQty;
|
||||||
double get excessQty => double.tryParse(excessQtyController.text.trim()) ?? 0;
|
|
||||||
double get currentQty => acceptedQty + damagedQty + shortQty + excessQty;
|
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
acceptedQtyController.dispose();
|
acceptedQtyController.dispose();
|
||||||
damagedQtyController.dispose();
|
rejectedQtyController.dispose();
|
||||||
shortQtyController.dispose();
|
rateController.dispose();
|
||||||
excessQtyController.dispose();
|
|
||||||
batchNoController.dispose();
|
batchNoController.dispose();
|
||||||
expiryDateController.dispose();
|
storageLocationController.dispose();
|
||||||
|
rejectionReasonController.dispose();
|
||||||
remarksController.dispose();
|
remarksController.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,15 +92,19 @@ class GrnLineItemDraft {
|
|||||||
'line_no': lineNo,
|
'line_no': lineNo,
|
||||||
'current_qty': currentQty,
|
'current_qty': currentQty,
|
||||||
'accepted_qty': acceptedQty,
|
'accepted_qty': acceptedQty,
|
||||||
if (damagedQty > 0) 'damaged_qty': damagedQty,
|
if (rejectedQty > 0) 'rejected_qty': rejectedQty,
|
||||||
if (shortQty > 0) 'short_qty': shortQty,
|
if (rejectionReasonController.text.trim().isNotEmpty)
|
||||||
if (excessQty > 0) 'excess_qty': excessQty,
|
'rejection_reason': rejectionReasonController.text.trim(),
|
||||||
|
if (rateController.text.trim().isNotEmpty)
|
||||||
|
'rate': double.tryParse(rateController.text.trim()),
|
||||||
if (batchNoController.text.trim().isNotEmpty)
|
if (batchNoController.text.trim().isNotEmpty)
|
||||||
'batch_no': batchNoController.text.trim(),
|
'batch_no': batchNoController.text.trim(),
|
||||||
if (expiryDateController.text.trim().isNotEmpty)
|
if (mfgDate != null) 'mfg_date': DateFormatter.toApiDate(mfgDate!),
|
||||||
'expiry_date': expiryDateController.text.trim(),
|
if (expiryDate != null) 'expiry_date': DateFormatter.toApiDate(expiryDate!),
|
||||||
if (assetCategoryId != null && assetCategoryId!.isNotEmpty)
|
if (storageLocationController.text.trim().isNotEmpty)
|
||||||
'asset_category_id': int.tryParse(assetCategoryId!) ?? assetCategoryId,
|
'storage_location': storageLocationController.text.trim(),
|
||||||
|
if (assetCategoryId != null) 'asset_category_id': assetCategoryId,
|
||||||
|
if (assetSubcategoryId != null) 'asset_subcategory_id': assetSubcategoryId,
|
||||||
if (remarksController.text.trim().isNotEmpty)
|
if (remarksController.text.trim().isNotEmpty)
|
||||||
'remarks': remarksController.text.trim(),
|
'remarks': remarksController.text.trim(),
|
||||||
};
|
};
|
||||||
@ -98,6 +116,7 @@ List<GrnLineItemDraft> draftsFromPurchaseOrder(PurchaseOrderModel po) {
|
|||||||
final ordered = item.orderedQty ?? 0;
|
final ordered = item.orderedQty ?? 0;
|
||||||
final received = item.receivedQty ?? 0;
|
final received = item.receivedQty ?? 0;
|
||||||
final remaining = (ordered - received).clamp(0.0, double.infinity);
|
final remaining = (ordered - received).clamp(0.0, double.infinity);
|
||||||
|
final rate = item.rate;
|
||||||
return GrnLineItemDraft(
|
return GrnLineItemDraft(
|
||||||
poItemId: item.id,
|
poItemId: item.id,
|
||||||
lineNo: item.lineNo ?? 1,
|
lineNo: item.lineNo ?? 1,
|
||||||
@ -105,6 +124,9 @@ List<GrnLineItemDraft> draftsFromPurchaseOrder(PurchaseOrderModel po) {
|
|||||||
orderedQty: ordered,
|
orderedQty: ordered,
|
||||||
receivedQty: received,
|
receivedQty: received,
|
||||||
remainingQty: remaining,
|
remainingQty: remaining,
|
||||||
|
rateController: TextEditingController(
|
||||||
|
text: rate != null ? _formatQty(rate) : '',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
@ -114,13 +136,11 @@ class GrnLineItemsEditor extends StatelessWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.items,
|
required this.items,
|
||||||
required this.onChanged,
|
required this.onChanged,
|
||||||
this.assetCategoryOptions = const [],
|
|
||||||
this.readOnly = false,
|
this.readOnly = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final List<GrnLineItemDraft> items;
|
final List<GrnLineItemDraft> items;
|
||||||
final VoidCallback onChanged;
|
final VoidCallback onChanged;
|
||||||
final List<AppDropdownOption<String>> assetCategoryOptions;
|
|
||||||
final bool readOnly;
|
final bool readOnly;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -152,7 +172,6 @@ class GrnLineItemsEditor extends StatelessWidget {
|
|||||||
padding: EdgeInsets.only(bottom: i == items.length - 1 ? 0 : 12),
|
padding: EdgeInsets.only(bottom: i == items.length - 1 ? 0 : 12),
|
||||||
child: _GrnLineItemCard(
|
child: _GrnLineItemCard(
|
||||||
item: items[i],
|
item: items[i],
|
||||||
assetCategoryOptions: assetCategoryOptions,
|
|
||||||
readOnly: readOnly,
|
readOnly: readOnly,
|
||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
),
|
),
|
||||||
@ -162,25 +181,80 @@ class GrnLineItemsEditor extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _GrnLineItemCard extends StatelessWidget {
|
class _GrnLineItemCard extends ConsumerWidget {
|
||||||
const _GrnLineItemCard({
|
const _GrnLineItemCard({
|
||||||
required this.item,
|
required this.item,
|
||||||
required this.onChanged,
|
required this.onChanged,
|
||||||
required this.assetCategoryOptions,
|
|
||||||
required this.readOnly,
|
required this.readOnly,
|
||||||
});
|
});
|
||||||
|
|
||||||
final GrnLineItemDraft item;
|
final GrnLineItemDraft item;
|
||||||
final VoidCallback onChanged;
|
final VoidCallback onChanged;
|
||||||
final List<AppDropdownOption<String>> assetCategoryOptions;
|
|
||||||
final bool readOnly;
|
final bool readOnly;
|
||||||
|
|
||||||
static final _qtyFormatters = [
|
static final _qtyFormatters = [
|
||||||
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,4}')),
|
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,4}')),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
int? _dropdownValue(int? selected, Iterable<int> validIds) {
|
||||||
|
if (selected == null) return null;
|
||||||
|
return validIds.contains(selected) ? selected : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<AppDropdownOption<int>> _intOptions(List<FilterOptionModel> options) {
|
||||||
|
return options
|
||||||
|
.map((e) {
|
||||||
|
final id = int.tryParse(e.id);
|
||||||
|
if (id == null) return null;
|
||||||
|
return AppDropdownOption(value: id, label: e.name);
|
||||||
|
})
|
||||||
|
.whereType<AppDropdownOption<int>>()
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<AppDropdownOption<int>> _categoryOptions(
|
||||||
|
List<AssetCategoryModel> categories,
|
||||||
|
) {
|
||||||
|
return categories
|
||||||
|
.map((c) {
|
||||||
|
final id = int.tryParse(c.id);
|
||||||
|
if (id == null) return null;
|
||||||
|
return AppDropdownOption(value: id, label: c.name);
|
||||||
|
})
|
||||||
|
.whereType<AppDropdownOption<int>>()
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pickDate(
|
||||||
|
BuildContext context, {
|
||||||
|
required DateTime? current,
|
||||||
|
required ValueChanged<DateTime?> onPicked,
|
||||||
|
}) async {
|
||||||
|
final picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: current ?? DateTime.now(),
|
||||||
|
firstDate: DateTime(2020),
|
||||||
|
lastDate: DateTime(2100),
|
||||||
|
);
|
||||||
|
if (picked != null) onPicked(picked);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final categoryId = item.assetCategoryId;
|
||||||
|
final categoriesAsync = ref.watch(assetCategoriesProvider);
|
||||||
|
final categoryOptions = categoriesAsync.maybeWhen(
|
||||||
|
data: _categoryOptions,
|
||||||
|
orElse: () => const <AppDropdownOption<int>>[],
|
||||||
|
);
|
||||||
|
final categoryIds = categoryOptions.map((e) => e.value);
|
||||||
|
final subcategoriesAsync = ref.watch(grnAssetSubcategoriesProvider(categoryId));
|
||||||
|
final subcategoryOptions = subcategoriesAsync.maybeWhen(
|
||||||
|
data: _intOptions,
|
||||||
|
orElse: () => const <AppDropdownOption<int>>[],
|
||||||
|
);
|
||||||
|
final subcategoryIds = subcategoryOptions.map((e) => e.value);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@ -218,94 +292,20 @@ class _GrnLineItemCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!readOnly) ...[
|
if (!readOnly) ...[
|
||||||
const SizedBox(height: 12),
|
ResponsiveFormGrid(
|
||||||
Row(
|
children: _buildLineItemFields(
|
||||||
children: [
|
context: context,
|
||||||
Expanded(
|
categoryOptions: categoryOptions,
|
||||||
child: AppTextField(
|
categoryIds: categoryIds,
|
||||||
label: 'Accepted Qty *',
|
subcategoryOptions: subcategoryOptions,
|
||||||
controller: item.acceptedQtyController,
|
subcategoryIds: subcategoryIds,
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
|
||||||
inputFormatters: _qtyFormatters,
|
|
||||||
onChanged: (_) => onChanged(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: AppTextField(
|
|
||||||
label: 'Damaged Qty',
|
|
||||||
controller: item.damagedQtyController,
|
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
|
||||||
inputFormatters: _qtyFormatters,
|
|
||||||
onChanged: (_) => onChanged(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: AppTextField(
|
|
||||||
label: 'Short Qty',
|
|
||||||
controller: item.shortQtyController,
|
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
|
||||||
inputFormatters: _qtyFormatters,
|
|
||||||
onChanged: (_) => onChanged(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: AppTextField(
|
|
||||||
label: 'Excess Qty',
|
|
||||||
controller: item.excessQtyController,
|
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
|
||||||
inputFormatters: _qtyFormatters,
|
|
||||||
onChanged: (_) => onChanged(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: AppTextField(
|
|
||||||
label: 'Batch No',
|
|
||||||
controller: item.batchNoController,
|
|
||||||
onChanged: (_) => onChanged(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: AppTextField(
|
|
||||||
label: 'Expiry Date',
|
|
||||||
controller: item.expiryDateController,
|
|
||||||
hint: 'YYYY-MM-DD',
|
|
||||||
onChanged: (_) => onChanged(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (assetCategoryOptions.isNotEmpty) ...[
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
AppDropdown<String>(
|
|
||||||
label: 'Asset Category',
|
|
||||||
value: item.assetCategoryId,
|
|
||||||
options: assetCategoryOptions,
|
|
||||||
onChanged: (v) {
|
|
||||||
item.assetCategoryId = v;
|
|
||||||
onChanged();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
AppTextField(
|
|
||||||
label: 'Remarks',
|
|
||||||
controller: item.remarksController,
|
|
||||||
maxLines: 2,
|
|
||||||
onChanged: (_) => onChanged(),
|
|
||||||
),
|
|
||||||
] else ...[
|
] else ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'Accepted: ${_formatQty(item.acceptedQty)} · '
|
'Accepted: ${_formatQty(item.acceptedQty)} · '
|
||||||
|
'Rejected: ${_formatQty(item.rejectedQty)} · '
|
||||||
'Current: ${_formatQty(item.currentQty)}',
|
'Current: ${_formatQty(item.currentQty)}',
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
@ -314,6 +314,160 @@ class _GrnLineItemCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildLineItemFields({
|
||||||
|
required BuildContext context,
|
||||||
|
required List<AppDropdownOption<int>> categoryOptions,
|
||||||
|
required Iterable<int> categoryIds,
|
||||||
|
required List<AppDropdownOption<int>> subcategoryOptions,
|
||||||
|
required Iterable<int> subcategoryIds,
|
||||||
|
}) {
|
||||||
|
final hasCategory = item.assetCategoryId != null;
|
||||||
|
return [
|
||||||
|
AppTextField(
|
||||||
|
label: 'Accepted Qty *',
|
||||||
|
controller: item.acceptedQtyController,
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
inputFormatters: _qtyFormatters,
|
||||||
|
isDense: true,
|
||||||
|
onChanged: (_) => onChanged(),
|
||||||
|
),
|
||||||
|
AppTextField(
|
||||||
|
label: 'Rejected Qty',
|
||||||
|
controller: item.rejectedQtyController,
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
inputFormatters: _qtyFormatters,
|
||||||
|
isDense: true,
|
||||||
|
onChanged: (_) => onChanged(),
|
||||||
|
),
|
||||||
|
AppTextField(
|
||||||
|
label: 'Rate',
|
||||||
|
controller: item.rateController,
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
inputFormatters: _qtyFormatters,
|
||||||
|
isDense: true,
|
||||||
|
onChanged: (_) => onChanged(),
|
||||||
|
),
|
||||||
|
AppTextField(
|
||||||
|
label: 'Batch No',
|
||||||
|
controller: item.batchNoController,
|
||||||
|
isDense: true,
|
||||||
|
onChanged: (_) => onChanged(),
|
||||||
|
),
|
||||||
|
_GrnLineDateField(
|
||||||
|
label: 'Mfg Date',
|
||||||
|
value: item.mfgDate,
|
||||||
|
onTap: () => _pickDate(
|
||||||
|
context,
|
||||||
|
current: item.mfgDate,
|
||||||
|
onPicked: (date) {
|
||||||
|
item.mfgDate = date;
|
||||||
|
onChanged();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_GrnLineDateField(
|
||||||
|
label: 'Expiry Date',
|
||||||
|
value: item.expiryDate,
|
||||||
|
onTap: () => _pickDate(
|
||||||
|
context,
|
||||||
|
current: item.expiryDate,
|
||||||
|
onPicked: (date) {
|
||||||
|
item.expiryDate = date;
|
||||||
|
onChanged();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AppTextField(
|
||||||
|
label: 'Storage Location',
|
||||||
|
controller: item.storageLocationController,
|
||||||
|
isDense: true,
|
||||||
|
onChanged: (_) => onChanged(),
|
||||||
|
),
|
||||||
|
AppTextField(
|
||||||
|
label: 'Rejection Reason',
|
||||||
|
controller: item.rejectionReasonController,
|
||||||
|
isDense: true,
|
||||||
|
onChanged: (_) => onChanged(),
|
||||||
|
),
|
||||||
|
AppSearchableDropdown<int>(
|
||||||
|
key: ValueKey('line_${item.lineNo}_asset_category'),
|
||||||
|
label: 'Asset Category',
|
||||||
|
value: _dropdownValue(item.assetCategoryId, categoryIds),
|
||||||
|
searchHint: 'Search asset category...',
|
||||||
|
isDense: true,
|
||||||
|
enabled: categoryOptions.isNotEmpty,
|
||||||
|
options: categoryOptions,
|
||||||
|
onChanged: (v) {
|
||||||
|
item.assetCategoryId = v;
|
||||||
|
item.assetSubcategoryId = null;
|
||||||
|
onChanged();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
AppSearchableDropdown<int>(
|
||||||
|
key: ValueKey('line_${item.lineNo}_asset_subcategory'),
|
||||||
|
label: 'Asset Subcategory',
|
||||||
|
value: _dropdownValue(item.assetSubcategoryId, subcategoryIds),
|
||||||
|
searchHint: 'Search subcategory...',
|
||||||
|
isDense: true,
|
||||||
|
enabled: hasCategory && subcategoryOptions.isNotEmpty,
|
||||||
|
hint: !hasCategory
|
||||||
|
? 'Select category first'
|
||||||
|
: subcategoryOptions.isEmpty
|
||||||
|
? 'No subcategories found'
|
||||||
|
: null,
|
||||||
|
options: subcategoryOptions,
|
||||||
|
onChanged: (v) {
|
||||||
|
item.assetSubcategoryId = v;
|
||||||
|
onChanged();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
AppTextField(
|
||||||
|
label: 'Remarks',
|
||||||
|
controller: item.remarksController,
|
||||||
|
isDense: true,
|
||||||
|
maxLines: 2,
|
||||||
|
onChanged: (_) => onChanged(),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GrnLineDateField extends StatelessWidget {
|
||||||
|
const _GrnLineDateField({
|
||||||
|
required this.label,
|
||||||
|
required this.value,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final DateTime? value;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: InputDecorator(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: label,
|
||||||
|
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||||
|
isDense: true,
|
||||||
|
suffixIcon: const Icon(Icons.calendar_today_outlined, size: 18),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
value != null ? DateFormatter.displayDate(value) : 'Select date',
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||||
|
color: value != null ? null : Theme.of(context).hintColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GrnItemsTable extends StatelessWidget {
|
class GrnItemsTable extends StatelessWidget {
|
||||||
@ -344,25 +498,47 @@ class GrnItemsTable extends StatelessWidget {
|
|||||||
cellBuilder: (_, item) => Text(_formatQty(item.acceptedQty ?? 0)),
|
cellBuilder: (_, item) => Text(_formatQty(item.acceptedQty ?? 0)),
|
||||||
),
|
),
|
||||||
AppDataColumn(
|
AppDataColumn(
|
||||||
label: 'Damaged',
|
label: 'Rejected',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
cellBuilder: (_, item) => Text(_formatQty(item.damagedQty ?? 0)),
|
cellBuilder: (_, item) => Text(_formatQty(item.rejectedQty ?? 0)),
|
||||||
),
|
),
|
||||||
AppDataColumn(
|
AppDataColumn(
|
||||||
label: 'Short',
|
label: 'Rate',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
cellBuilder: (_, item) => Text(_formatQty(item.shortQty ?? 0)),
|
cellBuilder: (_, item) => Text(
|
||||||
|
item.rate != null ? _formatQty(item.rate!) : '—',
|
||||||
),
|
),
|
||||||
AppDataColumn(
|
|
||||||
label: 'Excess',
|
|
||||||
flex: 1,
|
|
||||||
cellBuilder: (_, item) => Text(_formatQty(item.excessQty ?? 0)),
|
|
||||||
),
|
),
|
||||||
AppDataColumn(
|
AppDataColumn(
|
||||||
label: 'Batch',
|
label: 'Batch',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
cellBuilder: (_, item) => Text(item.batchNo ?? '—'),
|
cellBuilder: (_, item) => Text(item.batchNo ?? '—'),
|
||||||
),
|
),
|
||||||
|
AppDataColumn(
|
||||||
|
label: 'Mfg Date',
|
||||||
|
flex: 1,
|
||||||
|
cellBuilder: (_, item) => Text(DateFormatter.displayDate(item.mfgDate)),
|
||||||
|
),
|
||||||
|
AppDataColumn(
|
||||||
|
label: 'Expiry',
|
||||||
|
flex: 1,
|
||||||
|
cellBuilder: (_, item) => Text(DateFormatter.displayDate(item.expiryDate)),
|
||||||
|
),
|
||||||
|
AppDataColumn(
|
||||||
|
label: 'Storage',
|
||||||
|
flex: 1,
|
||||||
|
cellBuilder: (_, item) => Text(item.storageLocation ?? '—'),
|
||||||
|
),
|
||||||
|
AppDataColumn(
|
||||||
|
label: 'Rejection Reason',
|
||||||
|
flex: 2,
|
||||||
|
cellBuilder: (_, item) => Text(item.rejectionReason ?? '—'),
|
||||||
|
),
|
||||||
|
AppDataColumn(
|
||||||
|
label: 'Remarks',
|
||||||
|
flex: 2,
|
||||||
|
cellBuilder: (_, item) => Text(item.remarks ?? '—'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
rows: items,
|
rows: items,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -151,6 +151,8 @@ class GrnItemModel with _$GrnItemModel {
|
|||||||
@JsonKey(name: 'storage_location') String? storageLocation,
|
@JsonKey(name: 'storage_location') String? storageLocation,
|
||||||
@JsonKey(name: 'asset_category_id', fromJson: _intFromJsonNullable)
|
@JsonKey(name: 'asset_category_id', fromJson: _intFromJsonNullable)
|
||||||
int? assetCategoryId,
|
int? assetCategoryId,
|
||||||
|
@JsonKey(name: 'asset_subcategory_id', fromJson: _intFromJsonNullable)
|
||||||
|
int? assetSubcategoryId,
|
||||||
@JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable) int? uomId,
|
@JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable) int? uomId,
|
||||||
@JsonKey(name: 'uom_name', readValue: _readUomName) String? uomName,
|
@JsonKey(name: 'uom_name', readValue: _readUomName) String? uomName,
|
||||||
String? remarks,
|
String? remarks,
|
||||||
|
|||||||
@ -813,6 +813,8 @@ mixin _$GrnItemModel {
|
|||||||
String? get storageLocation => throw _privateConstructorUsedError;
|
String? get storageLocation => throw _privateConstructorUsedError;
|
||||||
@JsonKey(name: 'asset_category_id', fromJson: _intFromJsonNullable)
|
@JsonKey(name: 'asset_category_id', fromJson: _intFromJsonNullable)
|
||||||
int? get assetCategoryId => throw _privateConstructorUsedError;
|
int? get assetCategoryId => throw _privateConstructorUsedError;
|
||||||
|
@JsonKey(name: 'asset_subcategory_id', fromJson: _intFromJsonNullable)
|
||||||
|
int? get assetSubcategoryId => throw _privateConstructorUsedError;
|
||||||
@JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable)
|
@JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable)
|
||||||
int? get uomId => throw _privateConstructorUsedError;
|
int? get uomId => throw _privateConstructorUsedError;
|
||||||
@JsonKey(name: 'uom_name', readValue: _readUomName)
|
@JsonKey(name: 'uom_name', readValue: _readUomName)
|
||||||
@ -866,6 +868,8 @@ abstract class $GrnItemModelCopyWith<$Res> {
|
|||||||
@JsonKey(name: 'storage_location') String? storageLocation,
|
@JsonKey(name: 'storage_location') String? storageLocation,
|
||||||
@JsonKey(name: 'asset_category_id', fromJson: _intFromJsonNullable)
|
@JsonKey(name: 'asset_category_id', fromJson: _intFromJsonNullable)
|
||||||
int? assetCategoryId,
|
int? assetCategoryId,
|
||||||
|
@JsonKey(name: 'asset_subcategory_id', fromJson: _intFromJsonNullable)
|
||||||
|
int? assetSubcategoryId,
|
||||||
@JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable) int? uomId,
|
@JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable) int? uomId,
|
||||||
@JsonKey(name: 'uom_name', readValue: _readUomName) String? uomName,
|
@JsonKey(name: 'uom_name', readValue: _readUomName) String? uomName,
|
||||||
String? remarks,
|
String? remarks,
|
||||||
@ -907,6 +911,7 @@ class _$GrnItemModelCopyWithImpl<$Res, $Val extends GrnItemModel>
|
|||||||
Object? expiryDate = freezed,
|
Object? expiryDate = freezed,
|
||||||
Object? storageLocation = freezed,
|
Object? storageLocation = freezed,
|
||||||
Object? assetCategoryId = freezed,
|
Object? assetCategoryId = freezed,
|
||||||
|
Object? assetSubcategoryId = freezed,
|
||||||
Object? uomId = freezed,
|
Object? uomId = freezed,
|
||||||
Object? uomName = freezed,
|
Object? uomName = freezed,
|
||||||
Object? remarks = freezed,
|
Object? remarks = freezed,
|
||||||
@ -993,6 +998,10 @@ class _$GrnItemModelCopyWithImpl<$Res, $Val extends GrnItemModel>
|
|||||||
? _value.assetCategoryId
|
? _value.assetCategoryId
|
||||||
: assetCategoryId // ignore: cast_nullable_to_non_nullable
|
: assetCategoryId // ignore: cast_nullable_to_non_nullable
|
||||||
as int?,
|
as int?,
|
||||||
|
assetSubcategoryId: freezed == assetSubcategoryId
|
||||||
|
? _value.assetSubcategoryId
|
||||||
|
: assetSubcategoryId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int?,
|
||||||
uomId: freezed == uomId
|
uomId: freezed == uomId
|
||||||
? _value.uomId
|
? _value.uomId
|
||||||
: uomId // ignore: cast_nullable_to_non_nullable
|
: uomId // ignore: cast_nullable_to_non_nullable
|
||||||
@ -1050,6 +1059,8 @@ abstract class _$$GrnItemModelImplCopyWith<$Res>
|
|||||||
@JsonKey(name: 'storage_location') String? storageLocation,
|
@JsonKey(name: 'storage_location') String? storageLocation,
|
||||||
@JsonKey(name: 'asset_category_id', fromJson: _intFromJsonNullable)
|
@JsonKey(name: 'asset_category_id', fromJson: _intFromJsonNullable)
|
||||||
int? assetCategoryId,
|
int? assetCategoryId,
|
||||||
|
@JsonKey(name: 'asset_subcategory_id', fromJson: _intFromJsonNullable)
|
||||||
|
int? assetSubcategoryId,
|
||||||
@JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable) int? uomId,
|
@JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable) int? uomId,
|
||||||
@JsonKey(name: 'uom_name', readValue: _readUomName) String? uomName,
|
@JsonKey(name: 'uom_name', readValue: _readUomName) String? uomName,
|
||||||
String? remarks,
|
String? remarks,
|
||||||
@ -1090,6 +1101,7 @@ class __$$GrnItemModelImplCopyWithImpl<$Res>
|
|||||||
Object? expiryDate = freezed,
|
Object? expiryDate = freezed,
|
||||||
Object? storageLocation = freezed,
|
Object? storageLocation = freezed,
|
||||||
Object? assetCategoryId = freezed,
|
Object? assetCategoryId = freezed,
|
||||||
|
Object? assetSubcategoryId = freezed,
|
||||||
Object? uomId = freezed,
|
Object? uomId = freezed,
|
||||||
Object? uomName = freezed,
|
Object? uomName = freezed,
|
||||||
Object? remarks = freezed,
|
Object? remarks = freezed,
|
||||||
@ -1176,6 +1188,10 @@ class __$$GrnItemModelImplCopyWithImpl<$Res>
|
|||||||
? _value.assetCategoryId
|
? _value.assetCategoryId
|
||||||
: assetCategoryId // ignore: cast_nullable_to_non_nullable
|
: assetCategoryId // ignore: cast_nullable_to_non_nullable
|
||||||
as int?,
|
as int?,
|
||||||
|
assetSubcategoryId: freezed == assetSubcategoryId
|
||||||
|
? _value.assetSubcategoryId
|
||||||
|
: assetSubcategoryId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int?,
|
||||||
uomId: freezed == uomId
|
uomId: freezed == uomId
|
||||||
? _value.uomId
|
? _value.uomId
|
||||||
: uomId // ignore: cast_nullable_to_non_nullable
|
: uomId // ignore: cast_nullable_to_non_nullable
|
||||||
@ -1225,6 +1241,8 @@ class _$GrnItemModelImpl implements _GrnItemModel {
|
|||||||
@JsonKey(name: 'storage_location') this.storageLocation,
|
@JsonKey(name: 'storage_location') this.storageLocation,
|
||||||
@JsonKey(name: 'asset_category_id', fromJson: _intFromJsonNullable)
|
@JsonKey(name: 'asset_category_id', fromJson: _intFromJsonNullable)
|
||||||
this.assetCategoryId,
|
this.assetCategoryId,
|
||||||
|
@JsonKey(name: 'asset_subcategory_id', fromJson: _intFromJsonNullable)
|
||||||
|
this.assetSubcategoryId,
|
||||||
@JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable) this.uomId,
|
@JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable) this.uomId,
|
||||||
@JsonKey(name: 'uom_name', readValue: _readUomName) this.uomName,
|
@JsonKey(name: 'uom_name', readValue: _readUomName) this.uomName,
|
||||||
this.remarks,
|
this.remarks,
|
||||||
@ -1294,6 +1312,9 @@ class _$GrnItemModelImpl implements _GrnItemModel {
|
|||||||
@JsonKey(name: 'asset_category_id', fromJson: _intFromJsonNullable)
|
@JsonKey(name: 'asset_category_id', fromJson: _intFromJsonNullable)
|
||||||
final int? assetCategoryId;
|
final int? assetCategoryId;
|
||||||
@override
|
@override
|
||||||
|
@JsonKey(name: 'asset_subcategory_id', fromJson: _intFromJsonNullable)
|
||||||
|
final int? assetSubcategoryId;
|
||||||
|
@override
|
||||||
@JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable)
|
@JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable)
|
||||||
final int? uomId;
|
final int? uomId;
|
||||||
@override
|
@override
|
||||||
@ -1304,7 +1325,7 @@ class _$GrnItemModelImpl implements _GrnItemModel {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'GrnItemModel(id: $id, grnId: $grnId, poItemId: $poItemId, itemId: $itemId, itemCode: $itemCode, itemName: $itemName, lineNo: $lineNo, currentQty: $currentQty, acceptedQty: $acceptedQty, damagedQty: $damagedQty, shortQty: $shortQty, excessQty: $excessQty, rejectedQty: $rejectedQty, rejectionReason: $rejectionReason, rate: $rate, batchNo: $batchNo, mfgDate: $mfgDate, expiryDate: $expiryDate, storageLocation: $storageLocation, assetCategoryId: $assetCategoryId, uomId: $uomId, uomName: $uomName, remarks: $remarks)';
|
return 'GrnItemModel(id: $id, grnId: $grnId, poItemId: $poItemId, itemId: $itemId, itemCode: $itemCode, itemName: $itemName, lineNo: $lineNo, currentQty: $currentQty, acceptedQty: $acceptedQty, damagedQty: $damagedQty, shortQty: $shortQty, excessQty: $excessQty, rejectedQty: $rejectedQty, rejectionReason: $rejectionReason, rate: $rate, batchNo: $batchNo, mfgDate: $mfgDate, expiryDate: $expiryDate, storageLocation: $storageLocation, assetCategoryId: $assetCategoryId, assetSubcategoryId: $assetSubcategoryId, uomId: $uomId, uomName: $uomName, remarks: $remarks)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -1345,6 +1366,8 @@ class _$GrnItemModelImpl implements _GrnItemModel {
|
|||||||
other.storageLocation == storageLocation) &&
|
other.storageLocation == storageLocation) &&
|
||||||
(identical(other.assetCategoryId, assetCategoryId) ||
|
(identical(other.assetCategoryId, assetCategoryId) ||
|
||||||
other.assetCategoryId == assetCategoryId) &&
|
other.assetCategoryId == assetCategoryId) &&
|
||||||
|
(identical(other.assetSubcategoryId, assetSubcategoryId) ||
|
||||||
|
other.assetSubcategoryId == assetSubcategoryId) &&
|
||||||
(identical(other.uomId, uomId) || other.uomId == uomId) &&
|
(identical(other.uomId, uomId) || other.uomId == uomId) &&
|
||||||
(identical(other.uomName, uomName) || other.uomName == uomName) &&
|
(identical(other.uomName, uomName) || other.uomName == uomName) &&
|
||||||
(identical(other.remarks, remarks) || other.remarks == remarks));
|
(identical(other.remarks, remarks) || other.remarks == remarks));
|
||||||
@ -1374,6 +1397,7 @@ class _$GrnItemModelImpl implements _GrnItemModel {
|
|||||||
expiryDate,
|
expiryDate,
|
||||||
storageLocation,
|
storageLocation,
|
||||||
assetCategoryId,
|
assetCategoryId,
|
||||||
|
assetSubcategoryId,
|
||||||
uomId,
|
uomId,
|
||||||
uomName,
|
uomName,
|
||||||
remarks,
|
remarks,
|
||||||
@ -1427,6 +1451,8 @@ abstract class _GrnItemModel implements GrnItemModel {
|
|||||||
@JsonKey(name: 'storage_location') final String? storageLocation,
|
@JsonKey(name: 'storage_location') final String? storageLocation,
|
||||||
@JsonKey(name: 'asset_category_id', fromJson: _intFromJsonNullable)
|
@JsonKey(name: 'asset_category_id', fromJson: _intFromJsonNullable)
|
||||||
final int? assetCategoryId,
|
final int? assetCategoryId,
|
||||||
|
@JsonKey(name: 'asset_subcategory_id', fromJson: _intFromJsonNullable)
|
||||||
|
final int? assetSubcategoryId,
|
||||||
@JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable) final int? uomId,
|
@JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable) final int? uomId,
|
||||||
@JsonKey(name: 'uom_name', readValue: _readUomName) final String? uomName,
|
@JsonKey(name: 'uom_name', readValue: _readUomName) final String? uomName,
|
||||||
final String? remarks,
|
final String? remarks,
|
||||||
@ -1496,6 +1522,9 @@ abstract class _GrnItemModel implements GrnItemModel {
|
|||||||
@JsonKey(name: 'asset_category_id', fromJson: _intFromJsonNullable)
|
@JsonKey(name: 'asset_category_id', fromJson: _intFromJsonNullable)
|
||||||
int? get assetCategoryId;
|
int? get assetCategoryId;
|
||||||
@override
|
@override
|
||||||
|
@JsonKey(name: 'asset_subcategory_id', fromJson: _intFromJsonNullable)
|
||||||
|
int? get assetSubcategoryId;
|
||||||
|
@override
|
||||||
@JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable)
|
@JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable)
|
||||||
int? get uomId;
|
int? get uomId;
|
||||||
@override
|
@override
|
||||||
|
|||||||
@ -88,6 +88,7 @@ _$GrnItemModelImpl _$$GrnItemModelImplFromJson(Map<String, dynamic> json) =>
|
|||||||
expiryDate: _dateFromJsonNullable(json['expiry_date']),
|
expiryDate: _dateFromJsonNullable(json['expiry_date']),
|
||||||
storageLocation: json['storage_location'] as String?,
|
storageLocation: json['storage_location'] as String?,
|
||||||
assetCategoryId: _intFromJsonNullable(json['asset_category_id']),
|
assetCategoryId: _intFromJsonNullable(json['asset_category_id']),
|
||||||
|
assetSubcategoryId: _intFromJsonNullable(json['asset_subcategory_id']),
|
||||||
uomId: _intFromJsonNullable(json['uom_id']),
|
uomId: _intFromJsonNullable(json['uom_id']),
|
||||||
uomName: _readUomName(json, 'uom_name') as String?,
|
uomName: _readUomName(json, 'uom_name') as String?,
|
||||||
remarks: json['remarks'] as String?,
|
remarks: json['remarks'] as String?,
|
||||||
@ -115,6 +116,7 @@ Map<String, dynamic> _$$GrnItemModelImplToJson(_$GrnItemModelImpl instance) =>
|
|||||||
'expiry_date': instance.expiryDate?.toIso8601String(),
|
'expiry_date': instance.expiryDate?.toIso8601String(),
|
||||||
'storage_location': instance.storageLocation,
|
'storage_location': instance.storageLocation,
|
||||||
'asset_category_id': instance.assetCategoryId,
|
'asset_category_id': instance.assetCategoryId,
|
||||||
|
'asset_subcategory_id': instance.assetSubcategoryId,
|
||||||
'uom_id': instance.uomId,
|
'uom_id': instance.uomId,
|
||||||
'uom_name': instance.uomName,
|
'uom_name': instance.uomName,
|
||||||
'remarks': instance.remarks,
|
'remarks': instance.remarks,
|
||||||
|
|||||||
@ -35,6 +35,7 @@ class AppSearchableDropdown<T> extends StatefulWidget {
|
|||||||
class _AppSearchableDropdownState<T> extends State<AppSearchableDropdown<T>> {
|
class _AppSearchableDropdownState<T> extends State<AppSearchableDropdown<T>> {
|
||||||
final _layerLink = LayerLink();
|
final _layerLink = LayerLink();
|
||||||
final _fieldKey = GlobalKey();
|
final _fieldKey = GlobalKey();
|
||||||
|
final _formFieldKey = UniqueKey();
|
||||||
OverlayEntry? _overlayEntry;
|
OverlayEntry? _overlayEntry;
|
||||||
bool _ignoreOutsideTap = false;
|
bool _ignoreOutsideTap = false;
|
||||||
|
|
||||||
@ -166,7 +167,7 @@ class _AppSearchableDropdownState<T> extends State<AppSearchableDropdown<T>> {
|
|||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
return FormField<T>(
|
return FormField<T>(
|
||||||
key: ValueKey<T?>(widget.value),
|
key: widget.key ?? _formFieldKey,
|
||||||
initialValue: widget.value,
|
initialValue: widget.value,
|
||||||
validator: widget.validator,
|
validator: widget.validator,
|
||||||
builder: (field) {
|
builder: (field) {
|
||||||
@ -206,14 +207,14 @@ class _AppSearchableDropdownState<T> extends State<AppSearchableDropdown<T>> {
|
|||||||
),
|
),
|
||||||
enabled: canOpen,
|
enabled: canOpen,
|
||||||
),
|
),
|
||||||
child: displayLabel == null
|
child: Text(
|
||||||
? const SizedBox.shrink()
|
displayLabel ?? '\u00A0',
|
||||||
: Text(
|
|
||||||
displayLabel,
|
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: theme.textTheme.bodyLarge?.copyWith(
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
color: colors.onSurface,
|
color: displayLabel == null
|
||||||
|
? Colors.transparent
|
||||||
|
: colors.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../core/utils/responsive_utils.dart';
|
||||||
|
|
||||||
Future<T?> showSidePanel<T>(
|
Future<T?> showSidePanel<T>(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
Widget panel, {
|
Widget panel, {
|
||||||
@ -266,6 +268,92 @@ class FormRowFour extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Responsive form grid: 4 cols (medium+), 2 cols (small).
|
||||||
|
class ResponsiveFormGrid extends StatelessWidget {
|
||||||
|
const ResponsiveFormGrid({
|
||||||
|
super.key,
|
||||||
|
required this.children,
|
||||||
|
this.fullWidthChildren = const [],
|
||||||
|
this.spacing = 12,
|
||||||
|
this.smallColumns = 2,
|
||||||
|
this.mediumColumns = 4,
|
||||||
|
this.largeColumns = 4,
|
||||||
|
this.mediumBreakpoint = AppBreakpoints.tablet,
|
||||||
|
this.largeBreakpoint = AppBreakpoints.desktop,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<Widget> children;
|
||||||
|
final List<Widget> fullWidthChildren;
|
||||||
|
final double spacing;
|
||||||
|
final int smallColumns;
|
||||||
|
final int mediumColumns;
|
||||||
|
final int largeColumns;
|
||||||
|
final double mediumBreakpoint;
|
||||||
|
final double largeBreakpoint;
|
||||||
|
|
||||||
|
List<List<Widget>> _chunk(List<Widget> items, int columns) {
|
||||||
|
if (items.isEmpty) return const [];
|
||||||
|
final rows = <List<Widget>>[];
|
||||||
|
for (var i = 0; i < items.length; i += columns) {
|
||||||
|
final end = (i + columns < items.length) ? i + columns : items.length;
|
||||||
|
rows.add(items.sublist(i, end));
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final width = constraints.maxWidth.isFinite && constraints.maxWidth > 0
|
||||||
|
? constraints.maxWidth
|
||||||
|
: MediaQuery.sizeOf(context).width;
|
||||||
|
final columns = formGridColumnsForWidth(
|
||||||
|
width,
|
||||||
|
smallColumns: smallColumns,
|
||||||
|
mediumColumns: mediumColumns,
|
||||||
|
largeColumns: largeColumns,
|
||||||
|
mediumBreakpoint: mediumBreakpoint,
|
||||||
|
largeBreakpoint: largeBreakpoint,
|
||||||
|
);
|
||||||
|
final rows = _chunk(children, columns);
|
||||||
|
final columnWidth = (width - (columns - 1) * spacing) / columns;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
for (final row in rows)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: spacing),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
for (var col = 0; col < columns; col++) ...[
|
||||||
|
if (col > 0) SizedBox(width: spacing),
|
||||||
|
SizedBox(
|
||||||
|
width: columnWidth,
|
||||||
|
child: col < row.length
|
||||||
|
? row[col]
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
for (var i = 0; i < fullWidthChildren.length; i++)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
bottom: i == fullWidthChildren.length - 1 ? 0 : spacing,
|
||||||
|
),
|
||||||
|
child: fullWidthChildren[i],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Responsive row of equal-width form fields.
|
/// Responsive row of equal-width form fields.
|
||||||
class FormRow extends StatelessWidget {
|
class FormRow extends StatelessWidget {
|
||||||
const FormRow({
|
const FormRow({
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user