Assets, Vendor, User
This commit is contained in:
parent
b23a74442f
commit
3e00b478e7
@ -40,6 +40,8 @@ class ApiEndpoints {
|
||||
// Asset Categories
|
||||
static const String assetCategories = '/masters/asset-categories';
|
||||
static String assetCategoryById(String id) => '/masters/asset-categories/$id';
|
||||
static const String assetSubcategories = '/masters/asset-subcategories';
|
||||
static String assetSubcategoryById(String id) => '/masters/asset-subcategories/$id';
|
||||
|
||||
// Masters
|
||||
static const String departments = '/masters/departments';
|
||||
@ -72,6 +74,8 @@ class ApiEndpoints {
|
||||
|
||||
// Vendors
|
||||
static const String vendors = '/vendors';
|
||||
static const String vendorGstTreatments = '/vendors/gst-treatments';
|
||||
static const String vendorSourceOfSupply = '/vendors/source-of-supply';
|
||||
static String vendorById(String id) => '/vendors/$id';
|
||||
static String vendorStatus(String id) => '/vendors/$id/status';
|
||||
static String vendorAddresses(String vendorId) => '/vendors/$vendorId/addresses';
|
||||
@ -84,6 +88,9 @@ class ApiEndpoints {
|
||||
'/vendors/$vendorId/bank-details';
|
||||
static String vendorBankDetailById(String vendorId, String bankDetailId) =>
|
||||
'/vendors/$vendorId/bank-details/$bankDetailId';
|
||||
static String vendorItems(String vendorId) => '/vendors/$vendorId/items';
|
||||
static String vendorItemById(String vendorId, String mappingId) =>
|
||||
'/vendors/$vendorId/items/$mappingId';
|
||||
|
||||
// Purchase Orders
|
||||
static const String purchaseOrders = '/purchase-orders';
|
||||
@ -105,13 +112,22 @@ class ApiEndpoints {
|
||||
static const String assets = '/assets';
|
||||
static String assetById(String id) => '/assets/$id';
|
||||
static String assetTransfer(String id) => '/assets/$id/transfer';
|
||||
static String assetTransferHistory(String id) => '/assets/$id/transfer-history';
|
||||
static const String assetDepreciationMethods = '/assets/depreciation-methods';
|
||||
static const String assetDepreciationCalculate = '/assets/depreciation/calculate';
|
||||
static const String assetAlertsExpiry = '/assets/alerts/expiry';
|
||||
static const String assetAlertsService = '/assets/alerts/service';
|
||||
static String assetAmc(String assetId) => '/assets/$assetId/amc';
|
||||
static String assetAmcById(String assetId, String contractId) =>
|
||||
'/assets/$assetId/amc/$contractId';
|
||||
static String assetAmcRenew(String assetId, String contractId) =>
|
||||
'/assets/$assetId/amc/$contractId/renew';
|
||||
static String assetServiceVisits(String assetId) => '/assets/$assetId/service-visits';
|
||||
static String assetServiceVisitById(String assetId, String visitId) =>
|
||||
'/assets/$assetId/service-visits/$visitId';
|
||||
static String assetInsurance(String assetId) => '/assets/$assetId/insurance';
|
||||
static String assetInsuranceById(String assetId, String policyId) =>
|
||||
'/assets/$assetId/insurance/$policyId';
|
||||
static String assetInsuranceRenew(String assetId, String policyId) =>
|
||||
'/assets/$assetId/insurance/$policyId/renew';
|
||||
|
||||
@ -130,7 +146,8 @@ class ApiEndpoints {
|
||||
static const String settings = '/settings';
|
||||
static const String settingsBranding = '/settings/branding';
|
||||
static const String settingsGeneral = '/settings/general';
|
||||
static const String settingsCompany = '/settings/company-profile';
|
||||
static const String settingsCompany = '/settings/company';
|
||||
static String settingsCompanyLogo = '/settings/company/logo';
|
||||
static const String settingsAsset = '/settings/asset';
|
||||
static const String settingsNotifications = '/settings/notifications';
|
||||
static const String settingsEmail = '/settings/email';
|
||||
|
||||
@ -206,6 +206,56 @@ class Validators {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Validates a positive decimal when a value is entered.
|
||||
static String? optionalPositiveDouble(
|
||||
String? value, {
|
||||
String fieldName = 'Value',
|
||||
}) {
|
||||
if (value == null || value.trim().isEmpty) return null;
|
||||
final parsed = double.tryParse(value.trim());
|
||||
if (parsed == null) return 'Enter a valid number';
|
||||
if (parsed <= 0) return '$fieldName must be greater than 0';
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Validates a non-negative decimal when a value is entered.
|
||||
static String? optionalNonNegativeDouble(
|
||||
String? value, {
|
||||
String fieldName = 'Value',
|
||||
}) {
|
||||
if (value == null || value.trim().isEmpty) return null;
|
||||
final parsed = double.tryParse(value.trim());
|
||||
if (parsed == null) return 'Enter a valid number';
|
||||
if (parsed < 0) return '$fieldName cannot be negative';
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Validates a positive whole number when a value is entered.
|
||||
static String? optionalPositiveInt(
|
||||
String? value, {
|
||||
String fieldName = 'Value',
|
||||
}) {
|
||||
if (value == null || value.trim().isEmpty) return null;
|
||||
final parsed = int.tryParse(value.trim());
|
||||
if (parsed == null) return 'Enter a valid whole number';
|
||||
if (parsed <= 0) return '$fieldName must be greater than 0';
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Validates a percentage between 0 and 100 when a value is entered.
|
||||
static String? optionalPercentage(
|
||||
String? value, {
|
||||
String fieldName = 'Percentage',
|
||||
}) {
|
||||
if (value == null || value.trim().isEmpty) return null;
|
||||
final parsed = double.tryParse(value.trim());
|
||||
if (parsed == null) return 'Enter a valid percentage';
|
||||
if (parsed < 0 || parsed > 100) {
|
||||
return '$fieldName must be between 0 and 100';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static final RegExp _roleNamePattern = RegExp(r'^[a-zA-Z0-9 ]+$');
|
||||
static final RegExp _masterNamePattern =
|
||||
RegExp(r'^[A-Za-z0-9 _/&.()\-]+$');
|
||||
|
||||
@ -45,6 +45,11 @@ class AssetRemoteDataSource {
|
||||
return AssetModel.fromJson(response.data['data'] as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
Future<List<AssetTransferHistoryModel>> getTransferHistory(String assetId) async {
|
||||
final response = await dio.get(ApiEndpoints.assetTransferHistory(assetId));
|
||||
return _parseList(response.data, AssetTransferHistoryModel.fromJson);
|
||||
}
|
||||
|
||||
Future<List<AssetCategoryModel>> getCategories() async {
|
||||
final response = await dio.get(
|
||||
ApiEndpoints.assetCategories,
|
||||
@ -53,6 +58,11 @@ class AssetRemoteDataSource {
|
||||
return _parseList(response.data, AssetCategoryModel.fromJson);
|
||||
}
|
||||
|
||||
Future<List<String>> getDepreciationMethods() async {
|
||||
final response = await dio.get(ApiEndpoints.assetDepreciationMethods);
|
||||
return _parseStringOptions(response.data);
|
||||
}
|
||||
|
||||
Future<PaginatedResponse<AssetAlertModel>> getExpiryAlerts({
|
||||
int? days,
|
||||
String? type,
|
||||
@ -92,6 +102,26 @@ class AssetRemoteDataSource {
|
||||
return AmcContractModel.fromJson(response.data['data'] as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
Future<AmcContractModel> getAmcContractById(
|
||||
String assetId,
|
||||
String contractId,
|
||||
) async {
|
||||
final response = await dio.get(ApiEndpoints.assetAmcById(assetId, contractId));
|
||||
return AmcContractModel.fromJson(response.data['data'] as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
Future<AmcContractModel> updateAmcContract(
|
||||
String assetId,
|
||||
String contractId,
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
final response = await dio.put(
|
||||
ApiEndpoints.assetAmcById(assetId, contractId),
|
||||
data: data,
|
||||
);
|
||||
return AmcContractModel.fromJson(response.data['data'] as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
Future<AmcContractModel> renewAmcContract(
|
||||
String assetId,
|
||||
String contractId,
|
||||
@ -117,6 +147,26 @@ class AssetRemoteDataSource {
|
||||
return ServiceVisitModel.fromJson(response.data['data'] as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
Future<ServiceVisitModel> getServiceVisitById(
|
||||
String assetId,
|
||||
String visitId,
|
||||
) async {
|
||||
final response = await dio.get(ApiEndpoints.assetServiceVisitById(assetId, visitId));
|
||||
return ServiceVisitModel.fromJson(response.data['data'] as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
Future<ServiceVisitModel> updateServiceVisit(
|
||||
String assetId,
|
||||
String visitId,
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
final response = await dio.put(
|
||||
ApiEndpoints.assetServiceVisitById(assetId, visitId),
|
||||
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);
|
||||
@ -132,6 +182,30 @@ class AssetRemoteDataSource {
|
||||
);
|
||||
}
|
||||
|
||||
Future<InsurancePolicyModel> getInsurancePolicyById(
|
||||
String assetId,
|
||||
String policyId,
|
||||
) async {
|
||||
final response = await dio.get(ApiEndpoints.assetInsuranceById(assetId, policyId));
|
||||
return InsurancePolicyModel.fromJson(
|
||||
response.data['data'] as Map<String, dynamic>,
|
||||
);
|
||||
}
|
||||
|
||||
Future<InsurancePolicyModel> updateInsurancePolicy(
|
||||
String assetId,
|
||||
String policyId,
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
final response = await dio.put(
|
||||
ApiEndpoints.assetInsuranceById(assetId, policyId),
|
||||
data: data,
|
||||
);
|
||||
return InsurancePolicyModel.fromJson(
|
||||
response.data['data'] as Map<String, dynamic>,
|
||||
);
|
||||
}
|
||||
|
||||
Future<InsurancePolicyModel> renewInsurancePolicy(
|
||||
String assetId,
|
||||
String policyId,
|
||||
@ -210,4 +284,29 @@ class AssetRemoteDataSource {
|
||||
totalPages: 1,
|
||||
);
|
||||
}
|
||||
|
||||
List<String> _parseStringOptions(dynamic body) {
|
||||
if (body is! Map<String, dynamic>) return const [];
|
||||
final raw = body['data'];
|
||||
final list = raw is List
|
||||
? raw
|
||||
: raw is Map<String, dynamic>
|
||||
? raw['items'] as List<dynamic>? ?? const []
|
||||
: const [];
|
||||
|
||||
return list
|
||||
.map((item) {
|
||||
if (item is String) return item.trim();
|
||||
if (item is Map<String, dynamic>) {
|
||||
final value =
|
||||
item['value']?.toString() ??
|
||||
item['code']?.toString() ??
|
||||
item['id']?.toString();
|
||||
return value?.trim() ?? '';
|
||||
}
|
||||
return '';
|
||||
})
|
||||
.where((value) => value.isNotEmpty)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,11 +50,21 @@ class AssetRepositoryImpl implements AssetRepository {
|
||||
return safeApiCall(() => dataSource.transferAsset(id, data));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<List<AssetTransferHistoryModel>>> getTransferHistory(String assetId) {
|
||||
return safeApiCall(() => dataSource.getTransferHistory(assetId));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<List<AssetCategoryModel>>> getCategories() {
|
||||
return safeApiCall(() => dataSource.getCategories());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<List<String>>> getDepreciationMethods() {
|
||||
return safeApiCall(() => dataSource.getDepreciationMethods());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<PaginatedResponse<AssetAlertModel>>> getExpiryAlerts({
|
||||
int? days,
|
||||
@ -85,6 +95,25 @@ class AssetRepositoryImpl implements AssetRepository {
|
||||
return safeApiCall(() => dataSource.createAmcContract(assetId, data));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<AmcContractModel>> getAmcContractById(
|
||||
String assetId,
|
||||
String contractId,
|
||||
) {
|
||||
return safeApiCall(() => dataSource.getAmcContractById(assetId, contractId));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<AmcContractModel>> updateAmcContract(
|
||||
String assetId,
|
||||
String contractId,
|
||||
Map<String, dynamic> data,
|
||||
) {
|
||||
return safeApiCall(
|
||||
() => dataSource.updateAmcContract(assetId, contractId, data),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<AmcContractModel>> renewAmcContract(
|
||||
String assetId,
|
||||
@ -107,6 +136,25 @@ class AssetRepositoryImpl implements AssetRepository {
|
||||
return safeApiCall(() => dataSource.logServiceVisit(assetId, data));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<ServiceVisitModel>> getServiceVisitById(
|
||||
String assetId,
|
||||
String visitId,
|
||||
) {
|
||||
return safeApiCall(() => dataSource.getServiceVisitById(assetId, visitId));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<ServiceVisitModel>> updateServiceVisit(
|
||||
String assetId,
|
||||
String visitId,
|
||||
Map<String, dynamic> data,
|
||||
) {
|
||||
return safeApiCall(
|
||||
() => dataSource.updateServiceVisit(assetId, visitId, data),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<List<InsurancePolicyModel>>> getInsurancePolicies(String assetId) {
|
||||
return safeApiCall(() => dataSource.getInsurancePolicies(assetId));
|
||||
@ -120,6 +168,25 @@ class AssetRepositoryImpl implements AssetRepository {
|
||||
return safeApiCall(() => dataSource.createInsurancePolicy(assetId, data));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<InsurancePolicyModel>> getInsurancePolicyById(
|
||||
String assetId,
|
||||
String policyId,
|
||||
) {
|
||||
return safeApiCall(() => dataSource.getInsurancePolicyById(assetId, policyId));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<InsurancePolicyModel>> updateInsurancePolicy(
|
||||
String assetId,
|
||||
String policyId,
|
||||
Map<String, dynamic> data,
|
||||
) {
|
||||
return safeApiCall(
|
||||
() => dataSource.updateInsurancePolicy(assetId, policyId, data),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<InsurancePolicyModel>> renewInsurancePolicy(
|
||||
String assetId,
|
||||
|
||||
@ -9,7 +9,9 @@ abstract class AssetRepository {
|
||||
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<AssetTransferHistoryModel>>> getTransferHistory(String assetId);
|
||||
Future<Result<List<AssetCategoryModel>>> getCategories();
|
||||
Future<Result<List<String>>> getDepreciationMethods();
|
||||
Future<Result<PaginatedResponse<AssetAlertModel>>> getExpiryAlerts({
|
||||
int? days,
|
||||
String? type,
|
||||
@ -22,6 +24,15 @@ abstract class AssetRepository {
|
||||
String assetId,
|
||||
Map<String, dynamic> data,
|
||||
);
|
||||
Future<Result<AmcContractModel>> getAmcContractById(
|
||||
String assetId,
|
||||
String contractId,
|
||||
);
|
||||
Future<Result<AmcContractModel>> updateAmcContract(
|
||||
String assetId,
|
||||
String contractId,
|
||||
Map<String, dynamic> data,
|
||||
);
|
||||
Future<Result<AmcContractModel>> renewAmcContract(
|
||||
String assetId,
|
||||
String contractId,
|
||||
@ -32,11 +43,29 @@ abstract class AssetRepository {
|
||||
String assetId,
|
||||
Map<String, dynamic> data,
|
||||
);
|
||||
Future<Result<ServiceVisitModel>> getServiceVisitById(
|
||||
String assetId,
|
||||
String visitId,
|
||||
);
|
||||
Future<Result<ServiceVisitModel>> updateServiceVisit(
|
||||
String assetId,
|
||||
String visitId,
|
||||
Map<String, dynamic> data,
|
||||
);
|
||||
Future<Result<List<InsurancePolicyModel>>> getInsurancePolicies(String assetId);
|
||||
Future<Result<InsurancePolicyModel>> createInsurancePolicy(
|
||||
String assetId,
|
||||
Map<String, dynamic> data,
|
||||
);
|
||||
Future<Result<InsurancePolicyModel>> getInsurancePolicyById(
|
||||
String assetId,
|
||||
String policyId,
|
||||
);
|
||||
Future<Result<InsurancePolicyModel>> updateInsurancePolicy(
|
||||
String assetId,
|
||||
String policyId,
|
||||
Map<String, dynamic> data,
|
||||
);
|
||||
Future<Result<InsurancePolicyModel>> renewInsurancePolicy(
|
||||
String assetId,
|
||||
String policyId,
|
||||
|
||||
@ -0,0 +1,202 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../core/constants/app_constants.dart';
|
||||
import '../../../../shared/models/grn_model.dart';
|
||||
import '../../../../shared/models/purchase_order_model.dart';
|
||||
import '../../../../shared/models/user_management_models.dart';
|
||||
import '../../../assets/data/repositories/asset_repository_impl.dart';
|
||||
import '../../../grn/data/repositories/grn_repository_impl.dart';
|
||||
import '../../../masters/data/datasources/master_remote_data_source.dart';
|
||||
import '../../../purchase_orders/data/repositories/purchase_order_repository_impl.dart';
|
||||
import '../../../users/presentation/providers/users_provider.dart';
|
||||
import '../../../vendors/data/repositories/vendor_repository_impl.dart';
|
||||
import '../../../../shared/models/vendor_model.dart';
|
||||
|
||||
class AssetFormLookups {
|
||||
const AssetFormLookups({
|
||||
this.plants = const [],
|
||||
this.departments = const [],
|
||||
this.warehouses = const [],
|
||||
this.vendors = const [],
|
||||
this.users = const [],
|
||||
this.purchaseOrders = const [],
|
||||
this.grns = const [],
|
||||
this.depreciationMethods = const [],
|
||||
});
|
||||
|
||||
final List<FilterOptionModel> plants;
|
||||
final List<FilterOptionModel> departments;
|
||||
final List<FilterOptionModel> warehouses;
|
||||
final List<FilterOptionModel> vendors;
|
||||
final List<FilterOptionModel> users;
|
||||
final List<FilterOptionModel> purchaseOrders;
|
||||
final List<FilterOptionModel> grns;
|
||||
final List<String> depreciationMethods;
|
||||
}
|
||||
|
||||
final assetFormLookupsProvider =
|
||||
FutureProvider.autoDispose<AssetFormLookups>((ref) async {
|
||||
final master = ref.watch(masterRemoteDataSourceProvider);
|
||||
|
||||
final plants = await _safeOptions(master.listPlants);
|
||||
final departments = await _safeOptions(master.listDepartments);
|
||||
final warehouses = await _safeOptions(master.listWarehouses);
|
||||
final vendors = await _safeVendorOptions(ref);
|
||||
final users = await _safeUserOptions(ref);
|
||||
final purchaseOrders = await _safePurchaseOrderOptions(ref);
|
||||
final grns = await _safeGrnOptions(ref);
|
||||
final depreciationMethods = await _safeDepreciationMethods(ref);
|
||||
|
||||
return AssetFormLookups(
|
||||
plants: plants,
|
||||
departments: departments,
|
||||
warehouses: warehouses,
|
||||
vendors: vendors,
|
||||
users: users,
|
||||
purchaseOrders: purchaseOrders,
|
||||
grns: grns,
|
||||
depreciationMethods: depreciationMethods,
|
||||
);
|
||||
});
|
||||
|
||||
final assetGrnItemsProvider =
|
||||
FutureProvider.autoDispose.family<List<FilterOptionModel>, int?>(
|
||||
(ref, grnId) async {
|
||||
if (grnId == null || grnId <= 0) return const [];
|
||||
final result =
|
||||
await ref.read(grnRepositoryProvider).getGrnById(grnId.toString());
|
||||
if (result.failure != null || result.data == null) return const [];
|
||||
|
||||
return result.data!.items.map((item) {
|
||||
final code = item.itemCode;
|
||||
final name = item.itemName;
|
||||
final line = item.lineNo;
|
||||
final label = [
|
||||
if (code != null && code.isNotEmpty) code,
|
||||
if (name != null && name.isNotEmpty) name,
|
||||
if (line != null) 'Line $line',
|
||||
].join(' · ');
|
||||
return FilterOptionModel(
|
||||
id: item.id,
|
||||
name: label.isEmpty ? 'Item #${item.id}' : label,
|
||||
);
|
||||
}).toList();
|
||||
},
|
||||
);
|
||||
|
||||
Future<List<FilterOptionModel>> _safeOptions(
|
||||
Future<List<FilterOptionModel>> Function() load,
|
||||
) async {
|
||||
try {
|
||||
return await load();
|
||||
} catch (_) {
|
||||
return const [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<FilterOptionModel>> _safeVendorOptions(Ref ref) async {
|
||||
try {
|
||||
final result = await ref.read(vendorRepositoryProvider).getVendors(
|
||||
const VendorListQuery(
|
||||
page: 1,
|
||||
limit: AppConstants.maxPageSize,
|
||||
status: 'active',
|
||||
),
|
||||
);
|
||||
if (result.failure != null || result.data == null) return const [];
|
||||
return result.data!.items
|
||||
.map(
|
||||
(vendor) => FilterOptionModel(
|
||||
id: vendor.id,
|
||||
name: vendor.vendorName,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
} catch (_) {
|
||||
return const [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<FilterOptionModel>> _safeUserOptions(Ref ref) async {
|
||||
try {
|
||||
final result = await ref.read(getUsersUseCaseProvider)(
|
||||
const UserListQuery(
|
||||
page: 1,
|
||||
limit: AppConstants.maxPageSize,
|
||||
status: 'active',
|
||||
isActive: true,
|
||||
),
|
||||
);
|
||||
if (result.failure != null || result.data == null) return const [];
|
||||
return result.data!.items
|
||||
.where((user) {
|
||||
final status = user.status.trim().toLowerCase();
|
||||
return status == 'active' && user.isActive;
|
||||
})
|
||||
.map(
|
||||
(user) => FilterOptionModel(
|
||||
id: user.id,
|
||||
name: user.fullName,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
} catch (_) {
|
||||
return const [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<FilterOptionModel>> _safePurchaseOrderOptions(Ref ref) async {
|
||||
try {
|
||||
final result =
|
||||
await ref.read(purchaseOrderRepositoryProvider).getPurchaseOrders(
|
||||
const PurchaseOrderListQuery(
|
||||
page: 1,
|
||||
limit: AppConstants.maxPageSize,
|
||||
),
|
||||
);
|
||||
if (result.failure != null || result.data == null) return const [];
|
||||
return result.data!.items
|
||||
.map(
|
||||
(po) => FilterOptionModel(
|
||||
id: po.id,
|
||||
name: po.poNo ?? 'PO #${po.id}',
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
} catch (_) {
|
||||
return const [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<FilterOptionModel>> _safeGrnOptions(Ref ref) async {
|
||||
try {
|
||||
final result = await ref.read(grnRepositoryProvider).getGrns(
|
||||
const GrnListQuery(page: 1, limit: AppConstants.maxPageSize),
|
||||
);
|
||||
if (result.failure != null || result.data == null) return const [];
|
||||
return result.data!.items
|
||||
.map(
|
||||
(grn) => FilterOptionModel(
|
||||
id: grn.id,
|
||||
name: grn.grnNumber ?? 'GRN #${grn.id}',
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
} catch (_) {
|
||||
return const [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<String>> _safeDepreciationMethods(Ref ref) async {
|
||||
try {
|
||||
final result = await ref.read(assetRepositoryProvider).getDepreciationMethods();
|
||||
if (result.failure != null || result.data == null) return const [];
|
||||
return result.data!
|
||||
.map((method) => method.trim().toUpperCase())
|
||||
.where((method) => method.isNotEmpty)
|
||||
.toSet()
|
||||
.toList();
|
||||
} catch (_) {
|
||||
return const [];
|
||||
}
|
||||
}
|
||||
@ -223,6 +223,17 @@ class AssetDetailNotifier extends FamilyAsyncNotifier<AssetDetailState, String>
|
||||
return result.data;
|
||||
}
|
||||
|
||||
Future<AmcContractModel?> updateAmc(
|
||||
String contractId,
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
final repository = ref.read(assetRepositoryProvider);
|
||||
final result = await repository.updateAmcContract(arg, contractId, 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);
|
||||
@ -231,6 +242,17 @@ class AssetDetailNotifier extends FamilyAsyncNotifier<AssetDetailState, String>
|
||||
return result.data;
|
||||
}
|
||||
|
||||
Future<ServiceVisitModel?> updateVisit(
|
||||
String visitId,
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
final repository = ref.read(assetRepositoryProvider);
|
||||
final result = await repository.updateServiceVisit(arg, visitId, 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);
|
||||
@ -238,8 +260,66 @@ class AssetDetailNotifier extends FamilyAsyncNotifier<AssetDetailState, String>
|
||||
await reload();
|
||||
return result.data;
|
||||
}
|
||||
|
||||
Future<InsurancePolicyModel?> updateInsurance(
|
||||
String policyId,
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
final repository = ref.read(assetRepositoryProvider);
|
||||
final result = await repository.updateInsurancePolicy(arg, policyId, data);
|
||||
if (result.failure != null) throw result.failure!;
|
||||
await reload();
|
||||
return result.data;
|
||||
}
|
||||
}
|
||||
|
||||
typedef AmcContractFormParams = ({String assetId, String contractId});
|
||||
|
||||
final amcContractFormProvider = FutureProvider.autoDispose
|
||||
.family<AmcContractModel, AmcContractFormParams>((ref, params) async {
|
||||
final repository = ref.read(assetRepositoryProvider);
|
||||
final result = await repository.getAmcContractById(
|
||||
params.assetId,
|
||||
params.contractId,
|
||||
);
|
||||
if (result.failure != null) throw result.failure!;
|
||||
return result.data!;
|
||||
});
|
||||
|
||||
typedef ServiceVisitFormParams = ({String assetId, String visitId});
|
||||
|
||||
final serviceVisitFormProvider = FutureProvider.autoDispose
|
||||
.family<ServiceVisitModel, ServiceVisitFormParams>((ref, params) async {
|
||||
final repository = ref.read(assetRepositoryProvider);
|
||||
final result = await repository.getServiceVisitById(
|
||||
params.assetId,
|
||||
params.visitId,
|
||||
);
|
||||
if (result.failure != null) throw result.failure!;
|
||||
return result.data!;
|
||||
});
|
||||
|
||||
typedef InsurancePolicyFormParams = ({String assetId, String policyId});
|
||||
|
||||
final insurancePolicyFormProvider = FutureProvider.autoDispose
|
||||
.family<InsurancePolicyModel, InsurancePolicyFormParams>((ref, params) async {
|
||||
final repository = ref.read(assetRepositoryProvider);
|
||||
final result = await repository.getInsurancePolicyById(
|
||||
params.assetId,
|
||||
params.policyId,
|
||||
);
|
||||
if (result.failure != null) throw result.failure!;
|
||||
return result.data!;
|
||||
});
|
||||
|
||||
final transferHistoryProvider = FutureProvider.autoDispose
|
||||
.family<List<AssetTransferHistoryModel>, String>((ref, assetId) async {
|
||||
final repository = ref.read(assetRepositoryProvider);
|
||||
final result = await repository.getTransferHistory(assetId);
|
||||
if (result.failure != null) throw result.failure!;
|
||||
return result.data ?? const [];
|
||||
});
|
||||
|
||||
final assetFormProvider =
|
||||
AsyncNotifierProvider.family.autoDispose<AssetFormNotifier, AssetModel?, String?>(
|
||||
AssetFormNotifier.new,
|
||||
|
||||
@ -15,11 +15,31 @@ import '../../../../shared/widgets/error_view.dart';
|
||||
import '../../../../shared/widgets/page_header.dart';
|
||||
import '../providers/assets_provider.dart';
|
||||
|
||||
class AssetAlertsScreen extends ConsumerWidget {
|
||||
class AssetAlertsScreen extends ConsumerStatefulWidget {
|
||||
const AssetAlertsScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ConsumerState<AssetAlertsScreen> createState() => _AssetAlertsScreenState();
|
||||
}
|
||||
|
||||
class _AssetAlertsScreenState extends ConsumerState<AssetAlertsScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final TabController _tabController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 2, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final alertsAsync = ref.watch(assetAlertsProvider);
|
||||
|
||||
return Padding(
|
||||
@ -30,41 +50,40 @@ class AssetAlertsScreen extends ConsumerWidget {
|
||||
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(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
_ExpiryAlertsTab(state: state),
|
||||
_ServiceAlertsTab(state: state),
|
||||
],
|
||||
data: (state) => 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),
|
||||
TabBar(
|
||||
controller: _tabController,
|
||||
tabs: const [
|
||||
Tab(text: 'Expiry Alerts'),
|
||||
Tab(text: 'Service Alerts'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
_ExpiryAlertsTab(state: state),
|
||||
_ServiceAlertsTab(state: state),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -87,7 +106,7 @@ class _ExpiryAlertsTab extends ConsumerWidget {
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 8,
|
||||
crossAxisAlignment: WrapCrossAlignment.start,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 160,
|
||||
@ -118,10 +137,13 @@ class _ExpiryAlertsTab extends ConsumerWidget {
|
||||
onChanged: notifier.setExpiryType,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => notifier.refresh(),
|
||||
icon: const Icon(Icons.refresh),
|
||||
tooltip: 'Refresh',
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: IconButton(
|
||||
onPressed: () => notifier.refresh(),
|
||||
icon: const Icon(Icons.refresh),
|
||||
tooltip: 'Refresh',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -179,6 +201,7 @@ class _ServiceAlertsTab extends ConsumerWidget {
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 8,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 200,
|
||||
@ -196,10 +219,13 @@ class _ServiceAlertsTab extends ConsumerWidget {
|
||||
onChanged: notifier.setServiceStatus,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => notifier.refresh(),
|
||||
icon: const Icon(Icons.refresh),
|
||||
tooltip: 'Refresh',
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: IconButton(
|
||||
onPressed: () => notifier.refresh(),
|
||||
icon: const Icon(Icons.refresh),
|
||||
tooltip: 'Refresh',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -77,7 +77,7 @@ class _AssetDetailScreenState extends ConsumerState<AssetDetailScreen>
|
||||
if (canEdit) ...[
|
||||
const SizedBox(width: 8),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => _showTransferDialog(state.asset),
|
||||
onPressed: _showTransferDialog,
|
||||
icon: const Icon(Icons.swap_horiz),
|
||||
label: const Text('Transfer'),
|
||||
),
|
||||
@ -107,9 +107,16 @@ class _AssetDetailScreenState extends ConsumerState<AssetDetailScreen>
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_OverviewTab(asset: state.asset),
|
||||
_OverviewTab(
|
||||
asset: state.asset,
|
||||
onOpenTransferHistory: _openTransferHistoryPanel,
|
||||
),
|
||||
_AmcTab(assetId: widget.assetId, contracts: state.amcContracts),
|
||||
_ServiceVisitsTab(assetId: widget.assetId, visits: state.serviceVisits),
|
||||
_ServiceVisitsTab(
|
||||
assetId: widget.assetId,
|
||||
visits: state.serviceVisits,
|
||||
amcContracts: state.amcContracts,
|
||||
),
|
||||
_InsuranceTab(assetId: widget.assetId, policies: state.insurancePolicies),
|
||||
],
|
||||
),
|
||||
@ -134,52 +141,36 @@ class _AssetDetailScreenState extends ConsumerState<AssetDetailScreen>
|
||||
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')),
|
||||
],
|
||||
),
|
||||
Future<void> _showTransferDialog() async {
|
||||
final transferred = await showSidePanel<bool>(
|
||||
context,
|
||||
TransferAssetPanel(assetId: widget.assetId),
|
||||
width: 520,
|
||||
);
|
||||
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())));
|
||||
}
|
||||
if (transferred == true && mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Asset transferred')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _openTransferHistoryPanel() async {
|
||||
await showSidePanel<void>(
|
||||
context,
|
||||
TransferHistoryPanel(assetId: widget.assetId),
|
||||
width: 520,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OverviewTab extends StatelessWidget {
|
||||
const _OverviewTab({required this.asset});
|
||||
const _OverviewTab({
|
||||
required this.asset,
|
||||
required this.onOpenTransferHistory,
|
||||
});
|
||||
|
||||
final AssetModel asset;
|
||||
final VoidCallback onOpenTransferHistory;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -196,12 +187,22 @@ class _OverviewTab extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'Asset Details',
|
||||
style: theme.textTheme.labelLarge?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Asset Details',
|
||||
style: theme.textTheme.labelLarge?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton.icon(
|
||||
onPressed: onOpenTransferHistory,
|
||||
icon: const Icon(Icons.history, size: 18),
|
||||
label: const Text('Transfer History'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_AssetInfoGrid(
|
||||
@ -209,7 +210,17 @@ class _OverviewTab extends StatelessWidget {
|
||||
_AssetInfo('Asset Name', asset.assetName),
|
||||
_AssetInfo('Asset Code', asset.assetCode ?? '—'),
|
||||
_AssetInfo('Category', asset.assetCategoryName ?? '—'),
|
||||
_AssetInfo('Subcategory', asset.assetSubcategoryName ?? '—'),
|
||||
_AssetInfo('Plant', asset.plantName ?? '—'),
|
||||
_AssetInfo('Serial Number', asset.serialNumber ?? '—'),
|
||||
_AssetInfo('Brand / Model', asset.brandModel ?? '—'),
|
||||
_AssetInfo('Manufacturer', asset.manufacturer ?? '—'),
|
||||
_AssetInfo(
|
||||
'Purchase Date',
|
||||
asset.purchaseDate != null
|
||||
? dateFormat.format(asset.purchaseDate!)
|
||||
: '—',
|
||||
),
|
||||
_AssetInfo(
|
||||
'Warranty Expiry',
|
||||
asset.warrantyExpiryDate != null
|
||||
@ -220,16 +231,36 @@ class _OverviewTab extends StatelessWidget {
|
||||
'Purchase Cost',
|
||||
asset.purchaseCost != null ? '₹${asset.purchaseCost}' : '—',
|
||||
),
|
||||
_AssetInfo.status(
|
||||
'Status',
|
||||
AppStatusChip(status: asset.status ?? 'active'),
|
||||
_AssetInfo(
|
||||
'Useful Life',
|
||||
asset.usefulLifeYears != null
|
||||
? '${asset.usefulLifeYears} years'
|
||||
: '—',
|
||||
),
|
||||
_AssetInfo(
|
||||
'Active',
|
||||
asset.isActive ? 'Yes' : 'No',
|
||||
'Depreciation',
|
||||
asset.depreciationMethod != null
|
||||
? '${asset.depreciationMethod}'
|
||||
'${asset.depreciationRate != null ? ' (${asset.depreciationRate}%)' : ''}'
|
||||
: '—',
|
||||
),
|
||||
_AssetInfo('Condition', assetConditionLabel(asset.condition)),
|
||||
_AssetInfo.widget(
|
||||
'Status',
|
||||
AppStatusChip(status: asset.status ?? 'IN_USE'),
|
||||
),
|
||||
_AssetInfo('Active', asset.isActive ? 'Yes' : 'No'),
|
||||
],
|
||||
),
|
||||
if (asset.remarks?.trim().isNotEmpty == true) ...[
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 20),
|
||||
child: Divider(height: 1),
|
||||
),
|
||||
_AssetInfoGrid(
|
||||
items: [_AssetInfo('Remarks', asset.remarks!)],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -315,7 +346,7 @@ class _AssetDetailTile extends StatelessWidget {
|
||||
class _AssetInfo {
|
||||
const _AssetInfo(this.label, this.value) : valueWidget = null;
|
||||
|
||||
const _AssetInfo.status(this.label, this.valueWidget) : value = null;
|
||||
const _AssetInfo.widget(this.label, this.valueWidget) : value = null;
|
||||
|
||||
final String label;
|
||||
final String? value;
|
||||
@ -363,7 +394,14 @@ class _AmcTab extends ConsumerWidget {
|
||||
),
|
||||
itemCount: contracts.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _AssetAmcCard(contract: contracts[index]);
|
||||
return _AssetAmcCard(
|
||||
contract: contracts[index],
|
||||
onEdit: () => _openEditAmcPanel(
|
||||
context,
|
||||
ref,
|
||||
contracts[index].id,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
@ -383,13 +421,35 @@ class _AmcTab extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _openEditAmcPanel(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
String contractId,
|
||||
) async {
|
||||
final saved = await showSidePanel<bool>(
|
||||
context,
|
||||
AddAmcPanel(assetId: assetId, contractId: contractId),
|
||||
width: 520,
|
||||
);
|
||||
if (saved == true && context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('AMC contract updated')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _ServiceVisitsTab extends ConsumerWidget {
|
||||
const _ServiceVisitsTab({required this.assetId, required this.visits});
|
||||
const _ServiceVisitsTab({
|
||||
required this.assetId,
|
||||
required this.visits,
|
||||
required this.amcContracts,
|
||||
});
|
||||
|
||||
final String assetId;
|
||||
final List<ServiceVisitModel> visits;
|
||||
final List<AmcContractModel> amcContracts;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@ -426,7 +486,14 @@ class _ServiceVisitsTab extends ConsumerWidget {
|
||||
),
|
||||
itemCount: visits.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _AssetServiceVisitCard(visit: visits[index]);
|
||||
return _AssetServiceVisitCard(
|
||||
visit: visits[index],
|
||||
onEdit: () => _openEditVisitPanel(
|
||||
context,
|
||||
ref,
|
||||
visits[index].id,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
@ -437,8 +504,11 @@ class _ServiceVisitsTab extends ConsumerWidget {
|
||||
Future<void> _openLogVisitPanel(BuildContext context, WidgetRef ref) async {
|
||||
final saved = await showSidePanel<bool>(
|
||||
context,
|
||||
LogServiceVisitPanel(assetId: assetId),
|
||||
width: 520,
|
||||
LogServiceVisitPanel(
|
||||
assetId: assetId,
|
||||
amcContracts: amcContracts,
|
||||
),
|
||||
width: 560,
|
||||
);
|
||||
if (saved == true && context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@ -446,6 +516,27 @@ class _ServiceVisitsTab extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _openEditVisitPanel(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
String visitId,
|
||||
) async {
|
||||
final saved = await showSidePanel<bool>(
|
||||
context,
|
||||
LogServiceVisitPanel(
|
||||
assetId: assetId,
|
||||
visitId: visitId,
|
||||
amcContracts: amcContracts,
|
||||
),
|
||||
width: 560,
|
||||
);
|
||||
if (saved == true && context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Service visit updated')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _InsuranceTab extends ConsumerWidget {
|
||||
@ -489,7 +580,14 @@ class _InsuranceTab extends ConsumerWidget {
|
||||
),
|
||||
itemCount: policies.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _AssetInsuranceCard(policy: policies[index]);
|
||||
return _AssetInsuranceCard(
|
||||
policy: policies[index],
|
||||
onEdit: () => _openEditInsurancePanel(
|
||||
context,
|
||||
ref,
|
||||
policies[index].id,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
@ -509,12 +607,33 @@ class _InsuranceTab extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _openEditInsurancePanel(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
String policyId,
|
||||
) async {
|
||||
final saved = await showSidePanel<bool>(
|
||||
context,
|
||||
AddInsurancePanel(assetId: assetId, policyId: policyId),
|
||||
width: 520,
|
||||
);
|
||||
if (saved == true && context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Insurance policy updated')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _AssetAmcCard extends StatelessWidget {
|
||||
const _AssetAmcCard({required this.contract});
|
||||
const _AssetAmcCard({
|
||||
required this.contract,
|
||||
this.onEdit,
|
||||
});
|
||||
|
||||
final AmcContractModel contract;
|
||||
final VoidCallback? onEdit;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -535,18 +654,40 @@ class _AssetAmcCard extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFCA8A04).withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.handyman_outlined,
|
||||
color: Color(0xFFCA8A04),
|
||||
size: 20,
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFCA8A04).withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.handyman_outlined,
|
||||
color: Color(0xFFCA8A04),
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (onEdit != null)
|
||||
CanPermission(
|
||||
module: 'assets',
|
||||
action: PermissionAction.update,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.edit_outlined, size: 18),
|
||||
tooltip: 'Edit',
|
||||
visualDensity: VisualDensity.compact,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 32,
|
||||
minHeight: 32,
|
||||
),
|
||||
onPressed: onEdit,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
@ -628,9 +769,13 @@ class _AssetAmcCard extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _AssetServiceVisitCard extends StatelessWidget {
|
||||
const _AssetServiceVisitCard({required this.visit});
|
||||
const _AssetServiceVisitCard({
|
||||
required this.visit,
|
||||
this.onEdit,
|
||||
});
|
||||
|
||||
final ServiceVisitModel visit;
|
||||
final VoidCallback? onEdit;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -646,18 +791,40 @@ class _AssetServiceVisitCard extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF2563EB).withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.build_outlined,
|
||||
color: Color(0xFF2563EB),
|
||||
size: 20,
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF2563EB).withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.build_outlined,
|
||||
color: Color(0xFF2563EB),
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (onEdit != null)
|
||||
CanPermission(
|
||||
module: 'assets',
|
||||
action: PermissionAction.update,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.edit_outlined, size: 18),
|
||||
tooltip: 'Edit',
|
||||
visualDensity: VisualDensity.compact,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 32,
|
||||
minHeight: 32,
|
||||
),
|
||||
onPressed: onEdit,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
@ -748,9 +915,13 @@ class _AssetServiceVisitCard extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _AssetInsuranceCard extends StatelessWidget {
|
||||
const _AssetInsuranceCard({required this.policy});
|
||||
const _AssetInsuranceCard({
|
||||
required this.policy,
|
||||
this.onEdit,
|
||||
});
|
||||
|
||||
final InsurancePolicyModel policy;
|
||||
final VoidCallback? onEdit;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -766,18 +937,40 @@ class _AssetInsuranceCard extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF16A34A).withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.shield_outlined,
|
||||
color: Color(0xFF16A34A),
|
||||
size: 20,
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF16A34A).withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.shield_outlined,
|
||||
color: Color(0xFF16A34A),
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (onEdit != null)
|
||||
CanPermission(
|
||||
module: 'assets',
|
||||
action: PermissionAction.update,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.edit_outlined, size: 18),
|
||||
tooltip: 'Edit',
|
||||
visualDensity: VisualDensity.compact,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 32,
|
||||
minHeight: 32,
|
||||
),
|
||||
onPressed: onEdit,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
|
||||
@ -16,6 +16,7 @@ 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/app_pagination.dart';
|
||||
import '../../../../shared/widgets/app_status_chip.dart';
|
||||
import '../../../../shared/widgets/app_table_action_icon.dart';
|
||||
import '../../../../shared/widgets/can_permission.dart';
|
||||
@ -74,8 +75,6 @@ class _AssetListScreenState extends ConsumerState<AssetListScreen> {
|
||||
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,
|
||||
@ -194,67 +193,18 @@ class _AssetListScreenState extends ConsumerState<AssetListScreen> {
|
||||
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'),
|
||||
),
|
||||
],
|
||||
child: AppPagination(
|
||||
currentPage: page,
|
||||
totalPages: state.totalPages,
|
||||
totalItems: total,
|
||||
pageSize: pageSize,
|
||||
itemLabel: 'assets',
|
||||
onPageChanged: ref
|
||||
.read(assetsListProvider.notifier)
|
||||
.setPage,
|
||||
onPageSizeChanged: ref
|
||||
.read(assetsListProvider.notifier)
|
||||
.setPageSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -393,13 +343,18 @@ class _AssetsFilterBar extends StatelessWidget {
|
||||
|
||||
@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,
|
||||
final searchField = Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Search',
|
||||
hintText: 'Search by name, code, category...',
|
||||
prefixIcon: Icon(Icons.search, size: 20),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
isDense: true,
|
||||
),
|
||||
onChanged: onSearch,
|
||||
),
|
||||
onChanged: onSearch,
|
||||
);
|
||||
|
||||
final filters = [
|
||||
@ -429,12 +384,14 @@ class _AssetsFilterBar extends StatelessWidget {
|
||||
children: [
|
||||
searchField,
|
||||
const SizedBox(height: 12),
|
||||
Wrap(spacing: 12, runSpacing: 12, children: filters),
|
||||
...filters.expand((f) => [f, const SizedBox(height: 12)]).toList()
|
||||
..removeLast(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(flex: 3, child: searchField),
|
||||
const SizedBox(width: 12),
|
||||
@ -503,7 +460,11 @@ class _AssetDataTable extends StatelessWidget {
|
||||
AppDataColumn(
|
||||
label: 'Asset Code',
|
||||
flex: 1,
|
||||
cellBuilder: (_, asset) => Text(asset.assetCode ?? '—'),
|
||||
cellBuilder: (_, asset) {
|
||||
final code = asset.assetCode;
|
||||
if (code == null || code.isEmpty) return const Text('—');
|
||||
return _AssetCodeBadge(code: code);
|
||||
},
|
||||
),
|
||||
AppDataColumn(
|
||||
label: 'Asset Name',
|
||||
@ -621,7 +582,10 @@ class _AssetMobileList extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(asset.assetCode ?? '—'),
|
||||
if (asset.assetCode != null && asset.assetCode!.isNotEmpty)
|
||||
_AssetCodeBadge(code: asset.assetCode!)
|
||||
else
|
||||
const Text('—'),
|
||||
Text('${asset.assetCategoryName ?? '—'} · ${asset.plantName ?? '—'}'),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
@ -654,3 +618,28 @@ class _AssetMobileList extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AssetCodeBadge extends StatelessWidget {
|
||||
const _AssetCodeBadge({required this.code});
|
||||
|
||||
final String code;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.7),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
code,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@ 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/asset_form_lookups_provider.dart';
|
||||
import '../providers/assets_provider.dart';
|
||||
|
||||
Future<void> openAssetFormPanel(
|
||||
@ -24,10 +25,11 @@ Future<void> openAssetFormPanel(
|
||||
String? assetId,
|
||||
}) async {
|
||||
ref.invalidate(assetFormProvider(assetId));
|
||||
final panelWidth = MediaQuery.sizeOf(context).width * 0.4;
|
||||
final saved = await showSidePanel<bool>(
|
||||
context,
|
||||
AssetFormPanel(assetId: assetId),
|
||||
width: 520,
|
||||
width: panelWidth,
|
||||
);
|
||||
if (saved == true && context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@ -57,35 +59,143 @@ class _AssetFormPanelState extends ConsumerState<AssetFormPanel> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _nameController = TextEditingController();
|
||||
final _costController = TextEditingController();
|
||||
final _serialController = TextEditingController();
|
||||
final _partNumberController = TextEditingController();
|
||||
final _brandModelController = TextEditingController();
|
||||
final _manufacturerController = TextEditingController();
|
||||
final _locationDetailController = TextEditingController();
|
||||
final _qrCodeController = TextEditingController();
|
||||
final _disposalReasonController = TextEditingController();
|
||||
final _disposalValueController = TextEditingController();
|
||||
final _usefulLifeController = TextEditingController();
|
||||
final _depreciationRateController = TextEditingController();
|
||||
final _salvageValueController = TextEditingController();
|
||||
final _remarksController = TextEditingController();
|
||||
int? _categoryId;
|
||||
int? _subcategoryId;
|
||||
int? _plantId;
|
||||
int? _departmentId;
|
||||
int? _warehouseId;
|
||||
int? _assignedToUserId;
|
||||
int? _vendorId;
|
||||
int? _poId;
|
||||
int? _grnId;
|
||||
int? _grnItemId;
|
||||
String? _status;
|
||||
String? _condition;
|
||||
String? _depreciationMethod;
|
||||
DateTime? _warrantyExpiry;
|
||||
DateTime? _purchaseDate;
|
||||
DateTime? _disposalDate;
|
||||
bool _isActive = true;
|
||||
bool _isSubmitting = false;
|
||||
String? _populatedSignature;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (!widget.isEditing) {
|
||||
_condition = 'NEW';
|
||||
_status = 'IN_USE';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_costController.dispose();
|
||||
_serialController.dispose();
|
||||
_partNumberController.dispose();
|
||||
_brandModelController.dispose();
|
||||
_manufacturerController.dispose();
|
||||
_locationDetailController.dispose();
|
||||
_qrCodeController.dispose();
|
||||
_disposalReasonController.dispose();
|
||||
_disposalValueController.dispose();
|
||||
_usefulLifeController.dispose();
|
||||
_depreciationRateController.dispose();
|
||||
_salvageValueController.dispose();
|
||||
_remarksController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String _assetSignature(AssetModel asset) =>
|
||||
'${asset.id}:${asset.assetCategoryId}:${asset.plantId}:'
|
||||
'${asset.warrantyExpiryDate?.toIso8601String()}:'
|
||||
'${asset.purchaseCost}:${asset.assetName}';
|
||||
'${asset.id}:${asset.assetCategoryId}:${asset.assetSubcategoryId}:'
|
||||
'${asset.plantId}:${asset.status}:${asset.assetName}';
|
||||
|
||||
int? _nullablePositiveId(int? id) {
|
||||
if (id == null || id <= 0) return null;
|
||||
return id;
|
||||
}
|
||||
|
||||
void _putOptionalId(Map<String, dynamic> payload, String key, int? value) {
|
||||
final normalized = _nullablePositiveId(value);
|
||||
if (normalized != null) payload[key] = normalized;
|
||||
}
|
||||
|
||||
void _putOptionalText(
|
||||
Map<String, dynamic> payload,
|
||||
String key,
|
||||
String value,
|
||||
) {
|
||||
final trimmed = value.trim();
|
||||
if (trimmed.isNotEmpty) payload[key] = trimmed;
|
||||
}
|
||||
|
||||
void _putOptionalDouble(
|
||||
Map<String, dynamic> payload,
|
||||
String key,
|
||||
String value,
|
||||
) {
|
||||
final trimmed = value.trim();
|
||||
if (trimmed.isEmpty) return;
|
||||
final parsed = double.tryParse(trimmed);
|
||||
if (parsed != null) payload[key] = parsed;
|
||||
}
|
||||
|
||||
int? _dropdownValue(int? selected, Iterable<int> validIds) {
|
||||
if (selected == null) return null;
|
||||
return validIds.contains(selected) ? selected : null;
|
||||
}
|
||||
|
||||
bool _isAssignableUser(int? userId) {
|
||||
if (userId == null || userId <= 0) return false;
|
||||
final users = ref.read(assetFormLookupsProvider).valueOrNull?.users ?? const [];
|
||||
return users.any((user) => int.tryParse(user.id) == userId);
|
||||
}
|
||||
|
||||
void _populateFromAsset(AssetModel asset) {
|
||||
setState(() {
|
||||
_nameController.text = asset.assetName;
|
||||
_categoryId = asset.assetCategoryId;
|
||||
_subcategoryId = asset.assetSubcategoryId;
|
||||
_plantId = asset.plantId;
|
||||
_departmentId = _nullablePositiveId(asset.departmentId);
|
||||
_warehouseId = _nullablePositiveId(asset.warehouseId);
|
||||
_assignedToUserId = _nullablePositiveId(asset.assignedToUserId);
|
||||
_vendorId = _nullablePositiveId(asset.vendorId);
|
||||
_poId = _nullablePositiveId(asset.poId);
|
||||
_grnId = _nullablePositiveId(asset.grnId);
|
||||
_grnItemId = _nullablePositiveId(asset.grnItemId);
|
||||
_status = asset.status;
|
||||
_condition = asset.condition;
|
||||
_depreciationMethod = asset.depreciationMethod;
|
||||
_warrantyExpiry = asset.warrantyExpiryDate;
|
||||
_purchaseDate = asset.purchaseDate;
|
||||
_disposalDate = asset.disposalDate;
|
||||
_isActive = asset.isActive;
|
||||
_serialController.text = asset.serialNumber ?? '';
|
||||
_partNumberController.text = asset.partNumber ?? '';
|
||||
_brandModelController.text = asset.brandModel ?? '';
|
||||
_manufacturerController.text = asset.manufacturer ?? '';
|
||||
_locationDetailController.text = asset.locationDetail ?? '';
|
||||
_qrCodeController.text = asset.qrCodeValue ?? '';
|
||||
_disposalReasonController.text = asset.disposalReason ?? '';
|
||||
_disposalValueController.text = asset.disposalValue?.toString() ?? '';
|
||||
_usefulLifeController.text = asset.usefulLifeYears?.toString() ?? '';
|
||||
_depreciationRateController.text = asset.depreciationRate?.toString() ?? '';
|
||||
_salvageValueController.text = asset.salvageValue?.toString() ?? '';
|
||||
_remarksController.text = asset.remarks ?? '';
|
||||
if (asset.purchaseCost != null) {
|
||||
_costController.text = asset.purchaseCost!.toStringAsFixed(
|
||||
asset.purchaseCost! % 1 == 0 ? 0 : 2,
|
||||
@ -97,33 +207,81 @@ class _AssetFormPanelState extends ConsumerState<AssetFormPanel> {
|
||||
}
|
||||
|
||||
Map<String, dynamic> _buildPayload() {
|
||||
return {
|
||||
final payload = <String, dynamic>{
|
||||
'asset_name': _nameController.text.trim(),
|
||||
'asset_category_id': _categoryId,
|
||||
'asset_subcategory_id': _subcategoryId,
|
||||
'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()),
|
||||
'is_active': _isActive,
|
||||
};
|
||||
|
||||
_putOptionalId(payload, 'department_id', _departmentId);
|
||||
_putOptionalId(payload, 'warehouse_id', _warehouseId);
|
||||
if (_isAssignableUser(_assignedToUserId)) {
|
||||
_putOptionalId(payload, 'assigned_to_user_id', _assignedToUserId);
|
||||
}
|
||||
_putOptionalId(payload, 'vendor_id', _vendorId);
|
||||
_putOptionalId(payload, 'po_id', _poId);
|
||||
_putOptionalId(payload, 'grn_id', _grnId);
|
||||
_putOptionalId(payload, 'grn_item_id', _grnItemId);
|
||||
|
||||
_putOptionalText(payload, 'serial_number', _serialController.text);
|
||||
_putOptionalText(payload, 'part_number', _partNumberController.text);
|
||||
_putOptionalText(payload, 'brand_model', _brandModelController.text);
|
||||
_putOptionalText(payload, 'manufacturer', _manufacturerController.text);
|
||||
_putOptionalText(payload, 'location_detail', _locationDetailController.text);
|
||||
_putOptionalText(payload, 'qr_code_value', _qrCodeController.text);
|
||||
_putOptionalText(payload, 'remarks', _remarksController.text);
|
||||
_putOptionalText(payload, 'disposal_reason', _disposalReasonController.text);
|
||||
|
||||
if (_purchaseDate != null) {
|
||||
payload['purchase_date'] = DateFormat('yyyy-MM-dd').format(_purchaseDate!);
|
||||
}
|
||||
if (_warrantyExpiry != null) {
|
||||
payload['warranty_expiry_date'] =
|
||||
DateFormat('yyyy-MM-dd').format(_warrantyExpiry!);
|
||||
}
|
||||
if (_disposalDate != null) {
|
||||
payload['disposal_date'] = DateFormat('yyyy-MM-dd').format(_disposalDate!);
|
||||
}
|
||||
|
||||
_putOptionalDouble(payload, 'purchase_cost', _costController.text);
|
||||
_putOptionalDouble(payload, 'salvage_value', _salvageValueController.text);
|
||||
_putOptionalDouble(payload, 'disposal_value', _disposalValueController.text);
|
||||
|
||||
final usefulLife = int.tryParse(_usefulLifeController.text.trim());
|
||||
if (usefulLife != null) payload['useful_life_years'] = usefulLife;
|
||||
|
||||
if (_depreciationMethod != null) {
|
||||
payload['depreciation_method'] = _depreciationMethod;
|
||||
}
|
||||
|
||||
final depreciationRate =
|
||||
double.tryParse(_depreciationRateController.text.trim());
|
||||
if (depreciationRate != null) payload['depreciation_rate'] = depreciationRate;
|
||||
|
||||
if (_condition != null) payload['condition'] = _condition;
|
||||
if (_status != null) payload['status'] = _status;
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
Future<void> _pickWarrantyDate() async {
|
||||
Future<void> _pickDate(void Function(DateTime) onPicked, DateTime? current) async {
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _warrantyExpiry ?? DateTime.now(),
|
||||
initialDate: current ?? DateTime.now(),
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime(2100),
|
||||
);
|
||||
if (picked != null) setState(() => _warrantyExpiry = picked);
|
||||
if (picked != null) setState(() => onPicked(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')),
|
||||
);
|
||||
|
||||
final businessError = _validateBusinessRules();
|
||||
if (businessError != null) {
|
||||
showSidePanelSnackBar(context, businessError);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -139,15 +297,67 @@ class _AssetFormPanelState extends ConsumerState<AssetFormPanel> {
|
||||
if (mounted) Navigator.of(context, rootNavigator: true).pop(true);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(e.toString())),
|
||||
);
|
||||
showSidePanelSnackBar(context, e.toString());
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _isSubmitting = false);
|
||||
}
|
||||
}
|
||||
|
||||
String? _validateBusinessRules() {
|
||||
if (_purchaseDate != null &&
|
||||
_warrantyExpiry != null &&
|
||||
_warrantyExpiry!.isBefore(_purchaseDate!)) {
|
||||
return 'Warranty expiry must be on or after purchase date';
|
||||
}
|
||||
|
||||
final purchaseCost = double.tryParse(_costController.text.trim());
|
||||
final salvageValue = double.tryParse(_salvageValueController.text.trim());
|
||||
if (purchaseCost != null &&
|
||||
salvageValue != null &&
|
||||
salvageValue > purchaseCost) {
|
||||
return 'Salvage value cannot exceed purchase cost';
|
||||
}
|
||||
|
||||
final hasDepreciationMethod =
|
||||
_depreciationMethod != null && _depreciationMethod!.trim().isNotEmpty;
|
||||
if (hasDepreciationMethod) {
|
||||
if (_usefulLifeController.text.trim().isEmpty) {
|
||||
return 'Useful life is required when depreciation method is set';
|
||||
}
|
||||
if (_depreciationRateController.text.trim().isEmpty) {
|
||||
return 'Depreciation rate is required when depreciation method is set';
|
||||
}
|
||||
}
|
||||
|
||||
final isDisposed = _status == 'DISPOSED' || _status == 'SCRAPPED';
|
||||
if (isDisposed) {
|
||||
if (_disposalDate == null) {
|
||||
return 'Disposal date is required for disposed or scrapped assets';
|
||||
}
|
||||
if (_disposalReasonController.text.trim().isEmpty) {
|
||||
return 'Disposal reason is required for disposed or scrapped assets';
|
||||
}
|
||||
}
|
||||
|
||||
final hasDisposalInput = _disposalDate != null ||
|
||||
_disposalReasonController.text.trim().isNotEmpty ||
|
||||
_disposalValueController.text.trim().isNotEmpty;
|
||||
if (hasDisposalInput && !isDisposed) {
|
||||
return 'Set status to Disposed or Scrapped when entering disposal details';
|
||||
}
|
||||
|
||||
if (_grnId != null) {
|
||||
final grnItems =
|
||||
ref.read(assetGrnItemsProvider(_grnId)).valueOrNull ?? const [];
|
||||
if (grnItems.isNotEmpty && _grnItemId == null) {
|
||||
return 'Please select a GRN item when a GRN is linked';
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final categoriesAsync = ref.watch(assetCategoriesProvider);
|
||||
@ -207,46 +417,489 @@ class _AssetFormPanelState extends ConsumerState<AssetFormPanel> {
|
||||
AsyncValue<List<AssetCategoryModel>> categoriesAsync,
|
||||
AsyncValue<List<FilterOptionModel>> plantsAsync,
|
||||
) {
|
||||
final subcategoriesAsync = ref.watch(assetSubcategoriesProvider(_categoryId));
|
||||
final lookupsAsync = ref.watch(assetFormLookupsProvider);
|
||||
final grnItemsAsync = ref.watch(assetGrnItemsProvider(_grnId));
|
||||
final depreciationMethods =
|
||||
lookupsAsync.valueOrNull?.depreciationMethods ?? const <String>[];
|
||||
|
||||
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),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SidePanelSection(
|
||||
title: 'BASIC DETAILS',
|
||||
children: [
|
||||
AppTextField(
|
||||
isDense: true,
|
||||
controller: _nameController,
|
||||
label: 'Asset Name *',
|
||||
validator: (v) {
|
||||
final requiredError =
|
||||
Validators.required(v, fieldName: 'Asset name');
|
||||
if (requiredError != null) return requiredError;
|
||||
return Validators.minLength(v!.trim(), 2,
|
||||
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: subcategoriesAsync.when(
|
||||
loading: () => const LinearProgressIndicator(),
|
||||
error: (_, __) => const Text('Failed to load subcategories'),
|
||||
data: (subcategories) => _subcategoryDropdown(subcategories),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
plantsAsync.when(
|
||||
loading: () => const LinearProgressIndicator(),
|
||||
error: (_, __) => const Text('Failed to load plants'),
|
||||
data: (plants) => _plantDropdown(plants),
|
||||
),
|
||||
],
|
||||
),
|
||||
right: plantsAsync.when(
|
||||
loading: () => const LinearProgressIndicator(),
|
||||
error: (_, __) => const Text('Failed to load plants'),
|
||||
data: (plants) => _plantDropdown(plants),
|
||||
SidePanelSection(
|
||||
title: 'IDENTIFICATION',
|
||||
children: [
|
||||
SidePanelFormRow(
|
||||
left: AppTextField(
|
||||
isDense: true,
|
||||
controller: _serialController,
|
||||
label: 'Serial Number',
|
||||
validator: (v) {
|
||||
if (v == null || v.trim().isEmpty) return null;
|
||||
return Validators.minLength(
|
||||
v.trim(),
|
||||
2,
|
||||
fieldName: 'Serial number',
|
||||
);
|
||||
},
|
||||
),
|
||||
right: AppTextField(
|
||||
isDense: true,
|
||||
controller: _partNumberController,
|
||||
label: 'Part Number',
|
||||
validator: (v) {
|
||||
if (v == null || v.trim().isEmpty) return null;
|
||||
return Validators.minLength(
|
||||
v.trim(),
|
||||
2,
|
||||
fieldName: 'Part number',
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SidePanelFormRow(
|
||||
left: AppTextField(
|
||||
isDense: true,
|
||||
controller: _brandModelController,
|
||||
label: 'Brand / Model',
|
||||
validator: (v) {
|
||||
if (v == null || v.trim().isEmpty) return null;
|
||||
return Validators.minLength(
|
||||
v.trim(),
|
||||
2,
|
||||
fieldName: 'Brand / model',
|
||||
);
|
||||
},
|
||||
),
|
||||
right: AppTextField(
|
||||
isDense: true,
|
||||
controller: _manufacturerController,
|
||||
label: 'Manufacturer',
|
||||
validator: (v) {
|
||||
if (v == null || v.trim().isEmpty) return null;
|
||||
return Validators.minLength(
|
||||
v.trim(),
|
||||
2,
|
||||
fieldName: 'Manufacturer',
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SidePanelFormRow(
|
||||
left: _AssetFormDateField(
|
||||
label: 'Warranty Expiry Date',
|
||||
value: _warrantyExpiry,
|
||||
onPick: _pickWarrantyDate,
|
||||
lookupsAsync.when(
|
||||
loading: () => const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12),
|
||||
child: LinearProgressIndicator(),
|
||||
),
|
||||
error: (_, __) => const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12),
|
||||
child: Text('Failed to load lookup options'),
|
||||
),
|
||||
data: (lookups) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SidePanelSection(
|
||||
title: 'LOCATION & ASSIGNMENT',
|
||||
children: [
|
||||
SidePanelFormRow(
|
||||
left: _optionalLookupDropdown(
|
||||
label: 'Department',
|
||||
value: _departmentId,
|
||||
options: lookups.departments,
|
||||
onChanged: (v) => setState(() => _departmentId = v),
|
||||
),
|
||||
right: _optionalLookupDropdown(
|
||||
label: 'Warehouse',
|
||||
value: _warehouseId,
|
||||
options: lookups.warehouses,
|
||||
onChanged: (v) => setState(() => _warehouseId = v),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
AppTextField(
|
||||
isDense: true,
|
||||
controller: _locationDetailController,
|
||||
label: 'Location Detail',
|
||||
validator: (v) {
|
||||
if (v == null || v.trim().isEmpty) return null;
|
||||
return Validators.minLength(
|
||||
v.trim(),
|
||||
2,
|
||||
fieldName: 'Location detail',
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_optionalLookupDropdown(
|
||||
label: 'Assigned To',
|
||||
value: _assignedToUserId,
|
||||
options: lookups.users,
|
||||
onChanged: (v) => setState(() => _assignedToUserId = v),
|
||||
emptyHint: 'Unassigned',
|
||||
),
|
||||
],
|
||||
),
|
||||
SidePanelSection(
|
||||
title: 'PROCUREMENT',
|
||||
children: [
|
||||
SidePanelFormRow(
|
||||
left: _optionalLookupDropdown(
|
||||
label: 'Vendor',
|
||||
value: _vendorId,
|
||||
options: lookups.vendors,
|
||||
onChanged: (v) => setState(() => _vendorId = v),
|
||||
),
|
||||
right: _optionalLookupDropdown(
|
||||
label: 'Purchase Order',
|
||||
value: _poId,
|
||||
options: lookups.purchaseOrders,
|
||||
onChanged: (v) => setState(() => _poId = v),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SidePanelFormRow(
|
||||
left: _optionalLookupDropdown(
|
||||
label: 'GRN',
|
||||
value: _grnId,
|
||||
options: lookups.grns,
|
||||
onChanged: (v) => setState(() {
|
||||
_grnId = v;
|
||||
_grnItemId = null;
|
||||
}),
|
||||
),
|
||||
right: grnItemsAsync.when(
|
||||
loading: () => const LinearProgressIndicator(),
|
||||
error: (_, __) => _optionalLookupDropdown(
|
||||
label: 'GRN Item',
|
||||
value: _grnItemId,
|
||||
options: const [],
|
||||
onChanged: (v) => setState(() => _grnItemId = v),
|
||||
enabled: false,
|
||||
),
|
||||
data: (items) => _optionalLookupDropdown(
|
||||
label: 'GRN Item',
|
||||
value: _grnItemId,
|
||||
options: items,
|
||||
onChanged: (v) => setState(() => _grnItemId = v),
|
||||
enabled: _grnId != null && items.isNotEmpty,
|
||||
emptyHint: _grnId == null
|
||||
? 'Select GRN first'
|
||||
: 'None',
|
||||
required: _grnId != null && items.isNotEmpty,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
right: AppTextField(
|
||||
controller: _costController,
|
||||
label: 'Purchase Cost',
|
||||
keyboardType: TextInputType.number,
|
||||
SidePanelSection(
|
||||
title: 'PURCHASE & DEPRECIATION',
|
||||
children: [
|
||||
SidePanelFormRow(
|
||||
left: _AssetFormDateField(
|
||||
label: 'Purchase Date',
|
||||
value: _purchaseDate,
|
||||
onPick: () =>
|
||||
_pickDate((d) => _purchaseDate = d, _purchaseDate),
|
||||
),
|
||||
right: _AssetFormDateField(
|
||||
label: 'Warranty Expiry Date',
|
||||
value: _warrantyExpiry,
|
||||
onPick: () =>
|
||||
_pickDate((d) => _warrantyExpiry = d, _warrantyExpiry),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SidePanelFormRow(
|
||||
left: AppTextField(
|
||||
isDense: true,
|
||||
controller: _costController,
|
||||
label: 'Purchase Cost',
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
validator: (v) => Validators.optionalPositiveDouble(
|
||||
v,
|
||||
fieldName: 'Purchase cost',
|
||||
),
|
||||
),
|
||||
right: AppTextField(
|
||||
isDense: true,
|
||||
controller: _usefulLifeController,
|
||||
label: 'Useful Life (Years)',
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (v) => Validators.optionalPositiveInt(
|
||||
v,
|
||||
fieldName: 'Useful life',
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SidePanelFormRow(
|
||||
left: AppDropdown<String>(
|
||||
isDense: true,
|
||||
label: 'Depreciation Method',
|
||||
value: _depreciationMethod,
|
||||
options: depreciationMethods
|
||||
.map((method) => AppDropdownOption(
|
||||
value: method,
|
||||
label: method,
|
||||
))
|
||||
.toList(),
|
||||
enabled: depreciationMethods.isNotEmpty,
|
||||
onChanged: (v) => setState(() => _depreciationMethod = v),
|
||||
),
|
||||
right: AppTextField(
|
||||
isDense: true,
|
||||
controller: _depreciationRateController,
|
||||
label: 'Depreciation Rate (%)',
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
validator: (v) => Validators.optionalPercentage(
|
||||
v,
|
||||
fieldName: 'Depreciation rate',
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SidePanelFormRow(
|
||||
left: AppTextField(
|
||||
isDense: true,
|
||||
controller: _salvageValueController,
|
||||
label: 'Salvage Value',
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
validator: (v) => Validators.optionalNonNegativeDouble(
|
||||
v,
|
||||
fieldName: 'Salvage value',
|
||||
),
|
||||
),
|
||||
right: const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
SidePanelSection(
|
||||
title: 'STATUS',
|
||||
children: [
|
||||
SidePanelFormRow(
|
||||
left: AppDropdown<String>(
|
||||
isDense: true,
|
||||
label: 'Condition *',
|
||||
value: _condition,
|
||||
options: assetConditionOptions
|
||||
.map((e) => AppDropdownOption(value: e.$1, label: e.$2))
|
||||
.toList(),
|
||||
onChanged: (v) => setState(() => _condition = v),
|
||||
validator: (v) => v == null ? 'Condition is required' : null,
|
||||
),
|
||||
right: AppDropdown<String>(
|
||||
isDense: true,
|
||||
label: 'Status *',
|
||||
value: _status,
|
||||
options: assetStatusOptions
|
||||
.map((e) => AppDropdownOption(value: e.$1, label: e.$2))
|
||||
.toList(),
|
||||
onChanged: (v) => setState(() => _status = v),
|
||||
validator: (v) => v == null ? 'Status is required' : null,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('Active'),
|
||||
subtitle: const Text('Inactive assets are hidden from active lists'),
|
||||
value: _isActive,
|
||||
onChanged: (value) => setState(() => _isActive = value),
|
||||
),
|
||||
],
|
||||
),
|
||||
SidePanelSection(
|
||||
title: 'DISPOSAL',
|
||||
children: [
|
||||
SidePanelFormRow(
|
||||
left: _AssetFormDateField(
|
||||
label: 'Disposal Date',
|
||||
value: _disposalDate,
|
||||
onPick: () =>
|
||||
_pickDate((d) => _disposalDate = d, _disposalDate),
|
||||
validator: () {
|
||||
final isDisposed =
|
||||
_status == 'DISPOSED' || _status == 'SCRAPPED';
|
||||
if (isDisposed && _disposalDate == null) {
|
||||
return 'Disposal date is required';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
right: AppTextField(
|
||||
isDense: true,
|
||||
controller: _disposalValueController,
|
||||
label: 'Disposal Value',
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
validator: (v) => Validators.optionalNonNegativeDouble(
|
||||
v,
|
||||
fieldName: 'Disposal value',
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
AppTextField(
|
||||
isDense: true,
|
||||
controller: _disposalReasonController,
|
||||
label: 'Disposal Reason',
|
||||
maxLines: 2,
|
||||
validator: (v) {
|
||||
final isDisposed =
|
||||
_status == 'DISPOSED' || _status == 'SCRAPPED';
|
||||
if (isDisposed) {
|
||||
return Validators.required(v, fieldName: 'Disposal reason');
|
||||
}
|
||||
if (v == null || v.trim().isEmpty) return null;
|
||||
return Validators.minLength(
|
||||
v.trim(),
|
||||
3,
|
||||
fieldName: 'Disposal reason',
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
SidePanelSection(
|
||||
title: 'OTHER',
|
||||
children: [
|
||||
AppTextField(
|
||||
isDense: true,
|
||||
controller: _qrCodeController,
|
||||
label: 'QR Code Value',
|
||||
validator: (v) {
|
||||
if (v == null || v.trim().isEmpty) return null;
|
||||
return Validators.minLength(
|
||||
v.trim(),
|
||||
2,
|
||||
fieldName: 'QR code value',
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
AppTextField(
|
||||
isDense: true,
|
||||
controller: _remarksController,
|
||||
label: 'Remarks',
|
||||
maxLines: 2,
|
||||
validator: (v) {
|
||||
if (v == null || v.trim().isEmpty) return null;
|
||||
return Validators.minLength(
|
||||
v.trim(),
|
||||
2,
|
||||
fieldName: 'Remarks',
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _optionalLookupDropdown({
|
||||
required String label,
|
||||
required int? value,
|
||||
required List<FilterOptionModel> options,
|
||||
required ValueChanged<int?> onChanged,
|
||||
String? emptyHint,
|
||||
bool enabled = true,
|
||||
bool required = false,
|
||||
}) {
|
||||
final dropdownOptions = <AppDropdownOption<int?>>[
|
||||
AppDropdownOption<int?>(value: null, label: emptyHint ?? 'None'),
|
||||
...options
|
||||
.map((option) {
|
||||
final id = int.tryParse(option.id);
|
||||
if (id == null || id <= 0) return null;
|
||||
return AppDropdownOption<int?>(value: id, label: option.name);
|
||||
})
|
||||
.whereType<AppDropdownOption<int?>>(),
|
||||
];
|
||||
final validIds = dropdownOptions
|
||||
.map((option) => option.value)
|
||||
.whereType<int>()
|
||||
.toList();
|
||||
|
||||
return AppSearchableDropdown<int?>(
|
||||
label: required ? '$label *' : label,
|
||||
value: value != null && validIds.contains(value) ? value : null,
|
||||
hint: emptyHint ?? 'None',
|
||||
searchHint: 'Search ${label.toLowerCase()}...',
|
||||
isDense: true,
|
||||
enabled: enabled && options.isNotEmpty,
|
||||
options: dropdownOptions,
|
||||
onChanged: onChanged,
|
||||
validator: required ? (v) => v == null ? '$label is required' : null : null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _subcategoryDropdown(List<FilterOptionModel> subcategories) {
|
||||
final ids = subcategories.map((c) => int.tryParse(c.id)).whereType<int>().toList();
|
||||
return AppSearchableDropdown<int>(
|
||||
label: 'Subcategory *',
|
||||
value: _dropdownValue(_subcategoryId, ids),
|
||||
searchHint: 'Search subcategory...',
|
||||
isDense: true,
|
||||
options: subcategories
|
||||
.map(
|
||||
(c) => AppDropdownOption(
|
||||
value: int.tryParse(c.id) ?? 0,
|
||||
label: c.name,
|
||||
),
|
||||
)
|
||||
.where((option) => option.value != 0)
|
||||
.toList(),
|
||||
onChanged: (v) => setState(() => _subcategoryId = v),
|
||||
validator: (v) => v == null ? 'Subcategory is required' : null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _categoryDropdown(List<AssetCategoryModel> categories) {
|
||||
final categoryIds = categories
|
||||
.map((c) => int.tryParse(c.id))
|
||||
@ -256,6 +909,7 @@ class _AssetFormPanelState extends ConsumerState<AssetFormPanel> {
|
||||
label: 'Category *',
|
||||
value: _dropdownValue(_categoryId, categoryIds),
|
||||
searchHint: 'Search category...',
|
||||
isDense: true,
|
||||
options: categories
|
||||
.map(
|
||||
(c) => AppDropdownOption(
|
||||
@ -265,7 +919,10 @@ class _AssetFormPanelState extends ConsumerState<AssetFormPanel> {
|
||||
)
|
||||
.where((option) => option.value != 0)
|
||||
.toList(),
|
||||
onChanged: (v) => setState(() => _categoryId = v),
|
||||
onChanged: (v) => setState(() {
|
||||
_categoryId = v;
|
||||
_subcategoryId = null;
|
||||
}),
|
||||
validator: (v) => v == null ? 'Category is required' : null,
|
||||
);
|
||||
}
|
||||
@ -279,6 +936,7 @@ class _AssetFormPanelState extends ConsumerState<AssetFormPanel> {
|
||||
label: 'Plant *',
|
||||
value: _dropdownValue(_plantId, plantIds),
|
||||
searchHint: 'Search plant...',
|
||||
isDense: true,
|
||||
options: plants
|
||||
.map(
|
||||
(p) => AppDropdownOption(
|
||||
@ -294,30 +952,67 @@ class _AssetFormPanelState extends ConsumerState<AssetFormPanel> {
|
||||
}
|
||||
}
|
||||
|
||||
class _AssetFormDateField extends StatelessWidget {
|
||||
class _AssetFormDateField extends StatefulWidget {
|
||||
const _AssetFormDateField({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.onPick,
|
||||
this.validator,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final DateTime? value;
|
||||
final VoidCallback onPick;
|
||||
final String? Function()? validator;
|
||||
|
||||
@override
|
||||
State<_AssetFormDateField> createState() => _AssetFormDateFieldState();
|
||||
}
|
||||
|
||||
class _AssetFormDateFieldState extends State<_AssetFormDateField> {
|
||||
late final TextEditingController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController(text: _displayText);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _AssetFormDateField oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
final text = _displayText;
|
||||
if (_controller.text != text) {
|
||||
_controller.text = text;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String get _displayText =>
|
||||
widget.value != null ? DateFormatter.displayDate(widget.value) : '';
|
||||
|
||||
@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),
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: TextFormField(
|
||||
readOnly: true,
|
||||
onTap: widget.onPick,
|
||||
controller: _controller,
|
||||
validator: (_) => widget.validator?.call(),
|
||||
decoration: InputDecoration(
|
||||
labelText: widget.label,
|
||||
hintText: 'Select date',
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
isDense: true,
|
||||
suffixIcon: const Icon(Icons.calendar_today_outlined, size: 20),
|
||||
),
|
||||
),
|
||||
controller: TextEditingController(text: displayText),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -326,3 +1021,10 @@ final assetPlantsProvider = FutureProvider<List<FilterOptionModel>>((ref) async
|
||||
final dataSource = ref.watch(masterRemoteDataSourceProvider);
|
||||
return dataSource.listPlants();
|
||||
});
|
||||
|
||||
final assetSubcategoriesProvider =
|
||||
FutureProvider.family<List<FilterOptionModel>, int?>((ref, categoryId) async {
|
||||
if (categoryId == null) return [];
|
||||
final dataSource = ref.watch(masterRemoteDataSourceProvider);
|
||||
return dataSource.listAssetSubcategories(assetCategoryId: categoryId);
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,40 +1,123 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../../core/constants/route_constants.dart';
|
||||
import '../../../../shared/widgets/error_view.dart';
|
||||
import '../../../../shared/widgets/app_button.dart';
|
||||
import '../../../../shared/widgets/app_card.dart';
|
||||
import '../../../../shared/widgets/app_empty_state.dart';
|
||||
import '../../../../shared/widgets/app_loading_view.dart';
|
||||
import '../../../../shared/widgets/page_header.dart';
|
||||
import '../../../settings/presentation/providers/settings_provider.dart';
|
||||
|
||||
class CompanyListScreen extends StatelessWidget {
|
||||
class CompanyListScreen extends ConsumerStatefulWidget {
|
||||
const CompanyListScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<CompanyListScreen> createState() => _CompanyListScreenState();
|
||||
}
|
||||
|
||||
class _CompanyListScreenState extends ConsumerState<CompanyListScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref.read(appSettingsProvider.notifier).refreshCompanyProfile();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final profile = ref.watch(appSettingsProvider).companyProfile;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
PageHeader(
|
||||
title: 'Companies',
|
||||
subtitle: 'Manage organizations',
|
||||
title: 'Company',
|
||||
subtitle: 'Organisation profile from server settings',
|
||||
actions: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => context.push('${RouteConstants.companies}/add'),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Add Company'),
|
||||
AppButton(
|
||||
label: 'Edit Company',
|
||||
expand: false,
|
||||
icon: Icons.edit_outlined,
|
||||
onPressed: () => context.push(RouteConstants.settingsCompanyProfile),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Expanded(
|
||||
child: EmptyStateView(
|
||||
title: 'No companies yet',
|
||||
description: 'Connect to the API to load companies, or add your first company.',
|
||||
icon: Icons.business_outlined,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: profile.companyName.isEmpty
|
||||
? const AppEmptyState(
|
||||
title: 'No company profile',
|
||||
description: 'Configure your organisation details in settings.',
|
||||
icon: Icons.business_outlined,
|
||||
)
|
||||
: AppCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
profile.companyName,
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_InfoRow(label: 'Mobile', value: profile.phone),
|
||||
_InfoRow(label: 'Email', value: profile.email),
|
||||
_InfoRow(label: 'Website', value: profile.website),
|
||||
_InfoRow(label: 'Address', value: profile.address),
|
||||
_InfoRow(
|
||||
label: 'City / State / Pincode',
|
||||
value: [
|
||||
profile.city,
|
||||
profile.state,
|
||||
profile.pincode,
|
||||
].where((e) => e.isNotEmpty).join(', '),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoRow extends StatelessWidget {
|
||||
const _InfoRow({required this.label, required this.value});
|
||||
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (value.isEmpty) return const SizedBox.shrink();
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 160,
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(child: Text(value)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -352,12 +352,37 @@ const masterDefinitions = <MasterDefinition>[
|
||||
MasterFieldDef(
|
||||
key: 'default_depreciation_method',
|
||||
label: 'Depreciation Method',
|
||||
type: MasterFieldType.dropdown,
|
||||
required: true,
|
||||
showInList: true,
|
||||
optionsMasterKey: 'asset_depreciation_methods',
|
||||
),
|
||||
_activeField,
|
||||
],
|
||||
),
|
||||
MasterDefinition(
|
||||
id: 'asset_subcategories',
|
||||
title: 'Asset Subcategories',
|
||||
subtitle: 'Sub-classification under asset categories',
|
||||
category: 'Assets',
|
||||
routeKey: 'asset-subcategories',
|
||||
apiPath: '/masters/asset-subcategories',
|
||||
module: 'asset_subcategories',
|
||||
icon: Icons.category_outlined,
|
||||
fields: [
|
||||
MasterFieldDef(
|
||||
key: 'asset_category_id',
|
||||
label: 'Asset Category',
|
||||
type: MasterFieldType.dropdown,
|
||||
required: true,
|
||||
showInList: true,
|
||||
optionsMasterKey: 'asset_categories',
|
||||
),
|
||||
MasterFieldDef(key: 'code', label: 'Code', required: true, showInList: true),
|
||||
MasterFieldDef(key: 'name', label: 'Name', required: true, showInList: true),
|
||||
_activeField,
|
||||
],
|
||||
),
|
||||
MasterDefinition(
|
||||
id: 'delivery_terms',
|
||||
title: 'Delivery Terms',
|
||||
|
||||
@ -2,6 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../core/constants/app_constants.dart';
|
||||
import '../../../../shared/models/export_file_result.dart';
|
||||
import '../../../assets/data/repositories/asset_repository_impl.dart';
|
||||
import '../../data/repositories/master_repository_impl.dart';
|
||||
import '../../domain/entities/master_definition.dart';
|
||||
|
||||
@ -280,6 +281,20 @@ class MasterFormNotifier extends FamilyAsyncNotifier<MasterFormState, MasterForm
|
||||
.toSet();
|
||||
|
||||
for (final key in keys) {
|
||||
if (key == 'asset_depreciation_methods') {
|
||||
final result = await ref.read(assetRepositoryProvider).getDepreciationMethods();
|
||||
if (result.failure != null) continue;
|
||||
final methods = (result.data ?? const <String>[])
|
||||
.map((method) => method.trim())
|
||||
.where((method) => method.isNotEmpty)
|
||||
.toSet()
|
||||
.toList();
|
||||
options[key] = methods
|
||||
.map((method) => <String, dynamic>{'id': method, 'name': method})
|
||||
.toList();
|
||||
continue;
|
||||
}
|
||||
|
||||
final def = masterDefinitionById(key);
|
||||
if (def == null) continue;
|
||||
final result = await ref.read(masterRepositoryProvider).listOptions(def);
|
||||
|
||||
@ -47,6 +47,37 @@ class MasterRemoteDataSource {
|
||||
Future<List<FilterOptionModel>> listAssetCategories() =>
|
||||
_listOptions(ApiEndpoints.assetCategories);
|
||||
|
||||
Future<List<FilterOptionModel>> listAssetSubcategories({int? assetCategoryId}) async {
|
||||
final response = await dio.get(
|
||||
ApiEndpoints.assetSubcategories,
|
||||
queryParameters: {
|
||||
'limit': AppConstants.maxPageSize,
|
||||
'is_active': true,
|
||||
if (assetCategoryId != null) 'asset_category_id': assetCategoryId,
|
||||
},
|
||||
);
|
||||
final body = response.data as Map<String, dynamic>;
|
||||
final raw = body['data'];
|
||||
|
||||
final list = raw is List<dynamic>
|
||||
? raw
|
||||
: raw is Map<String, dynamic>
|
||||
? raw['items'] as List<dynamic>? ?? const []
|
||||
: const [];
|
||||
|
||||
return list
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.where((item) => item['is_active'] != false)
|
||||
.map(
|
||||
(item) => FilterOptionModel(
|
||||
id: item['id']?.toString() ?? '',
|
||||
name: _optionLabel(item),
|
||||
),
|
||||
)
|
||||
.where((item) => item.id.isNotEmpty && item.name.isNotEmpty)
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<List<FilterOptionModel>> _listOptions(String endpoint) async {
|
||||
final response = await dio.get(
|
||||
endpoint,
|
||||
|
||||
@ -792,6 +792,8 @@ class _UsersTabState extends ConsumerState<_UsersTab> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
onPageChanged:
|
||||
ref.read(usersListProvider.notifier).setPage,
|
||||
onPageSizeChanged:
|
||||
ref.read(usersListProvider.notifier).setPageSize,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -838,14 +840,19 @@ class _UsersFilterBar extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final searchField = TextField(
|
||||
controller: searchController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Search by name, email, employee code...',
|
||||
prefixIcon: Icon(Icons.search, size: 20),
|
||||
isDense: true,
|
||||
final searchField = Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: TextField(
|
||||
controller: searchController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Search',
|
||||
hintText: 'Search by name, email, employee code...',
|
||||
prefixIcon: Icon(Icons.search, size: 20),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
isDense: true,
|
||||
),
|
||||
onChanged: onSearch,
|
||||
),
|
||||
onChanged: onSearch,
|
||||
);
|
||||
|
||||
final filters = [
|
||||
@ -867,8 +874,16 @@ class _UsersFilterBar extends StatelessWidget {
|
||||
items: statuses,
|
||||
onChanged: onStatusChanged,
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
];
|
||||
|
||||
final exportButton = Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: isExporting ? null : onExport,
|
||||
style: OutlinedButton.styleFrom(
|
||||
minimumSize: const Size(0, 48),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
),
|
||||
icon: isExporting
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
@ -878,12 +893,7 @@ class _UsersFilterBar extends StatelessWidget {
|
||||
: const Icon(Icons.download_outlined, size: 18),
|
||||
label: Text(isExporting ? 'Exporting...' : 'Export'),
|
||||
),
|
||||
];
|
||||
|
||||
final visibleFilters = [
|
||||
...filters.take(3),
|
||||
if (showExport) filters[3],
|
||||
];
|
||||
);
|
||||
|
||||
if (wrapped) {
|
||||
return Column(
|
||||
@ -891,12 +901,18 @@ class _UsersFilterBar extends StatelessWidget {
|
||||
children: [
|
||||
searchField,
|
||||
const SizedBox(height: 12),
|
||||
Wrap(spacing: 12, runSpacing: 12, children: visibleFilters),
|
||||
...filters.expand((f) => [f, const SizedBox(height: 12)]).toList()
|
||||
..removeLast(),
|
||||
if (showExport) ...[
|
||||
const SizedBox(height: 12),
|
||||
exportButton,
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(flex: 3, child: searchField),
|
||||
const SizedBox(width: 12),
|
||||
@ -907,7 +923,7 @@ class _UsersFilterBar extends StatelessWidget {
|
||||
Expanded(child: filters[2]),
|
||||
if (showExport) ...[
|
||||
const SizedBox(width: 12),
|
||||
filters[3],
|
||||
exportButton,
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
@ -82,7 +82,16 @@ class _AddUserPanelState extends ConsumerState<AddUserPanel> {
|
||||
_selectedDesignationId = user.designationId;
|
||||
_selectedPlantId = user.plantId;
|
||||
_selectedReportingToId = user.reportingTo;
|
||||
_selectedStatus = _statusFromApi(user.status);
|
||||
_selectedStatus = _statusLabelFromUser(user);
|
||||
}
|
||||
|
||||
String _statusLabelFromUser(ManagedUserModel user) {
|
||||
final normalized = user.status.trim().toLowerCase();
|
||||
if (normalized.isNotEmpty && normalized != 'active') {
|
||||
return _statusFromApi(user.status);
|
||||
}
|
||||
if (!user.isActive) return 'Inactive';
|
||||
return _statusFromApi(user.status);
|
||||
}
|
||||
|
||||
List<AppDropdownOption<String>> _toOptions(List<FilterOptionModel> items) {
|
||||
@ -224,12 +233,7 @@ class _AddUserPanelState extends ConsumerState<AddUserPanel> {
|
||||
),
|
||||
data: (formState) {
|
||||
if (formState.editingUser != null) {
|
||||
if (!_prefilled) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted || _prefilled) return;
|
||||
setState(() => _prefillFromUser(formState.editingUser!));
|
||||
});
|
||||
}
|
||||
_prefillFromUser(formState.editingUser!);
|
||||
}
|
||||
|
||||
return Form(
|
||||
|
||||
@ -167,10 +167,42 @@ class UserRolesCell extends StatelessWidget {
|
||||
return const Text('—');
|
||||
}
|
||||
|
||||
return Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: roles.map((role) => RoleBadge(label: role)).toList(),
|
||||
final theme = Theme.of(context);
|
||||
final extraCount = roles.length - 1;
|
||||
final tooltipMessage = roles.join('\n');
|
||||
|
||||
return Tooltip(
|
||||
message: tooltipMessage,
|
||||
preferBelow: true,
|
||||
waitDuration: const Duration(milliseconds: 250),
|
||||
child: SizedBox(
|
||||
height: 28,
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: RoleBadge(label: roles.first),
|
||||
),
|
||||
if (extraCount > 0) ...[
|
||||
const SizedBox(width: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest
|
||||
.withValues(alpha: 0.8),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
'+$extraCount',
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,4 +23,69 @@ class SettingsRemoteDataSource {
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
Future<CompanyProfileSettings?> fetchCompany() async {
|
||||
final response = await _dio.get(ApiEndpoints.settingsCompany);
|
||||
final data = response.data['data'];
|
||||
if (data is! Map<String, dynamic>) return null;
|
||||
return CompanyProfileSettings.fromApiJson(data);
|
||||
}
|
||||
|
||||
Future<CompanyProfileSettings> saveCompany(CompanyProfileSettings profile) async {
|
||||
final response = await _dio.put(
|
||||
ApiEndpoints.settingsCompany,
|
||||
data: profile.toApiJson(),
|
||||
);
|
||||
final data = response.data['data'];
|
||||
if (data is Map<String, dynamic>) {
|
||||
return CompanyProfileSettings.fromApiJson(data).copyWith(
|
||||
companyCode: profile.companyCode,
|
||||
registrationNumber: profile.registrationNumber,
|
||||
gstNumber: profile.gstNumber,
|
||||
faviconUrl: profile.faviconUrl,
|
||||
);
|
||||
}
|
||||
return profile;
|
||||
}
|
||||
|
||||
Future<String?> uploadCompanyLogo(List<int> bytes, String filename) async {
|
||||
final formData = FormData.fromMap({
|
||||
'logo': MultipartFile.fromBytes(bytes, filename: filename),
|
||||
});
|
||||
final response = await _dio.post(
|
||||
ApiEndpoints.settingsCompanyLogo,
|
||||
data: formData,
|
||||
);
|
||||
final data = response.data['data'];
|
||||
if (data is Map<String, dynamic>) {
|
||||
return data['logo_url'] as String? ?? data['logo'] as String?;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<EmailConfigurationSettings?> fetchEmail() async {
|
||||
final response = await _dio.get(ApiEndpoints.settingsEmail);
|
||||
final data = response.data['data'];
|
||||
if (data is! Map<String, dynamic>) return null;
|
||||
return EmailConfigurationSettings.fromApiJson(data);
|
||||
}
|
||||
|
||||
Future<EmailConfigurationSettings> saveEmail(
|
||||
EmailConfigurationSettings email,
|
||||
) async {
|
||||
final response = await _dio.put(
|
||||
ApiEndpoints.settingsEmail,
|
||||
data: email.toApiJson(),
|
||||
);
|
||||
final data = response.data['data'];
|
||||
if (data is Map<String, dynamic>) {
|
||||
return EmailConfigurationSettings.fromApiJson(data).copyWith(
|
||||
allocationTemplate: email.allocationTemplate,
|
||||
returnTemplate: email.returnTemplate,
|
||||
maintenanceTemplate: email.maintenanceTemplate,
|
||||
warrantyTemplate: email.warrantyTemplate,
|
||||
);
|
||||
}
|
||||
return email;
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,17 +16,147 @@ class SettingsRepositoryImpl implements SettingsRepository {
|
||||
@override
|
||||
Future<Result<AppSettings>> getSettings() async {
|
||||
return safeApiCall(() async {
|
||||
// Local-first until settings API is available.
|
||||
return await local.read() ?? const AppSettings();
|
||||
final localSettings = await local.read() ?? const AppSettings();
|
||||
try {
|
||||
final company = await remote.fetchCompany();
|
||||
final email = await remote.fetchEmail();
|
||||
return localSettings.copyWith(
|
||||
companyProfile: company ?? localSettings.companyProfile,
|
||||
email: email != null
|
||||
? localSettings.email.copyWith(
|
||||
smtpHost: email.smtpHost,
|
||||
smtpPort: email.smtpPort,
|
||||
smtpUsername: email.smtpUsername,
|
||||
smtpPassword: email.smtpPassword.isNotEmpty
|
||||
? email.smtpPassword
|
||||
: localSettings.email.smtpPassword,
|
||||
senderEmail: email.senderEmail,
|
||||
senderName: email.senderName,
|
||||
)
|
||||
: localSettings.email,
|
||||
);
|
||||
} catch (_) {
|
||||
return localSettings;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<AppSettings>> saveSettings(AppSettings settings) async {
|
||||
return safeApiCall(() async {
|
||||
// Always persist locally; remote sync can be wired when API is ready.
|
||||
await local.write(settings);
|
||||
return settings;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<CompanyProfileSettings>> fetchCompanyProfile() async {
|
||||
return safeApiCall(() async {
|
||||
final remoteProfile = await remote.fetchCompany();
|
||||
if (remoteProfile != null) {
|
||||
final current = await local.read();
|
||||
final merged = (current?.companyProfile ?? const CompanyProfileSettings())
|
||||
.copyWith(
|
||||
companyName: remoteProfile.companyName,
|
||||
address: remoteProfile.address,
|
||||
city: remoteProfile.city,
|
||||
state: remoteProfile.state,
|
||||
pincode: remoteProfile.pincode,
|
||||
email: remoteProfile.email,
|
||||
phone: remoteProfile.phone,
|
||||
website: remoteProfile.website,
|
||||
logoUrl: remoteProfile.logoUrl.isNotEmpty
|
||||
? remoteProfile.logoUrl
|
||||
: current?.companyProfile.logoUrl ?? '',
|
||||
);
|
||||
await local.write((current ?? const AppSettings()).copyWith(
|
||||
companyProfile: merged,
|
||||
));
|
||||
return merged;
|
||||
}
|
||||
return (await local.read())?.companyProfile ??
|
||||
const CompanyProfileSettings();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<CompanyProfileSettings>> saveCompanyProfile(
|
||||
CompanyProfileSettings profile,
|
||||
) async {
|
||||
return safeApiCall(() async {
|
||||
final saved = await remote.saveCompany(profile);
|
||||
final current = await local.read() ?? const AppSettings();
|
||||
final merged = profile.copyWith(
|
||||
companyName: saved.companyName.isNotEmpty
|
||||
? saved.companyName
|
||||
: profile.companyName,
|
||||
address: saved.address.isNotEmpty ? saved.address : profile.address,
|
||||
city: saved.city.isNotEmpty ? saved.city : profile.city,
|
||||
state: saved.state.isNotEmpty ? saved.state : profile.state,
|
||||
pincode: saved.pincode.isNotEmpty ? saved.pincode : profile.pincode,
|
||||
email: saved.email.isNotEmpty ? saved.email : profile.email,
|
||||
phone: saved.phone.isNotEmpty ? saved.phone : profile.phone,
|
||||
website: saved.website.isNotEmpty ? saved.website : profile.website,
|
||||
logoUrl: saved.logoUrl.isNotEmpty ? saved.logoUrl : profile.logoUrl,
|
||||
);
|
||||
await local.write(current.copyWith(companyProfile: merged));
|
||||
return merged;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<String?>> uploadCompanyLogo(
|
||||
List<int> bytes,
|
||||
String filename,
|
||||
) async {
|
||||
return safeApiCall<String?>(() => remote.uploadCompanyLogo(bytes, filename));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<EmailConfigurationSettings>> fetchEmailSettings() async {
|
||||
return safeApiCall(() async {
|
||||
final remoteEmail = await remote.fetchEmail();
|
||||
if (remoteEmail != null) {
|
||||
final current = await local.read() ?? const AppSettings();
|
||||
final merged = current.email.copyWith(
|
||||
smtpHost: remoteEmail.smtpHost,
|
||||
smtpPort: remoteEmail.smtpPort,
|
||||
smtpUsername: remoteEmail.smtpUsername,
|
||||
smtpPassword: remoteEmail.smtpPassword.isNotEmpty
|
||||
? remoteEmail.smtpPassword
|
||||
: current.email.smtpPassword,
|
||||
senderEmail: remoteEmail.senderEmail,
|
||||
senderName: remoteEmail.senderName,
|
||||
);
|
||||
await local.write(current.copyWith(email: merged));
|
||||
return merged;
|
||||
}
|
||||
return (await local.read())?.email ?? const EmailConfigurationSettings();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<EmailConfigurationSettings>> saveEmailSettings(
|
||||
EmailConfigurationSettings email,
|
||||
) async {
|
||||
return safeApiCall(() async {
|
||||
final saved = await remote.saveEmail(email);
|
||||
final current = await local.read() ?? const AppSettings();
|
||||
final merged = email.copyWith(
|
||||
smtpHost: saved.smtpHost.isNotEmpty ? saved.smtpHost : email.smtpHost,
|
||||
smtpPort: saved.smtpPort,
|
||||
smtpUsername:
|
||||
saved.smtpUsername.isNotEmpty ? saved.smtpUsername : email.smtpUsername,
|
||||
smtpPassword: saved.smtpPassword.isNotEmpty
|
||||
? saved.smtpPassword
|
||||
: email.smtpPassword,
|
||||
senderEmail:
|
||||
saved.senderEmail.isNotEmpty ? saved.senderEmail : email.senderEmail,
|
||||
senderName:
|
||||
saved.senderName.isNotEmpty ? saved.senderName : email.senderName,
|
||||
);
|
||||
await local.write(current.copyWith(email: merged));
|
||||
return merged;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,6 +73,9 @@ class CompanyProfileSettings {
|
||||
this.registrationNumber = '',
|
||||
this.gstNumber = '',
|
||||
this.address = '',
|
||||
this.city = '',
|
||||
this.state = '',
|
||||
this.pincode = '',
|
||||
this.email = '',
|
||||
this.phone = '',
|
||||
this.website = '',
|
||||
@ -85,6 +88,9 @@ class CompanyProfileSettings {
|
||||
final String registrationNumber;
|
||||
final String gstNumber;
|
||||
final String address;
|
||||
final String city;
|
||||
final String state;
|
||||
final String pincode;
|
||||
final String email;
|
||||
final String phone;
|
||||
final String website;
|
||||
@ -97,6 +103,9 @@ class CompanyProfileSettings {
|
||||
String? registrationNumber,
|
||||
String? gstNumber,
|
||||
String? address,
|
||||
String? city,
|
||||
String? state,
|
||||
String? pincode,
|
||||
String? email,
|
||||
String? phone,
|
||||
String? website,
|
||||
@ -109,6 +118,9 @@ class CompanyProfileSettings {
|
||||
registrationNumber: registrationNumber ?? this.registrationNumber,
|
||||
gstNumber: gstNumber ?? this.gstNumber,
|
||||
address: address ?? this.address,
|
||||
city: city ?? this.city,
|
||||
state: state ?? this.state,
|
||||
pincode: pincode ?? this.pincode,
|
||||
email: email ?? this.email,
|
||||
phone: phone ?? this.phone,
|
||||
website: website ?? this.website,
|
||||
@ -123,6 +135,9 @@ class CompanyProfileSettings {
|
||||
'registrationNumber': registrationNumber,
|
||||
'gstNumber': gstNumber,
|
||||
'address': address,
|
||||
'city': city,
|
||||
'state': state,
|
||||
'pincode': pincode,
|
||||
'email': email,
|
||||
'phone': phone,
|
||||
'website': website,
|
||||
@ -130,17 +145,41 @@ class CompanyProfileSettings {
|
||||
'faviconUrl': faviconUrl,
|
||||
};
|
||||
|
||||
Map<String, dynamic> toApiJson() {
|
||||
final payload = <String, dynamic>{};
|
||||
if (companyName.isNotEmpty) payload['org_name'] = companyName;
|
||||
if (phone.isNotEmpty) payload['mobile'] = phone;
|
||||
if (email.isNotEmpty) payload['email'] = email;
|
||||
if (website.isNotEmpty) payload['website'] = website;
|
||||
if (address.isNotEmpty) payload['address'] = address;
|
||||
if (city.isNotEmpty) payload['city'] = city;
|
||||
if (state.isNotEmpty) payload['state'] = state;
|
||||
if (pincode.isNotEmpty) payload['pincode'] = pincode;
|
||||
return payload;
|
||||
}
|
||||
|
||||
factory CompanyProfileSettings.fromJson(Map<String, dynamic> json) =>
|
||||
CompanyProfileSettings.fromApiJson(json);
|
||||
|
||||
factory CompanyProfileSettings.fromApiJson(Map<String, dynamic> json) =>
|
||||
CompanyProfileSettings(
|
||||
companyName: json['companyName'] as String? ?? '',
|
||||
companyName: json['org_name'] as String? ??
|
||||
json['companyName'] as String? ??
|
||||
'',
|
||||
companyCode: json['companyCode'] as String? ?? '',
|
||||
registrationNumber: json['registrationNumber'] as String? ?? '',
|
||||
gstNumber: json['gstNumber'] as String? ?? '',
|
||||
address: json['address'] as String? ?? '',
|
||||
city: json['city'] as String? ?? '',
|
||||
state: json['state'] as String? ?? '',
|
||||
pincode: json['pincode'] as String? ?? '',
|
||||
email: json['email'] as String? ?? '',
|
||||
phone: json['phone'] as String? ?? '',
|
||||
phone: json['mobile'] as String? ?? json['phone'] as String? ?? '',
|
||||
website: json['website'] as String? ?? '',
|
||||
logoUrl: json['logoUrl'] as String? ?? '',
|
||||
logoUrl: json['logo_url'] as String? ??
|
||||
json['logoUrl'] as String? ??
|
||||
json['logo'] as String? ??
|
||||
'',
|
||||
faviconUrl: json['faviconUrl'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
@ -411,14 +450,34 @@ class EmailConfigurationSettings {
|
||||
'warrantyTemplate': warrantyTemplate,
|
||||
};
|
||||
|
||||
Map<String, dynamic> toApiJson() {
|
||||
final payload = <String, dynamic>{};
|
||||
if (smtpHost.isNotEmpty) payload['smtp_host'] = smtpHost;
|
||||
if (smtpPort > 0) payload['smtp_port'] = smtpPort;
|
||||
if (smtpUsername.isNotEmpty) payload['smtp_username'] = smtpUsername;
|
||||
if (smtpPassword.isNotEmpty) payload['smtp_password'] = smtpPassword;
|
||||
if (senderEmail.isNotEmpty) payload['sender_email'] = senderEmail;
|
||||
if (senderName.isNotEmpty) payload['sender_name'] = senderName;
|
||||
return payload;
|
||||
}
|
||||
|
||||
factory EmailConfigurationSettings.fromJson(Map<String, dynamic> json) =>
|
||||
EmailConfigurationSettings.fromApiJson(json);
|
||||
|
||||
factory EmailConfigurationSettings.fromApiJson(Map<String, dynamic> json) =>
|
||||
EmailConfigurationSettings(
|
||||
smtpHost: json['smtpHost'] as String? ?? '',
|
||||
smtpPort: json['smtpPort'] as int? ?? 587,
|
||||
smtpUsername: json['smtpUsername'] as String? ?? '',
|
||||
smtpPassword: json['smtpPassword'] as String? ?? '',
|
||||
senderEmail: json['senderEmail'] as String? ?? '',
|
||||
senderName: json['senderName'] as String? ?? '',
|
||||
smtpHost: json['smtp_host'] as String? ?? json['smtpHost'] as String? ?? '',
|
||||
smtpPort: (json['smtp_port'] as num?)?.toInt() ??
|
||||
(json['smtpPort'] as num?)?.toInt() ??
|
||||
587,
|
||||
smtpUsername:
|
||||
json['smtp_username'] as String? ?? json['smtpUsername'] as String? ?? '',
|
||||
smtpPassword:
|
||||
json['smtp_password'] as String? ?? json['smtpPassword'] as String? ?? '',
|
||||
senderEmail:
|
||||
json['sender_email'] as String? ?? json['senderEmail'] as String? ?? '',
|
||||
senderName:
|
||||
json['sender_name'] as String? ?? json['senderName'] as String? ?? '',
|
||||
allocationTemplate: json['allocationTemplate'] as String? ??
|
||||
'Your asset {{asset_name}} has been allocated.',
|
||||
returnTemplate: json['returnTemplate'] as String? ??
|
||||
|
||||
@ -4,4 +4,16 @@ import '../entities/app_settings.dart';
|
||||
abstract class SettingsRepository {
|
||||
Future<Result<AppSettings>> getSettings();
|
||||
Future<Result<AppSettings>> saveSettings(AppSettings settings);
|
||||
Future<Result<CompanyProfileSettings>> fetchCompanyProfile();
|
||||
Future<Result<CompanyProfileSettings>> saveCompanyProfile(
|
||||
CompanyProfileSettings profile,
|
||||
);
|
||||
Future<Result<String?>> uploadCompanyLogo(
|
||||
List<int> bytes,
|
||||
String filename,
|
||||
);
|
||||
Future<Result<EmailConfigurationSettings>> fetchEmailSettings();
|
||||
Future<Result<EmailConfigurationSettings>> saveEmailSettings(
|
||||
EmailConfigurationSettings email,
|
||||
);
|
||||
}
|
||||
|
||||
@ -40,6 +40,7 @@ final appSettingsProvider =
|
||||
return AppSettingsNotifier(
|
||||
getSettings: ref.watch(getSettingsUseCaseProvider),
|
||||
saveSettings: ref.watch(saveSettingsUseCaseProvider),
|
||||
repository: ref.watch(settingsRepositoryProvider),
|
||||
faviconStore: FaviconStore(ref.watch(sharedPreferencesProvider)),
|
||||
);
|
||||
});
|
||||
@ -48,9 +49,11 @@ class AppSettingsNotifier extends StateNotifier<AppSettings> {
|
||||
AppSettingsNotifier({
|
||||
required GetSettingsUseCase getSettings,
|
||||
required SaveSettingsUseCase saveSettings,
|
||||
required SettingsRepository repository,
|
||||
required FaviconStore faviconStore,
|
||||
}) : _getSettings = getSettings,
|
||||
_saveSettings = saveSettings,
|
||||
_repository = repository,
|
||||
_faviconStore = faviconStore,
|
||||
super(const AppSettings()) {
|
||||
_load();
|
||||
@ -58,6 +61,7 @@ class AppSettingsNotifier extends StateNotifier<AppSettings> {
|
||||
|
||||
final GetSettingsUseCase _getSettings;
|
||||
final SaveSettingsUseCase _saveSettings;
|
||||
final SettingsRepository _repository;
|
||||
final FaviconStore _faviconStore;
|
||||
|
||||
Future<void> _load() async {
|
||||
@ -66,6 +70,20 @@ class AppSettingsNotifier extends StateNotifier<AppSettings> {
|
||||
_faviconStore.apply();
|
||||
}
|
||||
|
||||
Future<void> refreshCompanyProfile() async {
|
||||
final result = await _repository.fetchCompanyProfile();
|
||||
if (result.failure == null && result.data != null) {
|
||||
state = state.copyWith(companyProfile: result.data!);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> refreshEmailSettings() async {
|
||||
final result = await _repository.fetchEmailSettings();
|
||||
if (result.failure == null && result.data != null) {
|
||||
state = state.copyWith(email: result.data!);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _persist(AppSettings settings) async {
|
||||
state = settings;
|
||||
final result = await _saveSettings(settings);
|
||||
@ -77,9 +95,28 @@ class AppSettingsNotifier extends StateNotifier<AppSettings> {
|
||||
}
|
||||
|
||||
Future<void> updateCompanyProfile(CompanyProfileSettings profile) async {
|
||||
final result = await _repository.saveCompanyProfile(profile);
|
||||
if (result.failure == null && result.data != null) {
|
||||
state = state.copyWith(companyProfile: result.data!);
|
||||
return;
|
||||
}
|
||||
await _persist(state.copyWith(companyProfile: profile));
|
||||
}
|
||||
|
||||
Future<String?> uploadCompanyLogo(List<int> bytes, String filename) async {
|
||||
final result = await _repository.uploadCompanyLogo(bytes, filename);
|
||||
if (result.failure == null && result.data != null) {
|
||||
final logoUrl = result.data!;
|
||||
await _persist(
|
||||
state.copyWith(
|
||||
companyProfile: state.companyProfile.copyWith(logoUrl: logoUrl),
|
||||
),
|
||||
);
|
||||
return logoUrl;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> updateUiPreferences(UiPreferencesSettings prefs) async {
|
||||
await _persist(state.copyWith(uiPreferences: prefs));
|
||||
}
|
||||
@ -93,6 +130,12 @@ class AppSettingsNotifier extends StateNotifier<AppSettings> {
|
||||
}
|
||||
|
||||
Future<void> updateEmail(EmailConfigurationSettings email) async {
|
||||
final result = await _repository.saveEmailSettings(email);
|
||||
if (result.failure == null && result.data != null) {
|
||||
state = state.copyWith(email: result.data!);
|
||||
await _saveSettings(state);
|
||||
return;
|
||||
}
|
||||
await _persist(state.copyWith(email: email));
|
||||
}
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ import '../../../../core/utils/favicon_store.dart';
|
||||
import '../../../../core/utils/favicon_updater.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 '../../../../shared/widgets/sidebar_logo.dart';
|
||||
import '../../domain/entities/app_settings.dart';
|
||||
@ -31,6 +32,9 @@ class _CompanyProfileSettingsScreenState
|
||||
late final TextEditingController _registrationController;
|
||||
late final TextEditingController _gstController;
|
||||
late final TextEditingController _addressController;
|
||||
late final TextEditingController _cityController;
|
||||
late final TextEditingController _stateController;
|
||||
late final TextEditingController _pincodeController;
|
||||
late final TextEditingController _emailController;
|
||||
late final TextEditingController _phoneController;
|
||||
late final TextEditingController _websiteController;
|
||||
@ -43,12 +47,18 @@ class _CompanyProfileSettingsScreenState
|
||||
final profile = ref.read(appSettingsProvider).companyProfile;
|
||||
final faviconFromPrefs =
|
||||
FaviconStore(ref.read(sharedPreferencesProvider)).read();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref.read(appSettingsProvider.notifier).refreshCompanyProfile();
|
||||
});
|
||||
_nameController = TextEditingController(text: profile.companyName);
|
||||
_codeController = TextEditingController(text: profile.companyCode);
|
||||
_registrationController =
|
||||
TextEditingController(text: profile.registrationNumber);
|
||||
_gstController = TextEditingController(text: profile.gstNumber);
|
||||
_addressController = TextEditingController(text: profile.address);
|
||||
_cityController = TextEditingController(text: profile.city);
|
||||
_stateController = TextEditingController(text: profile.state);
|
||||
_pincodeController = TextEditingController(text: profile.pincode);
|
||||
_emailController = TextEditingController(text: profile.email);
|
||||
_phoneController = TextEditingController(text: profile.phone);
|
||||
_websiteController = TextEditingController(text: profile.website);
|
||||
@ -67,6 +77,9 @@ class _CompanyProfileSettingsScreenState
|
||||
_registrationController.dispose();
|
||||
_gstController.dispose();
|
||||
_addressController.dispose();
|
||||
_cityController.dispose();
|
||||
_stateController.dispose();
|
||||
_pincodeController.dispose();
|
||||
_emailController.dispose();
|
||||
_phoneController.dispose();
|
||||
_websiteController.dispose();
|
||||
@ -89,6 +102,9 @@ class _CompanyProfileSettingsScreenState
|
||||
registrationNumber: _registrationController.text.trim(),
|
||||
gstNumber: _gstController.text.trim(),
|
||||
address: _addressController.text.trim(),
|
||||
city: _cityController.text.trim(),
|
||||
state: _stateController.text.trim(),
|
||||
pincode: _pincodeController.text.trim(),
|
||||
email: _emailController.text.trim(),
|
||||
phone: _phoneController.text.trim(),
|
||||
website: _websiteController.text.trim(),
|
||||
@ -137,9 +153,38 @@ class _CompanyProfileSettingsScreenState
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _pickLogo() => _pickImage((dataUri) {
|
||||
_logoUrlController.text = dataUri;
|
||||
});
|
||||
Future<void> _pickLogo() async {
|
||||
final result = await FilePicker.pickFiles(
|
||||
type: FileType.image,
|
||||
withData: true,
|
||||
);
|
||||
|
||||
if (result == null || result.files.isEmpty) return;
|
||||
|
||||
final file = result.files.single;
|
||||
final bytes = file.bytes;
|
||||
if (bytes == null) return;
|
||||
|
||||
final uploadedUrl = await ref.read(appSettingsProvider.notifier).uploadCompanyLogo(
|
||||
bytes,
|
||||
file.name,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (uploadedUrl != null && uploadedUrl.isNotEmpty) {
|
||||
setState(() => _logoUrlController.text = uploadedUrl);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Logo uploaded')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final ext = (file.extension ?? 'png').toLowerCase();
|
||||
final mime = ext == 'jpg' ? 'jpeg' : ext;
|
||||
final dataUri = 'data:image/$mime;base64,${base64Encode(bytes)}';
|
||||
setState(() => _logoUrlController.text = dataUri);
|
||||
}
|
||||
|
||||
Future<void> _pickFavicon() => _pickImage((dataUri) {
|
||||
_faviconUrlController.text = dataUri;
|
||||
@ -184,7 +229,24 @@ class _CompanyProfileSettingsScreenState
|
||||
AppTextField(
|
||||
controller: _addressController,
|
||||
label: 'Address',
|
||||
maxLines: 3,
|
||||
maxLines: 2,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SidePanelFormRow(
|
||||
left: AppTextField(
|
||||
controller: _cityController,
|
||||
label: 'City',
|
||||
),
|
||||
right: AppTextField(
|
||||
controller: _stateController,
|
||||
label: 'State',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
AppTextField(
|
||||
controller: _pincodeController,
|
||||
label: 'Pincode',
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
AppTextField(
|
||||
|
||||
@ -34,6 +34,9 @@ class _EmailConfigurationScreenState
|
||||
void initState() {
|
||||
super.initState();
|
||||
final email = ref.read(appSettingsProvider).email;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref.read(appSettingsProvider.notifier).refreshEmailSettings();
|
||||
});
|
||||
_hostController = TextEditingController(text: email.smtpHost);
|
||||
_portController = TextEditingController(text: '${email.smtpPort}');
|
||||
_usernameController = TextEditingController(text: email.smtpUsername);
|
||||
|
||||
@ -2,6 +2,7 @@ import 'package:dio/dio.dart';
|
||||
|
||||
import '../../../../core/constants/api_endpoints.dart';
|
||||
import '../../../../shared/models/api_response.dart';
|
||||
import '../../../../shared/models/user_management_models.dart';
|
||||
import '../../../../shared/models/vendor_model.dart';
|
||||
|
||||
class VendorRemoteDataSource {
|
||||
@ -152,6 +153,39 @@ class VendorRemoteDataSource {
|
||||
await dio.delete(ApiEndpoints.vendorBankDetailById(vendorId, bankDetailId));
|
||||
}
|
||||
|
||||
Future<List<FilterOptionModel>> getGstTreatments() async {
|
||||
final response = await dio.get(ApiEndpoints.vendorGstTreatments);
|
||||
return _parseDropdownOptions(response.data);
|
||||
}
|
||||
|
||||
Future<List<FilterOptionModel>> getSourceOfSupply() async {
|
||||
final response = await dio.get(ApiEndpoints.vendorSourceOfSupply);
|
||||
return _parseDropdownOptions(response.data);
|
||||
}
|
||||
|
||||
List<FilterOptionModel> _parseDropdownOptions(dynamic body) {
|
||||
if (body is! Map<String, dynamic>) return [];
|
||||
final raw = body['data'];
|
||||
final list = raw is List<dynamic>
|
||||
? raw
|
||||
: raw is Map<String, dynamic>
|
||||
? raw['items'] as List<dynamic>? ?? const []
|
||||
: const [];
|
||||
return list.map((item) {
|
||||
if (item is String) {
|
||||
return FilterOptionModel(id: item, name: item);
|
||||
}
|
||||
if (item is Map<String, dynamic>) {
|
||||
final value = item['value']?.toString() ?? item['id']?.toString() ?? '';
|
||||
final label = item['label']?.toString() ??
|
||||
item['name']?.toString() ??
|
||||
value;
|
||||
return FilterOptionModel(id: value, name: label);
|
||||
}
|
||||
return const FilterOptionModel(id: '', name: '');
|
||||
}).where((item) => item.id.isNotEmpty).toList();
|
||||
}
|
||||
|
||||
Map<String, dynamic> _queryToMap(VendorListQuery query) {
|
||||
return {
|
||||
'page': query.page,
|
||||
|
||||
@ -231,6 +231,14 @@ class _OverviewTab extends StatelessWidget {
|
||||
_VendorInfo('Vendor Code', vendor.vendorCode ?? '—'),
|
||||
_VendorInfo('Vendor Name', vendor.vendorName),
|
||||
_VendorInfo('Type', vendorTypeLabel(vendor.vendorType)),
|
||||
_VendorInfo(
|
||||
'GST Treatment',
|
||||
gstTreatmentLabel(vendor.gstTreatment),
|
||||
),
|
||||
_VendorInfo(
|
||||
'Source of Supply',
|
||||
vendor.sourceOfSupply ?? '—',
|
||||
),
|
||||
_VendorInfo('GSTIN', vendor.gstin ?? '—'),
|
||||
_VendorInfo('PAN', vendor.pan ?? '—'),
|
||||
_VendorInfo('Payment Term', vendor.paymentTermName ?? '—'),
|
||||
|
||||
@ -13,6 +13,7 @@ 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 '../../data/repositories/vendor_repository_impl.dart';
|
||||
import '../providers/vendors_provider.dart';
|
||||
|
||||
Future<void> openVendorFormPanel(
|
||||
@ -58,6 +59,8 @@ class _VendorFormPanelState extends ConsumerState<VendorFormPanel> {
|
||||
final _creditDaysController = TextEditingController();
|
||||
final _remarksController = TextEditingController();
|
||||
String? _vendorType;
|
||||
String? _gstTreatment;
|
||||
String? _sourceOfSupply;
|
||||
int? _paymentTermId;
|
||||
bool _isActive = true;
|
||||
bool _isSubmitting = false;
|
||||
@ -74,13 +77,16 @@ class _VendorFormPanelState extends ConsumerState<VendorFormPanel> {
|
||||
}
|
||||
|
||||
String _vendorSignature(VendorModel vendor) =>
|
||||
'${vendor.id}:${vendor.vendorType}:${vendor.paymentTermId}:'
|
||||
'${vendor.id}:${vendor.vendorType}:${vendor.gstTreatment}:'
|
||||
'${vendor.sourceOfSupply}:${vendor.paymentTermId}:'
|
||||
'${vendor.creditPeriodDays}:${vendor.isActive}:${vendor.vendorName}';
|
||||
|
||||
void _populateFromVendor(VendorModel vendor) {
|
||||
setState(() {
|
||||
_nameController.text = vendor.vendorName;
|
||||
_vendorType = vendor.vendorType;
|
||||
_gstTreatment = vendor.gstTreatment;
|
||||
_sourceOfSupply = vendor.sourceOfSupply;
|
||||
_gstinController.text = vendor.gstin ?? '';
|
||||
_panController.text = vendor.pan ?? '';
|
||||
_paymentTermId = vendor.paymentTermId;
|
||||
@ -95,6 +101,9 @@ class _VendorFormPanelState extends ConsumerState<VendorFormPanel> {
|
||||
return {
|
||||
'vendor_name': _nameController.text.trim(),
|
||||
'vendor_type': _vendorType,
|
||||
if (_gstTreatment != null) 'gst_treatment': _gstTreatment,
|
||||
if (_sourceOfSupply != null && _sourceOfSupply!.isNotEmpty)
|
||||
'source_of_supply': _sourceOfSupply,
|
||||
if (_gstinController.text.trim().isNotEmpty)
|
||||
'gstin': _gstinController.text.trim(),
|
||||
if (_panController.text.trim().isNotEmpty) 'pan': _panController.text.trim(),
|
||||
@ -140,6 +149,8 @@ class _VendorFormPanelState extends ConsumerState<VendorFormPanel> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final paymentTermsAsync = ref.watch(vendorPaymentTermsProvider);
|
||||
final gstTreatmentsAsync = ref.watch(vendorGstTreatmentsProvider);
|
||||
final sourceOfSupplyAsync = ref.watch(vendorSourceOfSupplyProvider);
|
||||
|
||||
if (widget.isEditing) {
|
||||
ref.listen(vendorFormProvider(widget.vendorId), (prev, next) {
|
||||
@ -185,13 +196,25 @@ class _VendorFormPanelState extends ConsumerState<VendorFormPanel> {
|
||||
e is Failure ? e : Failure.unknown(message: e.toString()),
|
||||
onRetry: () => ref.invalidate(vendorFormProvider(widget.vendorId)),
|
||||
),
|
||||
data: (_) => _buildForm(paymentTermsAsync),
|
||||
data: (_) => _buildForm(
|
||||
paymentTermsAsync,
|
||||
gstTreatmentsAsync,
|
||||
sourceOfSupplyAsync,
|
||||
),
|
||||
)
|
||||
: _buildForm(paymentTermsAsync),
|
||||
: _buildForm(
|
||||
paymentTermsAsync,
|
||||
gstTreatmentsAsync,
|
||||
sourceOfSupplyAsync,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildForm(AsyncValue<List<FilterOptionModel>> paymentTermsAsync) {
|
||||
Widget _buildForm(
|
||||
AsyncValue<List<FilterOptionModel>> paymentTermsAsync,
|
||||
AsyncValue<List<FilterOptionModel>> gstTreatmentsAsync,
|
||||
AsyncValue<List<FilterOptionModel>> sourceOfSupplyAsync,
|
||||
) {
|
||||
return Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
@ -213,6 +236,46 @@ class _VendorFormPanelState extends ConsumerState<VendorFormPanel> {
|
||||
validator: (v) => v == null ? 'Vendor type is required' : null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SidePanelFormRow(
|
||||
left: gstTreatmentsAsync.when(
|
||||
loading: () => const LinearProgressIndicator(),
|
||||
error: (_, __) => AppDropdown<String>(
|
||||
label: 'GST Treatment',
|
||||
value: _gstTreatment,
|
||||
options: gstTreatmentOptions
|
||||
.map((e) => AppDropdownOption(value: e.$1, label: e.$2))
|
||||
.toList(),
|
||||
onChanged: (v) => setState(() => _gstTreatment = v),
|
||||
),
|
||||
data: (options) => AppSearchableDropdown<String>(
|
||||
label: 'GST Treatment',
|
||||
value: _gstTreatment,
|
||||
searchHint: 'Search GST treatment...',
|
||||
options: options
|
||||
.map((o) => AppDropdownOption(value: o.id, label: o.name))
|
||||
.toList(),
|
||||
onChanged: (v) => setState(() => _gstTreatment = v),
|
||||
),
|
||||
),
|
||||
right: sourceOfSupplyAsync.when(
|
||||
loading: () => const LinearProgressIndicator(),
|
||||
error: (_, __) => AppTextField(
|
||||
controller: TextEditingController(text: _sourceOfSupply ?? ''),
|
||||
label: 'Source of Supply',
|
||||
onChanged: (v) => _sourceOfSupply = v,
|
||||
),
|
||||
data: (options) => AppSearchableDropdown<String>(
|
||||
label: 'Source of Supply',
|
||||
value: _sourceOfSupply,
|
||||
searchHint: 'Search state...',
|
||||
options: options
|
||||
.map((o) => AppDropdownOption(value: o.id, label: o.name))
|
||||
.toList(),
|
||||
onChanged: (v) => setState(() => _sourceOfSupply = v),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SidePanelFormRow(
|
||||
left: AppTextField(
|
||||
controller: _gstinController,
|
||||
@ -286,3 +349,15 @@ final vendorPaymentTermsProvider =
|
||||
final dataSource = ref.watch(masterRemoteDataSourceProvider);
|
||||
return dataSource.listPaymentTerms();
|
||||
});
|
||||
|
||||
final vendorGstTreatmentsProvider =
|
||||
FutureProvider<List<FilterOptionModel>>((ref) async {
|
||||
final dataSource = ref.watch(vendorRemoteDataSourceProvider);
|
||||
return dataSource.getGstTreatments();
|
||||
});
|
||||
|
||||
final vendorSourceOfSupplyProvider =
|
||||
FutureProvider<List<FilterOptionModel>>((ref) async {
|
||||
final dataSource = ref.watch(vendorRemoteDataSourceProvider);
|
||||
return dataSource.getSourceOfSupply();
|
||||
});
|
||||
|
||||
@ -64,6 +64,38 @@ Object? _readPlantId(Map<dynamic, dynamic> json, String key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Object? _readAssetSubcategoryName(Map<dynamic, dynamic> json, String key) {
|
||||
final flat = json['asset_subcategory_name'];
|
||||
if (flat is String && flat.isNotEmpty) return flat;
|
||||
return _readNestedName(json, 'asset_subcategory');
|
||||
}
|
||||
|
||||
Object? _readAssetSubcategoryId(Map<dynamic, dynamic> json, String key) {
|
||||
final flat = json['asset_subcategory_id'];
|
||||
if (flat != null) return flat;
|
||||
final nested = json['asset_subcategory'];
|
||||
if (nested is Map) return nested['id'];
|
||||
return null;
|
||||
}
|
||||
|
||||
Object? _readDepartmentName(Map<dynamic, dynamic> json, String key) {
|
||||
final flat = json['department_name'];
|
||||
if (flat is String && flat.isNotEmpty) return flat;
|
||||
return _readNestedName(json, 'department');
|
||||
}
|
||||
|
||||
Object? _readWarehouseName(Map<dynamic, dynamic> json, String key) {
|
||||
final flat = json['warehouse_name'];
|
||||
if (flat is String && flat.isNotEmpty) return flat;
|
||||
return _readNestedName(json, 'warehouse');
|
||||
}
|
||||
|
||||
Object? _readVendorNameFromNested(Map<dynamic, dynamic> json, String key) {
|
||||
final flat = json['vendor_name'];
|
||||
if (flat is String && flat.isNotEmpty) return flat;
|
||||
return _readNestedName(json, 'vendor');
|
||||
}
|
||||
|
||||
@freezed
|
||||
class AssetCategoryModel with _$AssetCategoryModel {
|
||||
const factory AssetCategoryModel({
|
||||
@ -97,13 +129,50 @@ class AssetModel with _$AssetModel {
|
||||
int? assetCategoryId,
|
||||
@JsonKey(name: 'asset_category_name', readValue: _readAssetCategoryName)
|
||||
String? assetCategoryName,
|
||||
@JsonKey(
|
||||
name: 'asset_subcategory_id',
|
||||
readValue: _readAssetSubcategoryId,
|
||||
fromJson: _intFromJsonNullable,
|
||||
)
|
||||
int? assetSubcategoryId,
|
||||
@JsonKey(name: 'asset_subcategory_name', readValue: _readAssetSubcategoryName)
|
||||
String? assetSubcategoryName,
|
||||
@JsonKey(name: 'plant_id', readValue: _readPlantId, fromJson: _intFromJsonNullable)
|
||||
int? plantId,
|
||||
@JsonKey(name: 'plant_name', readValue: _readPlantName) String? plantName,
|
||||
@JsonKey(name: 'brand_model') String? brandModel,
|
||||
String? manufacturer,
|
||||
@JsonKey(name: 'serial_number') String? serialNumber,
|
||||
@JsonKey(name: 'part_number') String? partNumber,
|
||||
@JsonKey(name: 'department_id', fromJson: _intFromJsonNullable) int? departmentId,
|
||||
@JsonKey(name: 'department_name', readValue: _readDepartmentName)
|
||||
String? departmentName,
|
||||
@JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable) int? warehouseId,
|
||||
@JsonKey(name: 'warehouse_name', readValue: _readWarehouseName) String? warehouseName,
|
||||
@JsonKey(name: 'location_detail') String? locationDetail,
|
||||
@JsonKey(name: 'assigned_to_user_id', fromJson: _intFromJsonNullable)
|
||||
int? assignedToUserId,
|
||||
@JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? vendorId,
|
||||
@JsonKey(name: 'vendor_name', readValue: _readVendorNameFromNested) String? vendorName,
|
||||
@JsonKey(name: 'po_id', fromJson: _intFromJsonNullable) int? poId,
|
||||
@JsonKey(name: 'grn_id', fromJson: _intFromJsonNullable) int? grnId,
|
||||
@JsonKey(name: 'grn_item_id', fromJson: _intFromJsonNullable) int? grnItemId,
|
||||
@JsonKey(name: 'purchase_date', fromJson: _dateFromJsonNullable) DateTime? purchaseDate,
|
||||
@JsonKey(name: 'purchase_cost', fromJson: _doubleFromJsonNullable) double? purchaseCost,
|
||||
@JsonKey(name: 'useful_life_years', fromJson: _intFromJsonNullable) int? usefulLifeYears,
|
||||
@JsonKey(name: 'depreciation_method') String? depreciationMethod,
|
||||
@JsonKey(name: 'depreciation_rate', fromJson: _doubleFromJsonNullable)
|
||||
double? depreciationRate,
|
||||
@JsonKey(name: 'salvage_value', fromJson: _doubleFromJsonNullable) double? salvageValue,
|
||||
@JsonKey(name: 'warranty_expiry_date', fromJson: _dateFromJsonNullable)
|
||||
DateTime? warrantyExpiryDate,
|
||||
@JsonKey(name: 'purchase_cost', fromJson: _doubleFromJsonNullable) double? purchaseCost,
|
||||
String? condition,
|
||||
String? status,
|
||||
@JsonKey(name: 'qr_code_value') String? qrCodeValue,
|
||||
@JsonKey(name: 'disposal_date', fromJson: _dateFromJsonNullable) DateTime? disposalDate,
|
||||
@JsonKey(name: 'disposal_reason') String? disposalReason,
|
||||
@JsonKey(name: 'disposal_value', fromJson: _doubleFromJsonNullable) double? disposalValue,
|
||||
String? remarks,
|
||||
@JsonKey(name: 'is_active') @Default(true) bool isActive,
|
||||
@JsonKey(name: 'created_at', fromJson: _dateFromJsonNullable) DateTime? createdAt,
|
||||
@JsonKey(name: 'updated_at', fromJson: _dateFromJsonNullable) DateTime? updatedAt,
|
||||
@ -112,6 +181,45 @@ class AssetModel with _$AssetModel {
|
||||
factory AssetModel.fromJson(Map<String, dynamic> json) => _$AssetModelFromJson(json);
|
||||
}
|
||||
|
||||
const assetConditionOptions = [
|
||||
('NEW', 'New'),
|
||||
('GOOD', 'Good'),
|
||||
('FAIR', 'Fair'),
|
||||
('POOR', 'Poor'),
|
||||
];
|
||||
|
||||
const assetStatusOptions = [
|
||||
('IN_USE', 'In Use'),
|
||||
('IDLE', 'Idle'),
|
||||
('UNDER_MAINTENANCE', 'Under Maintenance'),
|
||||
('DISPOSED', 'Disposed'),
|
||||
('SCRAPPED', 'Scrapped'),
|
||||
];
|
||||
|
||||
const depreciationMethodOptions = [
|
||||
('SLM', 'SLM'),
|
||||
('WDV', 'WDV'),
|
||||
('OTHER', 'Other'),
|
||||
];
|
||||
|
||||
String assetConditionLabel(String? value) {
|
||||
if (value == null) return '—';
|
||||
return assetConditionOptions
|
||||
.where((e) => e.$1 == value)
|
||||
.map((e) => e.$2)
|
||||
.firstOrNull ??
|
||||
value;
|
||||
}
|
||||
|
||||
String assetStatusLabel(String? value) {
|
||||
if (value == null) return '—';
|
||||
return assetStatusOptions
|
||||
.where((e) => e.$1 == value)
|
||||
.map((e) => e.$2)
|
||||
.firstOrNull ??
|
||||
value.replaceAll('_', ' ');
|
||||
}
|
||||
|
||||
@freezed
|
||||
class AmcContractModel with _$AmcContractModel {
|
||||
const factory AmcContractModel({
|
||||
@ -123,9 +231,21 @@ class AmcContractModel with _$AmcContractModel {
|
||||
@JsonKey(name: 'contract_type') String? contractType,
|
||||
@JsonKey(name: 'start_date', fromJson: _dateFromJson) required DateTime startDate,
|
||||
@JsonKey(name: 'end_date', fromJson: _dateFromJson) required DateTime endDate,
|
||||
@JsonKey(name: 'renewal_date', fromJson: _dateFromJsonNullable)
|
||||
DateTime? renewalDate,
|
||||
@JsonKey(name: 'annual_cost', fromJson: _doubleFromJsonNullable) double? annualCost,
|
||||
@JsonKey(name: 'payment_frequency') String? paymentFrequency,
|
||||
@JsonKey(name: 'service_frequency') String? serviceFrequency,
|
||||
@JsonKey(name: 'visits_per_year', fromJson: _intFromJsonNullable) int? visitsPerYear,
|
||||
@JsonKey(name: 'contact_person') String? contactPerson,
|
||||
@JsonKey(name: 'contact_phone') String? contactPhone,
|
||||
@JsonKey(name: 'contact_email') String? contactEmail,
|
||||
@JsonKey(name: 'scope_of_work') String? scopeOfWork,
|
||||
String? exclusions,
|
||||
String? remarks,
|
||||
@JsonKey(name: 'is_active') @Default(true) bool isActive,
|
||||
@Default('active') String status,
|
||||
DateTime? createdAt,
|
||||
@JsonKey(name: 'created_at', fromJson: _dateFromJsonNullable) DateTime? createdAt,
|
||||
}) = _AmcContractModel;
|
||||
|
||||
factory AmcContractModel.fromJson(Map<String, dynamic> json) =>
|
||||
@ -139,12 +259,26 @@ class ServiceVisitModel with _$ServiceVisitModel {
|
||||
@JsonKey(name: 'asset_id', fromJson: _idFromJson) String? assetId,
|
||||
@JsonKey(name: 'visit_type') required String visitType,
|
||||
@JsonKey(name: 'visit_date', fromJson: _dateFromJson) required DateTime visitDate,
|
||||
@JsonKey(name: 'amc_contract_id', fromJson: _intFromJsonNullable) int? amcContractId,
|
||||
@JsonKey(name: 'visit_number', fromJson: _intFromJsonNullable) int? visitNumber,
|
||||
@JsonKey(name: 'complaint_no') String? complaintNo,
|
||||
@JsonKey(name: 'complaint_date', fromJson: _dateFromJsonNullable) DateTime? complaintDate,
|
||||
@JsonKey(name: 'complaint_desc') String? complaintDesc,
|
||||
@JsonKey(name: 'engineer_name') String? engineerName,
|
||||
@JsonKey(name: 'engineer_phone') String? engineerPhone,
|
||||
@JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? vendorId,
|
||||
@JsonKey(name: 'vendor_name') String? vendorName,
|
||||
@JsonKey(name: 'work_done') String? workDone,
|
||||
@JsonKey(name: 'parts_replaced') String? partsReplaced,
|
||||
@JsonKey(name: 'next_service_date', fromJson: _dateFromJsonNullable)
|
||||
DateTime? nextServiceDate,
|
||||
@Default('COMPLETED') String status,
|
||||
DateTime? createdAt,
|
||||
@JsonKey(name: 'downtime_hours', fromJson: _doubleFromJsonNullable) double? downtimeHours,
|
||||
@JsonKey(name: 'service_cost', fromJson: _doubleFromJsonNullable) double? serviceCost,
|
||||
@JsonKey(name: 'is_under_amc') @Default(false) bool isUnderAmc,
|
||||
@JsonKey(name: 'asset_condition_after') String? assetConditionAfter,
|
||||
String? remarks,
|
||||
@JsonKey(name: 'created_at', fromJson: _dateFromJsonNullable) DateTime? createdAt,
|
||||
}) = _ServiceVisitModel;
|
||||
|
||||
factory ServiceVisitModel.fromJson(Map<String, dynamic> json) =>
|
||||
@ -158,13 +292,25 @@ class InsurancePolicyModel with _$InsurancePolicyModel {
|
||||
@JsonKey(name: 'asset_id', fromJson: _idFromJson) String? assetId,
|
||||
@JsonKey(name: 'policy_no') required String policyNo,
|
||||
@JsonKey(name: 'insurer_name') required String insurerName,
|
||||
@JsonKey(name: 'insurer_branch') String? insurerBranch,
|
||||
@JsonKey(name: 'insurer_contact') String? insurerContact,
|
||||
@JsonKey(name: 'insurer_phone') String? insurerPhone,
|
||||
@JsonKey(name: 'insurer_email') String? insurerEmail,
|
||||
@JsonKey(name: 'policy_type') String? policyType,
|
||||
@JsonKey(name: 'sum_insured', fromJson: _doubleFromJsonNullable) double? sumInsured,
|
||||
@JsonKey(name: 'annual_premium', fromJson: _doubleFromJsonNullable) double? annualPremium,
|
||||
@JsonKey(name: 'policy_start_date', fromJson: _dateFromJson)
|
||||
required DateTime policyStartDate,
|
||||
@JsonKey(name: 'policy_end_date', fromJson: _dateFromJson) required DateTime policyEndDate,
|
||||
@JsonKey(name: 'renewal_date', fromJson: _dateFromJsonNullable) DateTime? renewalDate,
|
||||
@JsonKey(name: 'is_auto_renewal') @Default(false) bool isAutoRenewal,
|
||||
@JsonKey(name: 'premium_paid') @Default(false) bool premiumPaid,
|
||||
@JsonKey(name: 'premium_paid_date', fromJson: _dateFromJsonNullable)
|
||||
DateTime? premiumPaidDate,
|
||||
String? remarks,
|
||||
@JsonKey(name: 'is_active') @Default(true) bool isActive,
|
||||
@Default('active') String status,
|
||||
DateTime? createdAt,
|
||||
@JsonKey(name: 'created_at', fromJson: _dateFromJsonNullable) DateTime? createdAt,
|
||||
}) = _InsurancePolicyModel;
|
||||
|
||||
factory InsurancePolicyModel.fromJson(Map<String, dynamic> json) =>
|
||||
@ -191,3 +337,76 @@ class AssetAlertModel with _$AssetAlertModel {
|
||||
factory AssetAlertModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$AssetAlertModelFromJson(json);
|
||||
}
|
||||
|
||||
class AssetTransferHistoryModel {
|
||||
const AssetTransferHistoryModel({
|
||||
required this.id,
|
||||
this.transferDate,
|
||||
this.reason,
|
||||
this.fromPlantName,
|
||||
this.toPlantName,
|
||||
this.fromDepartmentName,
|
||||
this.toDepartmentName,
|
||||
this.fromWarehouseName,
|
||||
this.toWarehouseName,
|
||||
this.fromUserName,
|
||||
this.toUserName,
|
||||
this.createdAt,
|
||||
this.createdByName,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final DateTime? transferDate;
|
||||
final String? reason;
|
||||
final String? fromPlantName;
|
||||
final String? toPlantName;
|
||||
final String? fromDepartmentName;
|
||||
final String? toDepartmentName;
|
||||
final String? fromWarehouseName;
|
||||
final String? toWarehouseName;
|
||||
final String? fromUserName;
|
||||
final String? toUserName;
|
||||
final DateTime? createdAt;
|
||||
final String? createdByName;
|
||||
|
||||
factory AssetTransferHistoryModel.fromJson(Map<String, dynamic> json) {
|
||||
String? readName(String key) {
|
||||
final value = json[key];
|
||||
if (value is String && value.trim().isNotEmpty) return value.trim();
|
||||
return null;
|
||||
}
|
||||
|
||||
String? readNestedName(String key) {
|
||||
final nested = json[key];
|
||||
if (nested is Map<String, dynamic>) {
|
||||
final name = nested['name'];
|
||||
if (name is String && name.trim().isNotEmpty) return name.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
DateTime? parseDate(dynamic value) {
|
||||
if (value == null) return null;
|
||||
if (value is DateTime) return value;
|
||||
return DateTime.tryParse(value.toString());
|
||||
}
|
||||
|
||||
return AssetTransferHistoryModel(
|
||||
id: _idFromJson(json['id']),
|
||||
transferDate: parseDate(json['transfer_date']),
|
||||
reason: readName('reason'),
|
||||
fromPlantName: readName('from_plant_name') ?? readNestedName('from_plant'),
|
||||
toPlantName: readName('to_plant_name') ?? readNestedName('to_plant'),
|
||||
fromDepartmentName:
|
||||
readName('from_department_name') ?? readNestedName('from_department'),
|
||||
toDepartmentName: readName('to_department_name') ?? readNestedName('to_department'),
|
||||
fromWarehouseName:
|
||||
readName('from_warehouse_name') ?? readNestedName('from_warehouse'),
|
||||
toWarehouseName: readName('to_warehouse_name') ?? readNestedName('to_warehouse'),
|
||||
fromUserName: readName('from_user_name') ?? readNestedName('from_user'),
|
||||
toUserName: readName('to_user_name') ?? readNestedName('to_user'),
|
||||
createdAt: parseDate(json['created_at']),
|
||||
createdByName: readName('created_by_name') ?? readNestedName('created_by'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -50,11 +50,42 @@ _$AssetModelImpl _$$AssetModelImplFromJson(Map<String, dynamic> json) =>
|
||||
),
|
||||
assetCategoryName:
|
||||
_readAssetCategoryName(json, 'asset_category_name') as String?,
|
||||
assetSubcategoryId: _intFromJsonNullable(
|
||||
_readAssetSubcategoryId(json, 'asset_subcategory_id'),
|
||||
),
|
||||
assetSubcategoryName:
|
||||
_readAssetSubcategoryName(json, 'asset_subcategory_name') as String?,
|
||||
plantId: _intFromJsonNullable(_readPlantId(json, 'plant_id')),
|
||||
plantName: _readPlantName(json, 'plant_name') as String?,
|
||||
warrantyExpiryDate: _dateFromJsonNullable(json['warranty_expiry_date']),
|
||||
brandModel: json['brand_model'] as String?,
|
||||
manufacturer: json['manufacturer'] as String?,
|
||||
serialNumber: json['serial_number'] as String?,
|
||||
partNumber: json['part_number'] as String?,
|
||||
departmentId: _intFromJsonNullable(json['department_id']),
|
||||
departmentName: _readDepartmentName(json, 'department_name') as String?,
|
||||
warehouseId: _intFromJsonNullable(json['warehouse_id']),
|
||||
warehouseName: _readWarehouseName(json, 'warehouse_name') as String?,
|
||||
locationDetail: json['location_detail'] as String?,
|
||||
assignedToUserId: _intFromJsonNullable(json['assigned_to_user_id']),
|
||||
vendorId: _intFromJsonNullable(json['vendor_id']),
|
||||
vendorName: _readVendorNameFromNested(json, 'vendor_name') as String?,
|
||||
poId: _intFromJsonNullable(json['po_id']),
|
||||
grnId: _intFromJsonNullable(json['grn_id']),
|
||||
grnItemId: _intFromJsonNullable(json['grn_item_id']),
|
||||
purchaseDate: _dateFromJsonNullable(json['purchase_date']),
|
||||
purchaseCost: _doubleFromJsonNullable(json['purchase_cost']),
|
||||
usefulLifeYears: _intFromJsonNullable(json['useful_life_years']),
|
||||
depreciationMethod: json['depreciation_method'] as String?,
|
||||
depreciationRate: _doubleFromJsonNullable(json['depreciation_rate']),
|
||||
salvageValue: _doubleFromJsonNullable(json['salvage_value']),
|
||||
warrantyExpiryDate: _dateFromJsonNullable(json['warranty_expiry_date']),
|
||||
condition: json['condition'] as String?,
|
||||
status: json['status'] as String?,
|
||||
qrCodeValue: json['qr_code_value'] as String?,
|
||||
disposalDate: _dateFromJsonNullable(json['disposal_date']),
|
||||
disposalReason: json['disposal_reason'] as String?,
|
||||
disposalValue: _doubleFromJsonNullable(json['disposal_value']),
|
||||
remarks: json['remarks'] as String?,
|
||||
isActive: json['is_active'] as bool? ?? true,
|
||||
createdAt: _dateFromJsonNullable(json['created_at']),
|
||||
updatedAt: _dateFromJsonNullable(json['updated_at']),
|
||||
@ -67,11 +98,39 @@ Map<String, dynamic> _$$AssetModelImplToJson(_$AssetModelImpl instance) =>
|
||||
'asset_code': instance.assetCode,
|
||||
'asset_category_id': instance.assetCategoryId,
|
||||
'asset_category_name': instance.assetCategoryName,
|
||||
'asset_subcategory_id': instance.assetSubcategoryId,
|
||||
'asset_subcategory_name': instance.assetSubcategoryName,
|
||||
'plant_id': instance.plantId,
|
||||
'plant_name': instance.plantName,
|
||||
'warranty_expiry_date': instance.warrantyExpiryDate?.toIso8601String(),
|
||||
'brand_model': instance.brandModel,
|
||||
'manufacturer': instance.manufacturer,
|
||||
'serial_number': instance.serialNumber,
|
||||
'part_number': instance.partNumber,
|
||||
'department_id': instance.departmentId,
|
||||
'department_name': instance.departmentName,
|
||||
'warehouse_id': instance.warehouseId,
|
||||
'warehouse_name': instance.warehouseName,
|
||||
'location_detail': instance.locationDetail,
|
||||
'assigned_to_user_id': instance.assignedToUserId,
|
||||
'vendor_id': instance.vendorId,
|
||||
'vendor_name': instance.vendorName,
|
||||
'po_id': instance.poId,
|
||||
'grn_id': instance.grnId,
|
||||
'grn_item_id': instance.grnItemId,
|
||||
'purchase_date': instance.purchaseDate?.toIso8601String(),
|
||||
'purchase_cost': instance.purchaseCost,
|
||||
'useful_life_years': instance.usefulLifeYears,
|
||||
'depreciation_method': instance.depreciationMethod,
|
||||
'depreciation_rate': instance.depreciationRate,
|
||||
'salvage_value': instance.salvageValue,
|
||||
'warranty_expiry_date': instance.warrantyExpiryDate?.toIso8601String(),
|
||||
'condition': instance.condition,
|
||||
'status': instance.status,
|
||||
'qr_code_value': instance.qrCodeValue,
|
||||
'disposal_date': instance.disposalDate?.toIso8601String(),
|
||||
'disposal_reason': instance.disposalReason,
|
||||
'disposal_value': instance.disposalValue,
|
||||
'remarks': instance.remarks,
|
||||
'is_active': instance.isActive,
|
||||
'created_at': instance.createdAt?.toIso8601String(),
|
||||
'updated_at': instance.updatedAt?.toIso8601String(),
|
||||
@ -88,11 +147,20 @@ _$AmcContractModelImpl _$$AmcContractModelImplFromJson(
|
||||
contractType: json['contract_type'] as String?,
|
||||
startDate: _dateFromJson(json['start_date']),
|
||||
endDate: _dateFromJson(json['end_date']),
|
||||
renewalDate: _dateFromJsonNullable(json['renewal_date']),
|
||||
annualCost: _doubleFromJsonNullable(json['annual_cost']),
|
||||
paymentFrequency: json['payment_frequency'] as String?,
|
||||
serviceFrequency: json['service_frequency'] as String?,
|
||||
visitsPerYear: _intFromJsonNullable(json['visits_per_year']),
|
||||
contactPerson: json['contact_person'] as String?,
|
||||
contactPhone: json['contact_phone'] as String?,
|
||||
contactEmail: json['contact_email'] as String?,
|
||||
scopeOfWork: json['scope_of_work'] as String?,
|
||||
exclusions: json['exclusions'] as String?,
|
||||
remarks: json['remarks'] as String?,
|
||||
isActive: json['is_active'] as bool? ?? true,
|
||||
status: json['status'] as String? ?? 'active',
|
||||
createdAt: json['createdAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['createdAt'] as String),
|
||||
createdAt: _dateFromJsonNullable(json['created_at']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$AmcContractModelImplToJson(
|
||||
@ -106,9 +174,20 @@ Map<String, dynamic> _$$AmcContractModelImplToJson(
|
||||
'contract_type': instance.contractType,
|
||||
'start_date': instance.startDate.toIso8601String(),
|
||||
'end_date': instance.endDate.toIso8601String(),
|
||||
'renewal_date': instance.renewalDate?.toIso8601String(),
|
||||
'annual_cost': instance.annualCost,
|
||||
'payment_frequency': instance.paymentFrequency,
|
||||
'service_frequency': instance.serviceFrequency,
|
||||
'visits_per_year': instance.visitsPerYear,
|
||||
'contact_person': instance.contactPerson,
|
||||
'contact_phone': instance.contactPhone,
|
||||
'contact_email': instance.contactEmail,
|
||||
'scope_of_work': instance.scopeOfWork,
|
||||
'exclusions': instance.exclusions,
|
||||
'remarks': instance.remarks,
|
||||
'is_active': instance.isActive,
|
||||
'status': instance.status,
|
||||
'createdAt': instance.createdAt?.toIso8601String(),
|
||||
'created_at': instance.createdAt?.toIso8601String(),
|
||||
};
|
||||
|
||||
_$ServiceVisitModelImpl _$$ServiceVisitModelImplFromJson(
|
||||
@ -118,13 +197,25 @@ _$ServiceVisitModelImpl _$$ServiceVisitModelImplFromJson(
|
||||
assetId: _idFromJson(json['asset_id']),
|
||||
visitType: json['visit_type'] as String,
|
||||
visitDate: _dateFromJson(json['visit_date']),
|
||||
amcContractId: _intFromJsonNullable(json['amc_contract_id']),
|
||||
visitNumber: _intFromJsonNullable(json['visit_number']),
|
||||
complaintNo: json['complaint_no'] as String?,
|
||||
complaintDate: _dateFromJsonNullable(json['complaint_date']),
|
||||
complaintDesc: json['complaint_desc'] as String?,
|
||||
engineerName: json['engineer_name'] as String?,
|
||||
engineerPhone: json['engineer_phone'] as String?,
|
||||
vendorId: _intFromJsonNullable(json['vendor_id']),
|
||||
vendorName: json['vendor_name'] as String?,
|
||||
workDone: json['work_done'] as String?,
|
||||
partsReplaced: json['parts_replaced'] as String?,
|
||||
nextServiceDate: _dateFromJsonNullable(json['next_service_date']),
|
||||
status: json['status'] as String? ?? 'COMPLETED',
|
||||
createdAt: json['createdAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['createdAt'] as String),
|
||||
downtimeHours: _doubleFromJsonNullable(json['downtime_hours']),
|
||||
serviceCost: _doubleFromJsonNullable(json['service_cost']),
|
||||
isUnderAmc: json['is_under_amc'] as bool? ?? false,
|
||||
assetConditionAfter: json['asset_condition_after'] as String?,
|
||||
remarks: json['remarks'] as String?,
|
||||
createdAt: _dateFromJsonNullable(json['created_at']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$ServiceVisitModelImplToJson(
|
||||
@ -134,11 +225,25 @@ Map<String, dynamic> _$$ServiceVisitModelImplToJson(
|
||||
'asset_id': instance.assetId,
|
||||
'visit_type': instance.visitType,
|
||||
'visit_date': instance.visitDate.toIso8601String(),
|
||||
'amc_contract_id': instance.amcContractId,
|
||||
'visit_number': instance.visitNumber,
|
||||
'complaint_no': instance.complaintNo,
|
||||
'complaint_date': instance.complaintDate?.toIso8601String(),
|
||||
'complaint_desc': instance.complaintDesc,
|
||||
'engineer_name': instance.engineerName,
|
||||
'engineer_phone': instance.engineerPhone,
|
||||
'vendor_id': instance.vendorId,
|
||||
'vendor_name': instance.vendorName,
|
||||
'work_done': instance.workDone,
|
||||
'parts_replaced': instance.partsReplaced,
|
||||
'next_service_date': instance.nextServiceDate?.toIso8601String(),
|
||||
'status': instance.status,
|
||||
'createdAt': instance.createdAt?.toIso8601String(),
|
||||
'downtime_hours': instance.downtimeHours,
|
||||
'service_cost': instance.serviceCost,
|
||||
'is_under_amc': instance.isUnderAmc,
|
||||
'asset_condition_after': instance.assetConditionAfter,
|
||||
'remarks': instance.remarks,
|
||||
'created_at': instance.createdAt?.toIso8601String(),
|
||||
};
|
||||
|
||||
_$InsurancePolicyModelImpl _$$InsurancePolicyModelImplFromJson(
|
||||
@ -148,14 +253,23 @@ _$InsurancePolicyModelImpl _$$InsurancePolicyModelImplFromJson(
|
||||
assetId: _idFromJson(json['asset_id']),
|
||||
policyNo: json['policy_no'] as String,
|
||||
insurerName: json['insurer_name'] as String,
|
||||
insurerBranch: json['insurer_branch'] as String?,
|
||||
insurerContact: json['insurer_contact'] as String?,
|
||||
insurerPhone: json['insurer_phone'] as String?,
|
||||
insurerEmail: json['insurer_email'] as String?,
|
||||
policyType: json['policy_type'] as String?,
|
||||
sumInsured: _doubleFromJsonNullable(json['sum_insured']),
|
||||
annualPremium: _doubleFromJsonNullable(json['annual_premium']),
|
||||
policyStartDate: _dateFromJson(json['policy_start_date']),
|
||||
policyEndDate: _dateFromJson(json['policy_end_date']),
|
||||
renewalDate: _dateFromJsonNullable(json['renewal_date']),
|
||||
isAutoRenewal: json['is_auto_renewal'] as bool? ?? false,
|
||||
premiumPaid: json['premium_paid'] as bool? ?? false,
|
||||
premiumPaidDate: _dateFromJsonNullable(json['premium_paid_date']),
|
||||
remarks: json['remarks'] as String?,
|
||||
isActive: json['is_active'] as bool? ?? true,
|
||||
status: json['status'] as String? ?? 'active',
|
||||
createdAt: json['createdAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['createdAt'] as String),
|
||||
createdAt: _dateFromJsonNullable(json['created_at']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$InsurancePolicyModelImplToJson(
|
||||
@ -165,12 +279,23 @@ Map<String, dynamic> _$$InsurancePolicyModelImplToJson(
|
||||
'asset_id': instance.assetId,
|
||||
'policy_no': instance.policyNo,
|
||||
'insurer_name': instance.insurerName,
|
||||
'insurer_branch': instance.insurerBranch,
|
||||
'insurer_contact': instance.insurerContact,
|
||||
'insurer_phone': instance.insurerPhone,
|
||||
'insurer_email': instance.insurerEmail,
|
||||
'policy_type': instance.policyType,
|
||||
'sum_insured': instance.sumInsured,
|
||||
'annual_premium': instance.annualPremium,
|
||||
'policy_start_date': instance.policyStartDate.toIso8601String(),
|
||||
'policy_end_date': instance.policyEndDate.toIso8601String(),
|
||||
'renewal_date': instance.renewalDate?.toIso8601String(),
|
||||
'is_auto_renewal': instance.isAutoRenewal,
|
||||
'premium_paid': instance.premiumPaid,
|
||||
'premium_paid_date': instance.premiumPaidDate?.toIso8601String(),
|
||||
'remarks': instance.remarks,
|
||||
'is_active': instance.isActive,
|
||||
'status': instance.status,
|
||||
'createdAt': instance.createdAt?.toIso8601String(),
|
||||
'created_at': instance.createdAt?.toIso8601String(),
|
||||
};
|
||||
|
||||
_$AssetAlertModelImpl _$$AssetAlertModelImplFromJson(
|
||||
|
||||
@ -36,6 +36,8 @@ class VendorModel with _$VendorModel {
|
||||
@JsonKey(name: 'vendor_code') String? vendorCode,
|
||||
@JsonKey(name: 'vendor_name') required String vendorName,
|
||||
@JsonKey(name: 'vendor_type') String? vendorType,
|
||||
@JsonKey(name: 'gst_treatment') String? gstTreatment,
|
||||
@JsonKey(name: 'source_of_supply') String? sourceOfSupply,
|
||||
String? gstin,
|
||||
String? pan,
|
||||
@JsonKey(name: 'payment_term_id', fromJson: _intFromJsonNullable)
|
||||
@ -136,6 +138,17 @@ const vendorTypeOptions = [
|
||||
('GENERAL', 'General'),
|
||||
];
|
||||
|
||||
const gstTreatmentOptions = [
|
||||
('REGULAR', 'Regular'),
|
||||
('COMPOSITION', 'Composition'),
|
||||
('UNREGISTERED', 'Unregistered'),
|
||||
('CONSUMER', 'Consumer'),
|
||||
('SEZ', 'SEZ'),
|
||||
('OVERSEAS', 'Overseas'),
|
||||
('DEEMED_EXPORT', 'Deemed Export'),
|
||||
('OTHER', 'Other'),
|
||||
];
|
||||
|
||||
const vendorStatusOptions = [
|
||||
('active', 'Active'),
|
||||
('inactive', 'Inactive'),
|
||||
@ -163,6 +176,15 @@ String vendorTypeLabel(String? value) {
|
||||
value;
|
||||
}
|
||||
|
||||
String gstTreatmentLabel(String? value) {
|
||||
if (value == null) return '—';
|
||||
return gstTreatmentOptions
|
||||
.where((e) => e.$1 == value)
|
||||
.map((e) => e.$2)
|
||||
.firstOrNull ??
|
||||
value.replaceAll('_', ' ');
|
||||
}
|
||||
|
||||
String vendorStatusLabel(String? value) {
|
||||
if (value == null) return '—';
|
||||
return vendorStatusOptions
|
||||
|
||||
@ -29,6 +29,10 @@ mixin _$VendorModel {
|
||||
String get vendorName => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'vendor_type')
|
||||
String? get vendorType => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'gst_treatment')
|
||||
String? get gstTreatment => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'source_of_supply')
|
||||
String? get sourceOfSupply => throw _privateConstructorUsedError;
|
||||
String? get gstin => throw _privateConstructorUsedError;
|
||||
String? get pan => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'payment_term_id', fromJson: _intFromJsonNullable)
|
||||
@ -73,6 +77,8 @@ abstract class $VendorModelCopyWith<$Res> {
|
||||
@JsonKey(name: 'vendor_code') String? vendorCode,
|
||||
@JsonKey(name: 'vendor_name') String vendorName,
|
||||
@JsonKey(name: 'vendor_type') String? vendorType,
|
||||
@JsonKey(name: 'gst_treatment') String? gstTreatment,
|
||||
@JsonKey(name: 'source_of_supply') String? sourceOfSupply,
|
||||
String? gstin,
|
||||
String? pan,
|
||||
@JsonKey(name: 'payment_term_id', fromJson: _intFromJsonNullable)
|
||||
@ -113,6 +119,8 @@ class _$VendorModelCopyWithImpl<$Res, $Val extends VendorModel>
|
||||
Object? vendorCode = freezed,
|
||||
Object? vendorName = null,
|
||||
Object? vendorType = freezed,
|
||||
Object? gstTreatment = freezed,
|
||||
Object? sourceOfSupply = freezed,
|
||||
Object? gstin = freezed,
|
||||
Object? pan = freezed,
|
||||
Object? paymentTermId = freezed,
|
||||
@ -145,6 +153,14 @@ class _$VendorModelCopyWithImpl<$Res, $Val extends VendorModel>
|
||||
? _value.vendorType
|
||||
: vendorType // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
gstTreatment: freezed == gstTreatment
|
||||
? _value.gstTreatment
|
||||
: gstTreatment // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
sourceOfSupply: freezed == sourceOfSupply
|
||||
? _value.sourceOfSupply
|
||||
: sourceOfSupply // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
gstin: freezed == gstin
|
||||
? _value.gstin
|
||||
: gstin // ignore: cast_nullable_to_non_nullable
|
||||
@ -217,6 +233,8 @@ abstract class _$$VendorModelImplCopyWith<$Res>
|
||||
@JsonKey(name: 'vendor_code') String? vendorCode,
|
||||
@JsonKey(name: 'vendor_name') String vendorName,
|
||||
@JsonKey(name: 'vendor_type') String? vendorType,
|
||||
@JsonKey(name: 'gst_treatment') String? gstTreatment,
|
||||
@JsonKey(name: 'source_of_supply') String? sourceOfSupply,
|
||||
String? gstin,
|
||||
String? pan,
|
||||
@JsonKey(name: 'payment_term_id', fromJson: _intFromJsonNullable)
|
||||
@ -256,6 +274,8 @@ class __$$VendorModelImplCopyWithImpl<$Res>
|
||||
Object? vendorCode = freezed,
|
||||
Object? vendorName = null,
|
||||
Object? vendorType = freezed,
|
||||
Object? gstTreatment = freezed,
|
||||
Object? sourceOfSupply = freezed,
|
||||
Object? gstin = freezed,
|
||||
Object? pan = freezed,
|
||||
Object? paymentTermId = freezed,
|
||||
@ -288,6 +308,14 @@ class __$$VendorModelImplCopyWithImpl<$Res>
|
||||
? _value.vendorType
|
||||
: vendorType // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
gstTreatment: freezed == gstTreatment
|
||||
? _value.gstTreatment
|
||||
: gstTreatment // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
sourceOfSupply: freezed == sourceOfSupply
|
||||
? _value.sourceOfSupply
|
||||
: sourceOfSupply // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
gstin: freezed == gstin
|
||||
? _value.gstin
|
||||
: gstin // ignore: cast_nullable_to_non_nullable
|
||||
@ -353,6 +381,8 @@ class _$VendorModelImpl implements _VendorModel {
|
||||
@JsonKey(name: 'vendor_code') this.vendorCode,
|
||||
@JsonKey(name: 'vendor_name') required this.vendorName,
|
||||
@JsonKey(name: 'vendor_type') this.vendorType,
|
||||
@JsonKey(name: 'gst_treatment') this.gstTreatment,
|
||||
@JsonKey(name: 'source_of_supply') this.sourceOfSupply,
|
||||
this.gstin,
|
||||
this.pan,
|
||||
@JsonKey(name: 'payment_term_id', fromJson: _intFromJsonNullable)
|
||||
@ -392,6 +422,12 @@ class _$VendorModelImpl implements _VendorModel {
|
||||
@JsonKey(name: 'vendor_type')
|
||||
final String? vendorType;
|
||||
@override
|
||||
@JsonKey(name: 'gst_treatment')
|
||||
final String? gstTreatment;
|
||||
@override
|
||||
@JsonKey(name: 'source_of_supply')
|
||||
final String? sourceOfSupply;
|
||||
@override
|
||||
final String? gstin;
|
||||
@override
|
||||
final String? pan;
|
||||
@ -446,7 +482,7 @@ class _$VendorModelImpl implements _VendorModel {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'VendorModel(id: $id, vendorCode: $vendorCode, vendorName: $vendorName, vendorType: $vendorType, gstin: $gstin, pan: $pan, paymentTermId: $paymentTermId, paymentTermName: $paymentTermName, creditPeriodDays: $creditPeriodDays, remarks: $remarks, status: $status, isActive: $isActive, createdAt: $createdAt, updatedAt: $updatedAt, addresses: $addresses, contacts: $contacts, bankDetails: $bankDetails)';
|
||||
return 'VendorModel(id: $id, vendorCode: $vendorCode, vendorName: $vendorName, vendorType: $vendorType, gstTreatment: $gstTreatment, sourceOfSupply: $sourceOfSupply, gstin: $gstin, pan: $pan, paymentTermId: $paymentTermId, paymentTermName: $paymentTermName, creditPeriodDays: $creditPeriodDays, remarks: $remarks, status: $status, isActive: $isActive, createdAt: $createdAt, updatedAt: $updatedAt, addresses: $addresses, contacts: $contacts, bankDetails: $bankDetails)';
|
||||
}
|
||||
|
||||
@override
|
||||
@ -461,6 +497,10 @@ class _$VendorModelImpl implements _VendorModel {
|
||||
other.vendorName == vendorName) &&
|
||||
(identical(other.vendorType, vendorType) ||
|
||||
other.vendorType == vendorType) &&
|
||||
(identical(other.gstTreatment, gstTreatment) ||
|
||||
other.gstTreatment == gstTreatment) &&
|
||||
(identical(other.sourceOfSupply, sourceOfSupply) ||
|
||||
other.sourceOfSupply == sourceOfSupply) &&
|
||||
(identical(other.gstin, gstin) || other.gstin == gstin) &&
|
||||
(identical(other.pan, pan) || other.pan == pan) &&
|
||||
(identical(other.paymentTermId, paymentTermId) ||
|
||||
@ -490,12 +530,14 @@ class _$VendorModelImpl implements _VendorModel {
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
int get hashCode => Object.hashAll([
|
||||
runtimeType,
|
||||
id,
|
||||
vendorCode,
|
||||
vendorName,
|
||||
vendorType,
|
||||
gstTreatment,
|
||||
sourceOfSupply,
|
||||
gstin,
|
||||
pan,
|
||||
paymentTermId,
|
||||
@ -509,7 +551,7 @@ class _$VendorModelImpl implements _VendorModel {
|
||||
const DeepCollectionEquality().hash(_addresses),
|
||||
const DeepCollectionEquality().hash(_contacts),
|
||||
const DeepCollectionEquality().hash(_bankDetails),
|
||||
);
|
||||
]);
|
||||
|
||||
/// Create a copy of VendorModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@ -531,6 +573,8 @@ abstract class _VendorModel implements VendorModel {
|
||||
@JsonKey(name: 'vendor_code') final String? vendorCode,
|
||||
@JsonKey(name: 'vendor_name') required final String vendorName,
|
||||
@JsonKey(name: 'vendor_type') final String? vendorType,
|
||||
@JsonKey(name: 'gst_treatment') final String? gstTreatment,
|
||||
@JsonKey(name: 'source_of_supply') final String? sourceOfSupply,
|
||||
final String? gstin,
|
||||
final String? pan,
|
||||
@JsonKey(name: 'payment_term_id', fromJson: _intFromJsonNullable)
|
||||
@ -568,6 +612,12 @@ abstract class _VendorModel implements VendorModel {
|
||||
@JsonKey(name: 'vendor_type')
|
||||
String? get vendorType;
|
||||
@override
|
||||
@JsonKey(name: 'gst_treatment')
|
||||
String? get gstTreatment;
|
||||
@override
|
||||
@JsonKey(name: 'source_of_supply')
|
||||
String? get sourceOfSupply;
|
||||
@override
|
||||
String? get gstin;
|
||||
@override
|
||||
String? get pan;
|
||||
|
||||
@ -13,6 +13,8 @@ _$VendorModelImpl _$$VendorModelImplFromJson(
|
||||
vendorCode: json['vendor_code'] as String?,
|
||||
vendorName: json['vendor_name'] as String,
|
||||
vendorType: json['vendor_type'] as String?,
|
||||
gstTreatment: json['gst_treatment'] as String?,
|
||||
sourceOfSupply: json['source_of_supply'] as String?,
|
||||
gstin: json['gstin'] as String?,
|
||||
pan: json['pan'] as String?,
|
||||
paymentTermId: _intFromJsonNullable(json['payment_term_id']),
|
||||
@ -48,6 +50,8 @@ Map<String, dynamic> _$$VendorModelImplToJson(_$VendorModelImpl instance) =>
|
||||
'vendor_code': instance.vendorCode,
|
||||
'vendor_name': instance.vendorName,
|
||||
'vendor_type': instance.vendorType,
|
||||
'gst_treatment': instance.gstTreatment,
|
||||
'source_of_supply': instance.sourceOfSupply,
|
||||
'gstin': instance.gstin,
|
||||
'pan': instance.pan,
|
||||
'payment_term_id': instance.paymentTermId,
|
||||
|
||||
@ -60,12 +60,6 @@ const List<MenuItem> appMenuItems = [
|
||||
route: RouteConstants.assets,
|
||||
module: 'assets',
|
||||
),
|
||||
MenuItem(
|
||||
label: 'Categories',
|
||||
icon: Icons.category_outlined,
|
||||
route: RouteConstants.assetCategories,
|
||||
module: 'asset_categories',
|
||||
),
|
||||
MenuItem(
|
||||
label: 'Alerts',
|
||||
icon: Icons.notifications_outlined,
|
||||
|
||||
@ -166,6 +166,7 @@ class _AppSearchableDropdownState<T> extends State<AppSearchableDropdown<T>> {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return FormField<T>(
|
||||
key: ValueKey<T?>(widget.value),
|
||||
initialValue: widget.value,
|
||||
validator: widget.validator,
|
||||
builder: (field) {
|
||||
|
||||
@ -44,8 +44,7 @@ class _AppSearchableMultiSelectDropdownState<T>
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String _displayText() {
|
||||
if (widget.values.isEmpty) return '';
|
||||
List<String> _selectedLabels() {
|
||||
final labels = <String>[];
|
||||
for (final value in widget.values) {
|
||||
for (final option in widget.options) {
|
||||
@ -55,7 +54,7 @@ class _AppSearchableMultiSelectDropdownState<T>
|
||||
}
|
||||
}
|
||||
}
|
||||
return labels.join(', ');
|
||||
return labels;
|
||||
}
|
||||
|
||||
void _removeOverlay() {
|
||||
@ -161,9 +160,9 @@ class _AppSearchableMultiSelectDropdownState<T>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final displayText = _displayText();
|
||||
|
||||
return FormField<List<T>>(
|
||||
key: ValueKey<String>(widget.values.join('\u0000')),
|
||||
initialValue: widget.values,
|
||||
validator: widget.validator,
|
||||
builder: (field) {
|
||||
@ -171,6 +170,10 @@ class _AppSearchableMultiSelectDropdownState<T>
|
||||
widget.hint ?? 'Select ${widget.label.toLowerCase()}';
|
||||
final canOpen = widget.enabled && widget.options.isNotEmpty;
|
||||
final colors = theme.colorScheme;
|
||||
final labels = _selectedLabels();
|
||||
final displayText = labels.join(', ');
|
||||
final tooltipMessage =
|
||||
labels.length > 1 ? labels.join('\n') : displayText;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
@ -202,12 +205,17 @@ class _AppSearchableMultiSelectDropdownState<T>
|
||||
),
|
||||
child: displayText.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: Text(
|
||||
displayText,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: colors.onSurface,
|
||||
: Tooltip(
|
||||
message: tooltipMessage,
|
||||
preferBelow: true,
|
||||
waitDuration: const Duration(milliseconds: 250),
|
||||
child: Text(
|
||||
displayText,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -30,7 +30,12 @@ Future<T?> showSidePanel<T>(
|
||||
child: SizedBox(
|
||||
width: panelWidth,
|
||||
height: MediaQuery.sizeOf(context).height,
|
||||
child: panel,
|
||||
child: ScaffoldMessenger(
|
||||
child: Scaffold(
|
||||
backgroundColor: theme.colorScheme.surface,
|
||||
body: panel,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -47,6 +52,21 @@ Future<T?> showSidePanel<T>(
|
||||
);
|
||||
}
|
||||
|
||||
/// Shows a snackbar above the side panel overlay (not behind it).
|
||||
void showSidePanelSnackBar(
|
||||
BuildContext context,
|
||||
String message, {
|
||||
Duration duration = const Duration(seconds: 4),
|
||||
}) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: duration,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class SidePanelScaffold extends StatelessWidget {
|
||||
const SidePanelScaffold({
|
||||
super.key,
|
||||
@ -180,7 +200,7 @@ class SidePanelFormRow extends StatelessWidget {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(child: left),
|
||||
SizedBox(width: spacing),
|
||||
|
||||
@ -18,6 +18,7 @@ class AppTextField extends StatelessWidget {
|
||||
this.inputFormatters,
|
||||
this.enabled = true,
|
||||
this.autofillHints,
|
||||
this.isDense = false,
|
||||
});
|
||||
|
||||
final TextEditingController controller;
|
||||
@ -34,6 +35,7 @@ class AppTextField extends StatelessWidget {
|
||||
final List<TextInputFormatter>? inputFormatters;
|
||||
final bool enabled;
|
||||
final Iterable<String>? autofillHints;
|
||||
final bool isDense;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -55,6 +57,7 @@ class AppTextField extends StatelessWidget {
|
||||
hintText: hint,
|
||||
prefixIcon: prefixIcon,
|
||||
suffixIcon: suffixIcon,
|
||||
isDense: isDense,
|
||||
floatingLabelBehavior:
|
||||
label != null ? FloatingLabelBehavior.always : null,
|
||||
),
|
||||
|
||||
22
pubspec.lock
22
pubspec.lock
@ -133,10 +133,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
version: "1.4.1"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -596,26 +596,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.17"
|
||||
version: "0.12.19"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.11.1"
|
||||
version: "0.13.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
version: "1.18.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1009,10 +1009,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.6"
|
||||
version: "0.7.11"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1142,5 +1142,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.9.2 <4.0.0"
|
||||
dart: ">=3.10.0-0 <4.0.0"
|
||||
flutter: ">=3.35.6"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user