table alignment

This commit is contained in:
Surendiran 2026-07-15 17:12:21 +05:30
parent c462374125
commit 5a28cfc0da
24 changed files with 740 additions and 237 deletions

View File

@ -73,6 +73,7 @@ class ApiEndpoints {
// Vendors // Vendors
static const String vendors = '/vendors'; static const String vendors = '/vendors';
static const String vendorsExport = '/vendors/export';
static const String vendorGstTreatments = '/vendors/gst-treatments'; static const String vendorGstTreatments = '/vendors/gst-treatments';
static const String vendorSourceOfSupply = '/vendors/source-of-supply'; static const String vendorSourceOfSupply = '/vendors/source-of-supply';
static String vendorById(String id) => '/vendors/$id'; static String vendorById(String id) => '/vendors/$id';
@ -93,6 +94,7 @@ class ApiEndpoints {
// Purchase Orders // Purchase Orders
static const String purchaseOrders = '/purchase-orders'; static const String purchaseOrders = '/purchase-orders';
static const String purchaseOrdersExport = '/purchase-orders/export';
static String purchaseOrderById(String id) => '/purchase-orders/$id'; static String purchaseOrderById(String id) => '/purchase-orders/$id';
static String purchaseOrderSubmit(String id) => '/purchase-orders/$id/submit'; static String purchaseOrderSubmit(String id) => '/purchase-orders/$id/submit';
static String purchaseOrderApprove(String id) => '/purchase-orders/$id/approve'; static String purchaseOrderApprove(String id) => '/purchase-orders/$id/approve';
@ -112,6 +114,7 @@ class ApiEndpoints {
// GRN // GRN
static const String grn = '/grn'; static const String grn = '/grn';
static const String grnExport = '/grn/export';
static String grnById(String id) => '/grn/$id'; static String grnById(String id) => '/grn/$id';
static String grnCancel(String id) => '/grn/$id/cancel'; static String grnCancel(String id) => '/grn/$id/cancel';
static String grnPdf(String id) => '/grn/$id/pdf'; static String grnPdf(String id) => '/grn/$id/pdf';
@ -123,6 +126,7 @@ class ApiEndpoints {
// Assets // Assets
static const String assets = '/assets'; static const String assets = '/assets';
static const String assetsExport = '/assets/export';
static String assetById(String id) => '/assets/$id'; static String assetById(String id) => '/assets/$id';
static String assetTransfer(String id) => '/assets/$id/transfer'; static String assetTransfer(String id) => '/assets/$id/transfer';
static String assetTransferHistory(String id) => '/assets/$id/transfer-history'; static String assetTransferHistory(String id) => '/assets/$id/transfer-history';

View File

@ -0,0 +1,31 @@
import 'package:dio/dio.dart';
/// Parses `Content-Disposition` for CSV/Excel downloads with a module fallback.
String exportFileNameFromResponse(
Response<List<int>> response, {
required String fallbackBase,
}) {
final disposition = response.headers.value('content-disposition');
if (disposition != null) {
final utf8Match = RegExp(
r"filename\*=UTF-8''([^;\n]+)",
caseSensitive: false,
).firstMatch(disposition);
if (utf8Match != null) {
return Uri.decodeComponent(utf8Match.group(1)!);
}
final match = RegExp(r'filename="?([^";\n]+)"?').firstMatch(disposition);
if (match != null) {
return match.group(1)!.trim();
}
}
final contentType =
response.headers.value('content-type')?.toLowerCase() ?? '';
if (contentType.contains('csv')) return '$fallbackBase.csv';
if (contentType.contains('sheet') || contentType.contains('excel')) {
return '$fallbackBase.xlsx';
}
return '$fallbackBase.csv';
}

View File

@ -1,23 +1,21 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import '../../../../core/constants/api_endpoints.dart'; import '../../../../core/constants/api_endpoints.dart';
import '../../../../core/utils/export_file_name.dart';
import '../../../../shared/models/api_response.dart'; import '../../../../shared/models/api_response.dart';
import '../../../../shared/models/asset_model.dart'; import '../../../../shared/models/asset_model.dart';
import '../../../../shared/models/entity_attachment_model.dart'; import '../../../../shared/models/entity_attachment_model.dart';
import '../../../../shared/models/export_file_result.dart';
class AssetRemoteDataSource { class AssetRemoteDataSource {
AssetRemoteDataSource({required this.dio}); AssetRemoteDataSource({required this.dio});
final Dio dio; final Dio dio;
Future<PaginatedResponse<AssetModel>> getAssets(PaginationParams params) async { Future<PaginatedResponse<AssetModel>> getAssets(AssetListQuery query) async {
final response = await dio.get( final response = await dio.get(
ApiEndpoints.assets, ApiEndpoints.assets,
queryParameters: { queryParameters: _queryToMap(query),
'page': params.page,
'limit': params.limit,
if (params.search != null && params.search!.isNotEmpty) 'search': params.search,
},
); );
return _parsePaginated(response.data, AssetModel.fromJson); return _parsePaginated(response.data, AssetModel.fromJson);
} }
@ -41,6 +39,18 @@ class AssetRemoteDataSource {
await dio.delete(ApiEndpoints.assetById(id)); await dio.delete(ApiEndpoints.assetById(id));
} }
Future<ExportFileResult> exportAssets(AssetListQuery query) async {
final response = await dio.get<List<int>>(
ApiEndpoints.assetsExport,
queryParameters: _exportQueryToMap(query),
options: Options(responseType: ResponseType.bytes),
);
return ExportFileResult(
bytes: response.data ?? <int>[],
fileName: exportFileNameFromResponse(response, fallbackBase: 'assets'),
);
}
Future<List<EntityAttachmentModel>> listAttachments(String assetId) async { Future<List<EntityAttachmentModel>> listAttachments(String assetId) async {
final response = await dio.get(ApiEndpoints.assetAttachments(assetId)); final response = await dio.get(ApiEndpoints.assetAttachments(assetId));
final data = response.data['data']; final data = response.data['data'];
@ -335,6 +345,29 @@ class AssetRemoteDataSource {
); );
} }
Map<String, dynamic> _queryToMap(AssetListQuery query) {
return {
'page': query.page,
'limit': query.limit,
..._exportQueryToMap(query),
};
}
Map<String, dynamic> _exportQueryToMap(AssetListQuery query) {
return {
if (query.search != null && query.search!.isNotEmpty) 'search': query.search,
if (query.status != null && query.status!.isNotEmpty) 'status': query.status,
if (query.condition != null && query.condition!.isNotEmpty)
'condition': query.condition,
if (query.itemCategoryId != null) 'item_category_id': query.itemCategoryId,
if (query.itemSubcategoryId != null)
'item_subcategory_id': query.itemSubcategoryId,
if (query.plantId != null) 'plant_id': query.plantId,
if (query.departmentId != null) 'department_id': query.departmentId,
if (query.isActive != null) 'is_active': query.isActive,
};
}
Map<String, dynamic> _asStringMap(dynamic value) { Map<String, dynamic> _asStringMap(dynamic value) {
if (value is Map<String, dynamic>) return value; if (value is Map<String, dynamic>) return value;
if (value is Map) return Map<String, dynamic>.from(value); if (value is Map) return Map<String, dynamic>.from(value);

View File

@ -5,6 +5,7 @@ import '../../../../core/network/dio_client.dart';
import '../../../../shared/models/api_response.dart'; import '../../../../shared/models/api_response.dart';
import '../../../../shared/models/asset_model.dart'; import '../../../../shared/models/asset_model.dart';
import '../../../../shared/models/entity_attachment_model.dart'; import '../../../../shared/models/entity_attachment_model.dart';
import '../../../../shared/models/export_file_result.dart';
import '../../domain/repositories/asset_repository.dart'; import '../../domain/repositories/asset_repository.dart';
import '../datasources/asset_remote_data_source.dart'; import '../datasources/asset_remote_data_source.dart';
@ -22,8 +23,13 @@ class AssetRepositoryImpl implements AssetRepository {
final AssetRemoteDataSource dataSource; final AssetRemoteDataSource dataSource;
@override @override
Future<Result<PaginatedResponse<AssetModel>>> getAssets(PaginationParams params) { Future<Result<PaginatedResponse<AssetModel>>> getAssets(AssetListQuery query) {
return safeApiCall(() => dataSource.getAssets(params)); return safeApiCall(() => dataSource.getAssets(query));
}
@override
Future<Result<ExportFileResult>> exportAssets(AssetListQuery query) {
return safeApiCall(() => dataSource.exportAssets(query));
} }
@override @override

View File

@ -2,9 +2,11 @@ import '../../../../core/network/api_handler.dart';
import '../../../../shared/models/api_response.dart'; import '../../../../shared/models/api_response.dart';
import '../../../../shared/models/asset_model.dart'; import '../../../../shared/models/asset_model.dart';
import '../../../../shared/models/entity_attachment_model.dart'; import '../../../../shared/models/entity_attachment_model.dart';
import '../../../../shared/models/export_file_result.dart';
abstract class AssetRepository { abstract class AssetRepository {
Future<Result<PaginatedResponse<AssetModel>>> getAssets(PaginationParams params); Future<Result<PaginatedResponse<AssetModel>>> getAssets(AssetListQuery query);
Future<Result<ExportFileResult>> exportAssets(AssetListQuery query);
Future<Result<AssetModel>> getAssetById(String id); Future<Result<AssetModel>> getAssetById(String id);
Future<Result<AssetModel>> createAsset(Map<String, dynamic> data); Future<Result<AssetModel>> createAsset(Map<String, dynamic> data);
Future<Result<AssetModel>> updateAsset(String id, Map<String, dynamic> data); Future<Result<AssetModel>> updateAsset(String id, Map<String, dynamic> data);

View File

@ -2,36 +2,39 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/utils/table_search.dart'; import '../../../../core/utils/table_search.dart';
import '../../../../shared/models/api_response.dart';
import '../../../../shared/models/asset_model.dart'; import '../../../../shared/models/asset_model.dart';
import '../../../../shared/models/entity_attachment_model.dart'; import '../../../../shared/models/entity_attachment_model.dart';
import '../../../../shared/models/export_file_result.dart';
import '../../data/repositories/asset_repository_impl.dart'; import '../../data/repositories/asset_repository_impl.dart';
class AssetsListState { class AssetsListState {
const AssetsListState({ const AssetsListState({
this.assets = const [], this.assets = const [],
this.query = const PaginationParams(limit: 20), this.query = const AssetListQuery(limit: 20),
this.total = 0, this.total = 0,
this.totalPages = 1, this.totalPages = 1,
this.isRefreshing = false, this.isRefreshing = false,
this.isExporting = false,
this.actionError, this.actionError,
this.actionSuccess, this.actionSuccess,
}); });
final List<AssetModel> assets; final List<AssetModel> assets;
final PaginationParams query; final AssetListQuery query;
final int total; final int total;
final int totalPages; final int totalPages;
final bool isRefreshing; final bool isRefreshing;
final bool isExporting;
final String? actionError; final String? actionError;
final String? actionSuccess; final String? actionSuccess;
AssetsListState copyWith({ AssetsListState copyWith({
List<AssetModel>? assets, List<AssetModel>? assets,
PaginationParams? query, AssetListQuery? query,
int? total, int? total,
int? totalPages, int? totalPages,
bool? isRefreshing, bool? isRefreshing,
bool? isExporting,
String? actionError, String? actionError,
String? actionSuccess, String? actionSuccess,
bool clearMessages = false, bool clearMessages = false,
@ -42,6 +45,7 @@ class AssetsListState {
total: total ?? this.total, total: total ?? this.total,
totalPages: totalPages ?? this.totalPages, totalPages: totalPages ?? this.totalPages,
isRefreshing: isRefreshing ?? this.isRefreshing, isRefreshing: isRefreshing ?? this.isRefreshing,
isExporting: isExporting ?? this.isExporting,
actionError: clearMessages ? null : actionError ?? this.actionError, actionError: clearMessages ? null : actionError ?? this.actionError,
actionSuccess: clearMessages ? null : actionSuccess ?? this.actionSuccess, actionSuccess: clearMessages ? null : actionSuccess ?? this.actionSuccess,
); );
@ -56,10 +60,10 @@ final assetsListProvider =
class AssetsListNotifier extends AutoDisposeAsyncNotifier<AssetsListState> { class AssetsListNotifier extends AutoDisposeAsyncNotifier<AssetsListState> {
@override @override
Future<AssetsListState> build() async { Future<AssetsListState> build() async {
return _load(const PaginationParams(limit: 20)); return _load(const AssetListQuery(limit: 20));
} }
Future<AssetsListState> _load(PaginationParams query) async { Future<AssetsListState> _load(AssetListQuery query) async {
final repository = ref.read(assetRepositoryProvider); final repository = ref.read(assetRepositoryProvider);
final result = await repository.getAssets(query); final result = await repository.getAssets(query);
if (result.failure != null) throw result.failure!; if (result.failure != null) throw result.failure!;
@ -82,7 +86,7 @@ class AssetsListNotifier extends AutoDisposeAsyncNotifier<AssetsListState> {
} }
} }
Future<void> applyQuery(PaginationParams query) async { Future<void> applyQuery(AssetListQuery query) async {
final previous = state.valueOrNull; final previous = state.valueOrNull;
if (previous == null) { if (previous == null) {
state = const AsyncLoading(); state = const AsyncLoading();
@ -97,7 +101,27 @@ class AssetsListNotifier extends AutoDisposeAsyncNotifier<AssetsListState> {
void setSearch(String search) { void setSearch(String search) {
final current = state.valueOrNull; final current = state.valueOrNull;
if (current == null) return; if (current == null) return;
applyQuery(current.query.copyWith(search: TableSearch.normalize(search), page: 1)); applyQuery(
current.query.copyWith(search: TableSearch.normalize(search), page: 1),
);
}
void setStatusFilter(String? status) {
final current = state.valueOrNull;
if (current == null) return;
applyQuery(current.query.copyWith(status: status, page: 1));
}
void setCategoryFilter(int? itemCategoryId) {
final current = state.valueOrNull;
if (current == null) return;
applyQuery(current.query.copyWith(itemCategoryId: itemCategoryId, page: 1));
}
void setPlantFilter(int? plantId) {
final current = state.valueOrNull;
if (current == null) return;
applyQuery(current.query.copyWith(plantId: plantId, page: 1));
} }
void setPage(int page) { void setPage(int page) {
@ -112,6 +136,30 @@ class AssetsListNotifier extends AutoDisposeAsyncNotifier<AssetsListState> {
applyQuery(current.query.copyWith(limit: limit, page: 1)); applyQuery(current.query.copyWith(limit: limit, page: 1));
} }
Future<ExportFileResult?> exportAssets() async {
final current = state.valueOrNull;
if (current == null) return null;
state = AsyncData(current.copyWith(isExporting: true, clearMessages: true));
final result =
await ref.read(assetRepositoryProvider).exportAssets(current.query);
final latest = state.valueOrNull ?? current;
if (result.failure != null) {
state = AsyncData(
latest.copyWith(
isExporting: false,
actionError: result.failure!.message,
),
);
return null;
}
state = AsyncData(latest.copyWith(isExporting: false));
return result.data;
}
Future<bool> deleteAsset(String id) async { Future<bool> deleteAsset(String id) async {
final repository = ref.read(assetRepositoryProvider); final repository = ref.read(assetRepositoryProvider);
final result = await repository.deleteAsset(id); final result = await repository.deleteAsset(id);

View File

@ -8,6 +8,7 @@ import '../../../../core/errors/failure.dart';
import '../../../../core/utils/formatters.dart'; import '../../../../core/utils/formatters.dart';
import '../../../../core/utils/responsive_utils.dart'; import '../../../../core/utils/responsive_utils.dart';
import '../../../../shared/models/asset_model.dart'; import '../../../../shared/models/asset_model.dart';
import '../../../../shared/models/user_management_models.dart';
import '../../../../shared/providers/permissions_provider.dart'; import '../../../../shared/providers/permissions_provider.dart';
import '../../../../shared/widgets/app_card.dart'; import '../../../../shared/widgets/app_card.dart';
import '../../../../shared/widgets/app_confirmation_dialog.dart'; import '../../../../shared/widgets/app_confirmation_dialog.dart';
@ -22,6 +23,7 @@ import '../../../../shared/widgets/app_table_action_icon.dart';
import '../../../../shared/widgets/can_permission.dart'; import '../../../../shared/widgets/can_permission.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 '../../../../shared/utils/file_download_helper.dart';
import '../providers/asset_categories_provider.dart'; import '../providers/asset_categories_provider.dart';
import '../providers/asset_form_lookups_provider.dart'; import '../providers/asset_form_lookups_provider.dart';
import '../providers/assets_provider.dart'; import '../providers/assets_provider.dart';
@ -36,15 +38,12 @@ class AssetListScreen extends ConsumerStatefulWidget {
} }
class _AssetListScreenState extends ConsumerState<AssetListScreen> { class _AssetListScreenState extends ConsumerState<AssetListScreen> {
String? _selectedCategory;
String? _selectedPlant;
String? _selectedStatus;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final assetsAsync = ref.watch(assetsListProvider); final assetsAsync = ref.watch(assetsListProvider);
final canEdit = ref.can('assets', PermissionAction.update); final canEdit = ref.can('assets', PermissionAction.update);
final canDelete = ref.can('assets', PermissionAction.delete); final canDelete = ref.can('assets', PermissionAction.delete);
final canExport = ref.can('assets', PermissionAction.export);
ref.listen(assetsListProvider, (prev, next) { ref.listen(assetsListProvider, (prev, next) {
final error = next.valueOrNull?.actionError; final error = next.valueOrNull?.actionError;
@ -66,26 +65,11 @@ class _AssetListScreenState extends ConsumerState<AssetListScreen> {
onRetry: () => ref.invalidate(assetsListProvider), onRetry: () => ref.invalidate(assetsListProvider),
), ),
data: (state) { data: (state) {
final allCategories = ref.watch(itemCategoriesProvider).valueOrNull ?? []; final allCategories =
final filteredAssets = _filterAssets(state.assets); ref.watch(itemCategoriesProvider).valueOrNull ?? [];
final categories = _categoryOptions(state.assets, allCategories); final lookups = ref.watch(assetFormLookupsProvider).valueOrNull;
final plants = _plantOptions(state.assets); final allPlants = lookups?.plants ?? const [];
final statuses = _statusOptions( final notifier = ref.read(assetsListProvider.notifier);
ref.watch(assetFormLookupsProvider).valueOrNull?.statuses ?? const [],
);
final categoryFilter = _selectedCategory ?? 'All Categories';
final plantFilter = _selectedPlant ?? 'All Plants';
final statusFilter = _selectedStatus ?? 'All Statuses';
final statusLabels = {
for (final option
in (ref.watch(assetFormLookupsProvider).valueOrNull?.statuses ??
const <AssetDropdownOption>[]))
option.value: option.label,
'All Statuses': 'All Statuses',
};
final page = state.query.page;
final pageSize = state.query.limit;
final total = state.total;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -94,6 +78,19 @@ class _AssetListScreenState extends ConsumerState<AssetListScreen> {
title: 'Asset Master', title: 'Asset Master',
subtitle: 'Manage plant assets, AMC, service & insurance', subtitle: 'Manage plant assets, AMC, service & insurance',
actions: [ actions: [
if (canExport)
OutlinedButton.icon(
onPressed: state.isExporting ? null : _exportAssets,
icon: state.isExporting
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.download_outlined),
label: Text(state.isExporting ? 'Exporting...' : 'Export'),
),
if (canExport) const SizedBox(width: 8),
OutlinedButton.icon( OutlinedButton.icon(
onPressed: () => context.go(RouteConstants.assetAlerts), onPressed: () => context.go(RouteConstants.assetAlerts),
icon: const Icon(Icons.notifications_outlined), icon: const Icon(Icons.notifications_outlined),
@ -113,10 +110,10 @@ class _AssetListScreenState extends ConsumerState<AssetListScreen> {
), ),
Expanded( Expanded(
child: RefreshIndicator( child: RefreshIndicator(
onRefresh: () => ref.read(assetsListProvider.notifier).refresh(), onRefresh: () => notifier.refresh(),
child: context.isMobile child: context.isMobile
? _AssetMobileList( ? _AssetMobileList(
assets: filteredAssets, assets: state.assets,
onView: _viewAsset, onView: _viewAsset,
onEdit: canEdit ? _editAsset : null, onEdit: canEdit ? _editAsset : null,
onDelete: canDelete ? _deleteAsset : null, onDelete: canDelete ? _deleteAsset : null,
@ -142,39 +139,21 @@ class _AssetListScreenState extends ConsumerState<AssetListScreen> {
builder: (context, constraints) { builder: (context, constraints) {
return _AssetsFilterBar( return _AssetsFilterBar(
wrapped: constraints.maxWidth < 1000, wrapped: constraints.maxWidth < 1000,
categoryFilter: categoryFilter, query: state.query,
plantFilter: plantFilter, categories: allCategories,
statusFilter: statusFilter, plants: allPlants,
categories: categories, statuses: lookups?.statuses ?? const [],
plants: plants, onSearch: notifier.setSearch,
statuses: statuses, onCategoryChanged:
statusLabels: statusLabels, notifier.setCategoryFilter,
onSearch: onPlantChanged: notifier.setPlantFilter,
ref.read(assetsListProvider.notifier).setSearch, onStatusChanged: notifier.setStatusFilter,
onCategoryChanged: (value) {
setState(() {
_selectedCategory =
value == 'All Categories' ? null : value;
});
},
onPlantChanged: (value) {
setState(() {
_selectedPlant =
value == 'All Plants' ? null : value;
});
},
onStatusChanged: (value) {
setState(() {
_selectedStatus =
value == 'All Statuses' ? null : value;
});
},
); );
}, },
), ),
), ),
const Divider(height: 1), const Divider(height: 1),
if (filteredAssets.isEmpty) if (state.assets.isEmpty)
Expanded( Expanded(
child: Center( child: Center(
child: Text( child: Text(
@ -193,7 +172,7 @@ class _AssetListScreenState extends ConsumerState<AssetListScreen> {
else else
Expanded( Expanded(
child: _AssetDataTable( child: _AssetDataTable(
assets: filteredAssets, assets: state.assets,
canEdit: canEdit, canEdit: canEdit,
canDelete: canDelete, canDelete: canDelete,
onView: _viewAsset, onView: _viewAsset,
@ -205,17 +184,13 @@ class _AssetListScreenState extends ConsumerState<AssetListScreen> {
Padding( Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: AppPagination( child: AppPagination(
currentPage: page, currentPage: state.query.page,
totalPages: state.totalPages, totalPages: state.totalPages,
totalItems: total, totalItems: state.total,
pageSize: pageSize, pageSize: state.query.limit,
itemLabel: 'assets', itemLabel: 'assets',
onPageChanged: ref onPageChanged: notifier.setPage,
.read(assetsListProvider.notifier) onPageSizeChanged: notifier.setPageSize,
.setPage,
onPageSizeChanged: ref
.read(assetsListProvider.notifier)
.setPageSize,
), ),
), ),
], ],
@ -230,58 +205,6 @@ class _AssetListScreenState extends ConsumerState<AssetListScreen> {
); );
} }
List<AssetModel> _filterAssets(List<AssetModel> assets) {
return assets.where((asset) {
if (_selectedCategory != null &&
asset.assetCategoryName != _selectedCategory) {
return false;
}
if (_selectedPlant != null && asset.plantName != _selectedPlant) {
return false;
}
if (_selectedStatus != null) {
final status = asset.status?.trim().toUpperCase().replaceAll(' ', '_');
if (status != _selectedStatus) return false;
}
return true;
}).toList();
}
List<String> _categoryOptions(
List<AssetModel> assets,
List<AssetCategoryModel> allCategories,
) {
final names = <String>{};
for (final asset in assets) {
final name = asset.assetCategoryName;
if (name != null && name.isNotEmpty) names.add(name);
}
for (final category in allCategories) {
names.add(category.name);
}
final sorted = names.toList()..sort();
return ['All Categories', ...sorted];
}
List<String> _plantOptions(List<AssetModel> assets) {
final names = assets
.map((a) => a.plantName)
.whereType<String>()
.where((n) => n.isNotEmpty)
.toSet()
.toList()
..sort();
return ['All Plants', ...names];
}
List<String> _statusOptions(List<AssetDropdownOption> apiStatuses) {
final labels = apiStatuses
.map((option) => option.value)
.where((value) => value.trim().isNotEmpty)
.toList();
return ['All Statuses', ...labels];
}
void _viewAsset(AssetModel asset) { void _viewAsset(AssetModel asset) {
context.push('${RouteConstants.assets}/${asset.id}'); context.push('${RouteConstants.assets}/${asset.id}');
} }
@ -290,6 +213,34 @@ class _AssetListScreenState extends ConsumerState<AssetListScreen> {
openAssetFormPanel(context, ref, assetId: asset.id); openAssetFormPanel(context, ref, assetId: asset.id);
} }
Future<void> _exportAssets() async {
final file = await ref.read(assetsListProvider.notifier).exportAssets();
if (!mounted) return;
if (file == null) {
final error = ref.read(assetsListProvider).valueOrNull?.actionError;
if (error != null) {
showAppToastFromSnackBar(context, SnackBar(content: Text(error)));
}
return;
}
final saved = await downloadFile(
bytes: file.bytes,
fileName: file.fileName,
);
if (!mounted) return;
showAppToastFromSnackBar(
context,
SnackBar(
content: Text(
saved ? 'Downloaded ${file.fileName}' : 'Export cancelled',
),
),
);
}
Future<void> _deleteAsset(AssetModel asset) async { Future<void> _deleteAsset(AssetModel asset) async {
final confirmed = await showAppConfirmationDialog( final confirmed = await showAppConfirmationDialog(
context: context, context: context,
@ -325,13 +276,10 @@ class _AssetListScreenState extends ConsumerState<AssetListScreen> {
class _AssetsFilterBar extends StatelessWidget { class _AssetsFilterBar extends StatelessWidget {
const _AssetsFilterBar({ const _AssetsFilterBar({
required this.wrapped, required this.wrapped,
required this.categoryFilter, required this.query,
required this.plantFilter,
required this.statusFilter,
required this.categories, required this.categories,
required this.plants, required this.plants,
required this.statuses, required this.statuses,
required this.statusLabels,
required this.onSearch, required this.onSearch,
required this.onCategoryChanged, required this.onCategoryChanged,
required this.onPlantChanged, required this.onPlantChanged,
@ -339,17 +287,14 @@ class _AssetsFilterBar extends StatelessWidget {
}); });
final bool wrapped; final bool wrapped;
final String categoryFilter; final AssetListQuery query;
final String plantFilter; final List<AssetCategoryModel> categories;
final String statusFilter; final List<FilterOptionModel> plants;
final List<String> categories; final List<AssetDropdownOption> statuses;
final List<String> plants;
final List<String> statuses;
final Map<String, String> statusLabels;
final ValueChanged<String> onSearch; final ValueChanged<String> onSearch;
final ValueChanged<String> onCategoryChanged; final ValueChanged<int?> onCategoryChanged;
final ValueChanged<String> onPlantChanged; final ValueChanged<int?> onPlantChanged;
final ValueChanged<String> onStatusChanged; final ValueChanged<String?> onStatusChanged;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -367,24 +312,61 @@ class _AssetsFilterBar extends StatelessWidget {
), ),
); );
final categoryOptions = <AppDropdownOption<int?>>[
const AppDropdownOption(value: null, label: 'All Categories'),
for (final category in categories)
if (int.tryParse(category.id) != null)
AppDropdownOption(
value: int.parse(category.id),
label: category.name,
),
];
final plantOptions = <AppDropdownOption<int?>>[
const AppDropdownOption(value: null, label: 'All Plants'),
for (final plant in plants)
if (int.tryParse(plant.id) != null)
AppDropdownOption(
value: int.parse(plant.id),
label: plant.name,
),
];
final statusOptions = <AppDropdownOption<String?>>[
const AppDropdownOption(value: null, label: 'All Statuses'),
for (final status in statuses)
if (status.value.trim().isNotEmpty)
AppDropdownOption(
value: status.value,
label: status.label.isNotEmpty
? status.label
: status.value.replaceAll('_', ' '),
),
];
final filters = [ final filters = [
_AssetFilterDropdown( AppSearchableDropdown<int?>(
value: categoryFilter,
label: 'Category', label: 'Category',
items: categories, value: query.itemCategoryId,
searchHint: 'Search category...',
isDense: true,
options: categoryOptions,
onChanged: onCategoryChanged, onChanged: onCategoryChanged,
), ),
_AssetFilterDropdown( AppSearchableDropdown<int?>(
value: plantFilter,
label: 'Plant', label: 'Plant',
items: plants, value: query.plantId,
searchHint: 'Search plant...',
isDense: true,
options: plantOptions,
onChanged: onPlantChanged, onChanged: onPlantChanged,
), ),
_AssetFilterDropdown( AppSearchableDropdown<String?>(
value: statusFilter,
label: 'Status', label: 'Status',
items: statuses, value: query.status,
itemLabels: statusLabels, searchHint: 'Search status...',
isDense: true,
options: statusOptions,
onChanged: onStatusChanged, onChanged: onStatusChanged,
), ),
]; ];
@ -416,43 +398,6 @@ class _AssetsFilterBar extends StatelessWidget {
} }
} }
class _AssetFilterDropdown extends StatelessWidget {
const _AssetFilterDropdown({
required this.value,
required this.label,
required this.items,
required this.onChanged,
this.itemLabels = const {},
});
final String value;
final String label;
final List<String> items;
final Map<String, String> itemLabels;
final ValueChanged<String> onChanged;
@override
Widget build(BuildContext context) {
return AppSearchableDropdown<String>(
label: label,
value: value,
isDense: true,
searchHint: 'Search $label...',
options: items
.map(
(item) => AppDropdownOption(
value: item,
label: itemLabels[item] ?? item.replaceAll('_', ' '),
),
)
.toList(),
onChanged: (v) {
if (v != null) onChanged(v);
},
);
}
}
class _AssetDataTable extends StatelessWidget { class _AssetDataTable extends StatelessWidget {
const _AssetDataTable({ const _AssetDataTable({
required this.assets, required this.assets,

View File

@ -1,7 +1,9 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import '../../../../core/constants/api_endpoints.dart'; import '../../../../core/constants/api_endpoints.dart';
import '../../../../core/utils/export_file_name.dart';
import '../../../../shared/models/api_response.dart'; import '../../../../shared/models/api_response.dart';
import '../../../../shared/models/export_file_result.dart';
import '../../../../shared/models/grn_model.dart'; import '../../../../shared/models/grn_model.dart';
class GrnRemoteDataSource { class GrnRemoteDataSource {
@ -48,6 +50,18 @@ class GrnRemoteDataSource {
return response.data ?? []; return response.data ?? [];
} }
Future<ExportFileResult> exportGrns(GrnListQuery query) async {
final response = await dio.get<List<int>>(
ApiEndpoints.grnExport,
queryParameters: _exportQueryToMap(query),
options: Options(responseType: ResponseType.bytes),
);
return ExportFileResult(
bytes: response.data ?? <int>[],
fileName: exportFileNameFromResponse(response, fallbackBase: 'grn'),
);
}
Future<List<GrnAttachmentModel>> listAttachments(String grnId) async { Future<List<GrnAttachmentModel>> listAttachments(String grnId) async {
final response = await dio.get(ApiEndpoints.grnAttachments(grnId)); final response = await dio.get(ApiEndpoints.grnAttachments(grnId));
final data = response.data['data']; final data = response.data['data'];
@ -106,6 +120,12 @@ class GrnRemoteDataSource {
return { return {
'page': query.page, 'page': query.page,
'limit': query.limit, 'limit': query.limit,
..._exportQueryToMap(query),
};
}
Map<String, dynamic> _exportQueryToMap(GrnListQuery query) {
return {
if (query.search != null && query.search!.isNotEmpty) 'search': query.search, if (query.search != null && query.search!.isNotEmpty) 'search': query.search,
if (query.status != null) 'status': query.status, if (query.status != null) 'status': query.status,
if (query.poId != null) 'po_id': query.poId, if (query.poId != null) 'po_id': query.poId,

View File

@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/network/api_handler.dart'; import '../../../../core/network/api_handler.dart';
import '../../../../core/network/dio_client.dart'; import '../../../../core/network/dio_client.dart';
import '../../../../shared/models/api_response.dart'; import '../../../../shared/models/api_response.dart';
import '../../../../shared/models/export_file_result.dart';
import '../../../../shared/models/grn_model.dart'; import '../../../../shared/models/grn_model.dart';
import '../../domain/repositories/grn_repository.dart'; import '../../domain/repositories/grn_repository.dart';
import '../datasources/grn_remote_data_source.dart'; import '../datasources/grn_remote_data_source.dart';
@ -25,6 +26,11 @@ class GrnRepositoryImpl implements GrnRepository {
return safeApiCall(() => dataSource.getGrns(query)); return safeApiCall(() => dataSource.getGrns(query));
} }
@override
Future<Result<ExportFileResult>> exportGrns(GrnListQuery query) {
return safeApiCall(() => dataSource.exportGrns(query));
}
@override @override
Future<Result<GrnModel>> getGrnById(String id) { Future<Result<GrnModel>> getGrnById(String id) {
return safeApiCall(() => dataSource.getGrnById(id)); return safeApiCall(() => dataSource.getGrnById(id));

View File

@ -1,9 +1,11 @@
import '../../../../core/network/api_handler.dart'; import '../../../../core/network/api_handler.dart';
import '../../../../shared/models/api_response.dart'; import '../../../../shared/models/api_response.dart';
import '../../../../shared/models/export_file_result.dart';
import '../../../../shared/models/grn_model.dart'; import '../../../../shared/models/grn_model.dart';
abstract class GrnRepository { abstract class GrnRepository {
Future<Result<PaginatedResponse<GrnModel>>> getGrns(GrnListQuery query); Future<Result<PaginatedResponse<GrnModel>>> getGrns(GrnListQuery query);
Future<Result<ExportFileResult>> exportGrns(GrnListQuery query);
Future<Result<GrnModel>> getGrnById(String id); Future<Result<GrnModel>> getGrnById(String id);
Future<Result<GrnModel>> createGrn(Map<String, dynamic> data); Future<Result<GrnModel>> createGrn(Map<String, dynamic> data);
Future<Result<GrnModel>> updateGrn(String id, Map<String, dynamic> data); Future<Result<GrnModel>> updateGrn(String id, Map<String, dynamic> data);

View File

@ -2,6 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/utils/table_search.dart'; import '../../../../core/utils/table_search.dart';
import '../../../../shared/models/export_file_result.dart';
import '../../../../shared/models/grn_model.dart'; import '../../../../shared/models/grn_model.dart';
import '../../data/repositories/grn_repository_impl.dart'; import '../../data/repositories/grn_repository_impl.dart';
@ -12,6 +13,7 @@ class GrnListState {
this.total = 0, this.total = 0,
this.totalPages = 1, this.totalPages = 1,
this.isRefreshing = false, this.isRefreshing = false,
this.isExporting = false,
this.actionError, this.actionError,
this.actionSuccess, this.actionSuccess,
}); });
@ -21,6 +23,7 @@ class GrnListState {
final int total; final int total;
final int totalPages; final int totalPages;
final bool isRefreshing; final bool isRefreshing;
final bool isExporting;
final String? actionError; final String? actionError;
final String? actionSuccess; final String? actionSuccess;
@ -30,6 +33,7 @@ class GrnListState {
int? total, int? total,
int? totalPages, int? totalPages,
bool? isRefreshing, bool? isRefreshing,
bool? isExporting,
String? actionError, String? actionError,
String? actionSuccess, String? actionSuccess,
bool clearMessages = false, bool clearMessages = false,
@ -40,6 +44,7 @@ class GrnListState {
total: total ?? this.total, total: total ?? this.total,
totalPages: totalPages ?? this.totalPages, totalPages: totalPages ?? this.totalPages,
isRefreshing: isRefreshing ?? this.isRefreshing, isRefreshing: isRefreshing ?? this.isRefreshing,
isExporting: isExporting ?? this.isExporting,
actionError: clearMessages ? null : actionError ?? this.actionError, actionError: clearMessages ? null : actionError ?? this.actionError,
actionSuccess: clearMessages ? null : actionSuccess ?? this.actionSuccess, actionSuccess: clearMessages ? null : actionSuccess ?? this.actionSuccess,
); );
@ -115,6 +120,30 @@ class GrnListNotifier extends AutoDisposeAsyncNotifier<GrnListState> {
if (current == null) return; if (current == null) return;
applyQuery(current.query.copyWith(limit: limit, page: 1)); applyQuery(current.query.copyWith(limit: limit, page: 1));
} }
Future<ExportFileResult?> exportGrns() async {
final current = state.valueOrNull;
if (current == null) return null;
state = AsyncData(current.copyWith(isExporting: true, clearMessages: true));
final result =
await ref.read(grnRepositoryProvider).exportGrns(current.query);
final latest = state.valueOrNull ?? current;
if (result.failure != null) {
state = AsyncData(
latest.copyWith(
isExporting: false,
actionError: result.failure!.message,
),
);
return null;
}
state = AsyncData(latest.copyWith(isExporting: false));
return result.data;
}
} }
final grnDetailProvider = final grnDetailProvider =

View File

@ -21,6 +21,7 @@ import '../../../../shared/widgets/app_table_action_icon.dart';
import '../../../../shared/widgets/app_table_shell.dart'; import '../../../../shared/widgets/app_table_shell.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 '../../../../shared/utils/file_download_helper.dart';
import '../providers/grn_provider.dart'; import '../providers/grn_provider.dart';
import '../widgets/grn_status_chip.dart'; import '../widgets/grn_status_chip.dart';
import '../../../../shared/widgets/app_toast.dart'; import '../../../../shared/widgets/app_toast.dart';
@ -45,6 +46,14 @@ class _GrnListScreenState extends ConsumerState<GrnListScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final listAsync = ref.watch(grnListProvider); final listAsync = ref.watch(grnListProvider);
final canEdit = ref.can('grn', PermissionAction.update); final canEdit = ref.can('grn', PermissionAction.update);
final canExport = ref.can('grn', PermissionAction.export);
ref.listen(grnListProvider, (prev, next) {
final error = next.valueOrNull?.actionError;
if (error != null && error != prev?.valueOrNull?.actionError) {
showAppToastFromSnackBar(context, SnackBar(content: Text(error)));
}
});
return Padding( return Padding(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
@ -80,6 +89,9 @@ class _GrnListScreenState extends ConsumerState<GrnListScreen> {
searchController: _searchController, searchController: _searchController,
query: state.query, query: state.query,
wrapped: constraints.maxWidth < 900, wrapped: constraints.maxWidth < 900,
showExport: canExport,
isExporting: state.isExporting,
onExport: _exportGrns,
onSearch: ref.read(grnListProvider.notifier).setSearch, onSearch: ref.read(grnListProvider.notifier).setSearch,
onStatusChanged: onStatusChanged:
ref.read(grnListProvider.notifier).setStatusFilter, ref.read(grnListProvider.notifier).setStatusFilter,
@ -132,6 +144,34 @@ class _GrnListScreenState extends ConsumerState<GrnListScreen> {
); );
} }
Future<void> _exportGrns() async {
final file = await ref.read(grnListProvider.notifier).exportGrns();
if (!mounted) return;
if (file == null) {
final error = ref.read(grnListProvider).valueOrNull?.actionError;
if (error != null) {
showAppToastFromSnackBar(context, SnackBar(content: Text(error)));
}
return;
}
final saved = await downloadFile(
bytes: file.bytes,
fileName: file.fileName,
);
if (!mounted) return;
showAppToastFromSnackBar(
context,
SnackBar(
content: Text(
saved ? 'Downloaded ${file.fileName}' : 'Export cancelled',
),
),
);
}
void _viewGrn(GrnModel grn) { void _viewGrn(GrnModel grn) {
context.push('${RouteConstants.grn}/${grn.id}'); context.push('${RouteConstants.grn}/${grn.id}');
} }
@ -154,6 +194,9 @@ class _FiltersBar extends StatelessWidget {
required this.wrapped, required this.wrapped,
required this.onSearch, required this.onSearch,
required this.onStatusChanged, required this.onStatusChanged,
this.showExport = false,
this.isExporting = false,
this.onExport,
}); });
final TextEditingController searchController; final TextEditingController searchController;
@ -161,6 +204,9 @@ class _FiltersBar extends StatelessWidget {
final bool wrapped; final bool wrapped;
final ValueChanged<String> onSearch; final ValueChanged<String> onSearch;
final ValueChanged<String?> onStatusChanged; final ValueChanged<String?> onStatusChanged;
final bool showExport;
final bool isExporting;
final VoidCallback? onExport;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -199,6 +245,18 @@ class _FiltersBar extends StatelessWidget {
), ),
); );
final exportButton = OutlinedButton.icon(
onPressed: isExporting ? null : onExport,
icon: isExporting
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.download_outlined, size: 18),
label: Text(isExporting ? 'Exporting...' : 'Export'),
);
if (wrapped) { if (wrapped) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
@ -206,6 +264,10 @@ class _FiltersBar extends StatelessWidget {
searchField, searchField,
const SizedBox(height: 12), const SizedBox(height: 12),
statusFilter, statusFilter,
if (showExport) ...[
const SizedBox(height: 12),
Align(alignment: Alignment.centerRight, child: exportButton),
],
], ],
); );
} }
@ -216,6 +278,10 @@ class _FiltersBar extends StatelessWidget {
Expanded(flex: 3, child: searchField), Expanded(flex: 3, child: searchField),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded(flex: 2, child: statusFilter), Expanded(flex: 2, child: statusFilter),
if (showExport) ...[
const SizedBox(width: 12),
exportButton,
],
], ],
); );
} }

View File

@ -1,8 +1,10 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import '../../../../core/constants/api_endpoints.dart'; import '../../../../core/constants/api_endpoints.dart';
import '../../../../core/utils/export_file_name.dart';
import '../../../../shared/models/api_response.dart'; import '../../../../shared/models/api_response.dart';
import '../../../../shared/models/entity_attachment_model.dart'; import '../../../../shared/models/entity_attachment_model.dart';
import '../../../../shared/models/export_file_result.dart';
import '../../../../shared/models/purchase_order_model.dart'; import '../../../../shared/models/purchase_order_model.dart';
class PurchaseOrderRemoteDataSource { class PurchaseOrderRemoteDataSource {
@ -128,6 +130,23 @@ class PurchaseOrderRemoteDataSource {
return response.data ?? []; return response.data ?? [];
} }
Future<ExportFileResult> exportPurchaseOrders(
PurchaseOrderListQuery query,
) async {
final response = await dio.get<List<int>>(
ApiEndpoints.purchaseOrdersExport,
queryParameters: _exportQueryToMap(query),
options: Options(responseType: ResponseType.bytes),
);
return ExportFileResult(
bytes: response.data ?? <int>[],
fileName: exportFileNameFromResponse(
response,
fallbackBase: 'purchase_orders',
),
);
}
Future<List<EntityAttachmentModel>> listAttachments(String poId) async { Future<List<EntityAttachmentModel>> listAttachments(String poId) async {
final response = final response =
await dio.get(ApiEndpoints.purchaseOrderAttachments(poId)); await dio.get(ApiEndpoints.purchaseOrderAttachments(poId));
@ -177,6 +196,12 @@ class PurchaseOrderRemoteDataSource {
return { return {
'page': query.page, 'page': query.page,
'limit': query.limit, 'limit': query.limit,
..._exportQueryToMap(query),
};
}
Map<String, dynamic> _exportQueryToMap(PurchaseOrderListQuery query) {
return {
if (query.search != null && query.search!.isNotEmpty) 'search': query.search, if (query.search != null && query.search!.isNotEmpty) 'search': query.search,
if (query.status != null) 'status': query.status, if (query.status != null) 'status': query.status,
if (query.poType != null) 'po_type': query.poType, if (query.poType != null) 'po_type': query.poType,

View File

@ -4,6 +4,7 @@ import '../../../../core/network/api_handler.dart';
import '../../../../core/network/dio_client.dart'; import '../../../../core/network/dio_client.dart';
import '../../../../shared/models/api_response.dart'; import '../../../../shared/models/api_response.dart';
import '../../../../shared/models/entity_attachment_model.dart'; import '../../../../shared/models/entity_attachment_model.dart';
import '../../../../shared/models/export_file_result.dart';
import '../../../../shared/models/purchase_order_model.dart'; import '../../../../shared/models/purchase_order_model.dart';
import '../../domain/repositories/purchase_order_repository.dart'; import '../../domain/repositories/purchase_order_repository.dart';
import '../datasources/purchase_order_remote_data_source.dart'; import '../datasources/purchase_order_remote_data_source.dart';
@ -31,6 +32,13 @@ class PurchaseOrderRepositoryImpl implements PurchaseOrderRepository {
return safeApiCall(() => dataSource.getPurchaseOrders(query)); return safeApiCall(() => dataSource.getPurchaseOrders(query));
} }
@override
Future<Result<ExportFileResult>> exportPurchaseOrders(
PurchaseOrderListQuery query,
) {
return safeApiCall(() => dataSource.exportPurchaseOrders(query));
}
@override @override
Future<Result<PurchaseOrderModel>> getPurchaseOrderById(String id) { Future<Result<PurchaseOrderModel>> getPurchaseOrderById(String id) {
return safeApiCall(() => dataSource.getPurchaseOrderById(id)); return safeApiCall(() => dataSource.getPurchaseOrderById(id));

View File

@ -1,12 +1,16 @@
import '../../../../core/network/api_handler.dart'; import '../../../../core/network/api_handler.dart';
import '../../../../shared/models/api_response.dart'; import '../../../../shared/models/api_response.dart';
import '../../../../shared/models/entity_attachment_model.dart'; import '../../../../shared/models/entity_attachment_model.dart';
import '../../../../shared/models/export_file_result.dart';
import '../../../../shared/models/purchase_order_model.dart'; import '../../../../shared/models/purchase_order_model.dart';
abstract class PurchaseOrderRepository { abstract class PurchaseOrderRepository {
Future<Result<PaginatedResponse<PurchaseOrderModel>>> getPurchaseOrders( Future<Result<PaginatedResponse<PurchaseOrderModel>>> getPurchaseOrders(
PurchaseOrderListQuery query, PurchaseOrderListQuery query,
); );
Future<Result<ExportFileResult>> exportPurchaseOrders(
PurchaseOrderListQuery query,
);
Future<Result<PurchaseOrderModel>> getPurchaseOrderById(String id); Future<Result<PurchaseOrderModel>> getPurchaseOrderById(String id);
Future<Result<PurchaseOrderModel>> createPurchaseOrder(Map<String, dynamic> data); Future<Result<PurchaseOrderModel>> createPurchaseOrder(Map<String, dynamic> data);
Future<Result<PurchaseOrderModel>> updatePurchaseOrder( Future<Result<PurchaseOrderModel>> updatePurchaseOrder(

View File

@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/utils/table_search.dart'; import '../../../../core/utils/table_search.dart';
import '../../../../shared/models/entity_attachment_model.dart'; import '../../../../shared/models/entity_attachment_model.dart';
import '../../../../shared/models/export_file_result.dart';
import '../../../../shared/models/purchase_order_model.dart'; import '../../../../shared/models/purchase_order_model.dart';
import '../../../grn/presentation/providers/grn_lookups_provider.dart'; import '../../../grn/presentation/providers/grn_lookups_provider.dart';
import '../../data/repositories/purchase_order_repository_impl.dart'; import '../../data/repositories/purchase_order_repository_impl.dart';
@ -14,6 +15,7 @@ class PurchaseOrdersListState {
this.total = 0, this.total = 0,
this.totalPages = 1, this.totalPages = 1,
this.isRefreshing = false, this.isRefreshing = false,
this.isExporting = false,
this.actionError, this.actionError,
this.actionSuccess, this.actionSuccess,
}); });
@ -23,6 +25,7 @@ class PurchaseOrdersListState {
final int total; final int total;
final int totalPages; final int totalPages;
final bool isRefreshing; final bool isRefreshing;
final bool isExporting;
final String? actionError; final String? actionError;
final String? actionSuccess; final String? actionSuccess;
@ -32,6 +35,7 @@ class PurchaseOrdersListState {
int? total, int? total,
int? totalPages, int? totalPages,
bool? isRefreshing, bool? isRefreshing,
bool? isExporting,
String? actionError, String? actionError,
String? actionSuccess, String? actionSuccess,
bool clearMessages = false, bool clearMessages = false,
@ -42,6 +46,7 @@ class PurchaseOrdersListState {
total: total ?? this.total, total: total ?? this.total,
totalPages: totalPages ?? this.totalPages, totalPages: totalPages ?? this.totalPages,
isRefreshing: isRefreshing ?? this.isRefreshing, isRefreshing: isRefreshing ?? this.isRefreshing,
isExporting: isExporting ?? this.isExporting,
actionError: clearMessages ? null : actionError ?? this.actionError, actionError: clearMessages ? null : actionError ?? this.actionError,
actionSuccess: clearMessages ? null : actionSuccess ?? this.actionSuccess, actionSuccess: clearMessages ? null : actionSuccess ?? this.actionSuccess,
); );
@ -125,6 +130,31 @@ class PurchaseOrdersListNotifier
applyQuery(current.query.copyWith(limit: limit, page: 1)); applyQuery(current.query.copyWith(limit: limit, page: 1));
} }
Future<ExportFileResult?> exportPurchaseOrders() async {
final current = state.valueOrNull;
if (current == null) return null;
state = AsyncData(current.copyWith(isExporting: true, clearMessages: true));
final result = await ref
.read(purchaseOrderRepositoryProvider)
.exportPurchaseOrders(current.query);
final latest = state.valueOrNull ?? current;
if (result.failure != null) {
state = AsyncData(
latest.copyWith(
isExporting: false,
actionError: result.failure!.message,
),
);
return null;
}
state = AsyncData(latest.copyWith(isExporting: false));
return result.data;
}
Future<bool> deletePurchaseOrder(String id) async { Future<bool> deletePurchaseOrder(String id) async {
final repository = ref.read(purchaseOrderRepositoryProvider); final repository = ref.read(purchaseOrderRepositoryProvider);
final result = await repository.deletePurchaseOrder(id); final result = await repository.deletePurchaseOrder(id);

View File

@ -23,6 +23,7 @@ import '../../../../shared/widgets/app_table_action_icon.dart';
import '../../../../shared/widgets/app_table_shell.dart'; import '../../../../shared/widgets/app_table_shell.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 '../../../../shared/utils/file_download_helper.dart';
import '../providers/purchase_orders_provider.dart'; import '../providers/purchase_orders_provider.dart';
import '../widgets/po_status_chip.dart'; import '../widgets/po_status_chip.dart';
import '../../../../shared/widgets/app_toast.dart'; import '../../../../shared/widgets/app_toast.dart';
@ -59,6 +60,7 @@ class _PurchaseOrderListScreenState extends ConsumerState<PurchaseOrderListScree
final ordersAsync = ref.watch(purchaseOrdersListProvider); final ordersAsync = ref.watch(purchaseOrdersListProvider);
final canEdit = ref.can('purchase_orders', PermissionAction.update); final canEdit = ref.can('purchase_orders', PermissionAction.update);
final canDelete = ref.can('purchase_orders', PermissionAction.delete); final canDelete = ref.can('purchase_orders', PermissionAction.delete);
final canExport = ref.can('purchase_orders', PermissionAction.export);
ref.listen(purchaseOrdersListProvider, (prev, next) { ref.listen(purchaseOrdersListProvider, (prev, next) {
final error = next.valueOrNull?.actionError; final error = next.valueOrNull?.actionError;
@ -108,6 +110,9 @@ class _PurchaseOrderListScreenState extends ConsumerState<PurchaseOrderListScree
query: state.query, query: state.query,
statusOptions: _statusFilterOptions(state.orders), statusOptions: _statusFilterOptions(state.orders),
wrapped: constraints.maxWidth < 900, wrapped: constraints.maxWidth < 900,
showExport: canExport,
isExporting: state.isExporting,
onExport: _exportPurchaseOrders,
onSearch: ref.read(purchaseOrdersListProvider.notifier).setSearch, onSearch: ref.read(purchaseOrdersListProvider.notifier).setSearch,
onStatusChanged: onStatusChanged:
ref.read(purchaseOrdersListProvider.notifier).setStatusFilter, ref.read(purchaseOrdersListProvider.notifier).setStatusFilter,
@ -186,6 +191,36 @@ class _PurchaseOrderListScreenState extends ConsumerState<PurchaseOrderListScree
context.push('${RouteConstants.purchaseOrders}/${order.id}/edit'); context.push('${RouteConstants.purchaseOrders}/${order.id}/edit');
} }
Future<void> _exportPurchaseOrders() async {
final file =
await ref.read(purchaseOrdersListProvider.notifier).exportPurchaseOrders();
if (!mounted) return;
if (file == null) {
final error =
ref.read(purchaseOrdersListProvider).valueOrNull?.actionError;
if (error != null) {
showAppToastFromSnackBar(context, SnackBar(content: Text(error)));
}
return;
}
final saved = await downloadFile(
bytes: file.bytes,
fileName: file.fileName,
);
if (!mounted) return;
showAppToastFromSnackBar(
context,
SnackBar(
content: Text(
saved ? 'Downloaded ${file.fileName}' : 'Export cancelled',
),
),
);
}
Future<void> _deleteOrder(PurchaseOrderModel order) async { Future<void> _deleteOrder(PurchaseOrderModel order) async {
if (!order.canDelete) { if (!order.canDelete) {
showAppToastFromSnackBar(context, showAppToastFromSnackBar(context,
@ -244,6 +279,9 @@ class _FiltersBar extends StatelessWidget {
required this.onSearch, required this.onSearch,
required this.onStatusChanged, required this.onStatusChanged,
required this.onPoTypeChanged, required this.onPoTypeChanged,
this.showExport = false,
this.isExporting = false,
this.onExport,
}); });
final TextEditingController searchController; final TextEditingController searchController;
@ -253,6 +291,9 @@ class _FiltersBar extends StatelessWidget {
final ValueChanged<String> onSearch; final ValueChanged<String> onSearch;
final ValueChanged<String?> onStatusChanged; final ValueChanged<String?> onStatusChanged;
final ValueChanged<String?> onPoTypeChanged; final ValueChanged<String?> onPoTypeChanged;
final bool showExport;
final bool isExporting;
final VoidCallback? onExport;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -286,6 +327,18 @@ class _FiltersBar extends StatelessWidget {
), ),
]; ];
final exportButton = OutlinedButton.icon(
onPressed: isExporting ? null : onExport,
icon: isExporting
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.download_outlined, size: 18),
label: Text(isExporting ? 'Exporting...' : 'Export'),
);
if (wrapped) { if (wrapped) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
@ -295,6 +348,10 @@ class _FiltersBar extends StatelessWidget {
filters[0], filters[0],
const SizedBox(height: 12), const SizedBox(height: 12),
filters[1], filters[1],
if (showExport) ...[
const SizedBox(height: 12),
Align(alignment: Alignment.centerRight, child: exportButton),
],
], ],
); );
} }
@ -307,6 +364,10 @@ class _FiltersBar extends StatelessWidget {
Expanded(flex: 2, child: filters[0]), Expanded(flex: 2, child: filters[0]),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded(flex: 2, child: filters[1]), Expanded(flex: 2, child: filters[1]),
if (showExport) ...[
const SizedBox(width: 12),
exportButton,
],
], ],
); );
} }

View File

@ -313,28 +313,30 @@ class _CompanyProfileSettingsScreenState
SettingsFormCard( SettingsFormCard(
title: 'Company Information', title: 'Company Information',
children: [ children: [
AppTextField( SidePanelFormRow(
controller: _nameController, left: AppTextField(
label: 'Company Name', controller: _nameController,
validator: (v) => label: 'Company Name',
Validators.required(v, fieldName: 'Company Name'), validator: (v) =>
Validators.required(v, fieldName: 'Company Name'),
),
right: AppTextField(
controller: _codeController,
label: 'Company Code',
),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
AppTextField( SidePanelFormRow(
controller: _codeController, left: AppTextField(
label: 'Company Code', controller: _registrationController,
), label: 'Registration Number',
const SizedBox(height: 16), ),
AppTextField( right: AppTextField(
controller: _registrationController, controller: _gstController,
label: 'Registration Number', label: 'GSTIN',
), validator: Validators.optionalGstin,
const SizedBox(height: 16), inputFormatters: Validators.gstinInput,
AppTextField( ),
controller: _gstController,
label: 'GSTIN',
validator: Validators.optionalGstin,
inputFormatters: Validators.gstinInput,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
AppTextField( AppTextField(
@ -354,31 +356,33 @@ class _CompanyProfileSettingsScreenState
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
AppTextField( SidePanelFormRow(
controller: _pincodeController, left: AppTextField(
label: 'Pincode', controller: _pincodeController,
keyboardType: TextInputType.number, label: 'Pincode',
keyboardType: TextInputType.number,
),
right: AppTextField(
controller: _emailController,
label: 'Email',
keyboardType: TextInputType.emailAddress,
validator: Validators.optionalEmail,
),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
AppTextField( SidePanelFormRow(
controller: _emailController, left: AppTextField(
label: 'Email', controller: _phoneController,
keyboardType: TextInputType.emailAddress, label: 'Mobile',
validator: Validators.optionalEmail, keyboardType: TextInputType.phone,
), validator: Validators.optionalMobile,
const SizedBox(height: 16), inputFormatters: Validators.mobileInput,
AppTextField( ),
controller: _phoneController, right: AppTextField(
label: 'Mobile', controller: _websiteController,
keyboardType: TextInputType.phone, label: 'Website',
validator: Validators.optionalMobile, keyboardType: TextInputType.url,
inputFormatters: Validators.mobileInput, ),
),
const SizedBox(height: 16),
AppTextField(
controller: _websiteController,
label: 'Website',
keyboardType: TextInputType.url,
), ),
], ],
), ),

View File

@ -1,7 +1,9 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import '../../../../core/constants/api_endpoints.dart'; import '../../../../core/constants/api_endpoints.dart';
import '../../../../core/utils/export_file_name.dart';
import '../../../../shared/models/api_response.dart'; import '../../../../shared/models/api_response.dart';
import '../../../../shared/models/export_file_result.dart';
import '../../../../shared/models/user_management_models.dart'; import '../../../../shared/models/user_management_models.dart';
import '../../../../shared/models/vendor_model.dart'; import '../../../../shared/models/vendor_model.dart';
@ -37,6 +39,18 @@ class VendorRemoteDataSource {
await dio.delete(ApiEndpoints.vendorById(id)); await dio.delete(ApiEndpoints.vendorById(id));
} }
Future<ExportFileResult> exportVendors(VendorListQuery query) async {
final response = await dio.get<List<int>>(
ApiEndpoints.vendorsExport,
queryParameters: _exportQueryToMap(query),
options: Options(responseType: ResponseType.bytes),
);
return ExportFileResult(
bytes: response.data ?? <int>[],
fileName: exportFileNameFromResponse(response, fallbackBase: 'vendors'),
);
}
Future<VendorModel> updateVendorStatus(String id, String status) async { Future<VendorModel> updateVendorStatus(String id, String status) async {
final response = await dio.patch( final response = await dio.patch(
ApiEndpoints.vendorStatus(id), ApiEndpoints.vendorStatus(id),
@ -190,6 +204,12 @@ class VendorRemoteDataSource {
return { return {
'page': query.page, 'page': query.page,
'limit': query.limit, 'limit': query.limit,
..._exportQueryToMap(query),
};
}
Map<String, dynamic> _exportQueryToMap(VendorListQuery query) {
return {
if (query.search != null && query.search!.isNotEmpty) 'search': query.search, if (query.search != null && query.search!.isNotEmpty) 'search': query.search,
if (query.status != null) 'status': query.status, if (query.status != null) 'status': query.status,
if (query.vendorType != null) 'vendor_type': query.vendorType, if (query.vendorType != null) 'vendor_type': query.vendorType,

View File

@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/network/api_handler.dart'; import '../../../../core/network/api_handler.dart';
import '../../../../core/network/dio_client.dart'; import '../../../../core/network/dio_client.dart';
import '../../../../shared/models/api_response.dart'; import '../../../../shared/models/api_response.dart';
import '../../../../shared/models/export_file_result.dart';
import '../../../../shared/models/vendor_model.dart'; import '../../../../shared/models/vendor_model.dart';
import '../../domain/repositories/vendor_repository.dart'; import '../../domain/repositories/vendor_repository.dart';
import '../datasources/vendor_remote_data_source.dart'; import '../datasources/vendor_remote_data_source.dart';
@ -25,6 +26,11 @@ class VendorRepositoryImpl implements VendorRepository {
return safeApiCall(() => dataSource.getVendors(query)); return safeApiCall(() => dataSource.getVendors(query));
} }
@override
Future<Result<ExportFileResult>> exportVendors(VendorListQuery query) {
return safeApiCall(() => dataSource.exportVendors(query));
}
@override @override
Future<Result<VendorModel>> getVendorById(String id) { Future<Result<VendorModel>> getVendorById(String id) {
return safeApiCall(() => dataSource.getVendorById(id)); return safeApiCall(() => dataSource.getVendorById(id));

View File

@ -1,9 +1,11 @@
import '../../../../core/network/api_handler.dart'; import '../../../../core/network/api_handler.dart';
import '../../../../shared/models/api_response.dart'; import '../../../../shared/models/api_response.dart';
import '../../../../shared/models/export_file_result.dart';
import '../../../../shared/models/vendor_model.dart'; import '../../../../shared/models/vendor_model.dart';
abstract class VendorRepository { abstract class VendorRepository {
Future<Result<PaginatedResponse<VendorModel>>> getVendors(VendorListQuery query); Future<Result<PaginatedResponse<VendorModel>>> getVendors(VendorListQuery query);
Future<Result<ExportFileResult>> exportVendors(VendorListQuery query);
Future<Result<VendorModel>> getVendorById(String id); Future<Result<VendorModel>> getVendorById(String id);
Future<Result<VendorModel>> createVendor(Map<String, dynamic> data); Future<Result<VendorModel>> createVendor(Map<String, dynamic> data);
Future<Result<VendorModel>> updateVendor(String id, Map<String, dynamic> data); Future<Result<VendorModel>> updateVendor(String id, Map<String, dynamic> data);

View File

@ -2,6 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/utils/table_search.dart'; import '../../../../core/utils/table_search.dart';
import '../../../../shared/models/export_file_result.dart';
import '../../../../shared/models/vendor_model.dart'; import '../../../../shared/models/vendor_model.dart';
import '../../data/repositories/vendor_repository_impl.dart'; import '../../data/repositories/vendor_repository_impl.dart';
@ -12,6 +13,7 @@ class VendorsListState {
this.total = 0, this.total = 0,
this.totalPages = 1, this.totalPages = 1,
this.isRefreshing = false, this.isRefreshing = false,
this.isExporting = false,
this.actionError, this.actionError,
this.actionSuccess, this.actionSuccess,
}); });
@ -21,6 +23,7 @@ class VendorsListState {
final int total; final int total;
final int totalPages; final int totalPages;
final bool isRefreshing; final bool isRefreshing;
final bool isExporting;
final String? actionError; final String? actionError;
final String? actionSuccess; final String? actionSuccess;
@ -30,6 +33,7 @@ class VendorsListState {
int? total, int? total,
int? totalPages, int? totalPages,
bool? isRefreshing, bool? isRefreshing,
bool? isExporting,
String? actionError, String? actionError,
String? actionSuccess, String? actionSuccess,
bool clearMessages = false, bool clearMessages = false,
@ -40,6 +44,7 @@ class VendorsListState {
total: total ?? this.total, total: total ?? this.total,
totalPages: totalPages ?? this.totalPages, totalPages: totalPages ?? this.totalPages,
isRefreshing: isRefreshing ?? this.isRefreshing, isRefreshing: isRefreshing ?? this.isRefreshing,
isExporting: isExporting ?? this.isExporting,
actionError: clearMessages ? null : actionError ?? this.actionError, actionError: clearMessages ? null : actionError ?? this.actionError,
actionSuccess: clearMessages ? null : actionSuccess ?? this.actionSuccess, actionSuccess: clearMessages ? null : actionSuccess ?? this.actionSuccess,
); );
@ -122,6 +127,30 @@ class VendorsListNotifier extends AutoDisposeAsyncNotifier<VendorsListState> {
applyQuery(current.query.copyWith(limit: limit, page: 1)); applyQuery(current.query.copyWith(limit: limit, page: 1));
} }
Future<ExportFileResult?> exportVendors() async {
final current = state.valueOrNull;
if (current == null) return null;
state = AsyncData(current.copyWith(isExporting: true, clearMessages: true));
final result =
await ref.read(vendorRepositoryProvider).exportVendors(current.query);
final latest = state.valueOrNull ?? current;
if (result.failure != null) {
state = AsyncData(
latest.copyWith(
isExporting: false,
actionError: result.failure!.message,
),
);
return null;
}
state = AsyncData(latest.copyWith(isExporting: false));
return result.data;
}
Future<bool> deleteVendor(String id) async { Future<bool> deleteVendor(String id) async {
final repository = ref.read(vendorRepositoryProvider); final repository = ref.read(vendorRepositoryProvider);
final result = await repository.deleteVendor(id); final result = await repository.deleteVendor(id);

View File

@ -23,6 +23,7 @@ import '../../../../shared/widgets/can_permission.dart';
import '../../../../shared/widgets/app_table_shell.dart'; import '../../../../shared/widgets/app_table_shell.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 '../../../../shared/utils/file_download_helper.dart';
import '../providers/vendors_provider.dart'; import '../providers/vendors_provider.dart';
import '../widgets/vendor_form_panel.dart'; import '../widgets/vendor_form_panel.dart';
import '../../../../shared/widgets/app_toast.dart'; import '../../../../shared/widgets/app_toast.dart';
@ -48,6 +49,7 @@ class _VendorListScreenState extends ConsumerState<VendorListScreen> {
final vendorsAsync = ref.watch(vendorsListProvider); final vendorsAsync = ref.watch(vendorsListProvider);
final canEdit = ref.can('vendors', PermissionAction.update); final canEdit = ref.can('vendors', PermissionAction.update);
final canDelete = ref.can('vendors', PermissionAction.delete); final canDelete = ref.can('vendors', PermissionAction.delete);
final canExport = ref.can('vendors', PermissionAction.export);
ref.listen(vendorsListProvider, (prev, next) { ref.listen(vendorsListProvider, (prev, next) {
final error = next.valueOrNull?.actionError; final error = next.valueOrNull?.actionError;
@ -94,6 +96,9 @@ class _VendorListScreenState extends ConsumerState<VendorListScreen> {
searchController: _searchController, searchController: _searchController,
query: state.query, query: state.query,
wrapped: constraints.maxWidth < 900, wrapped: constraints.maxWidth < 900,
showExport: canExport,
isExporting: state.isExporting,
onExport: _exportVendors,
onSearch: ref.read(vendorsListProvider.notifier).setSearch, onSearch: ref.read(vendorsListProvider.notifier).setSearch,
onStatusChanged: onStatusChanged:
ref.read(vendorsListProvider.notifier).setStatusFilter, ref.read(vendorsListProvider.notifier).setStatusFilter,
@ -159,6 +164,34 @@ class _VendorListScreenState extends ConsumerState<VendorListScreen> {
openVendorFormPanel(context, ref, vendorId: vendor.id); openVendorFormPanel(context, ref, vendorId: vendor.id);
} }
Future<void> _exportVendors() async {
final file = await ref.read(vendorsListProvider.notifier).exportVendors();
if (!mounted) return;
if (file == null) {
final error = ref.read(vendorsListProvider).valueOrNull?.actionError;
if (error != null) {
showAppToastFromSnackBar(context, SnackBar(content: Text(error)));
}
return;
}
final saved = await downloadFile(
bytes: file.bytes,
fileName: file.fileName,
);
if (!mounted) return;
showAppToastFromSnackBar(
context,
SnackBar(
content: Text(
saved ? 'Downloaded ${file.fileName}' : 'Export cancelled',
),
),
);
}
Future<void> _deleteVendor(VendorModel vendor) async { Future<void> _deleteVendor(VendorModel vendor) async {
final confirmed = await showAppConfirmationDialog( final confirmed = await showAppConfirmationDialog(
context: context, context: context,
@ -180,6 +213,9 @@ class _FiltersBar extends StatelessWidget {
required this.onSearch, required this.onSearch,
required this.onStatusChanged, required this.onStatusChanged,
required this.onVendorTypeChanged, required this.onVendorTypeChanged,
this.showExport = false,
this.isExporting = false,
this.onExport,
}); });
final TextEditingController searchController; final TextEditingController searchController;
@ -188,6 +224,9 @@ class _FiltersBar extends StatelessWidget {
final ValueChanged<String> onSearch; final ValueChanged<String> onSearch;
final ValueChanged<String?> onStatusChanged; final ValueChanged<String?> onStatusChanged;
final ValueChanged<String?> onVendorTypeChanged; final ValueChanged<String?> onVendorTypeChanged;
final bool showExport;
final bool isExporting;
final VoidCallback? onExport;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -226,6 +265,18 @@ class _FiltersBar extends StatelessWidget {
), ),
]; ];
final exportButton = OutlinedButton.icon(
onPressed: isExporting ? null : onExport,
icon: isExporting
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.download_outlined, size: 18),
label: Text(isExporting ? 'Exporting...' : 'Export'),
);
if (wrapped) { if (wrapped) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
@ -235,6 +286,10 @@ class _FiltersBar extends StatelessWidget {
filters[0], filters[0],
const SizedBox(height: 12), const SizedBox(height: 12),
filters[1], filters[1],
if (showExport) ...[
const SizedBox(height: 12),
Align(alignment: Alignment.centerRight, child: exportButton),
],
], ],
); );
} }
@ -247,6 +302,10 @@ class _FiltersBar extends StatelessWidget {
Expanded(flex: 2, child: filters[0]), Expanded(flex: 2, child: filters[0]),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded(flex: 2, child: filters[1]), Expanded(flex: 2, child: filters[1]),
if (showExport) ...[
const SizedBox(width: 12),
exportButton,
],
], ],
); );
} }

View File

@ -545,3 +545,66 @@ class AssetDepreciationPreviewModel {
); );
} }
} }
/// List/export filters for assets (matches GET /assets and /assets/export).
class AssetListQuery {
const AssetListQuery({
this.page = 1,
this.limit = 20,
this.search,
this.status,
this.condition,
this.itemCategoryId,
this.itemSubcategoryId,
this.plantId,
this.departmentId,
this.isActive,
});
final int page;
final int limit;
final String? search;
final String? status;
final String? condition;
final int? itemCategoryId;
final int? itemSubcategoryId;
final int? plantId;
final int? departmentId;
final bool? isActive;
AssetListQuery copyWith({
int? page,
int? limit,
Object? search = _unset,
Object? status = _unset,
Object? condition = _unset,
Object? itemCategoryId = _unset,
Object? itemSubcategoryId = _unset,
Object? plantId = _unset,
Object? departmentId = _unset,
Object? isActive = _unset,
}) {
return AssetListQuery(
page: page ?? this.page,
limit: limit ?? this.limit,
search: identical(search, _unset) ? this.search : search as String?,
status: identical(status, _unset) ? this.status : status as String?,
condition:
identical(condition, _unset) ? this.condition : condition as String?,
itemCategoryId: identical(itemCategoryId, _unset)
? this.itemCategoryId
: itemCategoryId as int?,
itemSubcategoryId: identical(itemSubcategoryId, _unset)
? this.itemSubcategoryId
: itemSubcategoryId as int?,
plantId: identical(plantId, _unset) ? this.plantId : plantId as int?,
departmentId: identical(departmentId, _unset)
? this.departmentId
: departmentId as int?,
isActive:
identical(isActive, _unset) ? this.isActive : isActive as bool?,
);
}
}
const _unset = Object();