From 37dd2e0f1631523875b83abeffbf5b4349336ffd Mon Sep 17 00:00:00 2001 From: DrMesta103 Date: Fri, 12 Jun 2026 23:46:48 +0330 Subject: [PATCH] fix: cancel counting, redesigned locations, scanner defaults, uncounted items, and restored dashboard header --- src/app/admin/discrepancies/page.js | 149 ++++++++++++++++--------- src/app/admin/locations/page.js | 128 ++++++++++++++++----- src/app/admin/users/[id]/page.js | 140 ++++++++++++++++++----- src/app/api/counting/cancel/route.js | 12 +- src/app/api/reports/uncounted/route.js | 67 ++++++----- src/app/counting/item/page.js | 2 +- src/app/counting/shelf/page.js | 2 +- src/components/Header.js | 85 +++++++++----- 8 files changed, 409 insertions(+), 176 deletions(-) diff --git a/src/app/admin/discrepancies/page.js b/src/app/admin/discrepancies/page.js index 2a48525..5815267 100644 --- a/src/app/admin/discrepancies/page.js +++ b/src/app/admin/discrepancies/page.js @@ -7,11 +7,11 @@ import { motion, AnimatePresence } from 'framer-motion'; export default function DiscrepancyDashboard() { const [warehouse, setWarehouse] = useState(''); const [warehouses, setWarehouses] = useState([]); - const [data, setData] = useState({ discrepancies: [], accurate: [] }); + const [data, setData] = useState({ discrepancies: [], accurate: [], uncounted: [] }); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(''); - const [filter, setFilter] = useState('all'); // all, surplus (مازاد), deficit (کسری) + const [filter, setFilter] = useState('all'); // all, surplus (مازاد), deficit (کسری), uncounted useEffect(() => { fetch('/api/settings') @@ -35,11 +35,21 @@ export default function DiscrepancyDashboard() { const fetchData = async () => { setLoading(true); try { - const res = await fetch(`/api/reports/discrepancies?warehouse=${warehouse}`); - if (res.ok) { - const result = await res.json(); - setData(result); + const [resDiscrepancies, resUncounted] = await Promise.all([ + fetch(`/api/reports/discrepancies?warehouse=${warehouse}`), + fetch(`/api/reports/uncounted?warehouse=${warehouse}`) + ]); + + let finalData = { discrepancies: [], accurate: [], uncounted: [] }; + if (resDiscrepancies.ok) { + const result = await resDiscrepancies.json(); + finalData = { ...finalData, ...result }; } + if (resUncounted.ok) { + const resultUncounted = await resUncounted.json(); + finalData.uncounted = resultUncounted.uncounted || []; + } + setData(finalData); } catch (e) { console.error(e); } finally { @@ -52,8 +62,18 @@ export default function DiscrepancyDashboard() { alert(`تسک بازشماری برای کالا "${productName}" (کد: ${productId}) صادر شد.`); }; - const filteredDiscrepancies = data.discrepancies.filter(item => { - const matchesSearch = item.product_name?.includes(search) || item.product_id?.toString().includes(search); + // We either show discrepancies or uncounted based on filter + const isUncountedMode = filter === 'uncounted'; + + let listToRender = isUncountedMode ? data.uncounted : data.discrepancies; + + const filteredItems = listToRender.filter(item => { + const name = item.product_name || item.Name || ''; + const id = (item.product_id || item.Code || '').toString(); + const matchesSearch = name.includes(search) || id.includes(search); + + if (isUncountedMode) return matchesSearch; + const matchesFilter = filter === 'all' ? true : filter === 'surplus' ? item.difference > 0 : @@ -100,24 +120,34 @@ export default function DiscrepancyDashboard() { {/* Stats */} -
+
-

کالاهای دارای مغایرت

-

{loading ? '-' : data.discrepancies.length}

+

مغایرت‌دار

+

{loading ? '-' : data.discrepancies.length}

+
+
+ +
+
+

شمارش‌نشده

+

{loading ? '-' : data.uncounted.length}

+
+
+
-

کالاهای بدون مغایرت

-

{loading ? '-' : data.accurate.length}

+

صحیح

+

{loading ? '-' : data.accurate.length}

@@ -135,7 +165,7 @@ export default function DiscrepancyDashboard() { />
- {['all', 'surplus', 'deficit'].map(f => ( + {['all', 'surplus', 'deficit', 'uncounted'].map(f => ( ))}
- {/* Discrepancies List (Card Based) */} + {/* Discrepancies / Uncounted List */}
- -

لیست کالا‌های نیازمند بررسی

+ +

+ {isUncountedMode ? 'لیست کالاهای شمارش‌نشده' : 'لیست کالا‌های نیازمند بررسی'} +

- {filteredDiscrepancies.length} مورد + {filteredItems.length} مورد
- {filteredDiscrepancies.map((item, idx) => ( + {filteredItems.map((item, idx) => { + const isUncounted = isUncountedMode; + const pName = isUncounted ? item.Name : item.product_name; + const pId = isUncounted ? item.Code : item.product_id; + + return ( {/* Top: Product Info & Badge */}
-

{item.product_name}

-

کد حسابفا: {item.product_id}

-
-
0 ? 'bg-blue-50 text-blue-600 border border-blue-100' : 'bg-red-50 text-red-600 border border-red-100'}`}> - {item.difference > 0 ? : } - {Math.abs(item.difference)} +

{pName}

+

کد حسابفا: {pId}

+ {!isUncounted && ( +
0 ? 'bg-blue-50 text-blue-600 border border-blue-100' : 'bg-red-50 text-red-600 border border-red-100'}`}> + {item.difference > 0 ? : } + {Math.abs(item.difference)} +
+ )} + {isUncounted && ( +
+ موجودی: {item.Stock} +
+ )}
{/* Middle: Stats */} -
-
- موجودی سیستم - {item.system_expected} + {!isUncounted && ( +
+
+ موجودی سیستم + {item.system_expected} +
+
+ شمارش شما + {item.total_counted} +
+
+ قفسه‌ها + + {item.locations?.length > 0 ? item.locations.join('، ') : '-'} + +
-
- شمارش شما - {item.total_counted} -
-
- قفسه‌ها - - {item.locations.length > 0 ? item.locations.join('، ') : '-'} - -
-
+ )} {/* Bottom: Actions */}
- + {!isUncounted && ( + + )}
- ))} + ); + })} - {filteredDiscrepancies.length === 0 && !loading && ( + {filteredItems.length === 0 && !loading && (
@@ -231,7 +280,7 @@ export default function DiscrepancyDashboard() { {loading && (
-

در حال بررسی و مقایسه اطلاعات...

+

در حال بررسی و استخراج اطلاعات...

)}
diff --git a/src/app/admin/locations/page.js b/src/app/admin/locations/page.js index 3e0688f..f421df0 100644 --- a/src/app/admin/locations/page.js +++ b/src/app/admin/locations/page.js @@ -3,21 +3,26 @@ import { useState, useEffect } from 'react'; import Header from '@/components/Header'; import dynamic from 'next/dynamic'; import { motion, AnimatePresence } from 'framer-motion'; -import { ScanLine, Plus, Search, Trash2, Edit2, Layers, MapPin, X, AlertCircle, XCircle, Box } from 'lucide-react'; +import { ScanLine, Plus, Trash2, Edit2, Layers, MapPin, X, AlertCircle, XCircle, Box } from 'lucide-react'; const Scanner = dynamic(() => import('@yudiel/react-qr-scanner').then(mod => mod.Scanner), { ssr: false }); export default function AdminLocations() { const [locations, setLocations] = useState([]); const [warehouses, setWarehouses] = useState([]); const [selectedWarehouse, setSelectedWarehouse] = useState(''); - const [newCode, setNewCode] = useState(''); + + // Separate states for location parts + const [floor, setFloor] = useState(''); + const [region, setRegion] = useState(''); + const [sector, setSector] = useState(''); + const [row, setRow] = useState(''); + const [loading, setLoading] = useState(false); - const [cameraEnabled, setCameraEnabled] = useState(false); + const [cameraEnabled, setCameraEnabled] = useState(true); const [camError, setCamError] = useState(''); const [activeFilter, setActiveFilter] = useState('all'); const [editingId, setEditingId] = useState(null); - const [editCode, setEditCode] = useState(''); const [toast, setToast] = useState({ show: false, message: '', isError: false }); @@ -57,14 +62,37 @@ export default function AdminLocations() { } }; - const handleAddOrEdit = async (codeValue) => { - const targetCode = editingId ? editCode : (codeValue || newCode); - if (!targetCode) return; + // Smart Parser for the main code input + const handleFullCodeChange = (e) => { + const val = e.target.value.toUpperCase(); + + // Attempt to parse regex: [Letter][Number][Letter][Number] e.g. C2F3 + const regex = /^([A-Za-z]+)(\d+)([A-Za-z]+)(\d+)$/; + const match = val.match(regex); + + if (match) { + setFloor(match[1]); + setRegion(match[2]); + setSector(match[3]); + setRow(match[4]); + } else { + // If it doesn't match perfectly, just use it as floor or something, but best to let users type in separate fields + // We will just leave it empty or partially fill. Actually, it's better to clear them or let them type. + } + }; + + const handleAddOrEdit = async () => { + if (!floor || !region || !sector || !row) { + showToast('لطفا همه مقادیر طبقه، منطقه، قطاع و ردیف را وارد کنید', true); + return; + } if (!selectedWarehouse) { showToast('لطفاً ابتدا انبار را انتخاب کنید', true); return; } + const generatedCode = `${floor.toUpperCase()}${region}${sector.toUpperCase()}${row}`; + setLoading(true); try { const method = editingId ? 'PUT' : 'POST'; @@ -73,15 +101,14 @@ export default function AdminLocations() { const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ code: targetCode, warehouse: selectedWarehouse }) + body: JSON.stringify({ code: generatedCode, warehouse: selectedWarehouse }) }); const data = await res.json(); if (res.ok) { showToast(editingId ? 'قفسه با موفقیت ویرایش شد' : 'قفسه با موفقیت ثبت شد!'); - setNewCode(''); + setFloor(''); setRegion(''); setSector(''); setRow(''); setEditingId(null); - setEditCode(''); fetchLocations(); } else { showToast(data.error || 'خطا در ثبت قفسه', true); @@ -112,8 +139,20 @@ export default function AdminLocations() { const handleScan = (detectedCodes) => { if (detectedCodes && detectedCodes.length > 0) { - const scannedValue = detectedCodes[0].rawValue; - handleAddOrEdit(scannedValue); + const scannedValue = detectedCodes[0].rawValue.toUpperCase(); + const regex = /^([A-Za-z]+)(\d+)([A-Za-z]+)(\d+)$/; + const match = scannedValue.match(regex); + if (match) { + setFloor(match[1]); + setRegion(match[2]); + setSector(match[3]); + setRow(match[4]); + showToast(`کد ${scannedValue} شناسایی و پر شد`); + // Optional: auto-submit here if desired + // setTimeout(handleAddOrEdit, 500); + } else { + showToast(`کد اسکن شده نامعتبر است: ${scannedValue}`, true); + } } }; @@ -136,6 +175,8 @@ export default function AdminLocations() { filteredLocations = filteredLocations.filter(loc => loc.floor === activeFilter); } + const generatedCodePreview = (floor || region || sector || row) ? `${floor.toUpperCase()}${region}${sector.toUpperCase()}${row}` : ''; + return (
@@ -148,7 +189,7 @@ export default function AdminLocations() {
{editingId ? 'ویرایش قفسه' : 'ثبت قفسه جدید'} {editingId && ( - )} @@ -165,25 +206,47 @@ export default function AdminLocations() { ))} -
+ {/* Smart Code Input */} +
+ editingId ? setEditCode(e.target.value) : setNewCode(e.target.value)} - placeholder="مثال: C2F2" - className="flex-1 bg-gray-50 border border-gray-200 rounded-[16px] px-4 py-3 text-center uppercase font-black text-gray-800 tracking-widest focus:outline-none focus:border-indigo-500 focus:bg-white transition-colors placeholder-gray-300" + onChange={handleFullCodeChange} + placeholder="C2F3" + className="w-full bg-indigo-50/50 border border-indigo-100 rounded-[16px] px-4 py-3 text-center uppercase font-black text-indigo-700 tracking-widest focus:outline-none focus:border-indigo-400 focus:bg-indigo-50 transition-colors placeholder-indigo-200" /> - handleAddOrEdit()} - disabled={loading} - className="w-14 bg-gray-900 text-white rounded-[16px] flex items-center justify-center transition-opacity disabled:opacity-50 shrink-0" - > - {loading ?
: (editingId ? : )} -
+
+
+ + setFloor(e.target.value)} placeholder="C" className="w-full bg-gray-50 border border-gray-200 rounded-xl px-2 py-2 text-center uppercase font-bold text-gray-800 focus:outline-none focus:border-indigo-500" /> +
+
+ + setRegion(e.target.value)} placeholder="2" className="w-full bg-gray-50 border border-gray-200 rounded-xl px-2 py-2 text-center font-bold text-gray-800 focus:outline-none focus:border-indigo-500" /> +
+
+ + setSector(e.target.value)} placeholder="F" className="w-full bg-gray-50 border border-gray-200 rounded-xl px-2 py-2 text-center uppercase font-bold text-gray-800 focus:outline-none focus:border-indigo-500" /> +
+
+ + setRow(e.target.value)} placeholder="3" className="w-full bg-gray-50 border border-gray-200 rounded-xl px-2 py-2 text-center font-bold text-gray-800 focus:outline-none focus:border-indigo-500" /> +
+
+ + + {loading ?
: (editingId ? : )} + {editingId ? `ویرایش ${generatedCodePreview}` : `ثبت قفسه ${generatedCodePreview}`} +
+
{cameraEnabled && ( @@ -214,7 +277,7 @@ export default function AdminLocations() { setCameraEnabled(true)} - className="w-full py-3.5 bg-indigo-50 text-indigo-600 text-sm font-extrabold rounded-[16px] transition-colors flex items-center justify-center gap-2" + className="w-full py-3.5 bg-indigo-50 text-indigo-600 text-sm font-extrabold rounded-[16px] transition-colors flex items-center justify-center gap-2 mt-2" > اسکن بارکد قفسه @@ -269,7 +332,7 @@ export default function AdminLocations() {
{loc.code} - {loc.warehouse ? `انبار: ${loc.warehouse} • ` : ''}طبقه {loc.floor} • منطقه {loc.region} • قطاع {loc.sector} + {loc.warehouse ? `انبار: ${loc.warehouse} • ` : ''}طبقه {loc.floor} • منطقه {loc.region} • قطاع {loc.sector} • ردیف {loc.row}
@@ -278,7 +341,14 @@ export default function AdminLocations() { {!hasData ? ( <> +
+ + ); + } + + // Inner pages sticky header return ( -
- {showBack ? ( - - ) : ( -
- {getInitial(user?.name)} -
- )} -
+
{title}
-
- {!showBack && user?.roles?.includes('ADMIN') && ( - - - - )} - +
+ {/* Decorative dot for symmetry */} +
);