Merge tag '1.14.5_cleaned'

project was cleaned
This commit is contained in:
2026-04-23 11:47:44 +03:00
5758 changed files with 2046069 additions and 16179 deletions

View File

@@ -14,6 +14,7 @@ import ItemComparisonModal from '@/components/ItemComparisonModal';
import StockAdjustmentPanel from '@/components/StockAdjustmentPanel';
import NewItemDialog from '@/components/NewItemDialog';
import ScannerSection from '@/components/ScannerSection';
import SearchModal from '@/components/inventory/SearchModal';
import { toast } from 'react-hot-toast';
import {
Package,
@@ -59,6 +60,7 @@ export default function Home() {
const [categories, setCategories] = useState<any[]>([]);
const [comparisonModal, setComparisonModal] = useState<{ show: boolean, newItem: any, existingItem: any, existingId: number | null }>({ show: false, newItem: null, existingItem: null, existingId: null });
const [comparisonLoading, setComparisonLoading] = useState(false);
const [showSearch, setShowSearch] = useState(false);
const { syncing, handleSync } = useSync({
isOnline,
@@ -152,6 +154,18 @@ export default function Home() {
};
}, []);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
setShowSearch(!showSearch);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [showSearch]);
const loadInventory = async () => {
const cached = await db.items.toArray();
setInventory(cached);
@@ -167,13 +181,35 @@ export default function Home() {
if (itemData.part_number) {
itemData.part_number = itemData.part_number.toLowerCase();
}
// 1. Add to local DB cache
// Prepare data for backend: convert Blob to base64 and rename fields
const backendData = { ...itemData };
delete backendData.extractedImageBlob; // Remove Blob from local DB version
// If extracted image blob is present, convert to base64 for backend
if (itemData.extractedImageBlob) {
const reader = new FileReader();
reader.readAsDataURL(itemData.extractedImageBlob);
await new Promise((resolve, reject) => {
reader.onload = () => {
const base64 = (reader.result as string).split(',')[1]; // Remove data URL prefix
backendData.extracted_image_bytes = base64;
backendData.image_processing = itemData.imageProcessing; // Use correct field name for API
delete backendData.imageProcessing; // Remove old field name
resolve(null);
};
reader.onerror = reject;
});
}
// 1. Add to local DB cache (without Blob)
await db.items.add(itemData);
// 2. If online, try to push to backend immediately
if (isOnline && currentUser) {
try {
await inventoryApi.createItem(currentUser.id, itemData);
await inventoryApi.createItem(currentUser.id, backendData);
toast.success("Item saved to cloud catalog!");
setShowOnboarding(false);
await loadInventory();
@@ -186,7 +222,7 @@ export default function Home() {
const detail = responseData.detail;
setComparisonModal({
show: true,
newItem: itemData,
newItem: backendData,
existingItem: detail.existing_item,
existingId: detail.existing_id
});
@@ -215,8 +251,28 @@ export default function Home() {
if (!comparisonModal.existingId) return;
setComparisonLoading(true);
try {
// Prepare data: convert Blob to base64 if present
const updateData = { ...comparisonModal.newItem };
if (updateData.extractedImageBlob) {
const reader = new FileReader();
reader.readAsDataURL(updateData.extractedImageBlob);
await new Promise((resolve, reject) => {
reader.onload = () => {
const base64 = (reader.result as string).split(',')[1];
updateData.extracted_image_bytes = base64;
updateData.image_processing = updateData.imageProcessing;
delete updateData.extractedImageBlob;
delete updateData.imageProcessing;
resolve(null);
};
reader.onerror = reject;
});
}
// Update existing item
await inventoryApi.updateItem(comparisonModal.existingId, comparisonModal.newItem);
await inventoryApi.updateItem(comparisonModal.existingId, updateData);
toast.success("Item updated successfully!");
setComparisonModal({ show: false, newItem: null, existingItem: null, existingId: null });
setShowOnboarding(false);
@@ -314,8 +370,8 @@ export default function Home() {
</datalist>
{/* Header */}
<header className="flex flex-col sm:flex-row justify-between sm:items-center gap-4 mb-6 w-full px-1">
<div className="flex items-center gap-3">
<header className="flex flex-col sm:flex-row justify-between sm:items-center gap-3 md:gap-4 mb-3 md:mb-4 w-full px-1">
<div className="flex items-center gap-2 md:gap-3">
<div className="p-1">
<img
src="/logo.png"
@@ -324,29 +380,36 @@ export default function Home() {
/>
</div>
<div>
<h1 className="text-2xl md:text-3xl lg:text-4xl xl:text-5xl font-black tracking-tight text-white leading-tight">TFM aInventory</h1>
<p className="text-xs md:text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl text-secondary font-bold tracking-tight mt-1 leading-relaxed">Check-in, Check-out & Trash Operations</p>
<h1 className="text-2xl md:text-3xl font-normal tracking-tight text-white leading-tight">TFM aInventory</h1>
<p className="text-xs md:text-sm text-secondary font-normal tracking-tight mt-1 leading-relaxed">Check-in, Check-out & Trash Operations</p>
</div>
</div>
<div className="flex flex-wrap items-center justify-between sm:justify-end gap-3 sm:gap-6 bg-surface/50 sm:bg-transparent px-4 lg:px-5 xl:px-6 py-2 sm:p-0 rounded-2xl border border-slate-800/50 sm:border-none">
<div className="flex flex-wrap items-center gap-4 sm:gap-6">
<div className="flex flex-wrap items-center justify-between sm:justify-end gap-2 md:gap-3 bg-surface/50 sm:bg-transparent px-3 py-2 sm:p-0 rounded-2xl border border-slate-800/50 sm:border-none">
<div className="flex flex-wrap items-center gap-2 md:gap-3">
{isScannerReady && (
<div className="flex items-center gap-2">
<div className="w-1.5 h-1.5 rounded-full bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.6)]" />
<span className="text-xs lg:text-sm xl:text-base lg:text-lg xl:text-xl lg:text-2xl xl:text-3xl lg:text-4xl xl:text-5xl font-black text-green-500/90 whitespace-nowrap">
<span className="text-xs font-normal text-green-500/90 whitespace-nowrap">
Scanner: OK
</span>
</div>
)}
<div className="flex items-center gap-2" data-testid={!isOnline ? "offline-indicator" : undefined}>
<div className={`w-1.5 h-1.5 rounded-full ${isOnline ? 'bg-green-500 animate-pulse shadow-[0_0_8px_rgba(34,197,94,0.6)]' : 'bg-rose-500 shadow-[0_0_8px_rgba(244,63,94,0.6)]'}`} />
<span className={`text-xs lg:text-sm xl:text-base lg:text-lg xl:text-xl lg:text-2xl xl:text-3xl lg:text-4xl xl:text-5xl font-black whitespace-nowrap ${isOnline ? 'text-green-500/90' : 'text-rose-500/90'}`} data-testid={!isOnline ? "offline-sync-indicator" : undefined}>
<span className={`text-xs font-normal whitespace-nowrap ${isOnline ? 'text-green-500/90' : 'text-rose-500/90'}`} data-testid={!isOnline ? "offline-sync-indicator" : undefined}>
Sync: {isOnline ? 'Active' : 'Offline'}
</span>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setShowSearch(true)}
className="p-2.5 bg-surface border border-slate-800 text-secondary rounded-xl hover:text-white transition-all active:scale-95"
title="Search (Ctrl+K)"
>
<Search size={18} />
</button>
<button
onClick={handleSync}
disabled={syncing}
@@ -389,6 +452,16 @@ export default function Home() {
loading={comparisonLoading}
/>
{/* Search Modal */}
<SearchModal
isOpen={showSearch}
onClose={() => setShowSearch(false)}
onSelectItem={(item) => {
setSelectedItem(item);
setShowSearch(false);
}}
/>
{/* Stock Adjustment Panel */}
<StockAdjustmentPanel
selectedItem={selectedItem}
@@ -424,21 +497,21 @@ export default function Home() {
{/* Box Contents Selection Modal */}
{boxMatches.length > 0 && !selectedItem && (
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 bg-background/80 animate-in fade-in duration-200">
<div className="w-full max-w-lg bg-surface border border-slate-800 rounded-[2.5rem] shadow-2xl p-6 lg:p-8 xl:p-10 overflow-hidden animate-in slide-in-from-bottom-10 duration-300 flex flex-col max-h-[85vh]">
<div className="flex justify-between items-center mb-6 shrink-0">
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center p-4 bg-background/80 animate-in fade-in duration-200">
<div className="w-full max-w-lg bg-surface border border-slate-800 rounded-[2.5rem] shadow-2xl p-4 md:p-6 overflow-hidden animate-in slide-in-from-bottom-10 duration-300 flex flex-col max-h-[85vh]">
<div className="flex justify-between items-center mb-3 md:mb-4 shrink-0">
<div>
<h3 className="text-xl lg:text-2xl xl:text-3xl lg:text-4xl xl:text-5xl font-black tracking-tight flex items-center gap-2">
<h3 className="text-xl font-normal tracking-tight flex items-center gap-2">
<Package className="text-primary" />
Box Contents
</h3>
<p className="text-xs lg:text-sm xl:text-base lg:text-lg xl:text-xl lg:text-2xl xl:text-3xl lg:text-4xl xl:text-5xl text-secondary font-bold mt-1">Select the item you want to {mode.replace('_', ' ').toLowerCase()}</p>
<p className="text-xs text-secondary font-normal mt-1">Select the item you want to {mode.replace('_', ' ').toLowerCase()}</p>
</div>
<button onClick={() => setBoxMatches([])} className="p-2 lg:p-3 xl:p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 hover:bg-slate-800 rounded-full">
<X size={20} />
</button>
</div>
<div className="overflow-y-auto w-full pr-2 space-y-3 pb-4">
<div className="overflow-y-auto w-full pr-2 space-y-2 md:space-y-3 pb-3 md:pb-4">
{boxMatches.map(item => (
<button
key={item.id}
@@ -447,11 +520,11 @@ export default function Home() {
setBoxMatches([]);
setAdjustType(mode === 'CHECK_IN' ? 'ADD' : 'REMOVE');
}}
className="w-full text-left bg-background/50 hover:bg-slate-800 border border-slate-800/80 p-4 lg:p-5 xl:p-6 lg:p-8 xl:p-10 rounded-2xl flex items-center gap-4 transition-all active:scale-[0.98] group"
className="w-full text-left bg-background/50 hover:bg-slate-800 border border-slate-800/80 p-3 md:p-4 rounded-2xl flex items-center gap-2 md:gap-3 transition-all active:scale-[0.98] group"
>
<div className="flex-1 min-w-0">
<p className="text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl font-black text-white">{item.name}</p>
<p className="text-xs lg:text-sm xl:text-base lg:text-lg xl:text-xl lg:text-2xl xl:text-3xl lg:text-4xl xl:text-5xl text-secondary font-bold mt-1">Part #: {item.part_number || 'N/A'}</p>
<p className="text-sm font-normal text-white">{item.name}</p>
<p className="text-xs text-secondary font-normal mt-1">Part #: {item.part_number || 'N/A'}</p>
</div>
<ChevronRight size={18} className="text-secondary group-hover:text-primary" />
</button>
@@ -463,8 +536,8 @@ export default function Home() {
{/* Footer Branding */}
<footer className="mt-20 mb-8 flex flex-col items-center gap-2 opacity-70">
<p className="text-sm lg:text-base xl:text-lg lg:text-xl xl:text-2xl lg:text-3xl xl:text-4xl font-bold text-secondary">Powered by TFM Group Software</p>
<footer className="mt-12 md:mt-20 mb-4 md:mb-8 flex flex-col items-center gap-2 opacity-70">
<p className="text-sm font-normal text-secondary">Powered by TFM Group Software</p>
<div className="h-px w-16 bg-slate-700/60" />
<p className="text-xs lg:text-sm xl:text-base lg:text-lg xl:text-xl lg:text-2xl xl:text-3xl lg:text-4xl xl:text-5xl font-mono text-secondary">v{versionData.version} {versionData.last_build} BUILD: dev-{(versionData as any).commit || 'N/A'}</p>
</footer>