grn changes
This commit is contained in:
parent
fa610b7efd
commit
83c73e31ae
@ -21,4 +21,24 @@ extension ResponsiveContext on BuildContext {
|
||||
if (isDesktop) return 1200;
|
||||
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(
|
||||
context: context,
|
||||
title: 'Delete Asset',
|
||||
message: 'Soft delete this asset?',
|
||||
message: 'Do you want to delete this asset?',
|
||||
confirmLabel: 'Delete',
|
||||
isDestructive: true,
|
||||
);
|
||||
|
||||
@ -5,17 +5,20 @@ import '../../../../shared/models/purchase_order_model.dart';
|
||||
import '../../../../shared/models/user_management_models.dart';
|
||||
import '../../../masters/data/datasources/master_remote_data_source.dart';
|
||||
import '../../../purchase_orders/data/repositories/purchase_order_repository_impl.dart';
|
||||
import '../../../users/presentation/providers/users_provider.dart';
|
||||
|
||||
class GrnLookups {
|
||||
const GrnLookups({
|
||||
this.warehouses = const [],
|
||||
this.receivablePurchaseOrders = const [],
|
||||
this.assetCategories = const [],
|
||||
this.users = const [],
|
||||
});
|
||||
|
||||
final List<FilterOptionModel> warehouses;
|
||||
final List<PurchaseOrderModel> receivablePurchaseOrders;
|
||||
final List<FilterOptionModel> assetCategories;
|
||||
final List<FilterOptionModel> users;
|
||||
}
|
||||
|
||||
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 assetCategories = await _safeOptions(master.listAssetCategories);
|
||||
final users = await _safeUserOptions(ref);
|
||||
|
||||
final receivablePos = <PurchaseOrderModel>[];
|
||||
for (final status in ['APPROVED', 'PARTIALLY_RECEIVED']) {
|
||||
@ -43,6 +47,7 @@ final grnLookupsProvider = FutureProvider.autoDispose<GrnLookups>((ref) async {
|
||||
warehouses: warehouses,
|
||||
receivablePurchaseOrders: receivablePos,
|
||||
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 =
|
||||
FutureProvider.autoDispose.family<PurchaseOrderModel?, String>((ref, poId) async {
|
||||
final result =
|
||||
|
||||
@ -7,6 +7,7 @@ import '../../../../core/constants/route_constants.dart';
|
||||
import '../../../../core/errors/failure.dart';
|
||||
import '../../../../core/utils/formatters.dart';
|
||||
import '../../../../shared/models/grn_model.dart';
|
||||
import '../../../../shared/models/user_management_models.dart';
|
||||
import '../../../../shared/providers/permissions_provider.dart';
|
||||
import '../../../../shared/utils/file_download_helper.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/error_view.dart';
|
||||
import '../../../../shared/widgets/page_header.dart';
|
||||
import '../providers/grn_lookups_provider.dart';
|
||||
import '../providers/grn_provider.dart';
|
||||
import '../widgets/grn_line_items_editor.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});
|
||||
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
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(
|
||||
child: Padding(
|
||||
@ -254,18 +266,16 @@ class _OverviewCard extends StatelessWidget {
|
||||
_GrnInfo('Vehicle No', grn.vehicleNo ?? '—'),
|
||||
_GrnInfo('LR No', grn.lrNo ?? '—'),
|
||||
_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;
|
||||
String? _selectedPoId;
|
||||
int? _warehouseId;
|
||||
int? _receivedById;
|
||||
int? _qualityCheckedById;
|
||||
final List<GrnLineItemDraft> _lines = [];
|
||||
bool _isSubmitting = false;
|
||||
String? _populatedSignature;
|
||||
@ -88,6 +90,8 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
||||
_vehicleNoController.text = grn.vehicleNo ?? '';
|
||||
_lrNoController.text = grn.lrNo ?? '';
|
||||
_lrDate = grn.lrDate;
|
||||
_receivedById = grn.receivedBy;
|
||||
_qualityCheckedById = grn.qualityCheckedBy;
|
||||
_remarksController.text = grn.remarks ?? '';
|
||||
});
|
||||
}
|
||||
@ -124,14 +128,6 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
||||
.toList();
|
||||
}
|
||||
|
||||
List<AppDropdownOption<String>> _assetCategoryOptions(
|
||||
List<FilterOptionModel> categories,
|
||||
) {
|
||||
return categories
|
||||
.map((e) => AppDropdownOption(value: e.id, label: e.name))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Map<String, dynamic> _buildCreatePayload() {
|
||||
final poId = int.tryParse(_selectedPoId ?? '');
|
||||
return {
|
||||
@ -149,8 +145,9 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
||||
'vehicle_no': _vehicleNoController.text.trim(),
|
||||
if (_lrNoController.text.trim().isNotEmpty) 'lr_no': _lrNoController.text.trim(),
|
||||
if (_lrDate != null) 'lr_date': DateFormatter.toApiDate(_lrDate!),
|
||||
if (_remarksController.text.trim().isNotEmpty)
|
||||
'remarks': _remarksController.text.trim(),
|
||||
if (_receivedById != null) 'received_by': _receivedById,
|
||||
if (_qualityCheckedById != null) 'quality_checked_by': _qualityCheckedById,
|
||||
'remarks': _remarksController.text.trim(),
|
||||
'items': _lines.map((line) => line.toPayload()).toList(),
|
||||
};
|
||||
}
|
||||
@ -168,8 +165,9 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
||||
'vehicle_no': _vehicleNoController.text.trim(),
|
||||
if (_lrNoController.text.trim().isNotEmpty) 'lr_no': _lrNoController.text.trim(),
|
||||
if (_lrDate != null) 'lr_date': DateFormatter.toApiDate(_lrDate!),
|
||||
if (_remarksController.text.trim().isNotEmpty)
|
||||
'remarks': _remarksController.text.trim(),
|
||||
if (_receivedById != null) 'received_by': _receivedById,
|
||||
if (_qualityCheckedById != null) 'quality_checked_by': _qualityCheckedById,
|
||||
'remarks': _remarksController.text.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
@ -364,7 +362,7 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
FormRowThree(
|
||||
ResponsiveFormGrid(
|
||||
children: [
|
||||
_DateField(
|
||||
label: 'GRN Date *',
|
||||
@ -382,6 +380,7 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
||||
label: 'Purchase Order *',
|
||||
value: _selectedPoId,
|
||||
searchHint: 'Search PO...',
|
||||
isDense: true,
|
||||
options: poOptions,
|
||||
onChanged: (v) async {
|
||||
setState(() => _selectedPoId = v);
|
||||
@ -408,16 +407,22 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
||||
v == null ? 'Purchase order is required' : null,
|
||||
)
|
||||
else
|
||||
InputDecorator(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Purchase Order',
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: InputDecorator(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Purchase Order',
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
isDense: true,
|
||||
),
|
||||
child: Text(existing?.poNumber ?? '—'),
|
||||
),
|
||||
child: Text(existing?.poNumber ?? '—'),
|
||||
),
|
||||
AppSearchableDropdown<int>(
|
||||
label: 'Warehouse *',
|
||||
value: _dropdownValue(_warehouseId, warehouseIds),
|
||||
searchHint: 'Search warehouse...',
|
||||
isDense: true,
|
||||
options: _intOptions(lookups.warehouses),
|
||||
onChanged: widget.isEditing
|
||||
? (_) {}
|
||||
@ -427,13 +432,10 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
||||
: (v) => v == null ? 'Warehouse is required' : null,
|
||||
enabled: !widget.isEditing,
|
||||
),
|
||||
],
|
||||
),
|
||||
FormRowThree(
|
||||
children: [
|
||||
AppTextField(
|
||||
label: 'Vendor Invoice No',
|
||||
controller: _vendorInvoiceNoController,
|
||||
isDense: true,
|
||||
),
|
||||
_DateField(
|
||||
label: 'Vendor Invoice Date',
|
||||
@ -449,18 +451,17 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
||||
controller: _vendorInvoiceAmountController,
|
||||
keyboardType:
|
||||
const TextInputType.numberWithOptions(decimal: true),
|
||||
isDense: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
FormRowThree(
|
||||
children: [
|
||||
AppTextField(
|
||||
label: 'Vehicle No',
|
||||
controller: _vehicleNoController,
|
||||
isDense: true,
|
||||
),
|
||||
AppTextField(
|
||||
label: 'LR No',
|
||||
controller: _lrNoController,
|
||||
isDense: true,
|
||||
),
|
||||
_DateField(
|
||||
label: 'LR Date',
|
||||
@ -470,13 +471,37 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
||||
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(
|
||||
label: 'Remarks',
|
||||
controller: _remarksController,
|
||||
maxLines: 3,
|
||||
isDense: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
AppTextField(
|
||||
label: 'Remarks',
|
||||
controller: _remarksController,
|
||||
maxLines: 3,
|
||||
),
|
||||
if (!widget.isEditing) ...[
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
@ -488,8 +513,6 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
||||
const SizedBox(height: 12),
|
||||
GrnLineItemsEditor(
|
||||
items: _lines,
|
||||
assetCategoryOptions:
|
||||
_assetCategoryOptions(lookups.assetCategories),
|
||||
onChanged: () => setState(() {}),
|
||||
),
|
||||
] else ...[
|
||||
@ -539,22 +562,27 @@ class _DateField extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: enabled ? onTap : null,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: InputDecorator(
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
suffixIcon: const Icon(Icons.calendar_today_outlined, size: 18),
|
||||
enabled: enabled,
|
||||
),
|
||||
child: Text(
|
||||
value != null ? DateFormatter.displayDate(value) : 'Select date',
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: value != null
|
||||
? null
|
||||
: Theme.of(context).hintColor,
|
||||
),
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: InkWell(
|
||||
onTap: enabled ? onTap : null,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: InputDecorator(
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
isDense: true,
|
||||
suffixIcon: const Icon(Icons.calendar_today_outlined, size: 18),
|
||||
enabled: enabled,
|
||||
),
|
||||
child: Text(
|
||||
value != null ? DateFormatter.displayDate(value) : 'Select date',
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: value != null
|
||||
? null
|
||||
: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -15,7 +15,6 @@ import '../../../../shared/widgets/app_dropdown.dart';
|
||||
import '../../../../shared/widgets/app_empty_state.dart';
|
||||
import '../../../../shared/widgets/app_loading_view.dart';
|
||||
import '../../../../shared/widgets/app_pagination.dart';
|
||||
import '../../../../shared/widgets/app_search_field.dart';
|
||||
import '../../../../shared/widgets/app_searchable_dropdown.dart';
|
||||
import '../../../../shared/widgets/can_permission.dart';
|
||||
import '../../../../shared/widgets/app_table_action_icon.dart';
|
||||
@ -165,12 +164,21 @@ class _FiltersBar extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final searchField = SizedBox(
|
||||
width: wrapped ? double.infinity : null,
|
||||
child: AppSearchField(
|
||||
controller: searchController,
|
||||
hint: 'Search GRN number, PO, vendor...',
|
||||
onChanged: onSearch,
|
||||
final searchField = Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: SizedBox(
|
||||
width: wrapped ? double.infinity : null,
|
||||
child: TextField(
|
||||
controller: searchController,
|
||||
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(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(flex: 3, child: searchField),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
@ -1,12 +1,20 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.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/purchase_order_model.dart';
|
||||
import '../../../../shared/models/user_management_models.dart';
|
||||
import '../../../../shared/widgets/app_data_table.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 '../../../assets/presentation/providers/asset_categories_provider.dart';
|
||||
import '../providers/grn_lookups_provider.dart';
|
||||
|
||||
String _formatQty(double value) {
|
||||
if (value % 1 == 0) return value.toInt().toString();
|
||||
@ -22,22 +30,27 @@ class GrnLineItemDraft {
|
||||
required this.receivedQty,
|
||||
required this.remainingQty,
|
||||
TextEditingController? acceptedQtyController,
|
||||
TextEditingController? damagedQtyController,
|
||||
TextEditingController? shortQtyController,
|
||||
TextEditingController? excessQtyController,
|
||||
TextEditingController? rejectedQtyController,
|
||||
TextEditingController? rateController,
|
||||
TextEditingController? batchNoController,
|
||||
TextEditingController? expiryDateController,
|
||||
TextEditingController? storageLocationController,
|
||||
TextEditingController? rejectionReasonController,
|
||||
TextEditingController? remarksController,
|
||||
this.mfgDate,
|
||||
this.expiryDate,
|
||||
this.assetCategoryId,
|
||||
this.assetSubcategoryId,
|
||||
}) : acceptedQtyController = acceptedQtyController ??
|
||||
TextEditingController(
|
||||
text: remainingQty > 0 ? _formatQty(remainingQty) : '',
|
||||
),
|
||||
damagedQtyController = damagedQtyController ?? TextEditingController(),
|
||||
shortQtyController = shortQtyController ?? TextEditingController(),
|
||||
excessQtyController = excessQtyController ?? TextEditingController(),
|
||||
rejectedQtyController = rejectedQtyController ?? TextEditingController(),
|
||||
rateController = rateController ?? TextEditingController(),
|
||||
batchNoController = batchNoController ?? TextEditingController(),
|
||||
expiryDateController = expiryDateController ?? TextEditingController(),
|
||||
storageLocationController =
|
||||
storageLocationController ?? TextEditingController(),
|
||||
rejectionReasonController =
|
||||
rejectionReasonController ?? TextEditingController(),
|
||||
remarksController = remarksController ?? TextEditingController();
|
||||
|
||||
final String poItemId;
|
||||
@ -47,27 +60,28 @@ class GrnLineItemDraft {
|
||||
final double receivedQty;
|
||||
final double remainingQty;
|
||||
final TextEditingController acceptedQtyController;
|
||||
final TextEditingController damagedQtyController;
|
||||
final TextEditingController shortQtyController;
|
||||
final TextEditingController excessQtyController;
|
||||
final TextEditingController rejectedQtyController;
|
||||
final TextEditingController rateController;
|
||||
final TextEditingController batchNoController;
|
||||
final TextEditingController expiryDateController;
|
||||
final TextEditingController storageLocationController;
|
||||
final TextEditingController rejectionReasonController;
|
||||
final TextEditingController remarksController;
|
||||
String? assetCategoryId;
|
||||
DateTime? mfgDate;
|
||||
DateTime? expiryDate;
|
||||
int? assetCategoryId;
|
||||
int? assetSubcategoryId;
|
||||
|
||||
double get acceptedQty => double.tryParse(acceptedQtyController.text.trim()) ?? 0;
|
||||
double get damagedQty => double.tryParse(damagedQtyController.text.trim()) ?? 0;
|
||||
double get shortQty => double.tryParse(shortQtyController.text.trim()) ?? 0;
|
||||
double get excessQty => double.tryParse(excessQtyController.text.trim()) ?? 0;
|
||||
double get currentQty => acceptedQty + damagedQty + shortQty + excessQty;
|
||||
double get rejectedQty => double.tryParse(rejectedQtyController.text.trim()) ?? 0;
|
||||
double get currentQty => acceptedQty + rejectedQty;
|
||||
|
||||
void dispose() {
|
||||
acceptedQtyController.dispose();
|
||||
damagedQtyController.dispose();
|
||||
shortQtyController.dispose();
|
||||
excessQtyController.dispose();
|
||||
rejectedQtyController.dispose();
|
||||
rateController.dispose();
|
||||
batchNoController.dispose();
|
||||
expiryDateController.dispose();
|
||||
storageLocationController.dispose();
|
||||
rejectionReasonController.dispose();
|
||||
remarksController.dispose();
|
||||
}
|
||||
|
||||
@ -78,15 +92,19 @@ class GrnLineItemDraft {
|
||||
'line_no': lineNo,
|
||||
'current_qty': currentQty,
|
||||
'accepted_qty': acceptedQty,
|
||||
if (damagedQty > 0) 'damaged_qty': damagedQty,
|
||||
if (shortQty > 0) 'short_qty': shortQty,
|
||||
if (excessQty > 0) 'excess_qty': excessQty,
|
||||
if (rejectedQty > 0) 'rejected_qty': rejectedQty,
|
||||
if (rejectionReasonController.text.trim().isNotEmpty)
|
||||
'rejection_reason': rejectionReasonController.text.trim(),
|
||||
if (rateController.text.trim().isNotEmpty)
|
||||
'rate': double.tryParse(rateController.text.trim()),
|
||||
if (batchNoController.text.trim().isNotEmpty)
|
||||
'batch_no': batchNoController.text.trim(),
|
||||
if (expiryDateController.text.trim().isNotEmpty)
|
||||
'expiry_date': expiryDateController.text.trim(),
|
||||
if (assetCategoryId != null && assetCategoryId!.isNotEmpty)
|
||||
'asset_category_id': int.tryParse(assetCategoryId!) ?? assetCategoryId,
|
||||
if (mfgDate != null) 'mfg_date': DateFormatter.toApiDate(mfgDate!),
|
||||
if (expiryDate != null) 'expiry_date': DateFormatter.toApiDate(expiryDate!),
|
||||
if (storageLocationController.text.trim().isNotEmpty)
|
||||
'storage_location': storageLocationController.text.trim(),
|
||||
if (assetCategoryId != null) 'asset_category_id': assetCategoryId,
|
||||
if (assetSubcategoryId != null) 'asset_subcategory_id': assetSubcategoryId,
|
||||
if (remarksController.text.trim().isNotEmpty)
|
||||
'remarks': remarksController.text.trim(),
|
||||
};
|
||||
@ -98,6 +116,7 @@ List<GrnLineItemDraft> draftsFromPurchaseOrder(PurchaseOrderModel po) {
|
||||
final ordered = item.orderedQty ?? 0;
|
||||
final received = item.receivedQty ?? 0;
|
||||
final remaining = (ordered - received).clamp(0.0, double.infinity);
|
||||
final rate = item.rate;
|
||||
return GrnLineItemDraft(
|
||||
poItemId: item.id,
|
||||
lineNo: item.lineNo ?? 1,
|
||||
@ -105,6 +124,9 @@ List<GrnLineItemDraft> draftsFromPurchaseOrder(PurchaseOrderModel po) {
|
||||
orderedQty: ordered,
|
||||
receivedQty: received,
|
||||
remainingQty: remaining,
|
||||
rateController: TextEditingController(
|
||||
text: rate != null ? _formatQty(rate) : '',
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
@ -114,13 +136,11 @@ class GrnLineItemsEditor extends StatelessWidget {
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.onChanged,
|
||||
this.assetCategoryOptions = const [],
|
||||
this.readOnly = false,
|
||||
});
|
||||
|
||||
final List<GrnLineItemDraft> items;
|
||||
final VoidCallback onChanged;
|
||||
final List<AppDropdownOption<String>> assetCategoryOptions;
|
||||
final bool readOnly;
|
||||
|
||||
@override
|
||||
@ -152,7 +172,6 @@ class GrnLineItemsEditor extends StatelessWidget {
|
||||
padding: EdgeInsets.only(bottom: i == items.length - 1 ? 0 : 12),
|
||||
child: _GrnLineItemCard(
|
||||
item: items[i],
|
||||
assetCategoryOptions: assetCategoryOptions,
|
||||
readOnly: readOnly,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
@ -162,25 +181,80 @@ class GrnLineItemsEditor extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _GrnLineItemCard extends StatelessWidget {
|
||||
class _GrnLineItemCard extends ConsumerWidget {
|
||||
const _GrnLineItemCard({
|
||||
required this.item,
|
||||
required this.onChanged,
|
||||
required this.assetCategoryOptions,
|
||||
required this.readOnly,
|
||||
});
|
||||
|
||||
final GrnLineItemDraft item;
|
||||
final VoidCallback onChanged;
|
||||
final List<AppDropdownOption<String>> assetCategoryOptions;
|
||||
final bool readOnly;
|
||||
|
||||
static final _qtyFormatters = [
|
||||
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
|
||||
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(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
@ -218,94 +292,20 @@ class _GrnLineItemCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
if (!readOnly) ...[
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: AppTextField(
|
||||
label: 'Accepted Qty *',
|
||||
controller: item.acceptedQtyController,
|
||||
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();
|
||||
},
|
||||
ResponsiveFormGrid(
|
||||
children: _buildLineItemFields(
|
||||
context: context,
|
||||
categoryOptions: categoryOptions,
|
||||
categoryIds: categoryIds,
|
||||
subcategoryOptions: subcategoryOptions,
|
||||
subcategoryIds: subcategoryIds,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
AppTextField(
|
||||
label: 'Remarks',
|
||||
controller: item.remarksController,
|
||||
maxLines: 2,
|
||||
onChanged: (_) => onChanged(),
|
||||
),
|
||||
] else ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Accepted: ${_formatQty(item.acceptedQty)} · '
|
||||
'Rejected: ${_formatQty(item.rejectedQty)} · '
|
||||
'Current: ${_formatQty(item.currentQty)}',
|
||||
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 {
|
||||
@ -344,25 +498,47 @@ class GrnItemsTable extends StatelessWidget {
|
||||
cellBuilder: (_, item) => Text(_formatQty(item.acceptedQty ?? 0)),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Damaged',
|
||||
label: 'Rejected',
|
||||
flex: 1,
|
||||
cellBuilder: (_, item) => Text(_formatQty(item.damagedQty ?? 0)),
|
||||
cellBuilder: (_, item) => Text(_formatQty(item.rejectedQty ?? 0)),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Short',
|
||||
label: 'Rate',
|
||||
flex: 1,
|
||||
cellBuilder: (_, item) => Text(_formatQty(item.shortQty ?? 0)),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Excess',
|
||||
flex: 1,
|
||||
cellBuilder: (_, item) => Text(_formatQty(item.excessQty ?? 0)),
|
||||
cellBuilder: (_, item) => Text(
|
||||
item.rate != null ? _formatQty(item.rate!) : '—',
|
||||
),
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Batch',
|
||||
flex: 1,
|
||||
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,
|
||||
);
|
||||
|
||||
@ -151,6 +151,8 @@ class GrnItemModel with _$GrnItemModel {
|
||||
@JsonKey(name: 'storage_location') String? storageLocation,
|
||||
@JsonKey(name: 'asset_category_id', fromJson: _intFromJsonNullable)
|
||||
int? assetCategoryId,
|
||||
@JsonKey(name: 'asset_subcategory_id', fromJson: _intFromJsonNullable)
|
||||
int? assetSubcategoryId,
|
||||
@JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable) int? uomId,
|
||||
@JsonKey(name: 'uom_name', readValue: _readUomName) String? uomName,
|
||||
String? remarks,
|
||||
|
||||
@ -813,6 +813,8 @@ mixin _$GrnItemModel {
|
||||
String? get storageLocation => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'asset_category_id', fromJson: _intFromJsonNullable)
|
||||
int? get assetCategoryId => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'asset_subcategory_id', fromJson: _intFromJsonNullable)
|
||||
int? get assetSubcategoryId => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable)
|
||||
int? get uomId => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'uom_name', readValue: _readUomName)
|
||||
@ -866,6 +868,8 @@ abstract class $GrnItemModelCopyWith<$Res> {
|
||||
@JsonKey(name: 'storage_location') String? storageLocation,
|
||||
@JsonKey(name: 'asset_category_id', fromJson: _intFromJsonNullable)
|
||||
int? assetCategoryId,
|
||||
@JsonKey(name: 'asset_subcategory_id', fromJson: _intFromJsonNullable)
|
||||
int? assetSubcategoryId,
|
||||
@JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable) int? uomId,
|
||||
@JsonKey(name: 'uom_name', readValue: _readUomName) String? uomName,
|
||||
String? remarks,
|
||||
@ -907,6 +911,7 @@ class _$GrnItemModelCopyWithImpl<$Res, $Val extends GrnItemModel>
|
||||
Object? expiryDate = freezed,
|
||||
Object? storageLocation = freezed,
|
||||
Object? assetCategoryId = freezed,
|
||||
Object? assetSubcategoryId = freezed,
|
||||
Object? uomId = freezed,
|
||||
Object? uomName = freezed,
|
||||
Object? remarks = freezed,
|
||||
@ -993,6 +998,10 @@ class _$GrnItemModelCopyWithImpl<$Res, $Val extends GrnItemModel>
|
||||
? _value.assetCategoryId
|
||||
: assetCategoryId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
assetSubcategoryId: freezed == assetSubcategoryId
|
||||
? _value.assetSubcategoryId
|
||||
: assetSubcategoryId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
uomId: freezed == uomId
|
||||
? _value.uomId
|
||||
: uomId // ignore: cast_nullable_to_non_nullable
|
||||
@ -1050,6 +1059,8 @@ abstract class _$$GrnItemModelImplCopyWith<$Res>
|
||||
@JsonKey(name: 'storage_location') String? storageLocation,
|
||||
@JsonKey(name: 'asset_category_id', fromJson: _intFromJsonNullable)
|
||||
int? assetCategoryId,
|
||||
@JsonKey(name: 'asset_subcategory_id', fromJson: _intFromJsonNullable)
|
||||
int? assetSubcategoryId,
|
||||
@JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable) int? uomId,
|
||||
@JsonKey(name: 'uom_name', readValue: _readUomName) String? uomName,
|
||||
String? remarks,
|
||||
@ -1090,6 +1101,7 @@ class __$$GrnItemModelImplCopyWithImpl<$Res>
|
||||
Object? expiryDate = freezed,
|
||||
Object? storageLocation = freezed,
|
||||
Object? assetCategoryId = freezed,
|
||||
Object? assetSubcategoryId = freezed,
|
||||
Object? uomId = freezed,
|
||||
Object? uomName = freezed,
|
||||
Object? remarks = freezed,
|
||||
@ -1176,6 +1188,10 @@ class __$$GrnItemModelImplCopyWithImpl<$Res>
|
||||
? _value.assetCategoryId
|
||||
: assetCategoryId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
assetSubcategoryId: freezed == assetSubcategoryId
|
||||
? _value.assetSubcategoryId
|
||||
: assetSubcategoryId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
uomId: freezed == uomId
|
||||
? _value.uomId
|
||||
: uomId // ignore: cast_nullable_to_non_nullable
|
||||
@ -1225,6 +1241,8 @@ class _$GrnItemModelImpl implements _GrnItemModel {
|
||||
@JsonKey(name: 'storage_location') this.storageLocation,
|
||||
@JsonKey(name: 'asset_category_id', fromJson: _intFromJsonNullable)
|
||||
this.assetCategoryId,
|
||||
@JsonKey(name: 'asset_subcategory_id', fromJson: _intFromJsonNullable)
|
||||
this.assetSubcategoryId,
|
||||
@JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable) this.uomId,
|
||||
@JsonKey(name: 'uom_name', readValue: _readUomName) this.uomName,
|
||||
this.remarks,
|
||||
@ -1294,6 +1312,9 @@ class _$GrnItemModelImpl implements _GrnItemModel {
|
||||
@JsonKey(name: 'asset_category_id', fromJson: _intFromJsonNullable)
|
||||
final int? assetCategoryId;
|
||||
@override
|
||||
@JsonKey(name: 'asset_subcategory_id', fromJson: _intFromJsonNullable)
|
||||
final int? assetSubcategoryId;
|
||||
@override
|
||||
@JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable)
|
||||
final int? uomId;
|
||||
@override
|
||||
@ -1304,7 +1325,7 @@ class _$GrnItemModelImpl implements _GrnItemModel {
|
||||
|
||||
@override
|
||||
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
|
||||
@ -1345,6 +1366,8 @@ class _$GrnItemModelImpl implements _GrnItemModel {
|
||||
other.storageLocation == storageLocation) &&
|
||||
(identical(other.assetCategoryId, assetCategoryId) ||
|
||||
other.assetCategoryId == assetCategoryId) &&
|
||||
(identical(other.assetSubcategoryId, assetSubcategoryId) ||
|
||||
other.assetSubcategoryId == assetSubcategoryId) &&
|
||||
(identical(other.uomId, uomId) || other.uomId == uomId) &&
|
||||
(identical(other.uomName, uomName) || other.uomName == uomName) &&
|
||||
(identical(other.remarks, remarks) || other.remarks == remarks));
|
||||
@ -1374,6 +1397,7 @@ class _$GrnItemModelImpl implements _GrnItemModel {
|
||||
expiryDate,
|
||||
storageLocation,
|
||||
assetCategoryId,
|
||||
assetSubcategoryId,
|
||||
uomId,
|
||||
uomName,
|
||||
remarks,
|
||||
@ -1427,6 +1451,8 @@ abstract class _GrnItemModel implements GrnItemModel {
|
||||
@JsonKey(name: 'storage_location') final String? storageLocation,
|
||||
@JsonKey(name: 'asset_category_id', fromJson: _intFromJsonNullable)
|
||||
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_name', readValue: _readUomName) final String? uomName,
|
||||
final String? remarks,
|
||||
@ -1496,6 +1522,9 @@ abstract class _GrnItemModel implements GrnItemModel {
|
||||
@JsonKey(name: 'asset_category_id', fromJson: _intFromJsonNullable)
|
||||
int? get assetCategoryId;
|
||||
@override
|
||||
@JsonKey(name: 'asset_subcategory_id', fromJson: _intFromJsonNullable)
|
||||
int? get assetSubcategoryId;
|
||||
@override
|
||||
@JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable)
|
||||
int? get uomId;
|
||||
@override
|
||||
|
||||
@ -88,6 +88,7 @@ _$GrnItemModelImpl _$$GrnItemModelImplFromJson(Map<String, dynamic> json) =>
|
||||
expiryDate: _dateFromJsonNullable(json['expiry_date']),
|
||||
storageLocation: json['storage_location'] as String?,
|
||||
assetCategoryId: _intFromJsonNullable(json['asset_category_id']),
|
||||
assetSubcategoryId: _intFromJsonNullable(json['asset_subcategory_id']),
|
||||
uomId: _intFromJsonNullable(json['uom_id']),
|
||||
uomName: _readUomName(json, 'uom_name') as String?,
|
||||
remarks: json['remarks'] as String?,
|
||||
@ -115,6 +116,7 @@ Map<String, dynamic> _$$GrnItemModelImplToJson(_$GrnItemModelImpl instance) =>
|
||||
'expiry_date': instance.expiryDate?.toIso8601String(),
|
||||
'storage_location': instance.storageLocation,
|
||||
'asset_category_id': instance.assetCategoryId,
|
||||
'asset_subcategory_id': instance.assetSubcategoryId,
|
||||
'uom_id': instance.uomId,
|
||||
'uom_name': instance.uomName,
|
||||
'remarks': instance.remarks,
|
||||
|
||||
@ -35,6 +35,7 @@ class AppSearchableDropdown<T> extends StatefulWidget {
|
||||
class _AppSearchableDropdownState<T> extends State<AppSearchableDropdown<T>> {
|
||||
final _layerLink = LayerLink();
|
||||
final _fieldKey = GlobalKey();
|
||||
final _formFieldKey = UniqueKey();
|
||||
OverlayEntry? _overlayEntry;
|
||||
bool _ignoreOutsideTap = false;
|
||||
|
||||
@ -166,7 +167,7 @@ class _AppSearchableDropdownState<T> extends State<AppSearchableDropdown<T>> {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return FormField<T>(
|
||||
key: ValueKey<T?>(widget.value),
|
||||
key: widget.key ?? _formFieldKey,
|
||||
initialValue: widget.value,
|
||||
validator: widget.validator,
|
||||
builder: (field) {
|
||||
@ -206,16 +207,16 @@ class _AppSearchableDropdownState<T> extends State<AppSearchableDropdown<T>> {
|
||||
),
|
||||
enabled: canOpen,
|
||||
),
|
||||
child: displayLabel == null
|
||||
? const SizedBox.shrink()
|
||||
: Text(
|
||||
displayLabel,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
displayLabel ?? '\u00A0',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: displayLabel == null
|
||||
? Colors.transparent
|
||||
: colors.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../core/utils/responsive_utils.dart';
|
||||
|
||||
Future<T?> showSidePanel<T>(
|
||||
BuildContext context,
|
||||
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.
|
||||
class FormRow extends StatelessWidget {
|
||||
const FormRow({
|
||||
|
||||
Loading…
Reference in New Issue
Block a user