asset form popup fix
This commit is contained in:
parent
fa310dd87c
commit
f496be0c7f
@ -73,25 +73,16 @@ class ApiEndpoints {
|
|||||||
// Assets
|
// Assets
|
||||||
static const String assets = '/assets';
|
static const String assets = '/assets';
|
||||||
static String assetById(String id) => '/assets/$id';
|
static String assetById(String id) => '/assets/$id';
|
||||||
static String assetQrCode(String id) => '/assets/$id/qr-code';
|
static String assetTransfer(String id) => '/assets/$id/transfer';
|
||||||
static const String assetSearch = '/assets/search';
|
static const String assetAlertsExpiry = '/assets/alerts/expiry';
|
||||||
|
static const String assetAlertsService = '/assets/alerts/service';
|
||||||
// Asset Allocations
|
static String assetAmc(String assetId) => '/assets/$assetId/amc';
|
||||||
static const String assetAllocations = '/asset-allocations';
|
static String assetAmcRenew(String assetId, String contractId) =>
|
||||||
static String assetAllocationById(String id) => '/asset-allocations/$id';
|
'/assets/$assetId/amc/$contractId/renew';
|
||||||
static String returnAsset(String id) => '/asset-allocations/$id/return';
|
static String assetServiceVisits(String assetId) => '/assets/$assetId/service-visits';
|
||||||
static String reassignAsset(String id) => '/asset-allocations/$id/reassign';
|
static String assetInsurance(String assetId) => '/assets/$assetId/insurance';
|
||||||
static String transferAsset(String id) => '/asset-allocations/$id/transfer';
|
static String assetInsuranceRenew(String assetId, String policyId) =>
|
||||||
static const String allocationHistory = '/asset-allocations/history';
|
'/assets/$assetId/insurance/$policyId/renew';
|
||||||
|
|
||||||
// Asset Maintenance
|
|
||||||
static const String assetMaintenance = '/asset-maintenance';
|
|
||||||
static String maintenanceById(String id) => '/asset-maintenance/$id';
|
|
||||||
|
|
||||||
// Asset Disposal
|
|
||||||
static const String assetDisposal = '/asset-disposal';
|
|
||||||
static String disposalById(String id) => '/asset-disposal/$id';
|
|
||||||
static String approveDisposal(String id) => '/asset-disposal/$id/approve';
|
|
||||||
|
|
||||||
// Dashboard
|
// Dashboard
|
||||||
static const String dashboardKpis = '/dashboard/kpis';
|
static const String dashboardKpis = '/dashboard/kpis';
|
||||||
|
|||||||
@ -45,11 +45,7 @@ class RouteConstants {
|
|||||||
static const String assetEdit = '/assets/:id/edit';
|
static const String assetEdit = '/assets/:id/edit';
|
||||||
static const String assetDetail = '/assets/:id';
|
static const String assetDetail = '/assets/:id';
|
||||||
static const String assetCategories = '/assets/categories';
|
static const String assetCategories = '/assets/categories';
|
||||||
static const String assetAllocations = '/assets/allocations';
|
static const String assetAlerts = '/assets/alerts';
|
||||||
static const String assetMaintenance = '/assets/maintenance';
|
|
||||||
static const String assetDisposal = '/assets/disposal';
|
|
||||||
static const String assetQrScan = '/assets/qr-scan';
|
|
||||||
static const String assetQrGenerate = '/assets/qr-generate';
|
|
||||||
|
|
||||||
// Master Data
|
// Master Data
|
||||||
static const String masterData = '/master-data';
|
static const String masterData = '/master-data';
|
||||||
|
|||||||
@ -0,0 +1,213 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
|
import '../../../../core/constants/api_endpoints.dart';
|
||||||
|
import '../../../../shared/models/api_response.dart';
|
||||||
|
import '../../../../shared/models/asset_model.dart';
|
||||||
|
|
||||||
|
class AssetRemoteDataSource {
|
||||||
|
AssetRemoteDataSource({required this.dio});
|
||||||
|
|
||||||
|
final Dio dio;
|
||||||
|
|
||||||
|
Future<PaginatedResponse<AssetModel>> getAssets(PaginationParams params) 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,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return _parsePaginated(response.data, AssetModel.fromJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AssetModel> getAssetById(String id) async {
|
||||||
|
final response = await dio.get(ApiEndpoints.assetById(id));
|
||||||
|
return AssetModel.fromJson(response.data['data'] as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AssetModel> createAsset(Map<String, dynamic> data) async {
|
||||||
|
final response = await dio.post(ApiEndpoints.assets, data: data);
|
||||||
|
return AssetModel.fromJson(response.data['data'] as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AssetModel> updateAsset(String id, Map<String, dynamic> data) async {
|
||||||
|
final response = await dio.put(ApiEndpoints.assetById(id), data: data);
|
||||||
|
return AssetModel.fromJson(response.data['data'] as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteAsset(String id) async {
|
||||||
|
await dio.delete(ApiEndpoints.assetById(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AssetModel> transferAsset(String id, Map<String, dynamic> data) async {
|
||||||
|
final response = await dio.post(ApiEndpoints.assetTransfer(id), data: data);
|
||||||
|
return AssetModel.fromJson(response.data['data'] as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<AssetCategoryModel>> getCategories() async {
|
||||||
|
final response = await dio.get(
|
||||||
|
ApiEndpoints.assetCategories,
|
||||||
|
queryParameters: const {'limit': 100, 'is_active': true},
|
||||||
|
);
|
||||||
|
return _parseList(response.data, AssetCategoryModel.fromJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<PaginatedResponse<AssetAlertModel>> getExpiryAlerts({
|
||||||
|
int? days,
|
||||||
|
String? type,
|
||||||
|
int page = 1,
|
||||||
|
int limit = 20,
|
||||||
|
}) async {
|
||||||
|
final response = await dio.get(
|
||||||
|
ApiEndpoints.assetAlertsExpiry,
|
||||||
|
queryParameters: {
|
||||||
|
'page': page,
|
||||||
|
'limit': limit,
|
||||||
|
if (days != null) 'days': days,
|
||||||
|
if (type != null) 'type': type,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return _parsePaginated(response.data, AssetAlertModel.fromJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<AssetAlertModel>> getServiceAlerts({String? status}) async {
|
||||||
|
final response = await dio.get(
|
||||||
|
ApiEndpoints.assetAlertsService,
|
||||||
|
queryParameters: {if (status != null) 'status': status},
|
||||||
|
);
|
||||||
|
return _parseList(response.data, AssetAlertModel.fromJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<AmcContractModel>> getAmcContracts(String assetId) async {
|
||||||
|
final response = await dio.get(ApiEndpoints.assetAmc(assetId));
|
||||||
|
return _parseList(response.data, AmcContractModel.fromJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AmcContractModel> createAmcContract(
|
||||||
|
String assetId,
|
||||||
|
Map<String, dynamic> data,
|
||||||
|
) async {
|
||||||
|
final response = await dio.post(ApiEndpoints.assetAmc(assetId), data: data);
|
||||||
|
return AmcContractModel.fromJson(response.data['data'] as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AmcContractModel> renewAmcContract(
|
||||||
|
String assetId,
|
||||||
|
String contractId,
|
||||||
|
Map<String, dynamic> data,
|
||||||
|
) async {
|
||||||
|
final response = await dio.patch(
|
||||||
|
ApiEndpoints.assetAmcRenew(assetId, contractId),
|
||||||
|
data: data,
|
||||||
|
);
|
||||||
|
return AmcContractModel.fromJson(response.data['data'] as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<ServiceVisitModel>> getServiceVisits(String assetId) async {
|
||||||
|
final response = await dio.get(ApiEndpoints.assetServiceVisits(assetId));
|
||||||
|
return _parseList(response.data, ServiceVisitModel.fromJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ServiceVisitModel> logServiceVisit(
|
||||||
|
String assetId,
|
||||||
|
Map<String, dynamic> data,
|
||||||
|
) async {
|
||||||
|
final response = await dio.post(ApiEndpoints.assetServiceVisits(assetId), data: data);
|
||||||
|
return ServiceVisitModel.fromJson(response.data['data'] as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<InsurancePolicyModel>> getInsurancePolicies(String assetId) async {
|
||||||
|
final response = await dio.get(ApiEndpoints.assetInsurance(assetId));
|
||||||
|
return _parseList(response.data, InsurancePolicyModel.fromJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<InsurancePolicyModel> createInsurancePolicy(
|
||||||
|
String assetId,
|
||||||
|
Map<String, dynamic> data,
|
||||||
|
) async {
|
||||||
|
final response = await dio.post(ApiEndpoints.assetInsurance(assetId), data: data);
|
||||||
|
return InsurancePolicyModel.fromJson(
|
||||||
|
response.data['data'] as Map<String, dynamic>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<InsurancePolicyModel> renewInsurancePolicy(
|
||||||
|
String assetId,
|
||||||
|
String policyId,
|
||||||
|
Map<String, dynamic> data,
|
||||||
|
) async {
|
||||||
|
final response = await dio.patch(
|
||||||
|
ApiEndpoints.assetInsuranceRenew(assetId, policyId),
|
||||||
|
data: data,
|
||||||
|
);
|
||||||
|
return InsurancePolicyModel.fromJson(
|
||||||
|
response.data['data'] as Map<String, dynamic>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<T> _parseList<T>(
|
||||||
|
dynamic body,
|
||||||
|
T Function(Map<String, dynamic>) fromJson,
|
||||||
|
) {
|
||||||
|
if (body is! Map<String, dynamic>) return [];
|
||||||
|
final raw = body['data'];
|
||||||
|
if (raw is List) {
|
||||||
|
return raw
|
||||||
|
.whereType<Map<String, dynamic>>()
|
||||||
|
.map(fromJson)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
if (raw is Map<String, dynamic>) {
|
||||||
|
final items = raw['items'];
|
||||||
|
if (items is List) {
|
||||||
|
return items
|
||||||
|
.whereType<Map<String, dynamic>>()
|
||||||
|
.map(fromJson)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
PaginatedResponse<T> _parsePaginated<T>(
|
||||||
|
dynamic body,
|
||||||
|
T Function(Map<String, dynamic>) fromJson,
|
||||||
|
) {
|
||||||
|
if (body is! Map<String, dynamic>) {
|
||||||
|
return const PaginatedResponse(
|
||||||
|
items: [],
|
||||||
|
page: 1,
|
||||||
|
limit: 20,
|
||||||
|
total: 0,
|
||||||
|
totalPages: 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final raw = body['data'];
|
||||||
|
final meta = body['meta'] as Map<String, dynamic>? ?? {};
|
||||||
|
|
||||||
|
if (raw is List) {
|
||||||
|
final items = raw.whereType<Map<String, dynamic>>().map(fromJson).toList();
|
||||||
|
return PaginatedResponse(
|
||||||
|
items: items,
|
||||||
|
page: (meta['page'] as num?)?.toInt() ?? 1,
|
||||||
|
limit: (meta['limit'] as num?)?.toInt() ?? items.length,
|
||||||
|
total: (meta['total'] as num?)?.toInt() ?? items.length,
|
||||||
|
totalPages: (meta['totalPages'] as num?)?.toInt() ?? 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (raw is Map<String, dynamic>) {
|
||||||
|
return PaginatedResponse.fromJson(raw, (json) => fromJson(json! as Map<String, dynamic>));
|
||||||
|
}
|
||||||
|
|
||||||
|
return const PaginatedResponse(
|
||||||
|
items: [],
|
||||||
|
page: 1,
|
||||||
|
limit: 20,
|
||||||
|
total: 0,
|
||||||
|
totalPages: 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,129 +1,131 @@
|
|||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../../../../core/constants/api_endpoints.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/asset_model.dart';
|
import '../../../../shared/models/asset_model.dart';
|
||||||
import '../../domain/repositories/asset_repository.dart';
|
import '../../domain/repositories/asset_repository.dart';
|
||||||
|
import '../datasources/asset_remote_data_source.dart';
|
||||||
|
|
||||||
|
final assetRemoteDataSourceProvider = Provider<AssetRemoteDataSource>((ref) {
|
||||||
|
return AssetRemoteDataSource(dio: ref.watch(dioProvider));
|
||||||
|
});
|
||||||
|
|
||||||
final assetRepositoryProvider = Provider<AssetRepository>((ref) {
|
final assetRepositoryProvider = Provider<AssetRepository>((ref) {
|
||||||
return AssetRepositoryImpl(dio: ref.watch(dioProvider));
|
return AssetRepositoryImpl(dataSource: ref.watch(assetRemoteDataSourceProvider));
|
||||||
});
|
});
|
||||||
|
|
||||||
class AssetRepositoryImpl implements AssetRepository {
|
class AssetRepositoryImpl implements AssetRepository {
|
||||||
AssetRepositoryImpl({required this.dio});
|
AssetRepositoryImpl({required this.dataSource});
|
||||||
|
|
||||||
final Dio dio;
|
final AssetRemoteDataSource dataSource;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Result<PaginatedResponse<AssetModel>>> getAssets(PaginationParams params) async {
|
Future<Result<PaginatedResponse<AssetModel>>> getAssets(PaginationParams params) {
|
||||||
return safeApiCall(() async {
|
return safeApiCall(() => dataSource.getAssets(params));
|
||||||
final response = await dio.get(ApiEndpoints.assets, queryParameters: {
|
|
||||||
'page': params.page,
|
|
||||||
'limit': params.limit,
|
|
||||||
if (params.search != null) 'search': params.search,
|
|
||||||
});
|
|
||||||
return PaginatedResponse<AssetModel>.fromJson(
|
|
||||||
response.data['data'] as Map<String, dynamic>,
|
|
||||||
(json) => AssetModel.fromJson(json! as Map<String, dynamic>),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Result<AssetModel>> getAssetById(String id) async {
|
Future<Result<AssetModel>> getAssetById(String id) {
|
||||||
return safeApiCall(() async {
|
return safeApiCall(() => dataSource.getAssetById(id));
|
||||||
final response = await dio.get(ApiEndpoints.assetById(id));
|
|
||||||
return AssetModel.fromJson(response.data['data'] as Map<String, dynamic>);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Result<AssetModel>> createAsset(Map<String, dynamic> data) async {
|
Future<Result<AssetModel>> createAsset(Map<String, dynamic> data) {
|
||||||
return safeApiCall(() async {
|
return safeApiCall(() => dataSource.createAsset(data));
|
||||||
final response = await dio.post(ApiEndpoints.assets, data: data);
|
|
||||||
return AssetModel.fromJson(response.data['data'] as Map<String, dynamic>);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Result<AssetModel>> updateAsset(String id, Map<String, dynamic> data) async {
|
Future<Result<AssetModel>> updateAsset(String id, Map<String, dynamic> data) {
|
||||||
return safeApiCall(() async {
|
return safeApiCall(() => dataSource.updateAsset(id, data));
|
||||||
final response = await dio.put(ApiEndpoints.assetById(id), data: data);
|
|
||||||
return AssetModel.fromJson(response.data['data'] as Map<String, dynamic>);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Result<List<AssetCategoryModel>>> getCategories() async {
|
Future<Result<void>> deleteAsset(String id) {
|
||||||
return safeApiCall(() async {
|
return safeApiCall(() => dataSource.deleteAsset(id));
|
||||||
final response = await dio.get(ApiEndpoints.assetCategories);
|
|
||||||
final rawData = response.data['data'];
|
|
||||||
final list = rawData is List<dynamic>
|
|
||||||
? rawData
|
|
||||||
: (rawData is Map<String, dynamic> ? rawData['items'] as List<dynamic>? : null) ??
|
|
||||||
<dynamic>[];
|
|
||||||
return list
|
|
||||||
.map((e) => AssetCategoryModel.fromJson(e as Map<String, dynamic>))
|
|
||||||
.toList();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Result<PaginatedResponse<AssetAllocationModel>>> getAllocations(
|
Future<Result<AssetModel>> transferAsset(String id, Map<String, dynamic> data) {
|
||||||
PaginationParams params,
|
return safeApiCall(() => dataSource.transferAsset(id, data));
|
||||||
) async {
|
|
||||||
return safeApiCall(() async {
|
|
||||||
final response = await dio.get(ApiEndpoints.assetAllocations, queryParameters: {
|
|
||||||
'page': params.page,
|
|
||||||
'limit': params.limit,
|
|
||||||
});
|
|
||||||
return PaginatedResponse<AssetAllocationModel>.fromJson(
|
|
||||||
response.data['data'] as Map<String, dynamic>,
|
|
||||||
(json) => AssetAllocationModel.fromJson(json! as Map<String, dynamic>),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Result<PaginatedResponse<AssetMaintenanceModel>>> getMaintenance(
|
Future<Result<List<AssetCategoryModel>>> getCategories() {
|
||||||
PaginationParams params,
|
return safeApiCall(() => dataSource.getCategories());
|
||||||
) async {
|
|
||||||
return safeApiCall(() async {
|
|
||||||
final response = await dio.get(ApiEndpoints.assetMaintenance, queryParameters: {
|
|
||||||
'page': params.page,
|
|
||||||
'limit': params.limit,
|
|
||||||
});
|
|
||||||
return PaginatedResponse<AssetMaintenanceModel>.fromJson(
|
|
||||||
response.data['data'] as Map<String, dynamic>,
|
|
||||||
(json) => AssetMaintenanceModel.fromJson(json! as Map<String, dynamic>),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Result<PaginatedResponse<AssetDisposalModel>>> getDisposals(
|
Future<Result<PaginatedResponse<AssetAlertModel>>> getExpiryAlerts({
|
||||||
PaginationParams params,
|
int? days,
|
||||||
) async {
|
String? type,
|
||||||
return safeApiCall(() async {
|
int page = 1,
|
||||||
final response = await dio.get(ApiEndpoints.assetDisposal, queryParameters: {
|
int limit = 20,
|
||||||
'page': params.page,
|
}) {
|
||||||
'limit': params.limit,
|
return safeApiCall(
|
||||||
});
|
() => dataSource.getExpiryAlerts(days: days, type: type, page: page, limit: limit),
|
||||||
return PaginatedResponse<AssetDisposalModel>.fromJson(
|
);
|
||||||
response.data['data'] as Map<String, dynamic>,
|
|
||||||
(json) => AssetDisposalModel.fromJson(json! as Map<String, dynamic>),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Result<AssetModel>> searchByQrCode(String code) async {
|
Future<Result<List<AssetAlertModel>>> getServiceAlerts({String? status}) {
|
||||||
return safeApiCall(() async {
|
return safeApiCall(() => dataSource.getServiceAlerts(status: status));
|
||||||
final response = await dio.get(ApiEndpoints.assetSearch, queryParameters: {'q': code});
|
}
|
||||||
return AssetModel.fromJson(response.data['data'] as Map<String, dynamic>);
|
|
||||||
});
|
@override
|
||||||
|
Future<Result<List<AmcContractModel>>> getAmcContracts(String assetId) {
|
||||||
|
return safeApiCall(() => dataSource.getAmcContracts(assetId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Result<AmcContractModel>> createAmcContract(
|
||||||
|
String assetId,
|
||||||
|
Map<String, dynamic> data,
|
||||||
|
) {
|
||||||
|
return safeApiCall(() => dataSource.createAmcContract(assetId, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Result<AmcContractModel>> renewAmcContract(
|
||||||
|
String assetId,
|
||||||
|
String contractId,
|
||||||
|
Map<String, dynamic> data,
|
||||||
|
) {
|
||||||
|
return safeApiCall(() => dataSource.renewAmcContract(assetId, contractId, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Result<List<ServiceVisitModel>>> getServiceVisits(String assetId) {
|
||||||
|
return safeApiCall(() => dataSource.getServiceVisits(assetId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Result<ServiceVisitModel>> logServiceVisit(
|
||||||
|
String assetId,
|
||||||
|
Map<String, dynamic> data,
|
||||||
|
) {
|
||||||
|
return safeApiCall(() => dataSource.logServiceVisit(assetId, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Result<List<InsurancePolicyModel>>> getInsurancePolicies(String assetId) {
|
||||||
|
return safeApiCall(() => dataSource.getInsurancePolicies(assetId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Result<InsurancePolicyModel>> createInsurancePolicy(
|
||||||
|
String assetId,
|
||||||
|
Map<String, dynamic> data,
|
||||||
|
) {
|
||||||
|
return safeApiCall(() => dataSource.createInsurancePolicy(assetId, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Result<InsurancePolicyModel>> renewInsurancePolicy(
|
||||||
|
String assetId,
|
||||||
|
String policyId,
|
||||||
|
Map<String, dynamic> data,
|
||||||
|
) {
|
||||||
|
return safeApiCall(() => dataSource.renewInsurancePolicy(assetId, policyId, data));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import '../../../../core/errors/failure.dart';
|
|
||||||
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/asset_model.dart';
|
import '../../../../shared/models/asset_model.dart';
|
||||||
@ -8,9 +7,39 @@ abstract class AssetRepository {
|
|||||||
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);
|
||||||
|
Future<Result<void>> deleteAsset(String id);
|
||||||
|
Future<Result<AssetModel>> transferAsset(String id, Map<String, dynamic> data);
|
||||||
Future<Result<List<AssetCategoryModel>>> getCategories();
|
Future<Result<List<AssetCategoryModel>>> getCategories();
|
||||||
Future<Result<PaginatedResponse<AssetAllocationModel>>> getAllocations(PaginationParams params);
|
Future<Result<PaginatedResponse<AssetAlertModel>>> getExpiryAlerts({
|
||||||
Future<Result<PaginatedResponse<AssetMaintenanceModel>>> getMaintenance(PaginationParams params);
|
int? days,
|
||||||
Future<Result<PaginatedResponse<AssetDisposalModel>>> getDisposals(PaginationParams params);
|
String? type,
|
||||||
Future<Result<AssetModel>> searchByQrCode(String code);
|
int page,
|
||||||
|
int limit,
|
||||||
|
});
|
||||||
|
Future<Result<List<AssetAlertModel>>> getServiceAlerts({String? status});
|
||||||
|
Future<Result<List<AmcContractModel>>> getAmcContracts(String assetId);
|
||||||
|
Future<Result<AmcContractModel>> createAmcContract(
|
||||||
|
String assetId,
|
||||||
|
Map<String, dynamic> data,
|
||||||
|
);
|
||||||
|
Future<Result<AmcContractModel>> renewAmcContract(
|
||||||
|
String assetId,
|
||||||
|
String contractId,
|
||||||
|
Map<String, dynamic> data,
|
||||||
|
);
|
||||||
|
Future<Result<List<ServiceVisitModel>>> getServiceVisits(String assetId);
|
||||||
|
Future<Result<ServiceVisitModel>> logServiceVisit(
|
||||||
|
String assetId,
|
||||||
|
Map<String, dynamic> data,
|
||||||
|
);
|
||||||
|
Future<Result<List<InsurancePolicyModel>>> getInsurancePolicies(String assetId);
|
||||||
|
Future<Result<InsurancePolicyModel>> createInsurancePolicy(
|
||||||
|
String assetId,
|
||||||
|
Map<String, dynamic> data,
|
||||||
|
);
|
||||||
|
Future<Result<InsurancePolicyModel>> renewInsurancePolicy(
|
||||||
|
String assetId,
|
||||||
|
String policyId,
|
||||||
|
Map<String, dynamic> data,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
408
lib/modules/assets/presentation/providers/assets_provider.dart
Normal file
408
lib/modules/assets/presentation/providers/assets_provider.dart
Normal file
@ -0,0 +1,408 @@
|
|||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../../../../shared/models/api_response.dart';
|
||||||
|
import '../../../../shared/models/asset_model.dart';
|
||||||
|
import '../../data/repositories/asset_repository_impl.dart';
|
||||||
|
|
||||||
|
class AssetsListState {
|
||||||
|
const AssetsListState({
|
||||||
|
this.assets = const [],
|
||||||
|
this.query = const PaginationParams(limit: 20),
|
||||||
|
this.total = 0,
|
||||||
|
this.totalPages = 1,
|
||||||
|
this.isRefreshing = false,
|
||||||
|
this.actionError,
|
||||||
|
this.actionSuccess,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<AssetModel> assets;
|
||||||
|
final PaginationParams query;
|
||||||
|
final int total;
|
||||||
|
final int totalPages;
|
||||||
|
final bool isRefreshing;
|
||||||
|
final String? actionError;
|
||||||
|
final String? actionSuccess;
|
||||||
|
|
||||||
|
AssetsListState copyWith({
|
||||||
|
List<AssetModel>? assets,
|
||||||
|
PaginationParams? query,
|
||||||
|
int? total,
|
||||||
|
int? totalPages,
|
||||||
|
bool? isRefreshing,
|
||||||
|
String? actionError,
|
||||||
|
String? actionSuccess,
|
||||||
|
bool clearMessages = false,
|
||||||
|
}) {
|
||||||
|
return AssetsListState(
|
||||||
|
assets: assets ?? this.assets,
|
||||||
|
query: query ?? this.query,
|
||||||
|
total: total ?? this.total,
|
||||||
|
totalPages: totalPages ?? this.totalPages,
|
||||||
|
isRefreshing: isRefreshing ?? this.isRefreshing,
|
||||||
|
actionError: clearMessages ? null : actionError ?? this.actionError,
|
||||||
|
actionSuccess: clearMessages ? null : actionSuccess ?? this.actionSuccess,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final assetsListProvider =
|
||||||
|
AsyncNotifierProvider.autoDispose<AssetsListNotifier, AssetsListState>(
|
||||||
|
AssetsListNotifier.new,
|
||||||
|
);
|
||||||
|
|
||||||
|
class AssetsListNotifier extends AutoDisposeAsyncNotifier<AssetsListState> {
|
||||||
|
@override
|
||||||
|
Future<AssetsListState> build() async {
|
||||||
|
return _load(const PaginationParams(limit: 20));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AssetsListState> _load(PaginationParams query) async {
|
||||||
|
final repository = ref.read(assetRepositoryProvider);
|
||||||
|
final result = await repository.getAssets(query);
|
||||||
|
if (result.failure != null) throw result.failure!;
|
||||||
|
final page = result.data!;
|
||||||
|
return AssetsListState(
|
||||||
|
assets: page.items,
|
||||||
|
query: query,
|
||||||
|
total: page.total,
|
||||||
|
totalPages: page.totalPages,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refresh() async {
|
||||||
|
final current = state.valueOrNull ?? const AssetsListState();
|
||||||
|
state = AsyncData(current.copyWith(isRefreshing: true, clearMessages: true));
|
||||||
|
try {
|
||||||
|
state = AsyncData(await _load(current.query));
|
||||||
|
} catch (e, st) {
|
||||||
|
state = AsyncError(e, st);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> applyQuery(PaginationParams query) async {
|
||||||
|
final previous = state.valueOrNull;
|
||||||
|
if (previous == null) {
|
||||||
|
state = const AsyncLoading();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
state = AsyncData(await _load(query));
|
||||||
|
} catch (e, st) {
|
||||||
|
state = AsyncError(e, st);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setSearch(String search) {
|
||||||
|
final current = state.valueOrNull;
|
||||||
|
if (current == null) return;
|
||||||
|
applyQuery(current.query.copyWith(search: search, page: 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setPage(int page) {
|
||||||
|
final current = state.valueOrNull;
|
||||||
|
if (current == null) return;
|
||||||
|
applyQuery(current.query.copyWith(page: page));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setPageSize(int limit) {
|
||||||
|
final current = state.valueOrNull;
|
||||||
|
if (current == null) return;
|
||||||
|
applyQuery(current.query.copyWith(limit: limit, page: 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> deleteAsset(String id) async {
|
||||||
|
final repository = ref.read(assetRepositoryProvider);
|
||||||
|
final result = await repository.deleteAsset(id);
|
||||||
|
if (result.failure != null) {
|
||||||
|
final current = state.valueOrNull;
|
||||||
|
if (current != null) {
|
||||||
|
state = AsyncData(current.copyWith(actionError: result.failure!.message));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
await refresh();
|
||||||
|
final current = state.valueOrNull;
|
||||||
|
if (current != null) {
|
||||||
|
state = AsyncData(current.copyWith(actionSuccess: 'Asset deleted'));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final assetDetailProvider =
|
||||||
|
AsyncNotifierProvider.family<AssetDetailNotifier, AssetDetailState, String>(
|
||||||
|
AssetDetailNotifier.new,
|
||||||
|
);
|
||||||
|
|
||||||
|
class AssetDetailState {
|
||||||
|
const AssetDetailState({
|
||||||
|
required this.asset,
|
||||||
|
this.amcContracts = const [],
|
||||||
|
this.serviceVisits = const [],
|
||||||
|
this.insurancePolicies = const [],
|
||||||
|
});
|
||||||
|
|
||||||
|
final AssetModel asset;
|
||||||
|
final List<AmcContractModel> amcContracts;
|
||||||
|
final List<ServiceVisitModel> serviceVisits;
|
||||||
|
final List<InsurancePolicyModel> insurancePolicies;
|
||||||
|
|
||||||
|
AssetDetailState copyWith({
|
||||||
|
AssetModel? asset,
|
||||||
|
List<AmcContractModel>? amcContracts,
|
||||||
|
List<ServiceVisitModel>? serviceVisits,
|
||||||
|
List<InsurancePolicyModel>? insurancePolicies,
|
||||||
|
}) {
|
||||||
|
return AssetDetailState(
|
||||||
|
asset: asset ?? this.asset,
|
||||||
|
amcContracts: amcContracts ?? this.amcContracts,
|
||||||
|
serviceVisits: serviceVisits ?? this.serviceVisits,
|
||||||
|
insurancePolicies: insurancePolicies ?? this.insurancePolicies,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AssetDetailNotifier extends FamilyAsyncNotifier<AssetDetailState, String> {
|
||||||
|
@override
|
||||||
|
Future<AssetDetailState> build(String arg) async {
|
||||||
|
return _loadAll(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AssetDetailState> _loadAll(String assetId) async {
|
||||||
|
final repository = ref.read(assetRepositoryProvider);
|
||||||
|
final assetResult = await repository.getAssetById(assetId);
|
||||||
|
if (assetResult.failure != null) throw assetResult.failure!;
|
||||||
|
|
||||||
|
final amcResult = await repository.getAmcContracts(assetId);
|
||||||
|
final visitsResult = await repository.getServiceVisits(assetId);
|
||||||
|
final insuranceResult = await repository.getInsurancePolicies(assetId);
|
||||||
|
|
||||||
|
return AssetDetailState(
|
||||||
|
asset: assetResult.data!,
|
||||||
|
amcContracts: amcResult.data ?? [],
|
||||||
|
serviceVisits: visitsResult.data ?? [],
|
||||||
|
insurancePolicies: insuranceResult.data ?? [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> reload() async {
|
||||||
|
state = const AsyncLoading();
|
||||||
|
state = AsyncData(await _loadAll(arg));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AssetModel?> updateAsset(Map<String, dynamic> data) async {
|
||||||
|
final repository = ref.read(assetRepositoryProvider);
|
||||||
|
final result = await repository.updateAsset(arg, data);
|
||||||
|
if (result.failure != null) throw result.failure!;
|
||||||
|
await reload();
|
||||||
|
ref.invalidate(assetsListProvider);
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> deleteAsset() async {
|
||||||
|
final repository = ref.read(assetRepositoryProvider);
|
||||||
|
final result = await repository.deleteAsset(arg);
|
||||||
|
if (result.failure != null) return false;
|
||||||
|
ref.invalidate(assetsListProvider);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AssetModel?> transferAsset(Map<String, dynamic> data) async {
|
||||||
|
final repository = ref.read(assetRepositoryProvider);
|
||||||
|
final result = await repository.transferAsset(arg, data);
|
||||||
|
if (result.failure != null) throw result.failure!;
|
||||||
|
await reload();
|
||||||
|
ref.invalidate(assetsListProvider);
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AmcContractModel?> createAmc(Map<String, dynamic> data) async {
|
||||||
|
final repository = ref.read(assetRepositoryProvider);
|
||||||
|
final result = await repository.createAmcContract(arg, data);
|
||||||
|
if (result.failure != null) throw result.failure!;
|
||||||
|
await reload();
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ServiceVisitModel?> logVisit(Map<String, dynamic> data) async {
|
||||||
|
final repository = ref.read(assetRepositoryProvider);
|
||||||
|
final result = await repository.logServiceVisit(arg, data);
|
||||||
|
if (result.failure != null) throw result.failure!;
|
||||||
|
await reload();
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<InsurancePolicyModel?> createInsurance(Map<String, dynamic> data) async {
|
||||||
|
final repository = ref.read(assetRepositoryProvider);
|
||||||
|
final result = await repository.createInsurancePolicy(arg, data);
|
||||||
|
if (result.failure != null) throw result.failure!;
|
||||||
|
await reload();
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final assetFormProvider =
|
||||||
|
AsyncNotifierProvider.family.autoDispose<AssetFormNotifier, AssetModel?, String?>(
|
||||||
|
AssetFormNotifier.new,
|
||||||
|
);
|
||||||
|
|
||||||
|
class AssetFormNotifier extends AutoDisposeFamilyAsyncNotifier<AssetModel?, String?> {
|
||||||
|
@override
|
||||||
|
Future<AssetModel?> build(String? arg) async {
|
||||||
|
if (arg == null) return null;
|
||||||
|
final repository = ref.read(assetRepositoryProvider);
|
||||||
|
final result = await repository.getAssetById(arg);
|
||||||
|
if (result.failure != null) throw result.failure!;
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AssetModel?> submitCreate(Map<String, dynamic> data) async {
|
||||||
|
final repository = ref.read(assetRepositoryProvider);
|
||||||
|
final result = await repository.createAsset(data);
|
||||||
|
if (result.failure != null) throw result.failure!;
|
||||||
|
ref.invalidate(assetsListProvider);
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AssetModel?> submitUpdate(String id, Map<String, dynamic> data) async {
|
||||||
|
final repository = ref.read(assetRepositoryProvider);
|
||||||
|
final result = await repository.updateAsset(id, data);
|
||||||
|
if (result.failure != null) throw result.failure!;
|
||||||
|
ref.invalidate(assetsListProvider);
|
||||||
|
ref.invalidate(assetDetailProvider(id));
|
||||||
|
|
||||||
|
final fresh = await repository.getAssetById(id);
|
||||||
|
final asset = fresh.data ?? result.data;
|
||||||
|
if (asset != null) {
|
||||||
|
state = AsyncData(asset);
|
||||||
|
}
|
||||||
|
return asset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AssetAlertsState {
|
||||||
|
const AssetAlertsState({
|
||||||
|
this.expiryAlerts = const [],
|
||||||
|
this.serviceAlerts = const [],
|
||||||
|
this.expiryDays = 30,
|
||||||
|
this.expiryType,
|
||||||
|
this.serviceStatus,
|
||||||
|
this.expiryPage = 1,
|
||||||
|
this.expiryTotalPages = 1,
|
||||||
|
this.isRefreshing = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<AssetAlertModel> expiryAlerts;
|
||||||
|
final List<AssetAlertModel> serviceAlerts;
|
||||||
|
final int expiryDays;
|
||||||
|
final String? expiryType;
|
||||||
|
final String? serviceStatus;
|
||||||
|
final int expiryPage;
|
||||||
|
final int expiryTotalPages;
|
||||||
|
final bool isRefreshing;
|
||||||
|
|
||||||
|
AssetAlertsState copyWith({
|
||||||
|
List<AssetAlertModel>? expiryAlerts,
|
||||||
|
List<AssetAlertModel>? serviceAlerts,
|
||||||
|
int? expiryDays,
|
||||||
|
String? expiryType,
|
||||||
|
String? serviceStatus,
|
||||||
|
int? expiryPage,
|
||||||
|
int? expiryTotalPages,
|
||||||
|
bool? isRefreshing,
|
||||||
|
bool clearExpiryType = false,
|
||||||
|
bool clearServiceStatus = false,
|
||||||
|
}) {
|
||||||
|
return AssetAlertsState(
|
||||||
|
expiryAlerts: expiryAlerts ?? this.expiryAlerts,
|
||||||
|
serviceAlerts: serviceAlerts ?? this.serviceAlerts,
|
||||||
|
expiryDays: expiryDays ?? this.expiryDays,
|
||||||
|
expiryType: clearExpiryType ? null : expiryType ?? this.expiryType,
|
||||||
|
serviceStatus: clearServiceStatus ? null : serviceStatus ?? this.serviceStatus,
|
||||||
|
expiryPage: expiryPage ?? this.expiryPage,
|
||||||
|
expiryTotalPages: expiryTotalPages ?? this.expiryTotalPages,
|
||||||
|
isRefreshing: isRefreshing ?? this.isRefreshing,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final assetAlertsProvider =
|
||||||
|
AsyncNotifierProvider<AssetAlertsNotifier, AssetAlertsState>(AssetAlertsNotifier.new);
|
||||||
|
|
||||||
|
class AssetAlertsNotifier extends AsyncNotifier<AssetAlertsState> {
|
||||||
|
@override
|
||||||
|
Future<AssetAlertsState> build() async {
|
||||||
|
return _load(const AssetAlertsState());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AssetAlertsState> _load(AssetAlertsState filters) async {
|
||||||
|
final repository = ref.read(assetRepositoryProvider);
|
||||||
|
final expiryResult = await repository.getExpiryAlerts(
|
||||||
|
days: filters.expiryDays,
|
||||||
|
type: filters.expiryType,
|
||||||
|
page: filters.expiryPage,
|
||||||
|
);
|
||||||
|
final serviceResult = await repository.getServiceAlerts(status: filters.serviceStatus);
|
||||||
|
|
||||||
|
if (expiryResult.failure != null) throw expiryResult.failure!;
|
||||||
|
|
||||||
|
final expiryPage = expiryResult.data!;
|
||||||
|
return AssetAlertsState(
|
||||||
|
expiryAlerts: expiryPage.items,
|
||||||
|
serviceAlerts: serviceResult.data ?? [],
|
||||||
|
expiryDays: filters.expiryDays,
|
||||||
|
expiryType: filters.expiryType,
|
||||||
|
serviceStatus: filters.serviceStatus,
|
||||||
|
expiryPage: expiryPage.page,
|
||||||
|
expiryTotalPages: expiryPage.totalPages,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refresh() async {
|
||||||
|
final current = state.valueOrNull ?? const AssetAlertsState();
|
||||||
|
state = AsyncData(current.copyWith(isRefreshing: true));
|
||||||
|
try {
|
||||||
|
state = AsyncData(await _load(current));
|
||||||
|
} catch (e, st) {
|
||||||
|
state = AsyncError(e, st);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setExpiryDays(int days) async {
|
||||||
|
final current = state.valueOrNull ?? const AssetAlertsState();
|
||||||
|
state = const AsyncLoading();
|
||||||
|
state = AsyncData(await _load(current.copyWith(expiryDays: days, expiryPage: 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setExpiryType(String? type) async {
|
||||||
|
final current = state.valueOrNull ?? const AssetAlertsState();
|
||||||
|
state = const AsyncLoading();
|
||||||
|
state = AsyncData(
|
||||||
|
await _load(
|
||||||
|
current.copyWith(
|
||||||
|
expiryType: type,
|
||||||
|
expiryPage: 1,
|
||||||
|
clearExpiryType: type == null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setServiceStatus(String? status) async {
|
||||||
|
final current = state.valueOrNull ?? const AssetAlertsState();
|
||||||
|
state = const AsyncLoading();
|
||||||
|
state = AsyncData(
|
||||||
|
await _load(
|
||||||
|
current.copyWith(
|
||||||
|
serviceStatus: status,
|
||||||
|
clearServiceStatus: status == null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setExpiryPage(int page) async {
|
||||||
|
final current = state.valueOrNull ?? const AssetAlertsState();
|
||||||
|
state = const AsyncLoading();
|
||||||
|
state = AsyncData(await _load(current.copyWith(expiryPage: page)));
|
||||||
|
}
|
||||||
|
}
|
||||||
271
lib/modules/assets/presentation/screens/asset_alerts_screen.dart
Normal file
271
lib/modules/assets/presentation/screens/asset_alerts_screen.dart
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
import '../../../../core/constants/route_constants.dart';
|
||||||
|
import '../../../../core/errors/failure.dart';
|
||||||
|
import '../../../../shared/models/asset_model.dart';
|
||||||
|
import '../../../../shared/widgets/app_card.dart';
|
||||||
|
import '../../../../shared/widgets/app_dropdown.dart';
|
||||||
|
import '../../../../shared/widgets/app_empty_state.dart';
|
||||||
|
import '../../../../shared/widgets/app_loading_view.dart';
|
||||||
|
import '../../../../shared/widgets/app_pagination.dart';
|
||||||
|
import '../../../../shared/widgets/error_view.dart';
|
||||||
|
import '../../../../shared/widgets/page_header.dart';
|
||||||
|
import '../providers/assets_provider.dart';
|
||||||
|
|
||||||
|
class AssetAlertsScreen extends ConsumerWidget {
|
||||||
|
const AssetAlertsScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final alertsAsync = ref.watch(assetAlertsProvider);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: alertsAsync.when(
|
||||||
|
loading: () => const AppLoadingView(message: 'Loading alerts...'),
|
||||||
|
error: (e, _) => ErrorView.fromFailure(
|
||||||
|
e is Failure ? e : Failure.unknown(message: e.toString()),
|
||||||
|
onRetry: () => ref.invalidate(assetAlertsProvider),
|
||||||
|
),
|
||||||
|
data: (state) => DefaultTabController(
|
||||||
|
length: 2,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
PageHeader(
|
||||||
|
title: 'Asset Alerts',
|
||||||
|
subtitle: 'Expiry and service due notifications',
|
||||||
|
actions: [
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: () => context.go(RouteConstants.assets),
|
||||||
|
icon: const Icon(Icons.inventory_2_outlined),
|
||||||
|
label: const Text('Asset Master'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const TabBar(
|
||||||
|
tabs: [
|
||||||
|
Tab(text: 'Expiry Alerts'),
|
||||||
|
Tab(text: 'Service Alerts'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Expanded(
|
||||||
|
child: TabBarView(
|
||||||
|
children: [
|
||||||
|
_ExpiryAlertsTab(state: state),
|
||||||
|
_ServiceAlertsTab(state: state),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ExpiryAlertsTab extends ConsumerWidget {
|
||||||
|
const _ExpiryAlertsTab({required this.state});
|
||||||
|
|
||||||
|
final AssetAlertsState state;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final notifier = ref.read(assetAlertsProvider.notifier);
|
||||||
|
final dateFormat = DateFormat('dd MMM yyyy');
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Wrap(
|
||||||
|
spacing: 12,
|
||||||
|
runSpacing: 8,
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 160,
|
||||||
|
child: AppDropdown<int>(
|
||||||
|
label: 'Days ahead',
|
||||||
|
value: state.expiryDays,
|
||||||
|
options: const [7, 15, 30, 60, 90]
|
||||||
|
.map((d) => AppDropdownOption(value: d, label: '$d days'))
|
||||||
|
.toList(),
|
||||||
|
onChanged: (v) {
|
||||||
|
if (v != null) notifier.setExpiryDays(v);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 180,
|
||||||
|
child: AppDropdown<String?>(
|
||||||
|
label: 'Type',
|
||||||
|
value: state.expiryType,
|
||||||
|
options: const [
|
||||||
|
AppDropdownOption(value: null, label: 'All'),
|
||||||
|
AppDropdownOption(value: 'AMC', label: 'AMC'),
|
||||||
|
AppDropdownOption(value: 'INSURANCE', label: 'Insurance'),
|
||||||
|
AppDropdownOption(value: 'WARRANTY', label: 'Warranty'),
|
||||||
|
],
|
||||||
|
onChanged: notifier.setExpiryType,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => notifier.refresh(),
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
tooltip: 'Refresh',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Expanded(
|
||||||
|
child: state.expiryAlerts.isEmpty
|
||||||
|
? const AppEmptyState(
|
||||||
|
title: 'No expiry alerts',
|
||||||
|
description: 'No AMC, insurance or warranty expiries in this window.',
|
||||||
|
icon: Icons.event_available_outlined,
|
||||||
|
)
|
||||||
|
: ListView.separated(
|
||||||
|
itemCount: state.expiryAlerts.length,
|
||||||
|
separatorBuilder: (_, __) => const SizedBox(height: 8),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final alert = state.expiryAlerts[index];
|
||||||
|
return _AlertCard(
|
||||||
|
alert: alert,
|
||||||
|
dateLabel: alert.expiryDate != null
|
||||||
|
? dateFormat.format(alert.expiryDate!)
|
||||||
|
: null,
|
||||||
|
onTap: alert.assetId != null
|
||||||
|
? () => context.push('${RouteConstants.assets}/${alert.assetId}')
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AppPagination(
|
||||||
|
currentPage: state.expiryPage,
|
||||||
|
totalPages: state.expiryTotalPages,
|
||||||
|
totalItems: state.expiryAlerts.length,
|
||||||
|
pageSize: 20,
|
||||||
|
onPageChanged: notifier.setExpiryPage,
|
||||||
|
onPageSizeChanged: (_) {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ServiceAlertsTab extends ConsumerWidget {
|
||||||
|
const _ServiceAlertsTab({required this.state});
|
||||||
|
|
||||||
|
final AssetAlertsState state;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final notifier = ref.read(assetAlertsProvider.notifier);
|
||||||
|
final dateFormat = DateFormat('dd MMM yyyy');
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Wrap(
|
||||||
|
spacing: 12,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 200,
|
||||||
|
child: AppDropdown<String?>(
|
||||||
|
label: 'Status',
|
||||||
|
value: state.serviceStatus,
|
||||||
|
options: const [
|
||||||
|
AppDropdownOption(value: null, label: 'All'),
|
||||||
|
AppDropdownOption(value: 'OVERDUE', label: 'Overdue'),
|
||||||
|
AppDropdownOption(value: 'DUE_THIS_WEEK', label: 'Due This Week'),
|
||||||
|
AppDropdownOption(value: 'DUE_THIS_MONTH', label: 'Due This Month'),
|
||||||
|
AppDropdownOption(value: 'UPCOMING', label: 'Upcoming'),
|
||||||
|
],
|
||||||
|
onChanged: notifier.setServiceStatus,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => notifier.refresh(),
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
tooltip: 'Refresh',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Expanded(
|
||||||
|
child: state.serviceAlerts.isEmpty
|
||||||
|
? const AppEmptyState(
|
||||||
|
title: 'No service alerts',
|
||||||
|
description: 'All services are up to date.',
|
||||||
|
icon: Icons.build_circle_outlined,
|
||||||
|
)
|
||||||
|
: ListView.separated(
|
||||||
|
itemCount: state.serviceAlerts.length,
|
||||||
|
separatorBuilder: (_, __) => const SizedBox(height: 8),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final alert = state.serviceAlerts[index];
|
||||||
|
return _AlertCard(
|
||||||
|
alert: alert,
|
||||||
|
dateLabel: alert.dueDate != null
|
||||||
|
? dateFormat.format(alert.dueDate!)
|
||||||
|
: null,
|
||||||
|
onTap: alert.assetId != null
|
||||||
|
? () => context.push('${RouteConstants.assets}/${alert.assetId}')
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AlertCard extends StatelessWidget {
|
||||||
|
const _AlertCard({
|
||||||
|
required this.alert,
|
||||||
|
this.dateLabel,
|
||||||
|
this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final AssetAlertModel alert;
|
||||||
|
final String? dateLabel;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AppCard(
|
||||||
|
child: ListTile(
|
||||||
|
onTap: onTap,
|
||||||
|
leading: Icon(
|
||||||
|
alert.type?.toUpperCase() == 'AMC'
|
||||||
|
? Icons.handshake_outlined
|
||||||
|
: alert.type?.toUpperCase() == 'INSURANCE'
|
||||||
|
? Icons.shield_outlined
|
||||||
|
: Icons.notifications_outlined,
|
||||||
|
),
|
||||||
|
title: Text(alert.assetName ?? alert.title ?? 'Asset ${alert.assetId ?? ''}'),
|
||||||
|
subtitle: Text(
|
||||||
|
[
|
||||||
|
if (alert.assetCode != null) alert.assetCode,
|
||||||
|
if (alert.type != null) alert.type,
|
||||||
|
if (alert.plantName != null) alert.plantName,
|
||||||
|
if (dateLabel != null) 'Due: $dateLabel',
|
||||||
|
if (alert.daysRemaining != null) '${alert.daysRemaining} days',
|
||||||
|
].whereType<String>().join(' · '),
|
||||||
|
),
|
||||||
|
trailing: alert.status != null
|
||||||
|
? Chip(label: Text(alert.status!, style: const TextStyle(fontSize: 11)))
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -36,7 +36,10 @@ class AssetCategoriesScreen extends ConsumerWidget {
|
|||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: const Icon(Icons.category_outlined),
|
leading: const Icon(Icons.category_outlined),
|
||||||
title: Text(category.name),
|
title: Text(category.name),
|
||||||
subtitle: Text(category.slug),
|
subtitle: Text(
|
||||||
|
'${category.code}'
|
||||||
|
'${category.defaultDepreciationMethod != null ? ' · ${category.defaultDepreciationMethod}' : ''}',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,77 +1,422 @@
|
|||||||
import '../../../../shared/widgets/app_card.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:qr_flutter/qr_flutter.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
import '../../../../core/constants/enums.dart';
|
||||||
|
import '../../../../core/constants/route_constants.dart';
|
||||||
|
import '../../../../core/errors/failure.dart';
|
||||||
|
import '../../../../shared/models/asset_model.dart';
|
||||||
|
import '../../../../shared/providers/permissions_provider.dart';
|
||||||
|
import '../../../../shared/widgets/app_card.dart';
|
||||||
|
import '../../../../shared/widgets/app_confirmation_dialog.dart';
|
||||||
|
import '../../../../shared/widgets/app_loading_view.dart';
|
||||||
|
import '../../../../shared/widgets/app_status_chip.dart';
|
||||||
|
import '../../../../shared/widgets/can_permission.dart';
|
||||||
|
import '../../../../shared/widgets/error_view.dart';
|
||||||
import '../../../../shared/widgets/page_header.dart';
|
import '../../../../shared/widgets/page_header.dart';
|
||||||
|
import '../../../../shared/widgets/app_side_panel.dart';
|
||||||
|
import '../providers/assets_provider.dart';
|
||||||
|
import '../widgets/asset_form_panel.dart';
|
||||||
|
import '../widgets/asset_side_panels.dart';
|
||||||
|
|
||||||
class AssetDetailScreen extends StatelessWidget {
|
class AssetDetailScreen extends ConsumerStatefulWidget {
|
||||||
const AssetDetailScreen({super.key, required this.assetId});
|
const AssetDetailScreen({super.key, required this.assetId});
|
||||||
|
|
||||||
final String assetId;
|
final String assetId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<AssetDetailScreen> createState() => _AssetDetailScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AssetDetailScreenState extends ConsumerState<AssetDetailScreen>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final TabController _tabController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_tabController = TabController(length: 4, vsync: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_tabController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SingleChildScrollView(
|
final detailAsync = ref.watch(assetDetailProvider(widget.assetId));
|
||||||
padding: const EdgeInsets.all(24),
|
final canEdit = ref.can('assets', PermissionAction.update);
|
||||||
child: Column(
|
final canDelete = ref.can('assets', PermissionAction.delete);
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
return detailAsync.when(
|
||||||
PageHeader(
|
loading: () => const AppLoadingView(message: 'Loading asset details...'),
|
||||||
title: 'Asset Details',
|
error: (e, _) => ErrorView.fromFailure(
|
||||||
subtitle: 'ID: $assetId',
|
e is Failure ? e : Failure.unknown(message: e.toString()),
|
||||||
),
|
onRetry: () => ref.invalidate(assetDetailProvider(widget.assetId)),
|
||||||
Row(
|
),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
data: (state) => Padding(
|
||||||
children: [
|
padding: const EdgeInsets.all(24),
|
||||||
Expanded(
|
child: Column(
|
||||||
flex: 2,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: AppCard(
|
children: [
|
||||||
child: Padding(
|
PageHeader(
|
||||||
padding: const EdgeInsets.all(24),
|
title: state.asset.assetName,
|
||||||
child: Column(
|
subtitle: state.asset.assetCode ?? 'Asset ID: ${state.asset.id}',
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
actions: [
|
||||||
children: [
|
if (canEdit)
|
||||||
_DetailRow(label: 'Asset Name', value: '—'),
|
OutlinedButton.icon(
|
||||||
_DetailRow(label: 'Asset Code', value: '—'),
|
onPressed: () =>
|
||||||
_DetailRow(label: 'Category', value: '—'),
|
openAssetFormPanel(context, ref, assetId: widget.assetId),
|
||||||
_DetailRow(label: 'Serial Number', value: '—'),
|
icon: const Icon(Icons.edit_outlined),
|
||||||
_DetailRow(label: 'Status', value: '—'),
|
label: const Text('Edit'),
|
||||||
_DetailRow(label: 'Branch', value: '—'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
if (canEdit) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: () => _showTransferDialog(state.asset),
|
||||||
|
icon: const Icon(Icons.swap_horiz),
|
||||||
|
label: const Text('Transfer'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (canDelete) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: _deleteAsset,
|
||||||
|
icon: const Icon(Icons.delete_outline),
|
||||||
|
label: const Text('Delete'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TabBar(
|
||||||
|
controller: _tabController,
|
||||||
|
tabs: const [
|
||||||
|
Tab(text: 'Overview'),
|
||||||
|
Tab(text: 'AMC'),
|
||||||
|
Tab(text: 'Service Visits'),
|
||||||
|
Tab(text: 'Insurance'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Expanded(
|
||||||
|
child: TabBarView(
|
||||||
|
controller: _tabController,
|
||||||
|
children: [
|
||||||
|
_OverviewTab(asset: state.asset),
|
||||||
|
_AmcTab(assetId: widget.assetId, contracts: state.amcContracts),
|
||||||
|
_ServiceVisitsTab(assetId: widget.assetId, visits: state.serviceVisits),
|
||||||
|
_InsuranceTab(assetId: widget.assetId, policies: state.insurancePolicies),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(width: 24),
|
),
|
||||||
AppCard(
|
],
|
||||||
child: Padding(
|
),
|
||||||
padding: const EdgeInsets.all(24),
|
),
|
||||||
child: Column(
|
);
|
||||||
children: [
|
}
|
||||||
const Text('QR Code'),
|
|
||||||
const SizedBox(height: 16),
|
Future<void> _deleteAsset() async {
|
||||||
QrImageView(
|
final confirmed = await showAppConfirmationDialog(
|
||||||
data: assetId,
|
context: context,
|
||||||
version: QrVersions.auto,
|
title: 'Delete Asset',
|
||||||
size: 160,
|
message: 'Soft delete this asset?',
|
||||||
),
|
confirmLabel: 'Delete',
|
||||||
],
|
isDestructive: true,
|
||||||
),
|
);
|
||||||
),
|
if (confirmed != true || !mounted) return;
|
||||||
|
|
||||||
|
final deleted = await ref.read(assetDetailProvider(widget.assetId).notifier).deleteAsset();
|
||||||
|
if (deleted && mounted) context.go(RouteConstants.assets);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showTransferDialog(AssetModel asset) async {
|
||||||
|
final plantIdController = TextEditingController(text: asset.plantId?.toString() ?? '');
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Transfer Asset'),
|
||||||
|
content: TextField(
|
||||||
|
controller: plantIdController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Target Plant ID',
|
||||||
|
helperText: 'Enter the plant ID to transfer this asset to',
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')),
|
||||||
|
ElevatedButton(onPressed: () => Navigator.pop(context, true), child: const Text('Transfer')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (confirmed != true || !mounted) return;
|
||||||
|
|
||||||
|
final plantId = int.tryParse(plantIdController.text.trim());
|
||||||
|
if (plantId == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ref.read(assetDetailProvider(widget.assetId).notifier).transferAsset({
|
||||||
|
'plant_id': plantId,
|
||||||
|
});
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Asset transferred')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OverviewTab extends StatelessWidget {
|
||||||
|
const _OverviewTab({required this.asset});
|
||||||
|
|
||||||
|
final AssetModel asset;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final dateFormat = DateFormat('dd MMM yyyy');
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: AppCard(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_DetailRow(label: 'Asset Name', value: asset.assetName),
|
||||||
|
_DetailRow(label: 'Asset Code', value: asset.assetCode ?? '—'),
|
||||||
|
_DetailRow(label: 'Category', value: asset.assetCategoryName ?? '—'),
|
||||||
|
_DetailRow(label: 'Plant', value: asset.plantName ?? '—'),
|
||||||
|
_DetailRow(
|
||||||
|
label: 'Warranty Expiry',
|
||||||
|
value: asset.warrantyExpiryDate != null
|
||||||
|
? dateFormat.format(asset.warrantyExpiryDate!)
|
||||||
|
: '—',
|
||||||
|
),
|
||||||
|
_DetailRow(
|
||||||
|
label: 'Purchase Cost',
|
||||||
|
value: asset.purchaseCost != null ? '₹${asset.purchaseCost}' : '—',
|
||||||
|
),
|
||||||
|
_DetailRow(
|
||||||
|
label: 'Status',
|
||||||
|
valueWidget: AppStatusChip(status: asset.status ?? 'active'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _AmcTab extends ConsumerWidget {
|
||||||
|
const _AmcTab({required this.assetId, required this.contracts});
|
||||||
|
|
||||||
|
final String assetId;
|
||||||
|
final List<AmcContractModel> contracts;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
CanPermission(
|
||||||
|
module: 'assets',
|
||||||
|
action: PermissionAction.create,
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () => _openAddAmcPanel(context, ref),
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: const Text('Add AMC'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Expanded(
|
||||||
|
child: contracts.isEmpty
|
||||||
|
? const Center(child: Text('No AMC contracts'))
|
||||||
|
: ListView.separated(
|
||||||
|
itemCount: contracts.length,
|
||||||
|
separatorBuilder: (_, __) => const SizedBox(height: 8),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final c = contracts[index];
|
||||||
|
return AppCard(
|
||||||
|
child: ListTile(
|
||||||
|
title: Text(c.contractNo ?? 'AMC #${c.id}'),
|
||||||
|
subtitle: Text(
|
||||||
|
'${c.vendorName ?? 'Vendor ${c.vendorId ?? '—'}'} · '
|
||||||
|
'${DateFormat('dd MMM yyyy').format(c.startDate)} – '
|
||||||
|
'${DateFormat('dd MMM yyyy').format(c.endDate)}',
|
||||||
|
),
|
||||||
|
trailing: c.annualCost != null ? Text('₹${c.annualCost}') : null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _openAddAmcPanel(BuildContext context, WidgetRef ref) async {
|
||||||
|
final saved = await showSidePanel<bool>(
|
||||||
|
context,
|
||||||
|
AddAmcPanel(assetId: assetId),
|
||||||
|
width: 520,
|
||||||
|
);
|
||||||
|
if (saved == true && context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('AMC contract created')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ServiceVisitsTab extends ConsumerWidget {
|
||||||
|
const _ServiceVisitsTab({required this.assetId, required this.visits});
|
||||||
|
|
||||||
|
final String assetId;
|
||||||
|
final List<ServiceVisitModel> visits;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
CanPermission(
|
||||||
|
module: 'assets',
|
||||||
|
action: PermissionAction.create,
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () => _openLogVisitPanel(context, ref),
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: const Text('Log Visit'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Expanded(
|
||||||
|
child: visits.isEmpty
|
||||||
|
? const Center(child: Text('No service visits'))
|
||||||
|
: ListView.separated(
|
||||||
|
itemCount: visits.length,
|
||||||
|
separatorBuilder: (_, __) => const SizedBox(height: 8),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final v = visits[index];
|
||||||
|
return AppCard(
|
||||||
|
child: ListTile(
|
||||||
|
title: Text(v.visitType),
|
||||||
|
subtitle: Text(
|
||||||
|
'${DateFormat('dd MMM yyyy').format(v.visitDate)}'
|
||||||
|
'${v.workDone != null ? ' · ${v.workDone}' : ''}',
|
||||||
|
),
|
||||||
|
trailing: AppStatusChip(status: v.status, compact: true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _openLogVisitPanel(BuildContext context, WidgetRef ref) async {
|
||||||
|
final saved = await showSidePanel<bool>(
|
||||||
|
context,
|
||||||
|
LogServiceVisitPanel(assetId: assetId),
|
||||||
|
width: 520,
|
||||||
|
);
|
||||||
|
if (saved == true && context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Service visit logged')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _InsuranceTab extends ConsumerWidget {
|
||||||
|
const _InsuranceTab({required this.assetId, required this.policies});
|
||||||
|
|
||||||
|
final String assetId;
|
||||||
|
final List<InsurancePolicyModel> policies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
CanPermission(
|
||||||
|
module: 'assets',
|
||||||
|
action: PermissionAction.create,
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () => _openAddInsurancePanel(context, ref),
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: const Text('Add Policy'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Expanded(
|
||||||
|
child: policies.isEmpty
|
||||||
|
? const Center(child: Text('No insurance policies'))
|
||||||
|
: ListView.separated(
|
||||||
|
itemCount: policies.length,
|
||||||
|
separatorBuilder: (_, __) => const SizedBox(height: 8),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final p = policies[index];
|
||||||
|
return AppCard(
|
||||||
|
child: ListTile(
|
||||||
|
title: Text(p.policyNo),
|
||||||
|
subtitle: Text(
|
||||||
|
'${p.insurerName} · '
|
||||||
|
'${DateFormat('dd MMM yyyy').format(p.policyStartDate)} – '
|
||||||
|
'${DateFormat('dd MMM yyyy').format(p.policyEndDate)}',
|
||||||
|
),
|
||||||
|
trailing: p.sumInsured != null ? Text('₹${p.sumInsured}') : null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _openAddInsurancePanel(BuildContext context, WidgetRef ref) async {
|
||||||
|
final saved = await showSidePanel<bool>(
|
||||||
|
context,
|
||||||
|
AddInsurancePanel(assetId: assetId),
|
||||||
|
width: 520,
|
||||||
|
);
|
||||||
|
if (saved == true && context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Insurance policy created')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _DetailRow extends StatelessWidget {
|
class _DetailRow extends StatelessWidget {
|
||||||
const _DetailRow({required this.label, required this.value});
|
const _DetailRow({
|
||||||
|
required this.label,
|
||||||
|
this.value,
|
||||||
|
this.valueWidget,
|
||||||
|
});
|
||||||
|
|
||||||
final String label;
|
final String label;
|
||||||
final String value;
|
final String? value;
|
||||||
|
final Widget? valueWidget;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -80,7 +425,7 @@ class _DetailRow extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 140,
|
width: 160,
|
||||||
child: Text(
|
child: Text(
|
||||||
label,
|
label,
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
@ -88,7 +433,9 @@ class _DetailRow extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(child: Text(value, style: Theme.of(context).textTheme.bodyLarge)),
|
Expanded(
|
||||||
|
child: valueWidget ?? Text(value ?? '—', style: Theme.of(context).textTheme.bodyLarge),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,114 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import '../../../../core/constants/enums.dart';
|
|
||||||
import '../../../../core/utils/validators.dart';
|
|
||||||
import '../../../../shared/widgets/app_button.dart';
|
|
||||||
import '../../../../shared/widgets/app_text_field.dart';
|
|
||||||
|
|
||||||
class AssetFormScreen extends StatefulWidget {
|
|
||||||
const AssetFormScreen({super.key, this.assetId});
|
|
||||||
|
|
||||||
final String? assetId;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<AssetFormScreen> createState() => _AssetFormScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AssetFormScreenState extends State<AssetFormScreen> {
|
|
||||||
final _formKey = GlobalKey<FormState>();
|
|
||||||
final _nameController = TextEditingController();
|
|
||||||
final _codeController = TextEditingController();
|
|
||||||
final _brandController = TextEditingController();
|
|
||||||
final _modelController = TextEditingController();
|
|
||||||
final _serialController = TextEditingController();
|
|
||||||
final _vendorController = TextEditingController();
|
|
||||||
final _costController = TextEditingController();
|
|
||||||
AssetCategoryType _category = AssetCategoryType.laptop;
|
|
||||||
AssetStatus _status = AssetStatus.available;
|
|
||||||
|
|
||||||
bool get isEditing => widget.assetId != null;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_nameController.dispose();
|
|
||||||
_codeController.dispose();
|
|
||||||
_brandController.dispose();
|
|
||||||
_modelController.dispose();
|
|
||||||
_serialController.dispose();
|
|
||||||
_vendorController.dispose();
|
|
||||||
_costController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(title: Text(isEditing ? 'Edit Asset' : 'Add Asset')),
|
|
||||||
body: SingleChildScrollView(
|
|
||||||
padding: const EdgeInsets.all(24),
|
|
||||||
child: Center(
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(maxWidth: 600),
|
|
||||||
child: Form(
|
|
||||||
key: _formKey,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
AppTextField(
|
|
||||||
controller: _nameController,
|
|
||||||
label: 'Asset Name',
|
|
||||||
validator: (v) => Validators.required(v, fieldName: 'Asset name'),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
AppTextField(
|
|
||||||
controller: _codeController,
|
|
||||||
label: 'Asset Code',
|
|
||||||
validator: (v) => Validators.required(v, fieldName: 'Asset code'),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
DropdownButtonFormField<AssetCategoryType>(
|
|
||||||
value: _category,
|
|
||||||
decoration: const InputDecoration(labelText: 'Category'),
|
|
||||||
items: AssetCategoryType.values
|
|
||||||
.map((c) => DropdownMenuItem(value: c, child: Text(c.label)))
|
|
||||||
.toList(),
|
|
||||||
onChanged: (v) => setState(() => _category = v!),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
AppTextField(controller: _brandController, label: 'Brand'),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
AppTextField(controller: _modelController, label: 'Model'),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
AppTextField(controller: _serialController, label: 'Serial Number'),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
AppTextField(
|
|
||||||
controller: _costController,
|
|
||||||
label: 'Purchase Cost',
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
AppTextField(controller: _vendorController, label: 'Vendor'),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
DropdownButtonFormField<AssetStatus>(
|
|
||||||
value: _status,
|
|
||||||
decoration: const InputDecoration(labelText: 'Status'),
|
|
||||||
items: AssetStatus.values
|
|
||||||
.map((s) => DropdownMenuItem(value: s, child: Text(s.label)))
|
|
||||||
.toList(),
|
|
||||||
onChanged: (v) => setState(() => _status = v!),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
AppButton(
|
|
||||||
label: isEditing ? 'Update Asset' : 'Create Asset',
|
|
||||||
onPressed: () {
|
|
||||||
if (_formKey.currentState!.validate()) Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,46 +1,802 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
import '../../../../core/constants/enums.dart';
|
||||||
import '../../../../core/constants/route_constants.dart';
|
import '../../../../core/constants/route_constants.dart';
|
||||||
|
import '../../../../core/errors/failure.dart';
|
||||||
|
import '../../../../core/utils/formatters.dart';
|
||||||
|
import '../../../../core/utils/responsive_utils.dart';
|
||||||
|
import '../../../../shared/models/asset_model.dart';
|
||||||
|
import '../../../../shared/providers/permissions_provider.dart';
|
||||||
|
import '../../../../shared/widgets/app_card.dart';
|
||||||
|
import '../../../../shared/widgets/app_confirmation_dialog.dart';
|
||||||
|
import '../../../../shared/widgets/app_dropdown.dart';
|
||||||
|
import '../../../../shared/widgets/app_empty_state.dart';
|
||||||
|
import '../../../../shared/widgets/app_loading_view.dart';
|
||||||
|
import '../../../../shared/widgets/app_searchable_dropdown.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 '../../../rbac/presentation/widgets/rbac_widgets.dart';
|
||||||
|
import '../providers/asset_categories_provider.dart';
|
||||||
|
import '../providers/assets_provider.dart';
|
||||||
|
import '../widgets/asset_form_panel.dart';
|
||||||
|
|
||||||
class AssetListScreen extends StatelessWidget {
|
class AssetListScreen extends ConsumerStatefulWidget {
|
||||||
const AssetListScreen({super.key});
|
const AssetListScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<AssetListScreen> createState() => _AssetListScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AssetListScreenState extends ConsumerState<AssetListScreen> {
|
||||||
|
static const _tableMinWidth = 1040.0;
|
||||||
|
|
||||||
|
String? _selectedCategory;
|
||||||
|
String? _selectedPlant;
|
||||||
|
String? _selectedStatus;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final assetsAsync = ref.watch(assetsListProvider);
|
||||||
|
final canEdit = ref.can('assets', PermissionAction.update);
|
||||||
|
final canDelete = ref.can('assets', PermissionAction.delete);
|
||||||
|
|
||||||
|
ref.listen(assetsListProvider, (prev, next) {
|
||||||
|
final error = next.valueOrNull?.actionError;
|
||||||
|
final success = next.valueOrNull?.actionSuccess;
|
||||||
|
if (error != null && error != prev?.valueOrNull?.actionError) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(error)));
|
||||||
|
}
|
||||||
|
if (success != null && success != prev?.valueOrNull?.actionSuccess) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(success)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
child: Column(
|
child: assetsAsync.when(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
loading: () => const AppLoadingView(message: 'Loading assets...'),
|
||||||
children: [
|
error: (error, _) => ErrorView.fromFailure(
|
||||||
PageHeader(
|
error is Failure ? error : Failure.unknown(message: error.toString()),
|
||||||
title: 'Asset Master',
|
onRetry: () => ref.invalidate(assetsListProvider),
|
||||||
subtitle: 'Manage all company assets',
|
),
|
||||||
actions: [
|
data: (state) {
|
||||||
OutlinedButton.icon(
|
final allCategories = ref.watch(assetCategoriesProvider).valueOrNull ?? [];
|
||||||
onPressed: () => context.go(RouteConstants.assetQrScan),
|
final filteredAssets = _filterAssets(state.assets);
|
||||||
icon: const Icon(Icons.qr_code_scanner),
|
final categories = _categoryOptions(state.assets, allCategories);
|
||||||
label: const Text('QR Scan'),
|
final plants = _plantOptions(state.assets);
|
||||||
|
final statuses = _statusOptions();
|
||||||
|
final categoryFilter = _selectedCategory ?? 'All categories';
|
||||||
|
final plantFilter = _selectedPlant ?? 'All plants';
|
||||||
|
final statusFilter = _selectedStatus ?? 'All statuses';
|
||||||
|
final page = state.query.page;
|
||||||
|
final pageSize = state.query.limit;
|
||||||
|
final total = state.total;
|
||||||
|
final start = total == 0 ? 0 : ((page - 1) * pageSize) + 1;
|
||||||
|
final end = (page * pageSize).clamp(0, total);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
PageHeader(
|
||||||
|
title: 'Asset Master',
|
||||||
|
subtitle: 'Manage plant assets, AMC, service & insurance',
|
||||||
|
actions: [
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: () => context.go(RouteConstants.assetAlerts),
|
||||||
|
icon: const Icon(Icons.notifications_outlined),
|
||||||
|
label: const Text('Alerts'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
CanPermission(
|
||||||
|
module: 'assets',
|
||||||
|
action: PermissionAction.create,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () => openAssetFormPanel(context, ref),
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: const Text('Add Asset'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(height: 16),
|
||||||
ElevatedButton.icon(
|
Expanded(
|
||||||
onPressed: () => context.push('${RouteConstants.assets}/add'),
|
child: RefreshIndicator(
|
||||||
icon: const Icon(Icons.add),
|
onRefresh: () => ref.read(assetsListProvider.notifier).refresh(),
|
||||||
label: const Text('Add Asset'),
|
child: context.isMobile
|
||||||
|
? _AssetMobileList(
|
||||||
|
assets: filteredAssets,
|
||||||
|
onView: _viewAsset,
|
||||||
|
onEdit: canEdit ? _editAsset : null,
|
||||||
|
onDelete: canDelete ? _deleteAsset : null,
|
||||||
|
)
|
||||||
|
: AppCard(
|
||||||
|
enableHover: false,
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
side: BorderSide(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.outline
|
||||||
|
.withValues(alpha: 0.12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return _AssetsFilterBar(
|
||||||
|
wrapped: constraints.maxWidth < 1000,
|
||||||
|
categoryFilter: categoryFilter,
|
||||||
|
plantFilter: plantFilter,
|
||||||
|
statusFilter: statusFilter,
|
||||||
|
categories: categories,
|
||||||
|
plants: plants,
|
||||||
|
statuses: statuses,
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
if (filteredAssets.isEmpty)
|
||||||
|
Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'No assets found',
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyLarge
|
||||||
|
?.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Expanded(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final tableWidth =
|
||||||
|
constraints.maxWidth < _tableMinWidth
|
||||||
|
? _tableMinWidth
|
||||||
|
: constraints.maxWidth;
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 4),
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints:
|
||||||
|
BoxConstraints(minWidth: tableWidth),
|
||||||
|
child: DataTable(
|
||||||
|
horizontalMargin: 20,
|
||||||
|
columnSpacing: 16,
|
||||||
|
headingRowColor: WidgetStateProperty.all(
|
||||||
|
Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainerHighest
|
||||||
|
.withValues(alpha: 0.4),
|
||||||
|
),
|
||||||
|
columns: const [
|
||||||
|
DataColumn(label: Text('ASSET')),
|
||||||
|
DataColumn(label: Text('CATEGORY')),
|
||||||
|
DataColumn(label: Text('PLANT')),
|
||||||
|
DataColumn(label: Text('WARRANTY')),
|
||||||
|
DataColumn(label: Text('STATUS')),
|
||||||
|
DataColumn(
|
||||||
|
label: _AssetTableActionsHeader(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
rows: filteredAssets.map((asset) {
|
||||||
|
final status =
|
||||||
|
assetStatusDisplay(asset.status);
|
||||||
|
return DataRow(
|
||||||
|
cells: [
|
||||||
|
DataCell(
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
UserAvatarChip(
|
||||||
|
name: asset.assetName,
|
||||||
|
initials:
|
||||||
|
assetInitials(asset),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
asset.assetName,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight:
|
||||||
|
FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
asset.assetCode ?? '—',
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodySmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DataCell(
|
||||||
|
RoleBadge(
|
||||||
|
label: asset.assetCategoryName ??
|
||||||
|
'—',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DataCell(
|
||||||
|
Text(asset.plantName ?? '—'),
|
||||||
|
),
|
||||||
|
DataCell(
|
||||||
|
Text(
|
||||||
|
DateFormatter.displayDate(
|
||||||
|
asset.warrantyExpiryDate,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DataCell(
|
||||||
|
StatusBadge(
|
||||||
|
label: status.label,
|
||||||
|
color: status.color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DataCell(
|
||||||
|
_AssetTableActionsCell(
|
||||||
|
child: _AssetTableActions(
|
||||||
|
canView: true,
|
||||||
|
canEdit: canEdit,
|
||||||
|
canDelete: canDelete,
|
||||||
|
onView: () => _viewAsset(asset),
|
||||||
|
onEdit: () => _editAsset(asset),
|
||||||
|
onDelete: () =>
|
||||||
|
_deleteAsset(asset),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Showing $start–$end of $total assets',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
TextButton(
|
||||||
|
onPressed: page > 1
|
||||||
|
? () => ref
|
||||||
|
.read(assetsListProvider.notifier)
|
||||||
|
.setPage(page - 1)
|
||||||
|
: null,
|
||||||
|
child: const Text('Previous'),
|
||||||
|
),
|
||||||
|
...List.generate(state.totalPages.clamp(0, 4), (i) {
|
||||||
|
final pageIndex = i + 1;
|
||||||
|
final selected = page == pageIndex;
|
||||||
|
return Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 2),
|
||||||
|
child: Material(
|
||||||
|
color: selected
|
||||||
|
? Theme.of(context).colorScheme.primary
|
||||||
|
: Colors.transparent,
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
child: InkWell(
|
||||||
|
customBorder: const CircleBorder(),
|
||||||
|
onTap: () => ref
|
||||||
|
.read(assetsListProvider.notifier)
|
||||||
|
.setPage(pageIndex),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'$pageIndex',
|
||||||
|
style: TextStyle(
|
||||||
|
color: selected
|
||||||
|
? Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onPrimary
|
||||||
|
: null,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
TextButton(
|
||||||
|
onPressed: page < state.totalPages
|
||||||
|
? () => ref
|
||||||
|
.read(assetsListProvider.notifier)
|
||||||
|
.setPage(page + 1)
|
||||||
|
: null,
|
||||||
|
child: const Text('Next'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
const Expanded(
|
},
|
||||||
child: EmptyStateView(
|
),
|
||||||
title: 'No assets yet',
|
);
|
||||||
description: 'Add assets to start tracking laptops, devices, furniture, and more.',
|
}
|
||||||
icon: Icons.inventory_2_outlined,
|
|
||||||
),
|
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 label = assetStatusDisplay(asset.status).label;
|
||||||
|
if (label != _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() {
|
||||||
|
return [
|
||||||
|
'All statuses',
|
||||||
|
'In Use',
|
||||||
|
'Available',
|
||||||
|
'Under Maintenance',
|
||||||
|
'Disposed',
|
||||||
|
'Inactive',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
void _viewAsset(AssetModel asset) {
|
||||||
|
context.push('${RouteConstants.assets}/${asset.id}');
|
||||||
|
}
|
||||||
|
|
||||||
|
void _editAsset(AssetModel asset) {
|
||||||
|
openAssetFormPanel(context, ref, assetId: asset.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _deleteAsset(AssetModel asset) async {
|
||||||
|
final confirmed = await showAppConfirmationDialog(
|
||||||
|
context: context,
|
||||||
|
title: 'Delete Asset',
|
||||||
|
message: 'Soft delete "${asset.assetName}"?',
|
||||||
|
confirmLabel: 'Delete',
|
||||||
|
isDestructive: true,
|
||||||
|
);
|
||||||
|
if (confirmed == true && mounted) {
|
||||||
|
await ref.read(assetsListProvider.notifier).deleteAsset(asset.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String assetInitials(AssetModel asset) {
|
||||||
|
final code = asset.assetCode;
|
||||||
|
if (code != null && code.isNotEmpty) {
|
||||||
|
return code.length >= 2 ? code.substring(0, 2).toUpperCase() : code.toUpperCase();
|
||||||
|
}
|
||||||
|
if (asset.assetName.isNotEmpty) {
|
||||||
|
final parts = asset.assetName.trim().split(RegExp(r'\s+'));
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
return '${parts[0][0]}${parts[1][0]}'.toUpperCase();
|
||||||
|
}
|
||||||
|
return asset.assetName[0].toUpperCase();
|
||||||
|
}
|
||||||
|
return 'A';
|
||||||
|
}
|
||||||
|
|
||||||
|
({String label, Color color}) assetStatusDisplay(String? status) {
|
||||||
|
return switch (status?.toUpperCase().replaceAll(' ', '_')) {
|
||||||
|
'IN_USE' => (label: 'In Use', color: const Color(0xFF16A34A)),
|
||||||
|
'AVAILABLE' => (label: 'Available', color: const Color(0xFF2563EB)),
|
||||||
|
'UNDER_MAINTENANCE' => (
|
||||||
|
label: 'Under Maintenance',
|
||||||
|
color: const Color(0xFFCA8A04),
|
||||||
|
),
|
||||||
|
'DISPOSED' => (label: 'Disposed', color: const Color(0xFF6B7280)),
|
||||||
|
'INACTIVE' => (label: 'Inactive', color: const Color(0xFFDC2626)),
|
||||||
|
_ when status != null && status.isNotEmpty => (
|
||||||
|
label: status.replaceAll('_', ' '),
|
||||||
|
color: const Color(0xFF16A34A),
|
||||||
|
),
|
||||||
|
_ => (label: 'Active', color: const Color(0xFF16A34A)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AssetsFilterBar extends StatelessWidget {
|
||||||
|
const _AssetsFilterBar({
|
||||||
|
required this.wrapped,
|
||||||
|
required this.categoryFilter,
|
||||||
|
required this.plantFilter,
|
||||||
|
required this.statusFilter,
|
||||||
|
required this.categories,
|
||||||
|
required this.plants,
|
||||||
|
required this.statuses,
|
||||||
|
required this.onSearch,
|
||||||
|
required this.onCategoryChanged,
|
||||||
|
required this.onPlantChanged,
|
||||||
|
required this.onStatusChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
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 ValueChanged<String> onSearch;
|
||||||
|
final ValueChanged<String> onCategoryChanged;
|
||||||
|
final ValueChanged<String> onPlantChanged;
|
||||||
|
final ValueChanged<String> onStatusChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final searchField = TextField(
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: 'Search by name, code, category...',
|
||||||
|
prefixIcon: Icon(Icons.search, size: 20),
|
||||||
|
isDense: true,
|
||||||
|
),
|
||||||
|
onChanged: onSearch,
|
||||||
|
);
|
||||||
|
|
||||||
|
final filters = [
|
||||||
|
_AssetFilterDropdown(
|
||||||
|
value: categoryFilter,
|
||||||
|
label: 'Category',
|
||||||
|
items: categories,
|
||||||
|
onChanged: onCategoryChanged,
|
||||||
|
),
|
||||||
|
_AssetFilterDropdown(
|
||||||
|
value: plantFilter,
|
||||||
|
label: 'Plant',
|
||||||
|
items: plants,
|
||||||
|
onChanged: onPlantChanged,
|
||||||
|
),
|
||||||
|
_AssetFilterDropdown(
|
||||||
|
value: statusFilter,
|
||||||
|
label: 'Status',
|
||||||
|
items: statuses,
|
||||||
|
onChanged: onStatusChanged,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (wrapped) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
searchField,
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Wrap(spacing: 12, runSpacing: 12, children: filters),
|
||||||
],
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(flex: 3, child: searchField),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(flex: 2, child: filters[0]),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(flex: 2, child: filters[1]),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(child: filters[2]),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AssetFilterDropdown extends StatelessWidget {
|
||||||
|
const _AssetFilterDropdown({
|
||||||
|
required this.value,
|
||||||
|
required this.label,
|
||||||
|
required this.items,
|
||||||
|
required this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String value;
|
||||||
|
final String label;
|
||||||
|
final List<String> items;
|
||||||
|
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: item))
|
||||||
|
.toList(),
|
||||||
|
onChanged: (v) {
|
||||||
|
if (v != null) onChanged(v);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AssetTableActions extends StatelessWidget {
|
||||||
|
const _AssetTableActions({
|
||||||
|
required this.onView,
|
||||||
|
required this.onEdit,
|
||||||
|
required this.onDelete,
|
||||||
|
this.canView = true,
|
||||||
|
this.canEdit = true,
|
||||||
|
this.canDelete = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const columnWidth = 108.0;
|
||||||
|
|
||||||
|
final VoidCallback onView;
|
||||||
|
final VoidCallback onEdit;
|
||||||
|
final VoidCallback onDelete;
|
||||||
|
final bool canView;
|
||||||
|
final bool canEdit;
|
||||||
|
final bool canDelete;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final muted = Theme.of(context).colorScheme.onSurfaceVariant;
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
if (canView)
|
||||||
|
_AssetActionIcon(
|
||||||
|
tooltip: 'View asset',
|
||||||
|
icon: Icons.visibility_outlined,
|
||||||
|
color: muted,
|
||||||
|
onPressed: onView,
|
||||||
|
),
|
||||||
|
if (canEdit)
|
||||||
|
_AssetActionIcon(
|
||||||
|
tooltip: 'Edit asset',
|
||||||
|
icon: Icons.edit_outlined,
|
||||||
|
color: muted,
|
||||||
|
onPressed: onEdit,
|
||||||
|
),
|
||||||
|
if (canDelete)
|
||||||
|
_AssetActionIcon(
|
||||||
|
tooltip: 'Delete asset',
|
||||||
|
icon: Icons.delete_outline,
|
||||||
|
color: muted,
|
||||||
|
onPressed: onDelete,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AssetTableActionsHeader extends StatelessWidget {
|
||||||
|
const _AssetTableActionsHeader();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const SizedBox(
|
||||||
|
width: _AssetTableActions.columnWidth,
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Text('ACTIONS'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _AssetTableActionsCell extends StatelessWidget {
|
||||||
|
const _AssetTableActionsCell({required this.child});
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
width: _AssetTableActions.columnWidth,
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 4),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AssetActionIcon extends StatelessWidget {
|
||||||
|
const _AssetActionIcon({
|
||||||
|
required this.tooltip,
|
||||||
|
required this.icon,
|
||||||
|
required this.color,
|
||||||
|
required this.onPressed,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String tooltip;
|
||||||
|
final IconData icon;
|
||||||
|
final Color color;
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Tooltip(
|
||||||
|
message: tooltip,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onPressed,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(6),
|
||||||
|
child: Icon(icon, size: 18, color: color),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AssetMobileList extends StatelessWidget {
|
||||||
|
const _AssetMobileList({
|
||||||
|
required this.assets,
|
||||||
|
required this.onView,
|
||||||
|
this.onEdit,
|
||||||
|
this.onDelete,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<AssetModel> assets;
|
||||||
|
final void Function(AssetModel) onView;
|
||||||
|
final void Function(AssetModel)? onEdit;
|
||||||
|
final void Function(AssetModel)? onDelete;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (assets.isEmpty) {
|
||||||
|
return ListView(
|
||||||
|
children: const [
|
||||||
|
AppEmptyState(
|
||||||
|
title: 'No assets found',
|
||||||
|
description: 'Add assets or adjust your search.',
|
||||||
|
icon: Icons.inventory_2_outlined,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.separated(
|
||||||
|
itemCount: assets.length,
|
||||||
|
separatorBuilder: (_, __) => const SizedBox(height: 8),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final asset = assets[index];
|
||||||
|
final status = assetStatusDisplay(asset.status);
|
||||||
|
|
||||||
|
return AppCard(
|
||||||
|
child: ListTile(
|
||||||
|
onTap: () => onView(asset),
|
||||||
|
leading: UserAvatarChip(
|
||||||
|
name: asset.assetName,
|
||||||
|
initials: assetInitials(asset),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
asset.assetName,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(asset.assetCode ?? '—'),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: RoleBadge(
|
||||||
|
label: asset.assetCategoryName ?? '—',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
StatusBadge(label: status.label, color: status.color),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (onEdit != null)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.edit_outlined),
|
||||||
|
onPressed: () => onEdit!(asset),
|
||||||
|
),
|
||||||
|
if (onDelete != null)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.delete_outline),
|
||||||
|
onPressed: () => onDelete!(asset),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
326
lib/modules/assets/presentation/widgets/asset_form_panel.dart
Normal file
326
lib/modules/assets/presentation/widgets/asset_form_panel.dart
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
import '../../../../core/errors/failure.dart';
|
||||||
|
import '../../../../core/utils/formatters.dart';
|
||||||
|
import '../../../../core/utils/validators.dart';
|
||||||
|
import '../../../../shared/models/asset_model.dart';
|
||||||
|
import '../../../../shared/models/user_management_models.dart' show FilterOptionModel;
|
||||||
|
import '../../../../shared/widgets/app_button.dart';
|
||||||
|
import '../../../../shared/widgets/app_loading_view.dart';
|
||||||
|
import '../../../../shared/widgets/app_side_panel.dart';
|
||||||
|
import '../../../../shared/widgets/app_text_field.dart';
|
||||||
|
import '../../../../shared/widgets/error_view.dart';
|
||||||
|
import '../../../masters/data/datasources/master_remote_data_source.dart';
|
||||||
|
import '../providers/asset_categories_provider.dart';
|
||||||
|
import '../providers/assets_provider.dart';
|
||||||
|
|
||||||
|
Future<void> openAssetFormPanel(
|
||||||
|
BuildContext context,
|
||||||
|
WidgetRef ref, {
|
||||||
|
String? assetId,
|
||||||
|
}) async {
|
||||||
|
ref.invalidate(assetFormProvider(assetId));
|
||||||
|
final saved = await showSidePanel<bool>(
|
||||||
|
context,
|
||||||
|
AssetFormPanel(assetId: assetId),
|
||||||
|
width: 520,
|
||||||
|
);
|
||||||
|
if (saved == true && context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
assetId == null
|
||||||
|
? 'Asset created successfully'
|
||||||
|
: 'Asset updated successfully',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AssetFormPanel extends ConsumerStatefulWidget {
|
||||||
|
const AssetFormPanel({super.key, this.assetId});
|
||||||
|
|
||||||
|
final String? assetId;
|
||||||
|
|
||||||
|
bool get isEditing => assetId != null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<AssetFormPanel> createState() => _AssetFormPanelState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AssetFormPanelState extends ConsumerState<AssetFormPanel> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final _nameController = TextEditingController();
|
||||||
|
final _costController = TextEditingController();
|
||||||
|
int? _categoryId;
|
||||||
|
int? _plantId;
|
||||||
|
DateTime? _warrantyExpiry;
|
||||||
|
bool _isSubmitting = false;
|
||||||
|
String? _populatedSignature;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_nameController.dispose();
|
||||||
|
_costController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _assetSignature(AssetModel asset) =>
|
||||||
|
'${asset.id}:${asset.assetCategoryId}:${asset.plantId}:'
|
||||||
|
'${asset.warrantyExpiryDate?.toIso8601String()}:'
|
||||||
|
'${asset.purchaseCost}:${asset.assetName}';
|
||||||
|
|
||||||
|
int? _dropdownValue(int? selected, Iterable<int> validIds) {
|
||||||
|
if (selected == null) return null;
|
||||||
|
return validIds.contains(selected) ? selected : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _populateFromAsset(AssetModel asset) {
|
||||||
|
setState(() {
|
||||||
|
_nameController.text = asset.assetName;
|
||||||
|
_categoryId = asset.assetCategoryId;
|
||||||
|
_plantId = asset.plantId;
|
||||||
|
_warrantyExpiry = asset.warrantyExpiryDate;
|
||||||
|
if (asset.purchaseCost != null) {
|
||||||
|
_costController.text = asset.purchaseCost!.toStringAsFixed(
|
||||||
|
asset.purchaseCost! % 1 == 0 ? 0 : 2,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_costController.clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _buildPayload() {
|
||||||
|
return {
|
||||||
|
'asset_name': _nameController.text.trim(),
|
||||||
|
'asset_category_id': _categoryId,
|
||||||
|
'plant_id': _plantId,
|
||||||
|
if (_warrantyExpiry != null)
|
||||||
|
'warranty_expiry_date': DateFormat('yyyy-MM-dd').format(_warrantyExpiry!),
|
||||||
|
if (_costController.text.trim().isNotEmpty)
|
||||||
|
'purchase_cost': double.tryParse(_costController.text.trim()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pickWarrantyDate() async {
|
||||||
|
final picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: _warrantyExpiry ?? DateTime.now(),
|
||||||
|
firstDate: DateTime(2000),
|
||||||
|
lastDate: DateTime(2100),
|
||||||
|
);
|
||||||
|
if (picked != null) setState(() => _warrantyExpiry = picked);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _submit() async {
|
||||||
|
if (!_formKey.currentState!.validate()) return;
|
||||||
|
if (_categoryId == null || _plantId == null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Please select category and plant')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() => _isSubmitting = true);
|
||||||
|
try {
|
||||||
|
final notifier = ref.read(assetFormProvider(widget.assetId).notifier);
|
||||||
|
final payload = _buildPayload();
|
||||||
|
if (widget.isEditing) {
|
||||||
|
await notifier.submitUpdate(widget.assetId!, payload);
|
||||||
|
} else {
|
||||||
|
await notifier.submitCreate(payload);
|
||||||
|
}
|
||||||
|
if (mounted) Navigator.of(context, rootNavigator: true).pop(true);
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(e.toString())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _isSubmitting = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final categoriesAsync = ref.watch(assetCategoriesProvider);
|
||||||
|
final plantsAsync = ref.watch(assetPlantsProvider);
|
||||||
|
|
||||||
|
if (widget.isEditing) {
|
||||||
|
ref.listen(assetFormProvider(widget.assetId), (prev, next) {
|
||||||
|
next.whenData((asset) {
|
||||||
|
if (asset == null || !mounted) return;
|
||||||
|
final signature = _assetSignature(asset);
|
||||||
|
if (_populatedSignature != signature) {
|
||||||
|
_populatedSignature = signature;
|
||||||
|
_populateFromAsset(asset);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final formAsync =
|
||||||
|
widget.isEditing ? ref.watch(assetFormProvider(widget.assetId)) : null;
|
||||||
|
|
||||||
|
return SidePanelScaffold(
|
||||||
|
title: widget.isEditing ? 'Edit asset' : 'Add asset',
|
||||||
|
footer: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
OutlinedButton(
|
||||||
|
onPressed: _isSubmitting
|
||||||
|
? null
|
||||||
|
: () => Navigator.of(context, rootNavigator: true).pop(),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
AppButton(
|
||||||
|
label: widget.isEditing ? 'Update asset' : 'Save asset',
|
||||||
|
expand: false,
|
||||||
|
icon: Icons.check,
|
||||||
|
isLoading: _isSubmitting,
|
||||||
|
onPressed: _isSubmitting ? null : _submit,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: widget.isEditing && formAsync != null
|
||||||
|
? formAsync.when(
|
||||||
|
loading: () => const AppLoadingView(message: 'Loading asset...'),
|
||||||
|
error: (e, _) => ErrorView.fromFailure(
|
||||||
|
e is Failure ? e : Failure.unknown(message: e.toString()),
|
||||||
|
onRetry: () => ref.invalidate(assetFormProvider(widget.assetId)),
|
||||||
|
),
|
||||||
|
data: (_) => _buildForm(categoriesAsync, plantsAsync),
|
||||||
|
)
|
||||||
|
: _buildForm(categoriesAsync, plantsAsync),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildForm(
|
||||||
|
AsyncValue<List<AssetCategoryModel>> categoriesAsync,
|
||||||
|
AsyncValue<List<FilterOptionModel>> plantsAsync,
|
||||||
|
) {
|
||||||
|
return Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
AppTextField(
|
||||||
|
controller: _nameController,
|
||||||
|
label: 'Asset Name *',
|
||||||
|
validator: (v) => Validators.required(v, fieldName: 'Asset name'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
SidePanelFormRow(
|
||||||
|
left: categoriesAsync.when(
|
||||||
|
loading: () => const LinearProgressIndicator(),
|
||||||
|
error: (_, __) => const Text('Failed to load categories'),
|
||||||
|
data: (categories) => _categoryDropdown(categories),
|
||||||
|
),
|
||||||
|
right: plantsAsync.when(
|
||||||
|
loading: () => const LinearProgressIndicator(),
|
||||||
|
error: (_, __) => const Text('Failed to load plants'),
|
||||||
|
data: (plants) => _plantDropdown(plants),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SidePanelFormRow(
|
||||||
|
left: _AssetFormDateField(
|
||||||
|
label: 'Warranty Expiry Date',
|
||||||
|
value: _warrantyExpiry,
|
||||||
|
onPick: _pickWarrantyDate,
|
||||||
|
),
|
||||||
|
right: AppTextField(
|
||||||
|
controller: _costController,
|
||||||
|
label: 'Purchase Cost',
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _categoryDropdown(List<AssetCategoryModel> categories) {
|
||||||
|
final categoryIds = categories
|
||||||
|
.map((c) => int.tryParse(c.id))
|
||||||
|
.whereType<int>()
|
||||||
|
.toList();
|
||||||
|
return DropdownButtonFormField<int>(
|
||||||
|
value: _dropdownValue(_categoryId, categoryIds),
|
||||||
|
isExpanded: true,
|
||||||
|
decoration: const InputDecoration(labelText: 'Category *'),
|
||||||
|
items: categories
|
||||||
|
.map(
|
||||||
|
(c) => DropdownMenuItem(
|
||||||
|
value: int.tryParse(c.id),
|
||||||
|
child: Text('${c.code} — ${c.name}'),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.where((item) => item.value != null)
|
||||||
|
.toList(),
|
||||||
|
onChanged: (v) => setState(() => _categoryId = v),
|
||||||
|
validator: (v) => v == null ? 'Category is required' : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _plantDropdown(List<FilterOptionModel> plants) {
|
||||||
|
final plantIds = plants
|
||||||
|
.map((p) => int.tryParse(p.id))
|
||||||
|
.whereType<int>()
|
||||||
|
.toList();
|
||||||
|
return DropdownButtonFormField<int>(
|
||||||
|
value: _dropdownValue(_plantId, plantIds),
|
||||||
|
isExpanded: true,
|
||||||
|
decoration: const InputDecoration(labelText: 'Plant *'),
|
||||||
|
items: plants
|
||||||
|
.map(
|
||||||
|
(p) => DropdownMenuItem(
|
||||||
|
value: int.tryParse(p.id),
|
||||||
|
child: Text(p.name),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.where((item) => item.value != null)
|
||||||
|
.toList(),
|
||||||
|
onChanged: (v) => setState(() => _plantId = v),
|
||||||
|
validator: (v) => v == null ? 'Plant is required' : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AssetFormDateField extends StatelessWidget {
|
||||||
|
const _AssetFormDateField({
|
||||||
|
required this.label,
|
||||||
|
required this.value,
|
||||||
|
required this.onPick,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final DateTime? value;
|
||||||
|
final VoidCallback onPick;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final displayText = value != null ? DateFormatter.displayDate(value) : '';
|
||||||
|
|
||||||
|
return TextFormField(
|
||||||
|
readOnly: true,
|
||||||
|
onTap: onPick,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: label,
|
||||||
|
hintText: 'Select date',
|
||||||
|
suffixIcon: const Icon(Icons.calendar_today_outlined, size: 20),
|
||||||
|
),
|
||||||
|
controller: TextEditingController(text: displayText),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final assetPlantsProvider = FutureProvider<List<FilterOptionModel>>((ref) async {
|
||||||
|
final dataSource = ref.watch(masterRemoteDataSourceProvider);
|
||||||
|
return dataSource.listPlants();
|
||||||
|
});
|
||||||
500
lib/modules/assets/presentation/widgets/asset_side_panels.dart
Normal file
500
lib/modules/assets/presentation/widgets/asset_side_panels.dart
Normal file
@ -0,0 +1,500 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../../../../core/utils/formatters.dart';
|
||||||
|
import '../../../../core/utils/validators.dart';
|
||||||
|
import '../../../../shared/widgets/app_button.dart';
|
||||||
|
import '../../../../shared/widgets/app_side_panel.dart';
|
||||||
|
import '../../../../shared/widgets/app_text_field.dart';
|
||||||
|
import '../providers/assets_provider.dart';
|
||||||
|
|
||||||
|
class AddAmcPanel extends ConsumerStatefulWidget {
|
||||||
|
const AddAmcPanel({super.key, required this.assetId});
|
||||||
|
|
||||||
|
final String assetId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<AddAmcPanel> createState() => _AddAmcPanelState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AddAmcPanelState extends ConsumerState<AddAmcPanel> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final _vendorIdController = TextEditingController();
|
||||||
|
final _contractNoController = TextEditingController();
|
||||||
|
final _annualCostController = TextEditingController();
|
||||||
|
DateTime? _startDate;
|
||||||
|
DateTime? _endDate;
|
||||||
|
String _contractType = 'COMPREHENSIVE';
|
||||||
|
bool _isSubmitting = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_vendorIdController.dispose();
|
||||||
|
_contractNoController.dispose();
|
||||||
|
_annualCostController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pickDate({required bool isStart}) async {
|
||||||
|
final picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: (isStart ? _startDate : _endDate) ?? DateTime.now(),
|
||||||
|
firstDate: DateTime(2000),
|
||||||
|
lastDate: DateTime(2100),
|
||||||
|
);
|
||||||
|
if (picked != null) {
|
||||||
|
setState(() {
|
||||||
|
if (isStart) {
|
||||||
|
_startDate = picked;
|
||||||
|
} else {
|
||||||
|
_endDate = picked;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _save() async {
|
||||||
|
if (!_formKey.currentState!.validate()) return;
|
||||||
|
if (_startDate == null || _endDate == null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Please select start and end dates')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final vendorId = int.tryParse(_vendorIdController.text.trim());
|
||||||
|
if (vendorId == null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Please enter a valid vendor ID')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() => _isSubmitting = true);
|
||||||
|
try {
|
||||||
|
await ref.read(assetDetailProvider(widget.assetId).notifier).createAmc({
|
||||||
|
'vendor_id': vendorId,
|
||||||
|
if (_contractNoController.text.isNotEmpty)
|
||||||
|
'contract_no': _contractNoController.text.trim(),
|
||||||
|
'contract_type': _contractType,
|
||||||
|
'start_date': DateFormatter.toApiDate(_startDate!),
|
||||||
|
'end_date': DateFormatter.toApiDate(_endDate!),
|
||||||
|
if (_annualCostController.text.isNotEmpty)
|
||||||
|
'annual_cost': double.tryParse(_annualCostController.text),
|
||||||
|
});
|
||||||
|
if (mounted) Navigator.of(context, rootNavigator: true).pop(true);
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _isSubmitting = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SidePanelScaffold(
|
||||||
|
title: 'Add AMC Contract',
|
||||||
|
footer: _panelFooter(
|
||||||
|
context,
|
||||||
|
isSubmitting: _isSubmitting,
|
||||||
|
saveLabel: 'Save contract',
|
||||||
|
onSave: _save,
|
||||||
|
),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
SidePanelFormRow(
|
||||||
|
left: AppTextField(
|
||||||
|
controller: _vendorIdController,
|
||||||
|
label: 'Vendor ID *',
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
validator: (v) => Validators.required(v, fieldName: 'Vendor ID'),
|
||||||
|
),
|
||||||
|
right: AppTextField(
|
||||||
|
controller: _contractNoController,
|
||||||
|
label: 'Contract No',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
value: _contractType,
|
||||||
|
isExpanded: true,
|
||||||
|
decoration: const InputDecoration(labelText: 'Contract Type'),
|
||||||
|
items: const [
|
||||||
|
'COMPREHENSIVE',
|
||||||
|
'LABOUR_ONLY',
|
||||||
|
'PARTS_ONLY',
|
||||||
|
'PREVENTIVE_ONLY',
|
||||||
|
]
|
||||||
|
.map((t) => DropdownMenuItem(value: t, child: Text(t)))
|
||||||
|
.toList(),
|
||||||
|
onChanged: (v) {
|
||||||
|
if (v != null) setState(() => _contractType = v);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
SidePanelFormRow(
|
||||||
|
left: _SidePanelDateField(
|
||||||
|
label: 'Start Date',
|
||||||
|
isRequired: true,
|
||||||
|
value: _startDate,
|
||||||
|
onPick: () => _pickDate(isStart: true),
|
||||||
|
),
|
||||||
|
right: _SidePanelDateField(
|
||||||
|
label: 'End Date',
|
||||||
|
isRequired: true,
|
||||||
|
value: _endDate,
|
||||||
|
onPick: () => _pickDate(isStart: false),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AppTextField(
|
||||||
|
controller: _annualCostController,
|
||||||
|
label: 'Annual Cost',
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LogServiceVisitPanel extends ConsumerStatefulWidget {
|
||||||
|
const LogServiceVisitPanel({super.key, required this.assetId});
|
||||||
|
|
||||||
|
final String assetId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<LogServiceVisitPanel> createState() => _LogServiceVisitPanelState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LogServiceVisitPanelState extends ConsumerState<LogServiceVisitPanel> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final _workDoneController = TextEditingController();
|
||||||
|
DateTime? _visitDate = DateTime.now();
|
||||||
|
String _visitType = 'PREVENTIVE';
|
||||||
|
String _status = 'COMPLETED';
|
||||||
|
bool _isSubmitting = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_workDoneController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pickVisitDate() async {
|
||||||
|
final picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: _visitDate ?? DateTime.now(),
|
||||||
|
firstDate: DateTime(2000),
|
||||||
|
lastDate: DateTime(2100),
|
||||||
|
);
|
||||||
|
if (picked != null) setState(() => _visitDate = picked);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _save() async {
|
||||||
|
if (_visitDate == null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Please select visit date')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() => _isSubmitting = true);
|
||||||
|
try {
|
||||||
|
await ref.read(assetDetailProvider(widget.assetId).notifier).logVisit({
|
||||||
|
'visit_type': _visitType,
|
||||||
|
'visit_date': DateFormatter.toApiDate(_visitDate!),
|
||||||
|
if (_workDoneController.text.isNotEmpty)
|
||||||
|
'work_done': _workDoneController.text.trim(),
|
||||||
|
'status': _status,
|
||||||
|
});
|
||||||
|
if (mounted) Navigator.of(context, rootNavigator: true).pop(true);
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _isSubmitting = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SidePanelScaffold(
|
||||||
|
title: 'Log Service Visit',
|
||||||
|
footer: _panelFooter(
|
||||||
|
context,
|
||||||
|
isSubmitting: _isSubmitting,
|
||||||
|
saveLabel: 'Save visit',
|
||||||
|
onSave: _save,
|
||||||
|
),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
SidePanelFormRow(
|
||||||
|
left: DropdownButtonFormField<String>(
|
||||||
|
value: _visitType,
|
||||||
|
isExpanded: true,
|
||||||
|
decoration: const InputDecoration(labelText: 'Visit Type *'),
|
||||||
|
items: const [
|
||||||
|
'PREVENTIVE',
|
||||||
|
'BREAKDOWN',
|
||||||
|
'INSPECTION',
|
||||||
|
'INSTALLATION',
|
||||||
|
'CALIBRATION',
|
||||||
|
'OTHER',
|
||||||
|
]
|
||||||
|
.map((t) => DropdownMenuItem(value: t, child: Text(t)))
|
||||||
|
.toList(),
|
||||||
|
onChanged: (v) {
|
||||||
|
if (v != null) setState(() => _visitType = v);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
right: DropdownButtonFormField<String>(
|
||||||
|
value: _status,
|
||||||
|
isExpanded: true,
|
||||||
|
decoration: const InputDecoration(labelText: 'Status'),
|
||||||
|
items: const [
|
||||||
|
'SCHEDULED',
|
||||||
|
'IN_PROGRESS',
|
||||||
|
'COMPLETED',
|
||||||
|
'CANCELLED',
|
||||||
|
'PENDING_PARTS',
|
||||||
|
]
|
||||||
|
.map((t) => DropdownMenuItem(value: t, child: Text(t)))
|
||||||
|
.toList(),
|
||||||
|
onChanged: (v) {
|
||||||
|
if (v != null) setState(() => _status = v);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_SidePanelDateField(
|
||||||
|
label: 'Visit Date',
|
||||||
|
isRequired: true,
|
||||||
|
value: _visitDate,
|
||||||
|
onPick: _pickVisitDate,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
AppTextField(
|
||||||
|
controller: _workDoneController,
|
||||||
|
label: 'Work Done',
|
||||||
|
maxLines: 4,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AddInsurancePanel extends ConsumerStatefulWidget {
|
||||||
|
const AddInsurancePanel({super.key, required this.assetId});
|
||||||
|
|
||||||
|
final String assetId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<AddInsurancePanel> createState() => _AddInsurancePanelState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AddInsurancePanelState extends ConsumerState<AddInsurancePanel> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final _policyNoController = TextEditingController();
|
||||||
|
final _insurerNameController = TextEditingController();
|
||||||
|
final _sumInsuredController = TextEditingController();
|
||||||
|
DateTime? _startDate;
|
||||||
|
DateTime? _endDate;
|
||||||
|
String _policyType = 'COMPREHENSIVE';
|
||||||
|
bool _isSubmitting = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_policyNoController.dispose();
|
||||||
|
_insurerNameController.dispose();
|
||||||
|
_sumInsuredController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pickDate({required bool isStart}) async {
|
||||||
|
final picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: (isStart ? _startDate : _endDate) ?? DateTime.now(),
|
||||||
|
firstDate: DateTime(2000),
|
||||||
|
lastDate: DateTime(2100),
|
||||||
|
);
|
||||||
|
if (picked != null) {
|
||||||
|
setState(() {
|
||||||
|
if (isStart) {
|
||||||
|
_startDate = picked;
|
||||||
|
} else {
|
||||||
|
_endDate = picked;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _save() async {
|
||||||
|
if (!_formKey.currentState!.validate()) return;
|
||||||
|
if (_startDate == null || _endDate == null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Please select start and end dates')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() => _isSubmitting = true);
|
||||||
|
try {
|
||||||
|
await ref.read(assetDetailProvider(widget.assetId).notifier).createInsurance({
|
||||||
|
'policy_no': _policyNoController.text.trim(),
|
||||||
|
'insurer_name': _insurerNameController.text.trim(),
|
||||||
|
'policy_type': _policyType,
|
||||||
|
if (_sumInsuredController.text.isNotEmpty)
|
||||||
|
'sum_insured': double.tryParse(_sumInsuredController.text),
|
||||||
|
'policy_start_date': DateFormatter.toApiDate(_startDate!),
|
||||||
|
'policy_end_date': DateFormatter.toApiDate(_endDate!),
|
||||||
|
});
|
||||||
|
if (mounted) Navigator.of(context, rootNavigator: true).pop(true);
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _isSubmitting = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SidePanelScaffold(
|
||||||
|
title: 'Add Insurance Policy',
|
||||||
|
footer: _panelFooter(
|
||||||
|
context,
|
||||||
|
isSubmitting: _isSubmitting,
|
||||||
|
saveLabel: 'Save policy',
|
||||||
|
onSave: _save,
|
||||||
|
),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
SidePanelFormRow(
|
||||||
|
left: AppTextField(
|
||||||
|
controller: _policyNoController,
|
||||||
|
label: 'Policy No *',
|
||||||
|
validator: (v) => Validators.required(v, fieldName: 'Policy no'),
|
||||||
|
),
|
||||||
|
right: AppTextField(
|
||||||
|
controller: _insurerNameController,
|
||||||
|
label: 'Insurer Name *',
|
||||||
|
validator: (v) => Validators.required(v, fieldName: 'Insurer name'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SidePanelFormRow(
|
||||||
|
left: DropdownButtonFormField<String>(
|
||||||
|
value: _policyType,
|
||||||
|
isExpanded: true,
|
||||||
|
decoration: const InputDecoration(labelText: 'Policy Type'),
|
||||||
|
items: const [
|
||||||
|
'FIRE_AND_ALLIED',
|
||||||
|
'MACHINERY_BREAKDOWN',
|
||||||
|
'COMPREHENSIVE',
|
||||||
|
'THIRD_PARTY',
|
||||||
|
'VEHICLE',
|
||||||
|
'OTHER',
|
||||||
|
]
|
||||||
|
.map((t) => DropdownMenuItem(value: t, child: Text(t)))
|
||||||
|
.toList(),
|
||||||
|
onChanged: (v) {
|
||||||
|
if (v != null) setState(() => _policyType = v);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
right: AppTextField(
|
||||||
|
controller: _sumInsuredController,
|
||||||
|
label: 'Sum Insured',
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SidePanelFormRow(
|
||||||
|
left: _SidePanelDateField(
|
||||||
|
label: 'Start Date',
|
||||||
|
isRequired: true,
|
||||||
|
value: _startDate,
|
||||||
|
onPick: () => _pickDate(isStart: true),
|
||||||
|
),
|
||||||
|
right: _SidePanelDateField(
|
||||||
|
label: 'End Date',
|
||||||
|
isRequired: true,
|
||||||
|
value: _endDate,
|
||||||
|
onPick: () => _pickDate(isStart: false),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _panelFooter(
|
||||||
|
BuildContext context, {
|
||||||
|
required bool isSubmitting,
|
||||||
|
required String saveLabel,
|
||||||
|
required VoidCallback onSave,
|
||||||
|
}) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
OutlinedButton(
|
||||||
|
onPressed: isSubmitting
|
||||||
|
? null
|
||||||
|
: () => Navigator.of(context, rootNavigator: true).pop(),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
AppButton(
|
||||||
|
label: saveLabel,
|
||||||
|
expand: false,
|
||||||
|
icon: Icons.check,
|
||||||
|
isLoading: isSubmitting,
|
||||||
|
onPressed: isSubmitting ? null : onSave,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SidePanelDateField extends StatelessWidget {
|
||||||
|
const _SidePanelDateField({
|
||||||
|
required this.label,
|
||||||
|
required this.value,
|
||||||
|
required this.onPick,
|
||||||
|
this.isRequired = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final DateTime? value;
|
||||||
|
final VoidCallback onPick;
|
||||||
|
final bool isRequired;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final labelText = isRequired ? '$label *' : label;
|
||||||
|
final displayText = value != null ? DateFormatter.displayDate(value) : '';
|
||||||
|
|
||||||
|
return TextFormField(
|
||||||
|
readOnly: true,
|
||||||
|
onTap: onPick,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: labelText,
|
||||||
|
hintText: 'Select date',
|
||||||
|
suffixIcon: const Icon(Icons.calendar_today_outlined, size: 20),
|
||||||
|
),
|
||||||
|
controller: TextEditingController(text: displayText),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -77,15 +77,10 @@ final _entries = [
|
|||||||
),
|
),
|
||||||
// Assets
|
// Assets
|
||||||
_GalleryEntry(title: 'Asset List', route: RouteConstants.assets, group: 'Assets'),
|
_GalleryEntry(title: 'Asset List', route: RouteConstants.assets, group: 'Assets'),
|
||||||
_GalleryEntry(title: 'Add Asset', route: RouteConstants.assetAdd, group: 'Assets'),
|
|
||||||
_GalleryEntry(title: 'Edit Asset', route: '/assets/demo-asset/edit', group: 'Assets'),
|
|
||||||
_GalleryEntry(title: 'Asset Detail', route: '/assets/demo-asset', group: 'Assets'),
|
_GalleryEntry(title: 'Asset Detail', route: '/assets/demo-asset', group: 'Assets'),
|
||||||
_GalleryEntry(title: 'Asset Categories', route: RouteConstants.assetCategories, group: 'Assets'),
|
_GalleryEntry(title: 'Asset Categories', route: RouteConstants.assetCategories, group: 'Assets'),
|
||||||
_GalleryEntry(title: 'Allocations', route: RouteConstants.assetAllocations, group: 'Assets'),
|
_GalleryEntry(title: 'Categories', route: RouteConstants.assetCategories, group: 'Assets'),
|
||||||
_GalleryEntry(title: 'Maintenance', route: RouteConstants.assetMaintenance, group: 'Assets'),
|
_GalleryEntry(title: 'Alerts', route: RouteConstants.assetAlerts, group: 'Assets'),
|
||||||
_GalleryEntry(title: 'Disposal', route: RouteConstants.assetDisposal, group: 'Assets'),
|
|
||||||
_GalleryEntry(title: 'QR Scan', route: RouteConstants.assetQrScan, group: 'Assets'),
|
|
||||||
_GalleryEntry(title: 'QR Generate', route: RouteConstants.assetQrGenerate, group: 'Assets'),
|
|
||||||
// Master data
|
// Master data
|
||||||
_GalleryEntry(title: 'Master Data Hub', route: RouteConstants.masterData, group: 'Master Data'),
|
_GalleryEntry(title: 'Master Data Hub', route: RouteConstants.masterData, group: 'Master Data'),
|
||||||
...masterDefinitions.map(
|
...masterDefinitions.map(
|
||||||
|
|||||||
@ -3,15 +3,80 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
|||||||
part 'asset_model.freezed.dart';
|
part 'asset_model.freezed.dart';
|
||||||
part 'asset_model.g.dart';
|
part 'asset_model.g.dart';
|
||||||
|
|
||||||
|
String _idFromJson(Object? value) => value?.toString() ?? '';
|
||||||
|
|
||||||
|
int? _intFromJsonNullable(Object? value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (value is int) return value;
|
||||||
|
if (value is num) return value.toInt();
|
||||||
|
return int.tryParse(value.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
double? _doubleFromJsonNullable(Object? value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (value is double) return value;
|
||||||
|
if (value is num) return value.toDouble();
|
||||||
|
return double.tryParse(value.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime _dateFromJson(Object? value) {
|
||||||
|
if (value is DateTime) return value;
|
||||||
|
return DateTime.parse(value.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime? _dateFromJsonNullable(Object? value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (value is DateTime) return value;
|
||||||
|
return DateTime.tryParse(value.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
Object? _readNestedName(Map<dynamic, dynamic> json, String nestedKey) {
|
||||||
|
final nested = json[nestedKey];
|
||||||
|
if (nested is Map) return nested['name'];
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object? _readAssetCategoryName(Map<dynamic, dynamic> json, String key) {
|
||||||
|
final flat = json['asset_category_name'];
|
||||||
|
if (flat is String && flat.isNotEmpty) return flat;
|
||||||
|
return _readNestedName(json, 'asset_category');
|
||||||
|
}
|
||||||
|
|
||||||
|
Object? _readPlantName(Map<dynamic, dynamic> json, String key) {
|
||||||
|
final flat = json['plant_name'];
|
||||||
|
if (flat is String && flat.isNotEmpty) return flat;
|
||||||
|
return _readNestedName(json, 'plant');
|
||||||
|
}
|
||||||
|
|
||||||
|
Object? _readAssetCategoryId(Map<dynamic, dynamic> json, String key) {
|
||||||
|
final flat = json['asset_category_id'];
|
||||||
|
if (flat != null) return flat;
|
||||||
|
final nested = json['asset_category'];
|
||||||
|
if (nested is Map) return nested['id'];
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object? _readPlantId(Map<dynamic, dynamic> json, String key) {
|
||||||
|
final flat = json['plant_id'];
|
||||||
|
if (flat != null) return flat;
|
||||||
|
final nested = json['plant'];
|
||||||
|
if (nested is Map) return nested['id'];
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class AssetCategoryModel with _$AssetCategoryModel {
|
class AssetCategoryModel with _$AssetCategoryModel {
|
||||||
const factory AssetCategoryModel({
|
const factory AssetCategoryModel({
|
||||||
required String id,
|
@JsonKey(fromJson: _idFromJson) required String id,
|
||||||
|
required String code,
|
||||||
required String name,
|
required String name,
|
||||||
required String slug,
|
@JsonKey(name: 'code_prefix') String? codePrefix,
|
||||||
String? description,
|
@JsonKey(name: 'default_useful_life_years', fromJson: _intFromJsonNullable)
|
||||||
@JsonKey(name: 'company_id') String? companyId,
|
int? defaultUsefulLifeYears,
|
||||||
|
@JsonKey(name: 'default_depreciation_method') String? defaultDepreciationMethod,
|
||||||
|
@JsonKey(name: 'is_active') @Default(true) bool isActive,
|
||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
|
DateTime? updatedAt,
|
||||||
}) = _AssetCategoryModel;
|
}) = _AssetCategoryModel;
|
||||||
|
|
||||||
factory AssetCategoryModel.fromJson(Map<String, dynamic> json) =>
|
factory AssetCategoryModel.fromJson(Map<String, dynamic> json) =>
|
||||||
@ -21,85 +86,108 @@ class AssetCategoryModel with _$AssetCategoryModel {
|
|||||||
@freezed
|
@freezed
|
||||||
class AssetModel with _$AssetModel {
|
class AssetModel with _$AssetModel {
|
||||||
const factory AssetModel({
|
const factory AssetModel({
|
||||||
required String id,
|
@JsonKey(fromJson: _idFromJson) required String id,
|
||||||
required String name,
|
@JsonKey(name: 'asset_name') required String assetName,
|
||||||
@JsonKey(name: 'asset_code') required String assetCode,
|
@JsonKey(name: 'asset_code') String? assetCode,
|
||||||
@JsonKey(name: 'category_id') required String categoryId,
|
@JsonKey(
|
||||||
@JsonKey(name: 'category_name') String? categoryName,
|
name: 'asset_category_id',
|
||||||
String? brand,
|
readValue: _readAssetCategoryId,
|
||||||
String? model,
|
fromJson: _intFromJsonNullable,
|
||||||
@JsonKey(name: 'serial_number') String? serialNumber,
|
)
|
||||||
@JsonKey(name: 'purchase_date') DateTime? purchaseDate,
|
int? assetCategoryId,
|
||||||
@JsonKey(name: 'purchase_cost') double? purchaseCost,
|
@JsonKey(name: 'asset_category_name', readValue: _readAssetCategoryName)
|
||||||
String? vendor,
|
String? assetCategoryName,
|
||||||
@JsonKey(name: 'warranty_start') DateTime? warrantyStart,
|
@JsonKey(name: 'plant_id', readValue: _readPlantId, fromJson: _intFromJsonNullable)
|
||||||
@JsonKey(name: 'warranty_end') DateTime? warrantyEnd,
|
int? plantId,
|
||||||
@Default('available') String status,
|
@JsonKey(name: 'plant_name', readValue: _readPlantName) String? plantName,
|
||||||
@JsonKey(name: 'branch_id') String? branchId,
|
@JsonKey(name: 'warranty_expiry_date', fromJson: _dateFromJsonNullable)
|
||||||
@JsonKey(name: 'branch_name') String? branchName,
|
DateTime? warrantyExpiryDate,
|
||||||
@JsonKey(name: 'company_id') String? companyId,
|
@JsonKey(name: 'purchase_cost', fromJson: _doubleFromJsonNullable) double? purchaseCost,
|
||||||
@JsonKey(name: 'qr_code_url') String? qrCodeUrl,
|
String? status,
|
||||||
DateTime? createdAt,
|
@JsonKey(name: 'is_active') @Default(true) bool isActive,
|
||||||
DateTime? updatedAt,
|
@JsonKey(name: 'created_at', fromJson: _dateFromJsonNullable) DateTime? createdAt,
|
||||||
|
@JsonKey(name: 'updated_at', fromJson: _dateFromJsonNullable) DateTime? updatedAt,
|
||||||
}) = _AssetModel;
|
}) = _AssetModel;
|
||||||
|
|
||||||
factory AssetModel.fromJson(Map<String, dynamic> json) => _$AssetModelFromJson(json);
|
factory AssetModel.fromJson(Map<String, dynamic> json) => _$AssetModelFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class AssetAllocationModel with _$AssetAllocationModel {
|
class AmcContractModel with _$AmcContractModel {
|
||||||
const factory AssetAllocationModel({
|
const factory AmcContractModel({
|
||||||
required String id,
|
@JsonKey(fromJson: _idFromJson) required String id,
|
||||||
@JsonKey(name: 'asset_id') required String assetId,
|
@JsonKey(name: 'asset_id', fromJson: _idFromJson) String? assetId,
|
||||||
@JsonKey(name: 'asset_name') String? assetName,
|
@JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? vendorId,
|
||||||
@JsonKey(name: 'employee_id') required String employeeId,
|
@JsonKey(name: 'vendor_name') String? vendorName,
|
||||||
@JsonKey(name: 'employee_name') String? employeeName,
|
@JsonKey(name: 'contract_no') String? contractNo,
|
||||||
@JsonKey(name: 'assigned_date') required DateTime assignedDate,
|
@JsonKey(name: 'contract_type') String? contractType,
|
||||||
@JsonKey(name: 'returned_date') DateTime? returnedDate,
|
@JsonKey(name: 'start_date', fromJson: _dateFromJson) required DateTime startDate,
|
||||||
String? remarks,
|
@JsonKey(name: 'end_date', fromJson: _dateFromJson) required DateTime endDate,
|
||||||
|
@JsonKey(name: 'annual_cost', fromJson: _doubleFromJsonNullable) double? annualCost,
|
||||||
@Default('active') String status,
|
@Default('active') String status,
|
||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
}) = _AssetAllocationModel;
|
}) = _AmcContractModel;
|
||||||
|
|
||||||
factory AssetAllocationModel.fromJson(Map<String, dynamic> json) =>
|
factory AmcContractModel.fromJson(Map<String, dynamic> json) =>
|
||||||
_$AssetAllocationModelFromJson(json);
|
_$AmcContractModelFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class AssetMaintenanceModel with _$AssetMaintenanceModel {
|
class ServiceVisitModel with _$ServiceVisitModel {
|
||||||
const factory AssetMaintenanceModel({
|
const factory ServiceVisitModel({
|
||||||
required String id,
|
@JsonKey(fromJson: _idFromJson) required String id,
|
||||||
@JsonKey(name: 'asset_id') required String assetId,
|
@JsonKey(name: 'asset_id', fromJson: _idFromJson) String? assetId,
|
||||||
@JsonKey(name: 'asset_name') String? assetName,
|
@JsonKey(name: 'visit_type') required String visitType,
|
||||||
required String description,
|
@JsonKey(name: 'visit_date', fromJson: _dateFromJson) required DateTime visitDate,
|
||||||
@JsonKey(name: 'service_vendor') String? serviceVendor,
|
@JsonKey(name: 'complaint_no') String? complaintNo,
|
||||||
double? cost,
|
@JsonKey(name: 'work_done') String? workDone,
|
||||||
@Default('open') String status,
|
@JsonKey(name: 'next_service_date', fromJson: _dateFromJsonNullable)
|
||||||
@JsonKey(name: 'requested_by') String? requestedBy,
|
DateTime? nextServiceDate,
|
||||||
@JsonKey(name: 'completed_date') DateTime? completedDate,
|
@Default('COMPLETED') String status,
|
||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
DateTime? updatedAt,
|
}) = _ServiceVisitModel;
|
||||||
}) = _AssetMaintenanceModel;
|
|
||||||
|
|
||||||
factory AssetMaintenanceModel.fromJson(Map<String, dynamic> json) =>
|
factory ServiceVisitModel.fromJson(Map<String, dynamic> json) =>
|
||||||
_$AssetMaintenanceModelFromJson(json);
|
_$ServiceVisitModelFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class AssetDisposalModel with _$AssetDisposalModel {
|
class InsurancePolicyModel with _$InsurancePolicyModel {
|
||||||
const factory AssetDisposalModel({
|
const factory InsurancePolicyModel({
|
||||||
required String id,
|
@JsonKey(fromJson: _idFromJson) required String id,
|
||||||
@JsonKey(name: 'asset_id') required String assetId,
|
@JsonKey(name: 'asset_id', fromJson: _idFromJson) String? assetId,
|
||||||
@JsonKey(name: 'asset_name') String? assetName,
|
@JsonKey(name: 'policy_no') required String policyNo,
|
||||||
@JsonKey(name: 'disposal_reason') required String disposalReason,
|
@JsonKey(name: 'insurer_name') required String insurerName,
|
||||||
@JsonKey(name: 'disposal_date') DateTime? disposalDate,
|
@JsonKey(name: 'policy_type') String? policyType,
|
||||||
@Default('pending') String status,
|
@JsonKey(name: 'sum_insured', fromJson: _doubleFromJsonNullable) double? sumInsured,
|
||||||
@JsonKey(name: 'requested_by') String? requestedBy,
|
@JsonKey(name: 'policy_start_date', fromJson: _dateFromJson)
|
||||||
@JsonKey(name: 'approved_by') String? approvedBy,
|
required DateTime policyStartDate,
|
||||||
|
@JsonKey(name: 'policy_end_date', fromJson: _dateFromJson) required DateTime policyEndDate,
|
||||||
|
@Default('active') String status,
|
||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
DateTime? updatedAt,
|
}) = _InsurancePolicyModel;
|
||||||
}) = _AssetDisposalModel;
|
|
||||||
|
|
||||||
factory AssetDisposalModel.fromJson(Map<String, dynamic> json) =>
|
factory InsurancePolicyModel.fromJson(Map<String, dynamic> json) =>
|
||||||
_$AssetDisposalModelFromJson(json);
|
_$InsurancePolicyModelFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class AssetAlertModel with _$AssetAlertModel {
|
||||||
|
const factory AssetAlertModel({
|
||||||
|
@JsonKey(fromJson: _idFromJson) required String id,
|
||||||
|
@JsonKey(name: 'asset_id', fromJson: _idFromJson) String? assetId,
|
||||||
|
@JsonKey(name: 'asset_name') String? assetName,
|
||||||
|
@JsonKey(name: 'asset_code') String? assetCode,
|
||||||
|
String? type,
|
||||||
|
String? title,
|
||||||
|
String? message,
|
||||||
|
@JsonKey(name: 'expiry_date', fromJson: _dateFromJsonNullable) DateTime? expiryDate,
|
||||||
|
@JsonKey(name: 'due_date', fromJson: _dateFromJsonNullable) DateTime? dueDate,
|
||||||
|
@JsonKey(name: 'days_remaining', fromJson: _intFromJsonNullable) int? daysRemaining,
|
||||||
|
String? status,
|
||||||
|
@JsonKey(name: 'plant_name') String? plantName,
|
||||||
|
}) = _AssetAlertModel;
|
||||||
|
|
||||||
|
factory AssetAlertModel.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$AssetAlertModelFromJson(json);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -9,189 +9,200 @@ part of 'asset_model.dart';
|
|||||||
_$AssetCategoryModelImpl _$$AssetCategoryModelImplFromJson(
|
_$AssetCategoryModelImpl _$$AssetCategoryModelImplFromJson(
|
||||||
Map<String, dynamic> json,
|
Map<String, dynamic> json,
|
||||||
) => _$AssetCategoryModelImpl(
|
) => _$AssetCategoryModelImpl(
|
||||||
id: json['id'] as String,
|
id: _idFromJson(json['id']),
|
||||||
|
code: json['code'] as String,
|
||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
slug: json['slug'] as String,
|
codePrefix: json['code_prefix'] as String?,
|
||||||
description: json['description'] as String?,
|
defaultUsefulLifeYears: _intFromJsonNullable(
|
||||||
companyId: json['company_id'] as String?,
|
json['default_useful_life_years'],
|
||||||
|
),
|
||||||
|
defaultDepreciationMethod: json['default_depreciation_method'] as String?,
|
||||||
|
isActive: json['is_active'] as bool? ?? true,
|
||||||
createdAt: json['createdAt'] == null
|
createdAt: json['createdAt'] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.parse(json['createdAt'] as String),
|
: DateTime.parse(json['createdAt'] as String),
|
||||||
|
updatedAt: json['updatedAt'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['updatedAt'] as String),
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$$AssetCategoryModelImplToJson(
|
Map<String, dynamic> _$$AssetCategoryModelImplToJson(
|
||||||
_$AssetCategoryModelImpl instance,
|
_$AssetCategoryModelImpl instance,
|
||||||
) => <String, dynamic>{
|
) => <String, dynamic>{
|
||||||
'id': instance.id,
|
'id': instance.id,
|
||||||
|
'code': instance.code,
|
||||||
'name': instance.name,
|
'name': instance.name,
|
||||||
'slug': instance.slug,
|
'code_prefix': instance.codePrefix,
|
||||||
'description': instance.description,
|
'default_useful_life_years': instance.defaultUsefulLifeYears,
|
||||||
'company_id': instance.companyId,
|
'default_depreciation_method': instance.defaultDepreciationMethod,
|
||||||
|
'is_active': instance.isActive,
|
||||||
'createdAt': instance.createdAt?.toIso8601String(),
|
'createdAt': instance.createdAt?.toIso8601String(),
|
||||||
|
'updatedAt': instance.updatedAt?.toIso8601String(),
|
||||||
};
|
};
|
||||||
|
|
||||||
_$AssetModelImpl _$$AssetModelImplFromJson(Map<String, dynamic> json) =>
|
_$AssetModelImpl _$$AssetModelImplFromJson(Map<String, dynamic> json) =>
|
||||||
_$AssetModelImpl(
|
_$AssetModelImpl(
|
||||||
id: json['id'] as String,
|
id: _idFromJson(json['id']),
|
||||||
name: json['name'] as String,
|
assetName: json['asset_name'] as String,
|
||||||
assetCode: json['asset_code'] as String,
|
assetCode: json['asset_code'] as String?,
|
||||||
categoryId: json['category_id'] as String,
|
assetCategoryId: _intFromJsonNullable(
|
||||||
categoryName: json['category_name'] as String?,
|
_readAssetCategoryId(json, 'asset_category_id'),
|
||||||
brand: json['brand'] as String?,
|
),
|
||||||
model: json['model'] as String?,
|
assetCategoryName:
|
||||||
serialNumber: json['serial_number'] as String?,
|
_readAssetCategoryName(json, 'asset_category_name') as String?,
|
||||||
purchaseDate: json['purchase_date'] == null
|
plantId: _intFromJsonNullable(_readPlantId(json, 'plant_id')),
|
||||||
? null
|
plantName: _readPlantName(json, 'plant_name') as String?,
|
||||||
: DateTime.parse(json['purchase_date'] as String),
|
warrantyExpiryDate: _dateFromJsonNullable(json['warranty_expiry_date']),
|
||||||
purchaseCost: (json['purchase_cost'] as num?)?.toDouble(),
|
purchaseCost: _doubleFromJsonNullable(json['purchase_cost']),
|
||||||
vendor: json['vendor'] as String?,
|
status: json['status'] as String?,
|
||||||
warrantyStart: json['warranty_start'] == null
|
isActive: json['is_active'] as bool? ?? true,
|
||||||
? null
|
createdAt: _dateFromJsonNullable(json['created_at']),
|
||||||
: DateTime.parse(json['warranty_start'] as String),
|
updatedAt: _dateFromJsonNullable(json['updated_at']),
|
||||||
warrantyEnd: json['warranty_end'] == null
|
|
||||||
? null
|
|
||||||
: DateTime.parse(json['warranty_end'] as String),
|
|
||||||
status: json['status'] as String? ?? 'available',
|
|
||||||
branchId: json['branch_id'] as String?,
|
|
||||||
branchName: json['branch_name'] as String?,
|
|
||||||
companyId: json['company_id'] as String?,
|
|
||||||
qrCodeUrl: json['qr_code_url'] as String?,
|
|
||||||
createdAt: json['createdAt'] == null
|
|
||||||
? null
|
|
||||||
: DateTime.parse(json['createdAt'] as String),
|
|
||||||
updatedAt: json['updatedAt'] == null
|
|
||||||
? null
|
|
||||||
: DateTime.parse(json['updatedAt'] as String),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$$AssetModelImplToJson(_$AssetModelImpl instance) =>
|
Map<String, dynamic> _$$AssetModelImplToJson(_$AssetModelImpl instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'id': instance.id,
|
'id': instance.id,
|
||||||
'name': instance.name,
|
'asset_name': instance.assetName,
|
||||||
'asset_code': instance.assetCode,
|
'asset_code': instance.assetCode,
|
||||||
'category_id': instance.categoryId,
|
'asset_category_id': instance.assetCategoryId,
|
||||||
'category_name': instance.categoryName,
|
'asset_category_name': instance.assetCategoryName,
|
||||||
'brand': instance.brand,
|
'plant_id': instance.plantId,
|
||||||
'model': instance.model,
|
'plant_name': instance.plantName,
|
||||||
'serial_number': instance.serialNumber,
|
'warranty_expiry_date': instance.warrantyExpiryDate?.toIso8601String(),
|
||||||
'purchase_date': instance.purchaseDate?.toIso8601String(),
|
|
||||||
'purchase_cost': instance.purchaseCost,
|
'purchase_cost': instance.purchaseCost,
|
||||||
'vendor': instance.vendor,
|
|
||||||
'warranty_start': instance.warrantyStart?.toIso8601String(),
|
|
||||||
'warranty_end': instance.warrantyEnd?.toIso8601String(),
|
|
||||||
'status': instance.status,
|
'status': instance.status,
|
||||||
'branch_id': instance.branchId,
|
'is_active': instance.isActive,
|
||||||
'branch_name': instance.branchName,
|
'created_at': instance.createdAt?.toIso8601String(),
|
||||||
'company_id': instance.companyId,
|
'updated_at': instance.updatedAt?.toIso8601String(),
|
||||||
'qr_code_url': instance.qrCodeUrl,
|
|
||||||
'createdAt': instance.createdAt?.toIso8601String(),
|
|
||||||
'updatedAt': instance.updatedAt?.toIso8601String(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
_$AssetAllocationModelImpl _$$AssetAllocationModelImplFromJson(
|
_$AmcContractModelImpl _$$AmcContractModelImplFromJson(
|
||||||
Map<String, dynamic> json,
|
Map<String, dynamic> json,
|
||||||
) => _$AssetAllocationModelImpl(
|
) => _$AmcContractModelImpl(
|
||||||
id: json['id'] as String,
|
id: _idFromJson(json['id']),
|
||||||
assetId: json['asset_id'] as String,
|
assetId: _idFromJson(json['asset_id']),
|
||||||
assetName: json['asset_name'] as String?,
|
vendorId: _intFromJsonNullable(json['vendor_id']),
|
||||||
employeeId: json['employee_id'] as String,
|
vendorName: json['vendor_name'] as String?,
|
||||||
employeeName: json['employee_name'] as String?,
|
contractNo: json['contract_no'] as String?,
|
||||||
assignedDate: DateTime.parse(json['assigned_date'] as String),
|
contractType: json['contract_type'] as String?,
|
||||||
returnedDate: json['returned_date'] == null
|
startDate: _dateFromJson(json['start_date']),
|
||||||
? null
|
endDate: _dateFromJson(json['end_date']),
|
||||||
: DateTime.parse(json['returned_date'] as String),
|
annualCost: _doubleFromJsonNullable(json['annual_cost']),
|
||||||
remarks: json['remarks'] as String?,
|
|
||||||
status: json['status'] as String? ?? 'active',
|
status: json['status'] as String? ?? 'active',
|
||||||
createdAt: json['createdAt'] == null
|
createdAt: json['createdAt'] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.parse(json['createdAt'] as String),
|
: DateTime.parse(json['createdAt'] as String),
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$$AssetAllocationModelImplToJson(
|
Map<String, dynamic> _$$AmcContractModelImplToJson(
|
||||||
_$AssetAllocationModelImpl instance,
|
_$AmcContractModelImpl instance,
|
||||||
) => <String, dynamic>{
|
) => <String, dynamic>{
|
||||||
'id': instance.id,
|
'id': instance.id,
|
||||||
'asset_id': instance.assetId,
|
'asset_id': instance.assetId,
|
||||||
'asset_name': instance.assetName,
|
'vendor_id': instance.vendorId,
|
||||||
'employee_id': instance.employeeId,
|
'vendor_name': instance.vendorName,
|
||||||
'employee_name': instance.employeeName,
|
'contract_no': instance.contractNo,
|
||||||
'assigned_date': instance.assignedDate.toIso8601String(),
|
'contract_type': instance.contractType,
|
||||||
'returned_date': instance.returnedDate?.toIso8601String(),
|
'start_date': instance.startDate.toIso8601String(),
|
||||||
'remarks': instance.remarks,
|
'end_date': instance.endDate.toIso8601String(),
|
||||||
|
'annual_cost': instance.annualCost,
|
||||||
'status': instance.status,
|
'status': instance.status,
|
||||||
'createdAt': instance.createdAt?.toIso8601String(),
|
'createdAt': instance.createdAt?.toIso8601String(),
|
||||||
};
|
};
|
||||||
|
|
||||||
_$AssetMaintenanceModelImpl _$$AssetMaintenanceModelImplFromJson(
|
_$ServiceVisitModelImpl _$$ServiceVisitModelImplFromJson(
|
||||||
Map<String, dynamic> json,
|
Map<String, dynamic> json,
|
||||||
) => _$AssetMaintenanceModelImpl(
|
) => _$ServiceVisitModelImpl(
|
||||||
id: json['id'] as String,
|
id: _idFromJson(json['id']),
|
||||||
assetId: json['asset_id'] as String,
|
assetId: _idFromJson(json['asset_id']),
|
||||||
assetName: json['asset_name'] as String?,
|
visitType: json['visit_type'] as String,
|
||||||
description: json['description'] as String,
|
visitDate: _dateFromJson(json['visit_date']),
|
||||||
serviceVendor: json['service_vendor'] as String?,
|
complaintNo: json['complaint_no'] as String?,
|
||||||
cost: (json['cost'] as num?)?.toDouble(),
|
workDone: json['work_done'] as String?,
|
||||||
status: json['status'] as String? ?? 'open',
|
nextServiceDate: _dateFromJsonNullable(json['next_service_date']),
|
||||||
requestedBy: json['requested_by'] as String?,
|
status: json['status'] as String? ?? 'COMPLETED',
|
||||||
completedDate: json['completed_date'] == null
|
|
||||||
? null
|
|
||||||
: DateTime.parse(json['completed_date'] as String),
|
|
||||||
createdAt: json['createdAt'] == null
|
createdAt: json['createdAt'] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.parse(json['createdAt'] as String),
|
: DateTime.parse(json['createdAt'] as String),
|
||||||
updatedAt: json['updatedAt'] == null
|
|
||||||
? null
|
|
||||||
: DateTime.parse(json['updatedAt'] as String),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$$AssetMaintenanceModelImplToJson(
|
Map<String, dynamic> _$$ServiceVisitModelImplToJson(
|
||||||
_$AssetMaintenanceModelImpl instance,
|
_$ServiceVisitModelImpl instance,
|
||||||
) => <String, dynamic>{
|
) => <String, dynamic>{
|
||||||
'id': instance.id,
|
'id': instance.id,
|
||||||
'asset_id': instance.assetId,
|
'asset_id': instance.assetId,
|
||||||
'asset_name': instance.assetName,
|
'visit_type': instance.visitType,
|
||||||
'description': instance.description,
|
'visit_date': instance.visitDate.toIso8601String(),
|
||||||
'service_vendor': instance.serviceVendor,
|
'complaint_no': instance.complaintNo,
|
||||||
'cost': instance.cost,
|
'work_done': instance.workDone,
|
||||||
|
'next_service_date': instance.nextServiceDate?.toIso8601String(),
|
||||||
'status': instance.status,
|
'status': instance.status,
|
||||||
'requested_by': instance.requestedBy,
|
|
||||||
'completed_date': instance.completedDate?.toIso8601String(),
|
|
||||||
'createdAt': instance.createdAt?.toIso8601String(),
|
'createdAt': instance.createdAt?.toIso8601String(),
|
||||||
'updatedAt': instance.updatedAt?.toIso8601String(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
_$AssetDisposalModelImpl _$$AssetDisposalModelImplFromJson(
|
_$InsurancePolicyModelImpl _$$InsurancePolicyModelImplFromJson(
|
||||||
Map<String, dynamic> json,
|
Map<String, dynamic> json,
|
||||||
) => _$AssetDisposalModelImpl(
|
) => _$InsurancePolicyModelImpl(
|
||||||
id: json['id'] as String,
|
id: _idFromJson(json['id']),
|
||||||
assetId: json['asset_id'] as String,
|
assetId: _idFromJson(json['asset_id']),
|
||||||
assetName: json['asset_name'] as String?,
|
policyNo: json['policy_no'] as String,
|
||||||
disposalReason: json['disposal_reason'] as String,
|
insurerName: json['insurer_name'] as String,
|
||||||
disposalDate: json['disposal_date'] == null
|
policyType: json['policy_type'] as String?,
|
||||||
? null
|
sumInsured: _doubleFromJsonNullable(json['sum_insured']),
|
||||||
: DateTime.parse(json['disposal_date'] as String),
|
policyStartDate: _dateFromJson(json['policy_start_date']),
|
||||||
status: json['status'] as String? ?? 'pending',
|
policyEndDate: _dateFromJson(json['policy_end_date']),
|
||||||
requestedBy: json['requested_by'] as String?,
|
status: json['status'] as String? ?? 'active',
|
||||||
approvedBy: json['approved_by'] as String?,
|
|
||||||
createdAt: json['createdAt'] == null
|
createdAt: json['createdAt'] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.parse(json['createdAt'] as String),
|
: DateTime.parse(json['createdAt'] as String),
|
||||||
updatedAt: json['updatedAt'] == null
|
|
||||||
? null
|
|
||||||
: DateTime.parse(json['updatedAt'] as String),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$$AssetDisposalModelImplToJson(
|
Map<String, dynamic> _$$InsurancePolicyModelImplToJson(
|
||||||
_$AssetDisposalModelImpl instance,
|
_$InsurancePolicyModelImpl instance,
|
||||||
|
) => <String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'asset_id': instance.assetId,
|
||||||
|
'policy_no': instance.policyNo,
|
||||||
|
'insurer_name': instance.insurerName,
|
||||||
|
'policy_type': instance.policyType,
|
||||||
|
'sum_insured': instance.sumInsured,
|
||||||
|
'policy_start_date': instance.policyStartDate.toIso8601String(),
|
||||||
|
'policy_end_date': instance.policyEndDate.toIso8601String(),
|
||||||
|
'status': instance.status,
|
||||||
|
'createdAt': instance.createdAt?.toIso8601String(),
|
||||||
|
};
|
||||||
|
|
||||||
|
_$AssetAlertModelImpl _$$AssetAlertModelImplFromJson(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
) => _$AssetAlertModelImpl(
|
||||||
|
id: _idFromJson(json['id']),
|
||||||
|
assetId: _idFromJson(json['asset_id']),
|
||||||
|
assetName: json['asset_name'] as String?,
|
||||||
|
assetCode: json['asset_code'] as String?,
|
||||||
|
type: json['type'] as String?,
|
||||||
|
title: json['title'] as String?,
|
||||||
|
message: json['message'] as String?,
|
||||||
|
expiryDate: _dateFromJsonNullable(json['expiry_date']),
|
||||||
|
dueDate: _dateFromJsonNullable(json['due_date']),
|
||||||
|
daysRemaining: _intFromJsonNullable(json['days_remaining']),
|
||||||
|
status: json['status'] as String?,
|
||||||
|
plantName: json['plant_name'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$AssetAlertModelImplToJson(
|
||||||
|
_$AssetAlertModelImpl instance,
|
||||||
) => <String, dynamic>{
|
) => <String, dynamic>{
|
||||||
'id': instance.id,
|
'id': instance.id,
|
||||||
'asset_id': instance.assetId,
|
'asset_id': instance.assetId,
|
||||||
'asset_name': instance.assetName,
|
'asset_name': instance.assetName,
|
||||||
'disposal_reason': instance.disposalReason,
|
'asset_code': instance.assetCode,
|
||||||
'disposal_date': instance.disposalDate?.toIso8601String(),
|
'type': instance.type,
|
||||||
|
'title': instance.title,
|
||||||
|
'message': instance.message,
|
||||||
|
'expiry_date': instance.expiryDate?.toIso8601String(),
|
||||||
|
'due_date': instance.dueDate?.toIso8601String(),
|
||||||
|
'days_remaining': instance.daysRemaining,
|
||||||
'status': instance.status,
|
'status': instance.status,
|
||||||
'requested_by': instance.requestedBy,
|
'plant_name': instance.plantName,
|
||||||
'approved_by': instance.approvedBy,
|
|
||||||
'createdAt': instance.createdAt?.toIso8601String(),
|
|
||||||
'updatedAt': instance.updatedAt?.toIso8601String(),
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,15 +5,10 @@ import 'package:go_router/go_router.dart';
|
|||||||
import '../../core/config/dev_config.dart';
|
import '../../core/config/dev_config.dart';
|
||||||
import '../../core/constants/route_constants.dart';
|
import '../../core/constants/route_constants.dart';
|
||||||
import '../../modules/dashboard/presentation/screens/dashboard_screen.dart';
|
import '../../modules/dashboard/presentation/screens/dashboard_screen.dart';
|
||||||
import '../../modules/assets/presentation/screens/asset_allocations_screen.dart';
|
import '../../modules/assets/presentation/screens/asset_alerts_screen.dart';
|
||||||
import '../../modules/assets/presentation/screens/asset_categories_screen.dart';
|
import '../../modules/assets/presentation/screens/asset_categories_screen.dart';
|
||||||
import '../../modules/assets/presentation/screens/asset_detail_screen.dart';
|
import '../../modules/assets/presentation/screens/asset_detail_screen.dart';
|
||||||
import '../../modules/assets/presentation/screens/asset_disposal_screen.dart';
|
|
||||||
import '../../modules/assets/presentation/screens/asset_form_screen.dart';
|
|
||||||
import '../../modules/assets/presentation/screens/asset_list_screen.dart';
|
import '../../modules/assets/presentation/screens/asset_list_screen.dart';
|
||||||
import '../../modules/assets/presentation/screens/asset_maintenance_screen.dart';
|
|
||||||
import '../../modules/assets/presentation/screens/asset_qr_generate_screen.dart';
|
|
||||||
import '../../modules/assets/presentation/screens/asset_qr_scan_screen.dart';
|
|
||||||
import '../../modules/auth/presentation/screens/change_password_screen.dart';
|
import '../../modules/auth/presentation/screens/change_password_screen.dart';
|
||||||
import '../../modules/auth/presentation/screens/forgot_password_screen.dart';
|
import '../../modules/auth/presentation/screens/forgot_password_screen.dart';
|
||||||
import '../../modules/auth/presentation/screens/login_screen.dart';
|
import '../../modules/auth/presentation/screens/login_screen.dart';
|
||||||
@ -210,44 +205,19 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
pageBuilder: (context, state) =>
|
pageBuilder: (context, state) =>
|
||||||
shellPage(state, const AssetListScreen()),
|
shellPage(state, const AssetListScreen()),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
|
||||||
path: 'add',
|
|
||||||
builder: (context, state) => const AssetFormScreen(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'categories',
|
path: 'categories',
|
||||||
builder: (context, state) => const AssetCategoriesScreen(),
|
builder: (context, state) => const AssetCategoriesScreen(),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'allocations',
|
path: 'alerts',
|
||||||
builder: (context, state) => const AssetAllocationsScreen(),
|
builder: (context, state) => const AssetAlertsScreen(),
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: 'maintenance',
|
|
||||||
builder: (context, state) => const AssetMaintenanceScreen(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: 'disposal',
|
|
||||||
builder: (context, state) => const AssetDisposalScreen(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: 'qr-scan',
|
|
||||||
builder: (context, state) => const AssetQrScanScreen(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: 'qr-generate',
|
|
||||||
builder: (context, state) => const AssetQrGenerateScreen(),
|
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: ':id',
|
path: ':id',
|
||||||
builder: (context, state) =>
|
builder: (context, state) =>
|
||||||
AssetDetailScreen(assetId: state.pathParameters['id']!),
|
AssetDetailScreen(assetId: state.pathParameters['id']!),
|
||||||
),
|
),
|
||||||
GoRoute(
|
|
||||||
path: ':id/edit',
|
|
||||||
builder: (context, state) =>
|
|
||||||
AssetFormScreen(assetId: state.pathParameters['id']),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
|
|||||||
@ -67,33 +67,9 @@ const List<MenuItem> appMenuItems = [
|
|||||||
module: 'asset_categories',
|
module: 'asset_categories',
|
||||||
),
|
),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
label: 'Allocations',
|
label: 'Alerts',
|
||||||
icon: Icons.assignment_ind_outlined,
|
icon: Icons.notifications_outlined,
|
||||||
route: RouteConstants.assetAllocations,
|
route: RouteConstants.assetAlerts,
|
||||||
module: 'asset_allocations',
|
|
||||||
),
|
|
||||||
MenuItem(
|
|
||||||
label: 'Maintenance',
|
|
||||||
icon: Icons.build_outlined,
|
|
||||||
route: RouteConstants.assetMaintenance,
|
|
||||||
module: 'asset_maintenance',
|
|
||||||
),
|
|
||||||
MenuItem(
|
|
||||||
label: 'Disposal',
|
|
||||||
icon: Icons.delete_outline,
|
|
||||||
route: RouteConstants.assetDisposal,
|
|
||||||
module: 'asset_disposal',
|
|
||||||
),
|
|
||||||
MenuItem(
|
|
||||||
label: 'QR Scan',
|
|
||||||
icon: Icons.qr_code_scanner_outlined,
|
|
||||||
route: RouteConstants.assetQrScan,
|
|
||||||
module: 'assets',
|
|
||||||
),
|
|
||||||
MenuItem(
|
|
||||||
label: 'QR Generate',
|
|
||||||
icon: Icons.qr_code_2_outlined,
|
|
||||||
route: RouteConstants.assetQrGenerate,
|
|
||||||
module: 'assets',
|
module: 'assets',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -49,26 +49,46 @@ class AppSidebar extends ConsumerStatefulWidget {
|
|||||||
|
|
||||||
class _AppSidebarState extends ConsumerState<AppSidebar> {
|
class _AppSidebarState extends ConsumerState<AppSidebar> {
|
||||||
final Set<String> _expandedMenus = <String>{};
|
final Set<String> _expandedMenus = <String>{};
|
||||||
|
final Set<String> _manuallyCollapsedMenus = <String>{};
|
||||||
|
|
||||||
List<menu.MenuItem> get _mainMenuItems =>
|
List<menu.MenuItem> get _mainMenuItems =>
|
||||||
widget.menuItems.where((item) => item.route != RouteConstants.settings).toList();
|
widget.menuItems.where((item) => item.route != RouteConstants.settings).toList();
|
||||||
|
|
||||||
bool _isSelected(String route) {
|
bool _routeMatches(String route) {
|
||||||
if (route == '/') return widget.currentRoute == route;
|
final current = widget.currentRoute;
|
||||||
return widget.currentRoute.startsWith(route);
|
if (route == '/') return current == route;
|
||||||
|
if (current == route) return true;
|
||||||
|
return current.startsWith('$route/');
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _isGroupActive(menu.MenuItem item) =>
|
bool _isSelected(String route) => _routeMatches(route);
|
||||||
item.children.any((child) => _isSelected(child.route));
|
|
||||||
|
|
||||||
bool _isGroupExpanded(menu.MenuItem item) =>
|
bool _isSelectedAmongSiblings(String route, List<menu.MenuItem> siblings) {
|
||||||
_expandedMenus.contains(item.route) || _isGroupActive(item);
|
if (!_routeMatches(route)) return false;
|
||||||
|
for (final sibling in siblings) {
|
||||||
|
if (sibling.route == route) continue;
|
||||||
|
if (sibling.route.length > route.length && _routeMatches(sibling.route)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isGroupActive(menu.MenuItem item) => item.children
|
||||||
|
.any((child) => _isSelectedAmongSiblings(child.route, item.children));
|
||||||
|
|
||||||
|
bool _isGroupExpanded(menu.MenuItem item) {
|
||||||
|
if (_manuallyCollapsedMenus.contains(item.route)) return false;
|
||||||
|
return _expandedMenus.contains(item.route) || _isGroupActive(item);
|
||||||
|
}
|
||||||
|
|
||||||
void _toggleGroup(menu.MenuItem item) {
|
void _toggleGroup(menu.MenuItem item) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (_expandedMenus.contains(item.route)) {
|
if (_isGroupExpanded(item)) {
|
||||||
_expandedMenus.remove(item.route);
|
_expandedMenus.remove(item.route);
|
||||||
|
_manuallyCollapsedMenus.add(item.route);
|
||||||
} else {
|
} else {
|
||||||
|
_manuallyCollapsedMenus.remove(item.route);
|
||||||
_expandedMenus.add(item.route);
|
_expandedMenus.add(item.route);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -81,6 +101,7 @@ class _AppSidebarState extends ConsumerState<AppSidebar> {
|
|||||||
for (final item in _mainMenuItems) {
|
for (final item in _mainMenuItems) {
|
||||||
if (item.children.isNotEmpty && _isGroupActive(item)) {
|
if (item.children.isNotEmpty && _isGroupActive(item)) {
|
||||||
_expandedMenus.add(item.route);
|
_expandedMenus.add(item.route);
|
||||||
|
_manuallyCollapsedMenus.remove(item.route);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -148,7 +169,8 @@ class _AppSidebarState extends ConsumerState<AppSidebar> {
|
|||||||
return _CollapsedFlyoutNavItem(
|
return _CollapsedFlyoutNavItem(
|
||||||
item: item,
|
item: item,
|
||||||
selected: _isGroupActive(item),
|
selected: _isGroupActive(item),
|
||||||
isChildSelected: _isSelected,
|
isChildSelected: (route) =>
|
||||||
|
_isSelectedAmongSiblings(route, item.children),
|
||||||
onChildTap: widget.onItemTap,
|
onChildTap: widget.onItemTap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -173,7 +195,10 @@ class _AppSidebarState extends ConsumerState<AppSidebar> {
|
|||||||
(child) => _SidebarNavItem(
|
(child) => _SidebarNavItem(
|
||||||
icon: child.icon,
|
icon: child.icon,
|
||||||
label: child.label,
|
label: child.label,
|
||||||
selected: _isSelected(child.route),
|
selected: _isSelectedAmongSiblings(
|
||||||
|
child.route,
|
||||||
|
item.children,
|
||||||
|
),
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
indent: _sidebarChildIndent,
|
indent: _sidebarChildIndent,
|
||||||
onTap: () => widget.onItemTap(child.route),
|
onTap: () => widget.onItemTap(child.route),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user