table alignment
This commit is contained in:
parent
c462374125
commit
5a28cfc0da
@ -73,6 +73,7 @@ class ApiEndpoints {
|
||||
|
||||
// Vendors
|
||||
static const String vendors = '/vendors';
|
||||
static const String vendorsExport = '/vendors/export';
|
||||
static const String vendorGstTreatments = '/vendors/gst-treatments';
|
||||
static const String vendorSourceOfSupply = '/vendors/source-of-supply';
|
||||
static String vendorById(String id) => '/vendors/$id';
|
||||
@ -93,6 +94,7 @@ class ApiEndpoints {
|
||||
|
||||
// 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 purchaseOrderSubmit(String id) => '/purchase-orders/$id/submit';
|
||||
static String purchaseOrderApprove(String id) => '/purchase-orders/$id/approve';
|
||||
@ -112,6 +114,7 @@ class ApiEndpoints {
|
||||
|
||||
// GRN
|
||||
static const String grn = '/grn';
|
||||
static const String grnExport = '/grn/export';
|
||||
static String grnById(String id) => '/grn/$id';
|
||||
static String grnCancel(String id) => '/grn/$id/cancel';
|
||||
static String grnPdf(String id) => '/grn/$id/pdf';
|
||||
@ -123,6 +126,7 @@ class ApiEndpoints {
|
||||
|
||||
// Assets
|
||||
static const String assets = '/assets';
|
||||
static const String assetsExport = '/assets/export';
|
||||
static String assetById(String id) => '/assets/$id';
|
||||
static String assetTransfer(String id) => '/assets/$id/transfer';
|
||||
static String assetTransferHistory(String id) => '/assets/$id/transfer-history';
|
||||
|
||||
31
lib/core/utils/export_file_name.dart
Normal file
31
lib/core/utils/export_file_name.dart
Normal 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';
|
||||
}
|
||||
@ -1,23 +1,21 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../../../../core/constants/api_endpoints.dart';
|
||||
import '../../../../core/utils/export_file_name.dart';
|
||||
import '../../../../shared/models/api_response.dart';
|
||||
import '../../../../shared/models/asset_model.dart';
|
||||
import '../../../../shared/models/entity_attachment_model.dart';
|
||||
import '../../../../shared/models/export_file_result.dart';
|
||||
|
||||
class AssetRemoteDataSource {
|
||||
AssetRemoteDataSource({required this.dio});
|
||||
|
||||
final Dio dio;
|
||||
|
||||
Future<PaginatedResponse<AssetModel>> getAssets(PaginationParams params) async {
|
||||
Future<PaginatedResponse<AssetModel>> getAssets(AssetListQuery query) async {
|
||||
final response = await dio.get(
|
||||
ApiEndpoints.assets,
|
||||
queryParameters: {
|
||||
'page': params.page,
|
||||
'limit': params.limit,
|
||||
if (params.search != null && params.search!.isNotEmpty) 'search': params.search,
|
||||
},
|
||||
queryParameters: _queryToMap(query),
|
||||
);
|
||||
return _parsePaginated(response.data, AssetModel.fromJson);
|
||||
}
|
||||
@ -41,6 +39,18 @@ class AssetRemoteDataSource {
|
||||
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 {
|
||||
final response = await dio.get(ApiEndpoints.assetAttachments(assetId));
|
||||
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) {
|
||||
if (value is Map<String, dynamic>) return value;
|
||||
if (value is Map) return Map<String, dynamic>.from(value);
|
||||
|
||||
@ -5,6 +5,7 @@ import '../../../../core/network/dio_client.dart';
|
||||
import '../../../../shared/models/api_response.dart';
|
||||
import '../../../../shared/models/asset_model.dart';
|
||||
import '../../../../shared/models/entity_attachment_model.dart';
|
||||
import '../../../../shared/models/export_file_result.dart';
|
||||
import '../../domain/repositories/asset_repository.dart';
|
||||
import '../datasources/asset_remote_data_source.dart';
|
||||
|
||||
@ -22,8 +23,13 @@ class AssetRepositoryImpl implements AssetRepository {
|
||||
final AssetRemoteDataSource dataSource;
|
||||
|
||||
@override
|
||||
Future<Result<PaginatedResponse<AssetModel>>> getAssets(PaginationParams params) {
|
||||
return safeApiCall(() => dataSource.getAssets(params));
|
||||
Future<Result<PaginatedResponse<AssetModel>>> getAssets(AssetListQuery query) {
|
||||
return safeApiCall(() => dataSource.getAssets(query));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<ExportFileResult>> exportAssets(AssetListQuery query) {
|
||||
return safeApiCall(() => dataSource.exportAssets(query));
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@ -2,9 +2,11 @@ import '../../../../core/network/api_handler.dart';
|
||||
import '../../../../shared/models/api_response.dart';
|
||||
import '../../../../shared/models/asset_model.dart';
|
||||
import '../../../../shared/models/entity_attachment_model.dart';
|
||||
import '../../../../shared/models/export_file_result.dart';
|
||||
|
||||
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>> createAsset(Map<String, dynamic> data);
|
||||
Future<Result<AssetModel>> updateAsset(String id, Map<String, dynamic> data);
|
||||
|
||||
@ -2,36 +2,39 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../core/utils/table_search.dart';
|
||||
|
||||
import '../../../../shared/models/api_response.dart';
|
||||
import '../../../../shared/models/asset_model.dart';
|
||||
import '../../../../shared/models/entity_attachment_model.dart';
|
||||
import '../../../../shared/models/export_file_result.dart';
|
||||
import '../../data/repositories/asset_repository_impl.dart';
|
||||
|
||||
class AssetsListState {
|
||||
const AssetsListState({
|
||||
this.assets = const [],
|
||||
this.query = const PaginationParams(limit: 20),
|
||||
this.query = const AssetListQuery(limit: 20),
|
||||
this.total = 0,
|
||||
this.totalPages = 1,
|
||||
this.isRefreshing = false,
|
||||
this.isExporting = false,
|
||||
this.actionError,
|
||||
this.actionSuccess,
|
||||
});
|
||||
|
||||
final List<AssetModel> assets;
|
||||
final PaginationParams query;
|
||||
final AssetListQuery query;
|
||||
final int total;
|
||||
final int totalPages;
|
||||
final bool isRefreshing;
|
||||
final bool isExporting;
|
||||
final String? actionError;
|
||||
final String? actionSuccess;
|
||||
|
||||
AssetsListState copyWith({
|
||||
List<AssetModel>? assets,
|
||||
PaginationParams? query,
|
||||
AssetListQuery? query,
|
||||
int? total,
|
||||
int? totalPages,
|
||||
bool? isRefreshing,
|
||||
bool? isExporting,
|
||||
String? actionError,
|
||||
String? actionSuccess,
|
||||
bool clearMessages = false,
|
||||
@ -42,6 +45,7 @@ class AssetsListState {
|
||||
total: total ?? this.total,
|
||||
totalPages: totalPages ?? this.totalPages,
|
||||
isRefreshing: isRefreshing ?? this.isRefreshing,
|
||||
isExporting: isExporting ?? this.isExporting,
|
||||
actionError: clearMessages ? null : actionError ?? this.actionError,
|
||||
actionSuccess: clearMessages ? null : actionSuccess ?? this.actionSuccess,
|
||||
);
|
||||
@ -56,10 +60,10 @@ final assetsListProvider =
|
||||
class AssetsListNotifier extends AutoDisposeAsyncNotifier<AssetsListState> {
|
||||
@override
|
||||
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 result = await repository.getAssets(query);
|
||||
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;
|
||||
if (previous == null) {
|
||||
state = const AsyncLoading();
|
||||
@ -97,7 +101,27 @@ class AssetsListNotifier extends AutoDisposeAsyncNotifier<AssetsListState> {
|
||||
void setSearch(String search) {
|
||||
final current = state.valueOrNull;
|
||||
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) {
|
||||
@ -112,6 +136,30 @@ class AssetsListNotifier extends AutoDisposeAsyncNotifier<AssetsListState> {
|
||||
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 {
|
||||
final repository = ref.read(assetRepositoryProvider);
|
||||
final result = await repository.deleteAsset(id);
|
||||
|
||||
@ -8,6 +8,7 @@ import '../../../../core/errors/failure.dart';
|
||||
import '../../../../core/utils/formatters.dart';
|
||||
import '../../../../core/utils/responsive_utils.dart';
|
||||
import '../../../../shared/models/asset_model.dart';
|
||||
import '../../../../shared/models/user_management_models.dart';
|
||||
import '../../../../shared/providers/permissions_provider.dart';
|
||||
import '../../../../shared/widgets/app_card.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/error_view.dart';
|
||||
import '../../../../shared/widgets/page_header.dart';
|
||||
import '../../../../shared/utils/file_download_helper.dart';
|
||||
import '../providers/asset_categories_provider.dart';
|
||||
import '../providers/asset_form_lookups_provider.dart';
|
||||
import '../providers/assets_provider.dart';
|
||||
@ -36,15 +38,12 @@ class AssetListScreen extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _AssetListScreenState extends ConsumerState<AssetListScreen> {
|
||||
String? _selectedCategory;
|
||||
String? _selectedPlant;
|
||||
String? _selectedStatus;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final assetsAsync = ref.watch(assetsListProvider);
|
||||
final canEdit = ref.can('assets', PermissionAction.update);
|
||||
final canDelete = ref.can('assets', PermissionAction.delete);
|
||||
final canExport = ref.can('assets', PermissionAction.export);
|
||||
|
||||
ref.listen(assetsListProvider, (prev, next) {
|
||||
final error = next.valueOrNull?.actionError;
|
||||
@ -66,26 +65,11 @@ class _AssetListScreenState extends ConsumerState<AssetListScreen> {
|
||||
onRetry: () => ref.invalidate(assetsListProvider),
|
||||
),
|
||||
data: (state) {
|
||||
final allCategories = ref.watch(itemCategoriesProvider).valueOrNull ?? [];
|
||||
final filteredAssets = _filterAssets(state.assets);
|
||||
final categories = _categoryOptions(state.assets, allCategories);
|
||||
final plants = _plantOptions(state.assets);
|
||||
final statuses = _statusOptions(
|
||||
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;
|
||||
final allCategories =
|
||||
ref.watch(itemCategoriesProvider).valueOrNull ?? [];
|
||||
final lookups = ref.watch(assetFormLookupsProvider).valueOrNull;
|
||||
final allPlants = lookups?.plants ?? const [];
|
||||
final notifier = ref.read(assetsListProvider.notifier);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -94,6 +78,19 @@ class _AssetListScreenState extends ConsumerState<AssetListScreen> {
|
||||
title: 'Asset Master',
|
||||
subtitle: 'Manage plant assets, AMC, service & insurance',
|
||||
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(
|
||||
onPressed: () => context.go(RouteConstants.assetAlerts),
|
||||
icon: const Icon(Icons.notifications_outlined),
|
||||
@ -113,10 +110,10 @@ class _AssetListScreenState extends ConsumerState<AssetListScreen> {
|
||||
),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => ref.read(assetsListProvider.notifier).refresh(),
|
||||
onRefresh: () => notifier.refresh(),
|
||||
child: context.isMobile
|
||||
? _AssetMobileList(
|
||||
assets: filteredAssets,
|
||||
assets: state.assets,
|
||||
onView: _viewAsset,
|
||||
onEdit: canEdit ? _editAsset : null,
|
||||
onDelete: canDelete ? _deleteAsset : null,
|
||||
@ -142,39 +139,21 @@ class _AssetListScreenState extends ConsumerState<AssetListScreen> {
|
||||
builder: (context, constraints) {
|
||||
return _AssetsFilterBar(
|
||||
wrapped: constraints.maxWidth < 1000,
|
||||
categoryFilter: categoryFilter,
|
||||
plantFilter: plantFilter,
|
||||
statusFilter: statusFilter,
|
||||
categories: categories,
|
||||
plants: plants,
|
||||
statuses: statuses,
|
||||
statusLabels: statusLabels,
|
||||
onSearch:
|
||||
ref.read(assetsListProvider.notifier).setSearch,
|
||||
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;
|
||||
});
|
||||
},
|
||||
query: state.query,
|
||||
categories: allCategories,
|
||||
plants: allPlants,
|
||||
statuses: lookups?.statuses ?? const [],
|
||||
onSearch: notifier.setSearch,
|
||||
onCategoryChanged:
|
||||
notifier.setCategoryFilter,
|
||||
onPlantChanged: notifier.setPlantFilter,
|
||||
onStatusChanged: notifier.setStatusFilter,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
if (filteredAssets.isEmpty)
|
||||
if (state.assets.isEmpty)
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text(
|
||||
@ -193,7 +172,7 @@ class _AssetListScreenState extends ConsumerState<AssetListScreen> {
|
||||
else
|
||||
Expanded(
|
||||
child: _AssetDataTable(
|
||||
assets: filteredAssets,
|
||||
assets: state.assets,
|
||||
canEdit: canEdit,
|
||||
canDelete: canDelete,
|
||||
onView: _viewAsset,
|
||||
@ -205,17 +184,13 @@ class _AssetListScreenState extends ConsumerState<AssetListScreen> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: AppPagination(
|
||||
currentPage: page,
|
||||
currentPage: state.query.page,
|
||||
totalPages: state.totalPages,
|
||||
totalItems: total,
|
||||
pageSize: pageSize,
|
||||
totalItems: state.total,
|
||||
pageSize: state.query.limit,
|
||||
itemLabel: 'assets',
|
||||
onPageChanged: ref
|
||||
.read(assetsListProvider.notifier)
|
||||
.setPage,
|
||||
onPageSizeChanged: ref
|
||||
.read(assetsListProvider.notifier)
|
||||
.setPageSize,
|
||||
onPageChanged: notifier.setPage,
|
||||
onPageSizeChanged: 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) {
|
||||
context.push('${RouteConstants.assets}/${asset.id}');
|
||||
}
|
||||
@ -290,6 +213,34 @@ class _AssetListScreenState extends ConsumerState<AssetListScreen> {
|
||||
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 {
|
||||
final confirmed = await showAppConfirmationDialog(
|
||||
context: context,
|
||||
@ -325,13 +276,10 @@ class _AssetListScreenState extends ConsumerState<AssetListScreen> {
|
||||
class _AssetsFilterBar extends StatelessWidget {
|
||||
const _AssetsFilterBar({
|
||||
required this.wrapped,
|
||||
required this.categoryFilter,
|
||||
required this.plantFilter,
|
||||
required this.statusFilter,
|
||||
required this.query,
|
||||
required this.categories,
|
||||
required this.plants,
|
||||
required this.statuses,
|
||||
required this.statusLabels,
|
||||
required this.onSearch,
|
||||
required this.onCategoryChanged,
|
||||
required this.onPlantChanged,
|
||||
@ -339,17 +287,14 @@ class _AssetsFilterBar extends StatelessWidget {
|
||||
});
|
||||
|
||||
final bool wrapped;
|
||||
final String categoryFilter;
|
||||
final String plantFilter;
|
||||
final String statusFilter;
|
||||
final List<String> categories;
|
||||
final List<String> plants;
|
||||
final List<String> statuses;
|
||||
final Map<String, String> statusLabels;
|
||||
final AssetListQuery query;
|
||||
final List<AssetCategoryModel> categories;
|
||||
final List<FilterOptionModel> plants;
|
||||
final List<AssetDropdownOption> statuses;
|
||||
final ValueChanged<String> onSearch;
|
||||
final ValueChanged<String> onCategoryChanged;
|
||||
final ValueChanged<String> onPlantChanged;
|
||||
final ValueChanged<String> onStatusChanged;
|
||||
final ValueChanged<int?> onCategoryChanged;
|
||||
final ValueChanged<int?> onPlantChanged;
|
||||
final ValueChanged<String?> onStatusChanged;
|
||||
|
||||
@override
|
||||
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 = [
|
||||
_AssetFilterDropdown(
|
||||
value: categoryFilter,
|
||||
AppSearchableDropdown<int?>(
|
||||
label: 'Category',
|
||||
items: categories,
|
||||
value: query.itemCategoryId,
|
||||
searchHint: 'Search category...',
|
||||
isDense: true,
|
||||
options: categoryOptions,
|
||||
onChanged: onCategoryChanged,
|
||||
),
|
||||
_AssetFilterDropdown(
|
||||
value: plantFilter,
|
||||
AppSearchableDropdown<int?>(
|
||||
label: 'Plant',
|
||||
items: plants,
|
||||
value: query.plantId,
|
||||
searchHint: 'Search plant...',
|
||||
isDense: true,
|
||||
options: plantOptions,
|
||||
onChanged: onPlantChanged,
|
||||
),
|
||||
_AssetFilterDropdown(
|
||||
value: statusFilter,
|
||||
AppSearchableDropdown<String?>(
|
||||
label: 'Status',
|
||||
items: statuses,
|
||||
itemLabels: statusLabels,
|
||||
value: query.status,
|
||||
searchHint: 'Search status...',
|
||||
isDense: true,
|
||||
options: statusOptions,
|
||||
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 {
|
||||
const _AssetDataTable({
|
||||
required this.assets,
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../../../../core/constants/api_endpoints.dart';
|
||||
import '../../../../core/utils/export_file_name.dart';
|
||||
import '../../../../shared/models/api_response.dart';
|
||||
import '../../../../shared/models/export_file_result.dart';
|
||||
import '../../../../shared/models/grn_model.dart';
|
||||
|
||||
class GrnRemoteDataSource {
|
||||
@ -48,6 +50,18 @@ class GrnRemoteDataSource {
|
||||
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 {
|
||||
final response = await dio.get(ApiEndpoints.grnAttachments(grnId));
|
||||
final data = response.data['data'];
|
||||
@ -106,6 +120,12 @@ class GrnRemoteDataSource {
|
||||
return {
|
||||
'page': query.page,
|
||||
'limit': query.limit,
|
||||
..._exportQueryToMap(query),
|
||||
};
|
||||
}
|
||||
|
||||
Map<String, dynamic> _exportQueryToMap(GrnListQuery query) {
|
||||
return {
|
||||
if (query.search != null && query.search!.isNotEmpty) 'search': query.search,
|
||||
if (query.status != null) 'status': query.status,
|
||||
if (query.poId != null) 'po_id': query.poId,
|
||||
|
||||
@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../../core/network/api_handler.dart';
|
||||
import '../../../../core/network/dio_client.dart';
|
||||
import '../../../../shared/models/api_response.dart';
|
||||
import '../../../../shared/models/export_file_result.dart';
|
||||
import '../../../../shared/models/grn_model.dart';
|
||||
import '../../domain/repositories/grn_repository.dart';
|
||||
import '../datasources/grn_remote_data_source.dart';
|
||||
@ -25,6 +26,11 @@ class GrnRepositoryImpl implements GrnRepository {
|
||||
return safeApiCall(() => dataSource.getGrns(query));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<ExportFileResult>> exportGrns(GrnListQuery query) {
|
||||
return safeApiCall(() => dataSource.exportGrns(query));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<GrnModel>> getGrnById(String id) {
|
||||
return safeApiCall(() => dataSource.getGrnById(id));
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import '../../../../core/network/api_handler.dart';
|
||||
import '../../../../shared/models/api_response.dart';
|
||||
import '../../../../shared/models/export_file_result.dart';
|
||||
import '../../../../shared/models/grn_model.dart';
|
||||
|
||||
abstract class GrnRepository {
|
||||
Future<Result<PaginatedResponse<GrnModel>>> getGrns(GrnListQuery query);
|
||||
Future<Result<ExportFileResult>> exportGrns(GrnListQuery query);
|
||||
Future<Result<GrnModel>> getGrnById(String id);
|
||||
Future<Result<GrnModel>> createGrn(Map<String, dynamic> data);
|
||||
Future<Result<GrnModel>> updateGrn(String id, Map<String, dynamic> data);
|
||||
|
||||
@ -2,6 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../core/utils/table_search.dart';
|
||||
|
||||
import '../../../../shared/models/export_file_result.dart';
|
||||
import '../../../../shared/models/grn_model.dart';
|
||||
import '../../data/repositories/grn_repository_impl.dart';
|
||||
|
||||
@ -12,6 +13,7 @@ class GrnListState {
|
||||
this.total = 0,
|
||||
this.totalPages = 1,
|
||||
this.isRefreshing = false,
|
||||
this.isExporting = false,
|
||||
this.actionError,
|
||||
this.actionSuccess,
|
||||
});
|
||||
@ -21,6 +23,7 @@ class GrnListState {
|
||||
final int total;
|
||||
final int totalPages;
|
||||
final bool isRefreshing;
|
||||
final bool isExporting;
|
||||
final String? actionError;
|
||||
final String? actionSuccess;
|
||||
|
||||
@ -30,6 +33,7 @@ class GrnListState {
|
||||
int? total,
|
||||
int? totalPages,
|
||||
bool? isRefreshing,
|
||||
bool? isExporting,
|
||||
String? actionError,
|
||||
String? actionSuccess,
|
||||
bool clearMessages = false,
|
||||
@ -40,6 +44,7 @@ class GrnListState {
|
||||
total: total ?? this.total,
|
||||
totalPages: totalPages ?? this.totalPages,
|
||||
isRefreshing: isRefreshing ?? this.isRefreshing,
|
||||
isExporting: isExporting ?? this.isExporting,
|
||||
actionError: clearMessages ? null : actionError ?? this.actionError,
|
||||
actionSuccess: clearMessages ? null : actionSuccess ?? this.actionSuccess,
|
||||
);
|
||||
@ -115,6 +120,30 @@ class GrnListNotifier extends AutoDisposeAsyncNotifier<GrnListState> {
|
||||
if (current == null) return;
|
||||
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 =
|
||||
|
||||
@ -21,6 +21,7 @@ import '../../../../shared/widgets/app_table_action_icon.dart';
|
||||
import '../../../../shared/widgets/app_table_shell.dart';
|
||||
import '../../../../shared/widgets/error_view.dart';
|
||||
import '../../../../shared/widgets/page_header.dart';
|
||||
import '../../../../shared/utils/file_download_helper.dart';
|
||||
import '../providers/grn_provider.dart';
|
||||
import '../widgets/grn_status_chip.dart';
|
||||
import '../../../../shared/widgets/app_toast.dart';
|
||||
@ -45,6 +46,14 @@ class _GrnListScreenState extends ConsumerState<GrnListScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final listAsync = ref.watch(grnListProvider);
|
||||
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(
|
||||
padding: const EdgeInsets.all(24),
|
||||
@ -80,6 +89,9 @@ class _GrnListScreenState extends ConsumerState<GrnListScreen> {
|
||||
searchController: _searchController,
|
||||
query: state.query,
|
||||
wrapped: constraints.maxWidth < 900,
|
||||
showExport: canExport,
|
||||
isExporting: state.isExporting,
|
||||
onExport: _exportGrns,
|
||||
onSearch: ref.read(grnListProvider.notifier).setSearch,
|
||||
onStatusChanged:
|
||||
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) {
|
||||
context.push('${RouteConstants.grn}/${grn.id}');
|
||||
}
|
||||
@ -154,6 +194,9 @@ class _FiltersBar extends StatelessWidget {
|
||||
required this.wrapped,
|
||||
required this.onSearch,
|
||||
required this.onStatusChanged,
|
||||
this.showExport = false,
|
||||
this.isExporting = false,
|
||||
this.onExport,
|
||||
});
|
||||
|
||||
final TextEditingController searchController;
|
||||
@ -161,6 +204,9 @@ class _FiltersBar extends StatelessWidget {
|
||||
final bool wrapped;
|
||||
final ValueChanged<String> onSearch;
|
||||
final ValueChanged<String?> onStatusChanged;
|
||||
final bool showExport;
|
||||
final bool isExporting;
|
||||
final VoidCallback? onExport;
|
||||
|
||||
@override
|
||||
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) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
@ -206,6 +264,10 @@ class _FiltersBar extends StatelessWidget {
|
||||
searchField,
|
||||
const SizedBox(height: 12),
|
||||
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),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(flex: 2, child: statusFilter),
|
||||
if (showExport) ...[
|
||||
const SizedBox(width: 12),
|
||||
exportButton,
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../../../../core/constants/api_endpoints.dart';
|
||||
import '../../../../core/utils/export_file_name.dart';
|
||||
import '../../../../shared/models/api_response.dart';
|
||||
import '../../../../shared/models/entity_attachment_model.dart';
|
||||
import '../../../../shared/models/export_file_result.dart';
|
||||
import '../../../../shared/models/purchase_order_model.dart';
|
||||
|
||||
class PurchaseOrderRemoteDataSource {
|
||||
@ -128,6 +130,23 @@ class PurchaseOrderRemoteDataSource {
|
||||
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 {
|
||||
final response =
|
||||
await dio.get(ApiEndpoints.purchaseOrderAttachments(poId));
|
||||
@ -177,6 +196,12 @@ class PurchaseOrderRemoteDataSource {
|
||||
return {
|
||||
'page': query.page,
|
||||
'limit': query.limit,
|
||||
..._exportQueryToMap(query),
|
||||
};
|
||||
}
|
||||
|
||||
Map<String, dynamic> _exportQueryToMap(PurchaseOrderListQuery query) {
|
||||
return {
|
||||
if (query.search != null && query.search!.isNotEmpty) 'search': query.search,
|
||||
if (query.status != null) 'status': query.status,
|
||||
if (query.poType != null) 'po_type': query.poType,
|
||||
|
||||
@ -4,6 +4,7 @@ import '../../../../core/network/api_handler.dart';
|
||||
import '../../../../core/network/dio_client.dart';
|
||||
import '../../../../shared/models/api_response.dart';
|
||||
import '../../../../shared/models/entity_attachment_model.dart';
|
||||
import '../../../../shared/models/export_file_result.dart';
|
||||
import '../../../../shared/models/purchase_order_model.dart';
|
||||
import '../../domain/repositories/purchase_order_repository.dart';
|
||||
import '../datasources/purchase_order_remote_data_source.dart';
|
||||
@ -31,6 +32,13 @@ class PurchaseOrderRepositoryImpl implements PurchaseOrderRepository {
|
||||
return safeApiCall(() => dataSource.getPurchaseOrders(query));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<ExportFileResult>> exportPurchaseOrders(
|
||||
PurchaseOrderListQuery query,
|
||||
) {
|
||||
return safeApiCall(() => dataSource.exportPurchaseOrders(query));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<PurchaseOrderModel>> getPurchaseOrderById(String id) {
|
||||
return safeApiCall(() => dataSource.getPurchaseOrderById(id));
|
||||
|
||||
@ -1,12 +1,16 @@
|
||||
import '../../../../core/network/api_handler.dart';
|
||||
import '../../../../shared/models/api_response.dart';
|
||||
import '../../../../shared/models/entity_attachment_model.dart';
|
||||
import '../../../../shared/models/export_file_result.dart';
|
||||
import '../../../../shared/models/purchase_order_model.dart';
|
||||
|
||||
abstract class PurchaseOrderRepository {
|
||||
Future<Result<PaginatedResponse<PurchaseOrderModel>>> getPurchaseOrders(
|
||||
PurchaseOrderListQuery query,
|
||||
);
|
||||
Future<Result<ExportFileResult>> exportPurchaseOrders(
|
||||
PurchaseOrderListQuery query,
|
||||
);
|
||||
Future<Result<PurchaseOrderModel>> getPurchaseOrderById(String id);
|
||||
Future<Result<PurchaseOrderModel>> createPurchaseOrder(Map<String, dynamic> data);
|
||||
Future<Result<PurchaseOrderModel>> updatePurchaseOrder(
|
||||
|
||||
@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../../core/utils/table_search.dart';
|
||||
|
||||
import '../../../../shared/models/entity_attachment_model.dart';
|
||||
import '../../../../shared/models/export_file_result.dart';
|
||||
import '../../../../shared/models/purchase_order_model.dart';
|
||||
import '../../../grn/presentation/providers/grn_lookups_provider.dart';
|
||||
import '../../data/repositories/purchase_order_repository_impl.dart';
|
||||
@ -14,6 +15,7 @@ class PurchaseOrdersListState {
|
||||
this.total = 0,
|
||||
this.totalPages = 1,
|
||||
this.isRefreshing = false,
|
||||
this.isExporting = false,
|
||||
this.actionError,
|
||||
this.actionSuccess,
|
||||
});
|
||||
@ -23,6 +25,7 @@ class PurchaseOrdersListState {
|
||||
final int total;
|
||||
final int totalPages;
|
||||
final bool isRefreshing;
|
||||
final bool isExporting;
|
||||
final String? actionError;
|
||||
final String? actionSuccess;
|
||||
|
||||
@ -32,6 +35,7 @@ class PurchaseOrdersListState {
|
||||
int? total,
|
||||
int? totalPages,
|
||||
bool? isRefreshing,
|
||||
bool? isExporting,
|
||||
String? actionError,
|
||||
String? actionSuccess,
|
||||
bool clearMessages = false,
|
||||
@ -42,6 +46,7 @@ class PurchaseOrdersListState {
|
||||
total: total ?? this.total,
|
||||
totalPages: totalPages ?? this.totalPages,
|
||||
isRefreshing: isRefreshing ?? this.isRefreshing,
|
||||
isExporting: isExporting ?? this.isExporting,
|
||||
actionError: clearMessages ? null : actionError ?? this.actionError,
|
||||
actionSuccess: clearMessages ? null : actionSuccess ?? this.actionSuccess,
|
||||
);
|
||||
@ -125,6 +130,31 @@ class PurchaseOrdersListNotifier
|
||||
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 {
|
||||
final repository = ref.read(purchaseOrderRepositoryProvider);
|
||||
final result = await repository.deletePurchaseOrder(id);
|
||||
|
||||
@ -23,6 +23,7 @@ import '../../../../shared/widgets/app_table_action_icon.dart';
|
||||
import '../../../../shared/widgets/app_table_shell.dart';
|
||||
import '../../../../shared/widgets/error_view.dart';
|
||||
import '../../../../shared/widgets/page_header.dart';
|
||||
import '../../../../shared/utils/file_download_helper.dart';
|
||||
import '../providers/purchase_orders_provider.dart';
|
||||
import '../widgets/po_status_chip.dart';
|
||||
import '../../../../shared/widgets/app_toast.dart';
|
||||
@ -59,6 +60,7 @@ class _PurchaseOrderListScreenState extends ConsumerState<PurchaseOrderListScree
|
||||
final ordersAsync = ref.watch(purchaseOrdersListProvider);
|
||||
final canEdit = ref.can('purchase_orders', PermissionAction.update);
|
||||
final canDelete = ref.can('purchase_orders', PermissionAction.delete);
|
||||
final canExport = ref.can('purchase_orders', PermissionAction.export);
|
||||
|
||||
ref.listen(purchaseOrdersListProvider, (prev, next) {
|
||||
final error = next.valueOrNull?.actionError;
|
||||
@ -108,6 +110,9 @@ class _PurchaseOrderListScreenState extends ConsumerState<PurchaseOrderListScree
|
||||
query: state.query,
|
||||
statusOptions: _statusFilterOptions(state.orders),
|
||||
wrapped: constraints.maxWidth < 900,
|
||||
showExport: canExport,
|
||||
isExporting: state.isExporting,
|
||||
onExport: _exportPurchaseOrders,
|
||||
onSearch: ref.read(purchaseOrdersListProvider.notifier).setSearch,
|
||||
onStatusChanged:
|
||||
ref.read(purchaseOrdersListProvider.notifier).setStatusFilter,
|
||||
@ -186,6 +191,36 @@ class _PurchaseOrderListScreenState extends ConsumerState<PurchaseOrderListScree
|
||||
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 {
|
||||
if (!order.canDelete) {
|
||||
showAppToastFromSnackBar(context,
|
||||
@ -244,6 +279,9 @@ class _FiltersBar extends StatelessWidget {
|
||||
required this.onSearch,
|
||||
required this.onStatusChanged,
|
||||
required this.onPoTypeChanged,
|
||||
this.showExport = false,
|
||||
this.isExporting = false,
|
||||
this.onExport,
|
||||
});
|
||||
|
||||
final TextEditingController searchController;
|
||||
@ -253,6 +291,9 @@ class _FiltersBar extends StatelessWidget {
|
||||
final ValueChanged<String> onSearch;
|
||||
final ValueChanged<String?> onStatusChanged;
|
||||
final ValueChanged<String?> onPoTypeChanged;
|
||||
final bool showExport;
|
||||
final bool isExporting;
|
||||
final VoidCallback? onExport;
|
||||
|
||||
@override
|
||||
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) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
@ -295,6 +348,10 @@ class _FiltersBar extends StatelessWidget {
|
||||
filters[0],
|
||||
const SizedBox(height: 12),
|
||||
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]),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(flex: 2, child: filters[1]),
|
||||
if (showExport) ...[
|
||||
const SizedBox(width: 12),
|
||||
exportButton,
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@ -313,28 +313,30 @@ class _CompanyProfileSettingsScreenState
|
||||
SettingsFormCard(
|
||||
title: 'Company Information',
|
||||
children: [
|
||||
AppTextField(
|
||||
controller: _nameController,
|
||||
label: 'Company Name',
|
||||
validator: (v) =>
|
||||
Validators.required(v, fieldName: 'Company Name'),
|
||||
SidePanelFormRow(
|
||||
left: AppTextField(
|
||||
controller: _nameController,
|
||||
label: 'Company Name',
|
||||
validator: (v) =>
|
||||
Validators.required(v, fieldName: 'Company Name'),
|
||||
),
|
||||
right: AppTextField(
|
||||
controller: _codeController,
|
||||
label: 'Company Code',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
AppTextField(
|
||||
controller: _codeController,
|
||||
label: 'Company Code',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
AppTextField(
|
||||
controller: _registrationController,
|
||||
label: 'Registration Number',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
AppTextField(
|
||||
controller: _gstController,
|
||||
label: 'GSTIN',
|
||||
validator: Validators.optionalGstin,
|
||||
inputFormatters: Validators.gstinInput,
|
||||
SidePanelFormRow(
|
||||
left: AppTextField(
|
||||
controller: _registrationController,
|
||||
label: 'Registration Number',
|
||||
),
|
||||
right: AppTextField(
|
||||
controller: _gstController,
|
||||
label: 'GSTIN',
|
||||
validator: Validators.optionalGstin,
|
||||
inputFormatters: Validators.gstinInput,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
AppTextField(
|
||||
@ -354,31 +356,33 @@ class _CompanyProfileSettingsScreenState
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
AppTextField(
|
||||
controller: _pincodeController,
|
||||
label: 'Pincode',
|
||||
keyboardType: TextInputType.number,
|
||||
SidePanelFormRow(
|
||||
left: AppTextField(
|
||||
controller: _pincodeController,
|
||||
label: 'Pincode',
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
right: AppTextField(
|
||||
controller: _emailController,
|
||||
label: 'Email',
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: Validators.optionalEmail,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
AppTextField(
|
||||
controller: _emailController,
|
||||
label: 'Email',
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: Validators.optionalEmail,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
AppTextField(
|
||||
controller: _phoneController,
|
||||
label: 'Mobile',
|
||||
keyboardType: TextInputType.phone,
|
||||
validator: Validators.optionalMobile,
|
||||
inputFormatters: Validators.mobileInput,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
AppTextField(
|
||||
controller: _websiteController,
|
||||
label: 'Website',
|
||||
keyboardType: TextInputType.url,
|
||||
SidePanelFormRow(
|
||||
left: AppTextField(
|
||||
controller: _phoneController,
|
||||
label: 'Mobile',
|
||||
keyboardType: TextInputType.phone,
|
||||
validator: Validators.optionalMobile,
|
||||
inputFormatters: Validators.mobileInput,
|
||||
),
|
||||
right: AppTextField(
|
||||
controller: _websiteController,
|
||||
label: 'Website',
|
||||
keyboardType: TextInputType.url,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../../../../core/constants/api_endpoints.dart';
|
||||
import '../../../../core/utils/export_file_name.dart';
|
||||
import '../../../../shared/models/api_response.dart';
|
||||
import '../../../../shared/models/export_file_result.dart';
|
||||
import '../../../../shared/models/user_management_models.dart';
|
||||
import '../../../../shared/models/vendor_model.dart';
|
||||
|
||||
@ -37,6 +39,18 @@ class VendorRemoteDataSource {
|
||||
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 {
|
||||
final response = await dio.patch(
|
||||
ApiEndpoints.vendorStatus(id),
|
||||
@ -190,6 +204,12 @@ class VendorRemoteDataSource {
|
||||
return {
|
||||
'page': query.page,
|
||||
'limit': query.limit,
|
||||
..._exportQueryToMap(query),
|
||||
};
|
||||
}
|
||||
|
||||
Map<String, dynamic> _exportQueryToMap(VendorListQuery query) {
|
||||
return {
|
||||
if (query.search != null && query.search!.isNotEmpty) 'search': query.search,
|
||||
if (query.status != null) 'status': query.status,
|
||||
if (query.vendorType != null) 'vendor_type': query.vendorType,
|
||||
|
||||
@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../../core/network/api_handler.dart';
|
||||
import '../../../../core/network/dio_client.dart';
|
||||
import '../../../../shared/models/api_response.dart';
|
||||
import '../../../../shared/models/export_file_result.dart';
|
||||
import '../../../../shared/models/vendor_model.dart';
|
||||
import '../../domain/repositories/vendor_repository.dart';
|
||||
import '../datasources/vendor_remote_data_source.dart';
|
||||
@ -25,6 +26,11 @@ class VendorRepositoryImpl implements VendorRepository {
|
||||
return safeApiCall(() => dataSource.getVendors(query));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<ExportFileResult>> exportVendors(VendorListQuery query) {
|
||||
return safeApiCall(() => dataSource.exportVendors(query));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<VendorModel>> getVendorById(String id) {
|
||||
return safeApiCall(() => dataSource.getVendorById(id));
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import '../../../../core/network/api_handler.dart';
|
||||
import '../../../../shared/models/api_response.dart';
|
||||
import '../../../../shared/models/export_file_result.dart';
|
||||
import '../../../../shared/models/vendor_model.dart';
|
||||
|
||||
abstract class VendorRepository {
|
||||
Future<Result<PaginatedResponse<VendorModel>>> getVendors(VendorListQuery query);
|
||||
Future<Result<ExportFileResult>> exportVendors(VendorListQuery query);
|
||||
Future<Result<VendorModel>> getVendorById(String id);
|
||||
Future<Result<VendorModel>> createVendor(Map<String, dynamic> data);
|
||||
Future<Result<VendorModel>> updateVendor(String id, Map<String, dynamic> data);
|
||||
|
||||
@ -2,6 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../core/utils/table_search.dart';
|
||||
|
||||
import '../../../../shared/models/export_file_result.dart';
|
||||
import '../../../../shared/models/vendor_model.dart';
|
||||
import '../../data/repositories/vendor_repository_impl.dart';
|
||||
|
||||
@ -12,6 +13,7 @@ class VendorsListState {
|
||||
this.total = 0,
|
||||
this.totalPages = 1,
|
||||
this.isRefreshing = false,
|
||||
this.isExporting = false,
|
||||
this.actionError,
|
||||
this.actionSuccess,
|
||||
});
|
||||
@ -21,6 +23,7 @@ class VendorsListState {
|
||||
final int total;
|
||||
final int totalPages;
|
||||
final bool isRefreshing;
|
||||
final bool isExporting;
|
||||
final String? actionError;
|
||||
final String? actionSuccess;
|
||||
|
||||
@ -30,6 +33,7 @@ class VendorsListState {
|
||||
int? total,
|
||||
int? totalPages,
|
||||
bool? isRefreshing,
|
||||
bool? isExporting,
|
||||
String? actionError,
|
||||
String? actionSuccess,
|
||||
bool clearMessages = false,
|
||||
@ -40,6 +44,7 @@ class VendorsListState {
|
||||
total: total ?? this.total,
|
||||
totalPages: totalPages ?? this.totalPages,
|
||||
isRefreshing: isRefreshing ?? this.isRefreshing,
|
||||
isExporting: isExporting ?? this.isExporting,
|
||||
actionError: clearMessages ? null : actionError ?? this.actionError,
|
||||
actionSuccess: clearMessages ? null : actionSuccess ?? this.actionSuccess,
|
||||
);
|
||||
@ -122,6 +127,30 @@ class VendorsListNotifier extends AutoDisposeAsyncNotifier<VendorsListState> {
|
||||
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 {
|
||||
final repository = ref.read(vendorRepositoryProvider);
|
||||
final result = await repository.deleteVendor(id);
|
||||
|
||||
@ -23,6 +23,7 @@ import '../../../../shared/widgets/can_permission.dart';
|
||||
import '../../../../shared/widgets/app_table_shell.dart';
|
||||
import '../../../../shared/widgets/error_view.dart';
|
||||
import '../../../../shared/widgets/page_header.dart';
|
||||
import '../../../../shared/utils/file_download_helper.dart';
|
||||
import '../providers/vendors_provider.dart';
|
||||
import '../widgets/vendor_form_panel.dart';
|
||||
import '../../../../shared/widgets/app_toast.dart';
|
||||
@ -48,6 +49,7 @@ class _VendorListScreenState extends ConsumerState<VendorListScreen> {
|
||||
final vendorsAsync = ref.watch(vendorsListProvider);
|
||||
final canEdit = ref.can('vendors', PermissionAction.update);
|
||||
final canDelete = ref.can('vendors', PermissionAction.delete);
|
||||
final canExport = ref.can('vendors', PermissionAction.export);
|
||||
|
||||
ref.listen(vendorsListProvider, (prev, next) {
|
||||
final error = next.valueOrNull?.actionError;
|
||||
@ -94,6 +96,9 @@ class _VendorListScreenState extends ConsumerState<VendorListScreen> {
|
||||
searchController: _searchController,
|
||||
query: state.query,
|
||||
wrapped: constraints.maxWidth < 900,
|
||||
showExport: canExport,
|
||||
isExporting: state.isExporting,
|
||||
onExport: _exportVendors,
|
||||
onSearch: ref.read(vendorsListProvider.notifier).setSearch,
|
||||
onStatusChanged:
|
||||
ref.read(vendorsListProvider.notifier).setStatusFilter,
|
||||
@ -159,6 +164,34 @@ class _VendorListScreenState extends ConsumerState<VendorListScreen> {
|
||||
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 {
|
||||
final confirmed = await showAppConfirmationDialog(
|
||||
context: context,
|
||||
@ -180,6 +213,9 @@ class _FiltersBar extends StatelessWidget {
|
||||
required this.onSearch,
|
||||
required this.onStatusChanged,
|
||||
required this.onVendorTypeChanged,
|
||||
this.showExport = false,
|
||||
this.isExporting = false,
|
||||
this.onExport,
|
||||
});
|
||||
|
||||
final TextEditingController searchController;
|
||||
@ -188,6 +224,9 @@ class _FiltersBar extends StatelessWidget {
|
||||
final ValueChanged<String> onSearch;
|
||||
final ValueChanged<String?> onStatusChanged;
|
||||
final ValueChanged<String?> onVendorTypeChanged;
|
||||
final bool showExport;
|
||||
final bool isExporting;
|
||||
final VoidCallback? onExport;
|
||||
|
||||
@override
|
||||
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) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
@ -235,6 +286,10 @@ class _FiltersBar extends StatelessWidget {
|
||||
filters[0],
|
||||
const SizedBox(height: 12),
|
||||
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]),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(flex: 2, child: filters[1]),
|
||||
if (showExport) ...[
|
||||
const SizedBox(width: 12),
|
||||
exportButton,
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user