Fixed inventory items photo thumbnail issue

This commit is contained in:
2026-09-01 07:44:23 +05:30
parent bfe0c1efbd
commit 3a5e812947
6 changed files with 130 additions and 22 deletions

View File

@@ -39,6 +39,7 @@ public class InventoryItem {
private Long vendorId;
private Long branchId;
private String purchaseRef;
private String photoUrl;
@Builder.Default
private String status = "AVAILABLE";
private LocalDateTime createdAt;

View File

@@ -55,6 +55,7 @@ public class InventoryItemService {
if (updatedItem.getVendorId() != null) existingItem.setVendorId(updatedItem.getVendorId());
if (updatedItem.getBranchId() != null) existingItem.setBranchId(updatedItem.getBranchId());
if (updatedItem.getPurchaseRef() != null) existingItem.setPurchaseRef(updatedItem.getPurchaseRef());
if (updatedItem.getPhotoUrl() != null) existingItem.setPhotoUrl(updatedItem.getPhotoUrl());
if (updatedItem.getStatus() != null) existingItem.setStatus(updatedItem.getStatus());
existingItem.setUpdatedAt(LocalDateTime.now());
return inventoryItemRepository.save(existingItem);

View File

@@ -195,6 +195,7 @@ public class PurchaseOrderService {
.huid(item.getHuid())
.grossWeight(item.getWeight())
.netWeight(item.getWeight())
.photoUrl(item.getPhotoUrl())
.status("AVAILABLE")
.build();

View File

@@ -383,6 +383,7 @@ CREATE TABLE IF NOT EXISTS inventory_items (
vendor_id INTEGER REFERENCES vendors(id) ON DELETE SET NULL,
branch_id INTEGER,
purchase_ref VARCHAR(100),
photo_url VARCHAR(1024),
status VARCHAR(50) DEFAULT 'AVAILABLE',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP

View File

@@ -21,6 +21,7 @@ class InventoryItem {
final int? vendorId;
final int? branchId;
final String? purchaseRef;
final String? photoUrl;
final String? status;
final DateTime? createdAt;
@@ -47,6 +48,7 @@ class InventoryItem {
this.vendorId,
this.branchId,
this.purchaseRef,
this.photoUrl,
this.status,
this.createdAt,
});
@@ -75,6 +77,7 @@ class InventoryItem {
vendorId: json['vendorId'],
branchId: json['branchId'],
purchaseRef: json['purchaseRef'],
photoUrl: json['photoUrl'] ?? json['photo_url'],
status: json['status'],
createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt']) : null,
);

View File

@@ -11,6 +11,7 @@ import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:kifi_app/features/vendor/presentation/purchase_order_details_screen.dart';
import 'package:kifi_app/features/vendor/providers/purchase_orders_provider.dart';
import 'package:kifi_app/core/utils/purity_utils.dart';
import '../../transactions/presentation/widgets/attachment_gallery_screen.dart';
class StockLedgerTab extends ConsumerStatefulWidget {
final Product product;
@@ -26,6 +27,18 @@ class _StockLedgerTabState extends ConsumerState<StockLedgerTab> {
bool _isLoading = true;
String _selectedFilter = 'IN_STOCK'; // 'IN_STOCK', 'SOLD', 'ALL'
String _resolveImageUrl(String path) {
if (path.startsWith('http://') || path.startsWith('https://')) {
return path;
}
final base = DioClient().dio.options.baseUrl;
final cleanPath = path.startsWith('/') ? path.substring(1) : path;
if (cleanPath.startsWith('api/kifi-v2/upload/view') || cleanPath.startsWith('upload/view')) {
return '$base/${cleanPath.replaceFirst('api/kifi-v2/', '')}';
}
return '$base/upload/view?path=${Uri.encodeComponent(cleanPath)}';
}
@override
void initState() {
super.initState();
@@ -163,6 +176,31 @@ class _StockLedgerTabState extends ConsumerState<StockLedgerTab> {
}
}
// Resolve purchase item thumbnail photo
String? itemPhotoUrl = (item.photoUrl != null && item.photoUrl!.isNotEmpty) ? item.photoUrl : null;
if (itemPhotoUrl == null && item.purchaseRef != null) {
final po = purchaseOrders.where((p) => p.poNumber == item.purchaseRef).firstOrNull;
if (po != null) {
final poItem = po.items.where((i) =>
(item.huid != null && i.huid == item.huid) ||
(item.sku != null && i.sku == item.sku) ||
i.productId == widget.product.id
).firstOrNull ?? po.items.firstOrNull;
if (poItem != null && poItem.photoUrl != null && poItem.photoUrl!.isNotEmpty) {
itemPhotoUrl = poItem.photoUrl;
}
}
}
String? finalDisplayUrl;
bool isProductFallback = false;
if (itemPhotoUrl != null) {
finalDisplayUrl = _resolveImageUrl(itemPhotoUrl);
} else if (widget.product.imageIds.isNotEmpty) {
finalDisplayUrl = '${DioClient().dio.options.baseUrl}/inventory/products/${widget.product.id}/images/${widget.product.imageIds.first}/content';
isProductFallback = true;
}
final purchasePrice = weight * purchaseRate;
final currentPrice = weight * currentRate;
final gainLoss = currentPrice - purchasePrice;
@@ -188,29 +226,92 @@ class _StockLedgerTabState extends ConsumerState<StockLedgerTab> {
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.grey.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: FutureBuilder<String?>(
future: DioClient().storage.read(key: 'jwt_token'),
builder: (context, snapshot) {
if (snapshot.hasData && widget.product.imageIds.isNotEmpty) {
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
'${DioClient().dio.options.baseUrl}/inventory/products/${widget.product.id}/images/${widget.product.imageIds.first}/content',
fit: BoxFit.cover,
headers: {'Authorization': 'Bearer ${snapshot.data}'},
FutureBuilder<String?>(
future: DioClient().storage.read(key: 'jwt_token'),
builder: (context, snapshot) {
final hasImage = finalDisplayUrl != null;
return GestureDetector(
onTap: hasImage
? () {
final token = snapshot.data;
final headers = (token != null && isProductFallback)
? {'Authorization': 'Bearer $token'}
: null;
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => AttachmentGalleryScreen(
images: [
NetworkImage(
finalDisplayUrl!,
headers: headers,
),
],
initialIndex: 0,
),
),
);
}
: null,
child: MouseRegion(
cursor: hasImage ? SystemMouseCursors.click : SystemMouseCursors.basic,
child: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: Colors.grey.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.grey.withValues(alpha: 0.2),
),
),
);
}
return const Icon(LucideIcons.image, color: Colors.grey, size: 20);
},
),
child: hasImage
? Stack(
children: [
Positioned.fill(
child: ClipRRect(
borderRadius: BorderRadius.circular(7),
child: Image.network(
finalDisplayUrl,
fit: BoxFit.cover,
headers: (snapshot.hasData && isProductFallback)
? {'Authorization': 'Bearer ${snapshot.data}'}
: null,
errorBuilder: (context, error, stackTrace) => const Icon(
LucideIcons.imageOff,
color: Colors.grey,
size: 20,
),
),
),
),
Positioned(
bottom: 2,
right: 2,
child: Container(
padding: const EdgeInsets.all(2),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(4),
),
child: const Icon(
LucideIcons.maximize2,
size: 8,
color: Colors.white,
),
),
),
],
)
: const Icon(
LucideIcons.image,
color: Colors.grey,
size: 20,
),
),
),
);
},
),
const SizedBox(width: 12),
Expanded(