From 22e3fa14159597a643994b9bf7494a6807815be8 Mon Sep 17 00:00:00 2001 From: DrMesta103 Date: Fri, 12 Jun 2026 21:29:39 +0330 Subject: [PATCH] feat: complete Phase 2 counting features, UI redesign, and discrepancy dashboard --- src/app/admin/discrepancies/page.js | 243 ++++++++++++------- src/app/admin/settings/page.js | 28 ++- src/app/api/counting/[id]/route.js | 28 +++ src/app/api/locations/validate/route.js | 35 +++ src/app/api/reports/uncounted/route.js | 51 ++++ src/app/counting/item/page.js | 223 ++++++++++++----- src/app/counting/shelf/page.js | 306 +++++++++++++++++------- src/app/dashboard/page.js | 90 ++++++- src/app/history/page.js | 208 +++++++++++++--- src/app/scan/page.js | 2 +- 10 files changed, 930 insertions(+), 284 deletions(-) create mode 100644 src/app/api/counting/[id]/route.js create mode 100644 src/app/api/locations/validate/route.js create mode 100644 src/app/api/reports/uncounted/route.js diff --git a/src/app/admin/discrepancies/page.js b/src/app/admin/discrepancies/page.js index cc6628c..2a48525 100644 --- a/src/app/admin/discrepancies/page.js +++ b/src/app/admin/discrepancies/page.js @@ -1,13 +1,17 @@ 'use client'; import { useState, useEffect } from 'react'; import Header from '@/components/Header'; -import { AlertTriangle, CheckCircle, PackageSearch, RefreshCw, Send, ListTree, ArrowDownRight, ArrowUpRight } from 'lucide-react'; +import { AlertTriangle, CheckCircle, PackageSearch, RefreshCw, Send, ListTree, ArrowDownRight, ArrowUpRight, Search, Filter } from 'lucide-react'; +import { motion, AnimatePresence } from 'framer-motion'; export default function DiscrepancyDashboard() { - const [warehouse, setWarehouse] = useState('11'); + const [warehouse, setWarehouse] = useState(''); const [warehouses, setWarehouses] = useState([]); const [data, setData] = useState({ discrepancies: [], accurate: [] }); const [loading, setLoading] = useState(true); + + const [search, setSearch] = useState(''); + const [filter, setFilter] = useState('all'); // all, surplus (مازاد), deficit (کسری) useEffect(() => { fetch('/api/settings') @@ -48,127 +52,188 @@ 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); + const matchesFilter = + filter === 'all' ? true : + filter === 'surplus' ? item.difference > 0 : + filter === 'deficit' ? item.difference < 0 : true; + + return matchesSearch && matchesFilter; + }); + return ( -
+
-
+
{/* Controls */} -
+
-
+

انتخاب انبار

-

انبار مورد نظر برای بررسی مغایرت

+

مبنای مقایسه موجودی حسابفا

- +
{/* Stats */} -
-
-
- +
+
+
+ +
+
+

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

+

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

-

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

-

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

-
-
- +
+
+ +
+
+

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

+

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

-

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

-

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

- {/* Discrepancies List */} -
-
- -

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

+ {/* Filters and Search */} +
+
+ + setSearch(e.target.value)} + placeholder="جستجوی کالا..." + className="w-full bg-gray-50 border border-gray-200 rounded-[16px] pr-11 pl-4 py-3 text-sm font-bold focus:outline-none focus:border-indigo-500 transition-all placeholder:font-normal" + /> +
+
+ {['all', 'surplus', 'deficit'].map(f => ( + + ))} +
+
+ + {/* Discrepancies List (Card Based) */} +
+
+ +

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

+ + {filteredDiscrepancies.length} مورد +
-
- - - - - - - - - - - - - {data.discrepancies.map((item, idx) => ( - - - - - - - - - ))} - {data.discrepancies.length === 0 && !loading && ( - - - - )} - {loading && ( - - - - )} - -
کالاموجودی سیستمشمارش شدهمغایرتقفسه‌هاعملیات
-

{item.product_name}

-

کد: {item.product_id}

-
{item.system_expected}{item.total_counted} -
0 ? 'bg-blue-100 text-blue-700' : 'bg-red-100 text-red-700'}`}> - {item.difference > 0 ? : } - {Math.abs(item.difference)} -
-
- {item.locations.length > 0 ? item.locations.join('، ') : 'پیدا نشد'} - -
- - -
-
- هیچ مغایرتی یافت نشد. همه چیز مرتب است! 🎉 -
- در حال پردازش و مقایسه اطلاعات... -
+
+ + {filteredDiscrepancies.map((item, idx) => ( + + {/* 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)} +
+
+ + {/* Middle: Stats */} +
+
+ موجودی سیستم + {item.system_expected} +
+
+ شمارش شما + {item.total_counted} +
+
+ قفسه‌ها + + {item.locations.length > 0 ? item.locations.join('، ') : '-'} + +
+
+ + {/* Bottom: Actions */} +
+ + +
+
+ ))} +
+ + {filteredDiscrepancies.length === 0 && !loading && ( +
+
+ +
+

موردی یافت نشد!

+

یا مغایرتی وجود ندارد یا جستجوی شما نتیجه‌ای نداشت.

+
+ )} + + {loading && ( +
+
+

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

+
+ )}
diff --git a/src/app/admin/settings/page.js b/src/app/admin/settings/page.js index 3f7d5a3..b1ca70a 100644 --- a/src/app/admin/settings/page.js +++ b/src/app/admin/settings/page.js @@ -7,7 +7,8 @@ import { motion, AnimatePresence } from 'framer-motion'; export default function SettingsPage() { const [settings, setSettings] = useState({ blind_counting: false, - correction_roles: ['ADMIN', 'SUPERVISOR'] + correction_roles: ['ADMIN', 'SUPERVISOR'], + uncounted_shelf_days: 10 }); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); @@ -160,6 +161,31 @@ export default function SettingsPage() { })}
+ + {/* Uncounted Shelves Warning Days */} +
+
+
+ +
+

هشدار قفسه‌های شمارش‌نشده

+
+

+ قفسه‌هایی که بیشتر از این تعداد روز از آخرین انبارگردانی‌شان گذشته باشد، در صفحه اصلی برای شمارش مجدد پیشنهاد می‌شوند. +

+ +
+ setSettings(s => ({ ...s, uncounted_shelf_days: Number(e.target.value) }))} + className="w-20 bg-white border border-gray-200 rounded-[12px] px-3 py-2 text-center font-black text-gray-800 focus:outline-none focus:border-indigo-500 transition-colors" + /> + روز +
+
{ + countMap.set(c.shelfCode, c._max.createdAt); + }); + + // Filter locations that have either NEVER been counted, or the latest count is older than cutoff + const uncounted = locations.filter(loc => { + const lastCountDate = countMap.get(loc.code); + if (!lastCountDate) return true; // Never counted + return new Date(lastCountDate) < cutoffDate; + }); + + return NextResponse.json({ uncounted }); + } catch (error) { + console.error('Fetch uncounted error:', error); + return NextResponse.json({ error: 'خطا در دریافت لیست قفسه‌ها' }, { status: 500 }); + } +} diff --git a/src/app/counting/item/page.js b/src/app/counting/item/page.js index 52e9d72..2a2d62b 100644 --- a/src/app/counting/item/page.js +++ b/src/app/counting/item/page.js @@ -2,9 +2,13 @@ import { useState, useEffect, Suspense } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import Header from '@/components/Header'; -import { Box, Layers, CheckCircle2 } from 'lucide-react'; +import { Box, Layers, CheckCircle2, ScanLine, X, AlertCircle } from 'lucide-react'; import { hasRole } from '@/lib/auth'; import { saveCountOffline, syncOfflineCounts } from '@/lib/offlineSync'; +import dynamic from 'next/dynamic'; +import { motion, AnimatePresence } from 'framer-motion'; + +const Scanner = dynamic(() => import('@yudiel/react-qr-scanner').then(mod => mod.Scanner), { ssr: false }); function ItemCountingContent() { const router = useRouter(); @@ -23,6 +27,10 @@ function ItemCountingContent() { const [newCount, setNewCount] = useState(''); const [submitLoading, setSubmitLoading] = useState(false); const [history, setHistory] = useState([]); + + const [cameraEnabled, setCameraEnabled] = useState(false); + const [camError, setCamError] = useState(''); + const [errorMsg, setErrorMsg] = useState(''); useEffect(() => { const userData = localStorage.getItem('user'); @@ -67,23 +75,64 @@ function ItemCountingContent() { } }; - const handleSubmit = async () => { - if (!shelfCode) return alert('کد قفسه را وارد کنید'); - if (newCount === '' || newCount === null) return alert('تعداد را وارد کنید'); + const handleScan = (detectedCodes) => { + if (detectedCodes && detectedCodes.length > 0) { + const scannedValue = detectedCodes[0].rawValue; + setShelfCode(scannedValue.toUpperCase()); + setCameraEnabled(false); + setErrorMsg(''); + } + }; + + const handleError = (error) => { + const msg = error?.message || error?.name || ''; + if (msg.includes('Requested device not found')) { + setCamError('دوربینی یافت نشد.'); + } else { + setCamError('خطا در دسترسی به دوربین.'); + } + }; + + const validateShelfAndSubmit = async () => { + setErrorMsg(''); + if (!shelfCode) { + setErrorMsg('لطفاً کد قفسه را وارد یا اسکن کنید'); + return; + } + if (newCount === '' || newCount === null) { + setErrorMsg('لطفاً تعداد را وارد کنید'); + return; + } setSubmitLoading(true); - const payload = { - product_id: code, - product_name: productName, - warehouse, - shelfCode: shelfCode.toUpperCase(), - old_count: oldCount || 0, - new_count: Number(newCount), - user_id: user?.id, - mode: 'ITEM' - }; try { + // 1. Validate shelf + const valRes = await fetch('/api/locations/validate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code: shelfCode.toUpperCase(), warehouse }) + }); + const valData = await valRes.json(); + + if (!valRes.ok) { + setErrorMsg(valData.error || 'قفسه نامعتبر است'); + setSubmitLoading(false); + return; + } + + // 2. Submit count + const payload = { + product_id: code, + product_name: productName, + warehouse, + shelfCode: shelfCode.toUpperCase(), + old_count: oldCount || 0, + new_count: Number(newCount), + user_id: user?.id, + mode: 'ITEM' + }; + const res = await fetch('/api/counting', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -95,11 +144,22 @@ function ItemCountingContent() { setShelfCode(''); setNewCount(''); } else { - alert('خطا در ثبت شمارش'); + setErrorMsg('خطا در ثبت شمارش در سرور'); } } catch (err) { + // 3. Fallback offline + const payload = { + product_id: code, + product_name: productName, + warehouse, + shelfCode: shelfCode.toUpperCase(), + old_count: oldCount || 0, + new_count: Number(newCount), + user_id: user?.id, + mode: 'ITEM' + }; await saveCountOffline(payload); - alert('ارتباط با سرور قطع است. اطلاعات در گوشی شما ذخیره شد و بعداً ارسال می‌شود.'); + alert('ارتباط با سرور قطع است. اطلاعات موقتاً در گوشی شما ذخیره شد.'); setHistory([{ shelf: shelfCode.toUpperCase(), count: newCount, offline: true }, ...history]); setShelfCode(''); setNewCount(''); @@ -115,72 +175,117 @@ function ItemCountingContent() { const isBlind = settings?.blind_counting && !hasRole(user?.roles, ['ADMIN', 'SUPERVISOR']); if (loading) { - return
در حال دریافت اطلاعات...
; + return ( +
+
+
+
+
+
+ ); } return ( -
+
{/* Top Banner */} -
-
- +
+
+
-

{productName}

-

کد: {code}

+

{productName}

+

کد: {code}

{!isBlind && oldCount !== null && ( -
- {oldCount} - سیستم +
+ {oldCount} + موجودی سیستم
)}
-
+
{/* Input Form */} -
-

ثبت شمارش در قفسه

+
+
-
-
-
- -
+

+ + ثبت شمارش در قفسه +

+ + {errorMsg && ( +
+ + {errorMsg} +
+ )} + +
+
setShelfCode(e.target.value)} - placeholder="کد قفسه (مثال A-1)..." - className="w-full pl-10 pr-4 py-3 bg-gray-50 border border-gray-200 rounded-[16px] text-center text-sm font-bold text-gray-800 uppercase focus:outline-none focus:border-gray-500 focus:ring-2 focus:ring-gray-100 transition-all" + onChange={(e) => { setShelfCode(e.target.value); setErrorMsg(''); }} + placeholder="مثال: C-1" + className="flex-1 py-3.5 bg-gray-50 border border-gray-200 rounded-[16px] text-center text-lg font-black text-gray-800 uppercase focus:outline-none focus:border-indigo-500 focus:bg-white transition-all placeholder:text-sm placeholder:font-normal placeholder:text-gray-400" /> + setCameraEnabled(true)} + className="w-14 bg-indigo-50 text-indigo-600 rounded-[16px] flex items-center justify-center shrink-0 hover:bg-indigo-100 transition-colors border border-indigo-100" + > + +
+ + {cameraEnabled && ( + + {camError ? ( +
{camError}
+ ) : ( +
+ +
+ )} + +
+ )} +
+ setNewCount(e.target.value)} - placeholder="تعداد یافت شده در این قفسه" - className="w-full border-2 border-gray-200 bg-white rounded-[16px] p-4 text-center text-xl font-black text-gray-800 focus:outline-none focus:border-gray-900 focus:ring-4 focus:ring-gray-100 transition-all placeholder:text-sm placeholder:font-medium placeholder:text-gray-300" + onChange={(e) => { setNewCount(e.target.value); setErrorMsg(''); }} + placeholder="تعداد یافت شده در این قفسه..." + className="w-full border-2 border-gray-200 bg-white rounded-[16px] p-4 text-center text-2xl font-black text-gray-800 focus:outline-none focus:border-indigo-500 transition-all placeholder:text-sm placeholder:font-medium placeholder:text-gray-300" />
@@ -188,17 +293,23 @@ function ItemCountingContent() { {/* History of this item in different shelves */} {history.length > 0 && ( -
-

مکان‌های یافت شده

+
+

مکان‌های ثبت شده شما

{history.map((item, idx) => ( -
-
- - قفسه {item.shelf} +
+
+
+ +
+
+ {item.shelf} + {item.offline && ثبت آفلاین} +
-
- {item.count} عدد +
+ شمارش شده + {item.count}
))} @@ -208,9 +319,9 @@ function ItemCountingContent() {
diff --git a/src/app/counting/shelf/page.js b/src/app/counting/shelf/page.js index b8fe6f6..a5cee91 100644 --- a/src/app/counting/shelf/page.js +++ b/src/app/counting/shelf/page.js @@ -1,10 +1,14 @@ 'use client'; -import { useState, useEffect, Suspense, useRef } from 'react'; +import { useState, useEffect, Suspense } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import Header from '@/components/Header'; -import { Lock, Unlock, ScanLine, Search, PlusCircle, Check, Box } from 'lucide-react'; +import { Lock, Unlock, ScanLine, Search, Check, Box, Layers, AlertCircle, X } from 'lucide-react'; import { hasRole } from '@/lib/auth'; import { saveCountOffline, syncOfflineCounts } from '@/lib/offlineSync'; +import dynamic from 'next/dynamic'; +import { motion, AnimatePresence } from 'framer-motion'; + +const Scanner = dynamic(() => import('@yudiel/react-qr-scanner').then(mod => mod.Scanner), { ssr: false }); function ShelfCountingContent() { const router = useRouter(); @@ -21,8 +25,13 @@ function ShelfCountingContent() { const [productName, setProductName] = useState(''); const [oldCount, setOldCount] = useState(null); const [newCount, setNewCount] = useState(''); + const [itemLoading, setItemLoading] = useState(false); const [submitLoading, setSubmitLoading] = useState(false); + const [errorMsg, setErrorMsg] = useState(''); + + const [cameraEnabled, setCameraEnabled] = useState(false); + const [camError, setCamError] = useState(''); // History of scanned items in this session const [history, setHistory] = useState([]); @@ -32,7 +41,6 @@ function ShelfCountingContent() { if (userData) setUser(JSON.parse(userData)); fetchSettings(); - // Attempt to sync offline counts when page loads and we're online window.addEventListener('online', syncOfflineCounts); return () => window.removeEventListener('online', syncOfflineCounts); }, []); @@ -65,11 +73,19 @@ function ShelfCountingContent() { } }; - const fetchItemData = async (code) => { - if (!code) return; + const fetchItemData = async (codeToFetch) => { + const code = codeToFetch || productCode; + if (!code) { + setErrorMsg('کد کالا را وارد کنید'); + return; + } + + setErrorMsg(''); setItemLoading(true); setProductName(''); setOldCount(null); + setNewCount(''); + try { const nameRes = await fetch('/api/hesabfa', { method: 'POST', @@ -77,7 +93,14 @@ function ShelfCountingContent() { body: JSON.stringify({ code, type: 'name' }) }); const nameData = await nameRes.json(); - setProductName(nameData?.Result?.Name || 'نامشخص'); + + if (!nameData?.Result?.Name || nameData?.Result?.Name === 'نامشخص' || nameData.error) { + setErrorMsg('کالایی با این کد در حسابفا یافت نشد.'); + setItemLoading(false); + return; + } + + setProductName(nameData.Result.Name); const qRes = await fetch('/api/hesabfa', { method: 'POST', @@ -90,28 +113,53 @@ function ShelfCountingContent() { setOldCount(wInfo?.Quantity ?? 0); } catch (error) { console.error(error); - setProductName('خطا در دریافت اطلاعات'); + setErrorMsg('خطا در ارتباط با حسابفا'); } finally { setItemLoading(false); } }; const handleProductCodeChange = (e) => { - const code = e.target.value; - setProductCode(code); + setProductCode(e.target.value); + setErrorMsg(''); + setProductName(''); }; const handleProductCodeKeyDown = (e) => { if (e.key === 'Enter') { - fetchItemData(productCode); + fetchItemData(); + } + }; + + const handleScan = (detectedCodes) => { + if (detectedCodes && detectedCodes.length > 0) { + const scannedValue = detectedCodes[0].rawValue; + setProductCode(scannedValue); + setCameraEnabled(false); + setCamError(''); + fetchItemData(scannedValue); + } + }; + + const handleError = (error) => { + const msg = error?.message || error?.name || ''; + if (msg.includes('Requested device not found')) { + setCamError('دوربینی یافت نشد.'); + } else { + setCamError('خطا در دسترسی به دوربین.'); } }; const handleSubmitItem = async () => { - if (newCount === '' || newCount === null) { - alert('لطفاً تعداد را وارد کنید'); + if (!productName || errorMsg) { + setErrorMsg('ابتدا از صحت کالا اطمینان حاصل کنید'); return; } + if (newCount === '' || newCount === null) { + setErrorMsg('لطفاً تعداد را وارد کنید'); + return; + } + setSubmitLoading(true); const payload = { @@ -138,8 +186,9 @@ function ShelfCountingContent() { setProductName(''); setOldCount(null); setNewCount(''); + setErrorMsg(''); } else { - alert('خطا در ثبت کالا'); + setErrorMsg('خطا در ثبت شمارش کالا در سرور'); } } catch (err) { // Network error (offline or server down) @@ -150,6 +199,7 @@ function ShelfCountingContent() { setProductName(''); setOldCount(null); setNewCount(''); + setErrorMsg(''); } finally { setSubmitLoading(false); } @@ -157,118 +207,190 @@ function ShelfCountingContent() { const isBlind = settings?.blind_counting && !hasRole(user?.roles, ['ADMIN', 'SUPERVISOR']); + if (loading) { + return ( +
+
+
+
+
+
+ ); + } + return ( -
+
{/* Top Banner */} -
-
-

شما در حال شمارش هستید

-
- -

قفسه {shelfCode}

+
+
+
+ +
+
+ در حال شمارش قفسه + {shelfCode}
+
-
+
- {/* Scanner Input */} -
-
- - اسکن کالای جدید در این قفسه -
+ {/* Scanner Form */} +
+
-
- - -
+

+ + اسکن کالای جدید در این قفسه +

- {itemLoading && ( -
- در حال استعلام از حسابفا... + {errorMsg && ( +
+ + {errorMsg}
)} + +
+
+ + fetchItemData()} + disabled={itemLoading || !productCode} + className="w-14 bg-indigo-50 text-indigo-600 rounded-[16px] flex items-center justify-center shrink-0 hover:bg-indigo-100 transition-colors border border-indigo-100 disabled:opacity-50" + > + {itemLoading ?
: } +
+ setCameraEnabled(true)} + className="w-14 bg-gray-900 text-white rounded-[16px] flex items-center justify-center shrink-0 shadow-md transition-colors" + > + + +
- {productName && !itemLoading && ( -
-
-
-

{productName}

-

کد: {productCode}

-
- {!isBlind && oldCount !== null && ( -
- {oldCount} - سیستم -
- )} -
- -
- setNewCount(e.target.value)} - placeholder="تعداد شمارش شده..." - className="flex-1 border-2 border-gray-200 bg-white rounded-[16px] p-3 text-center text-xl font-black text-gray-800 focus:outline-none focus:border-indigo-500 focus:ring-4 focus:ring-indigo-50 transition-all placeholder:text-sm placeholder:font-medium placeholder:text-gray-300" - /> - -
-
- )} + {camError ? ( +
{camError}
+ ) : ( +
+ +
+ )} + + + )} + + + {/* Product Details & Count input (only shown if valid product found) */} + + {productName && !itemLoading && ( + +
+
+
+ +
+
+

{productName}

+

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

+
+
+ {!isBlind && oldCount !== null && ( +
+ {oldCount} + موجودی سیستم +
+ )} +
+ +
+ { setNewCount(e.target.value); setErrorMsg(''); }} + placeholder="تعداد شمارش شده را وارد کنید..." + className="w-full border-2 border-gray-200 bg-white rounded-[16px] p-4 text-center text-2xl font-black text-gray-800 focus:outline-none focus:border-indigo-500 transition-all placeholder:text-sm placeholder:font-medium placeholder:text-gray-300" + /> + +
+
+ )} +
+
{/* History of this session */} {history.length > 0 && ( -
-

کالاهای ثبت شده در این قفسه

+
+

کالاهای ثبت شده تا الان

{history.map((item, idx) => ( -
+
{item.offline && (
)}
-
- +
+
-
+

{item.name}

-

{item.code} {item.offline && (آفلاین)}

+

کد: {item.code}

-
- {item.count} +
+ شمارش شما + + {item.count} +
))} diff --git a/src/app/dashboard/page.js b/src/app/dashboard/page.js index 3561952..24af916 100644 --- a/src/app/dashboard/page.js +++ b/src/app/dashboard/page.js @@ -1,11 +1,37 @@ 'use client'; +import { useState, useEffect } from 'react'; import Header from '@/components/Header'; import Link from 'next/link'; import { motion } from 'framer-motion'; -import { History, ScanLine, ListChecks } from 'lucide-react'; +import { History, ScanLine, ListChecks, AlertTriangle, Layers, MapPin } from 'lucide-react'; export default function Dashboard() { + const [uncountedShelves, setUncountedShelves] = useState([]); + const [settings, setSettings] = useState(null); + useEffect(() => { + fetchData(); + }, []); + + const fetchData = async () => { + try { + const setRes = await fetch('/api/settings'); + let currentSettings = { uncounted_shelf_days: 10 }; + if (setRes.ok) { + currentSettings = await setRes.json(); + setSettings(currentSettings); + } + + const uncRes = await fetch(`/api/reports/uncounted?days=${currentSettings.uncounted_shelf_days || 10}`); + if (uncRes.ok) { + const uncData = await uncRes.json(); + setUncountedShelves(uncData.uncounted || []); + } + } catch (e) { + console.error(e); + } + }; + const container = { hidden: { opacity: 0 }, show: { @@ -20,48 +46,86 @@ export default function Dashboard() { }; return ( -
+
- +
- +
- تاریخچه شمارش + تاریخچه شمارش
- -
- + +
+
+
- اسکن کالا + شروع انبارگردانی
- +
- +
- شمارش‌های من + شمارش‌های من
+ + {/* Suggested Shelves to count */} + {uncountedShelves.length > 0 && ( + +
+

+ + قفسه‌های پیشنهادی +

+ + بدون بررسی بالای {settings?.uncounted_shelf_days || 10} روز + +
+ +
+ {uncountedShelves.slice(0, 4).map((shelf, idx) => ( + +
+
+ +
+ {shelf.code} +
+
+ + طبقه {shelf.floor}، انبار {shelf.warehouse} +
+ + ))} +
+
+ )} +
); diff --git a/src/app/history/page.js b/src/app/history/page.js index d975584..3b99524 100644 --- a/src/app/history/page.js +++ b/src/app/history/page.js @@ -1,52 +1,196 @@ 'use client'; import { useState, useEffect } from 'react'; import Header from '@/components/Header'; +import { hasRole } from '@/lib/auth'; +import { Edit2, Check, X, Box, Layers, User, Save } from 'lucide-react'; +import { motion, AnimatePresence } from 'framer-motion'; export default function HistoryPage() { const [counts, setCounts] = useState([]); const [loading, setLoading] = useState(true); + + const [user, setUser] = useState(null); + const [settings, setSettings] = useState(null); + + const [editingId, setEditingId] = useState(null); + const [editValue, setEditValue] = useState(''); + const [saving, setSaving] = useState(false); useEffect(() => { - const fetchHistory = async () => { - try { - const res = await fetch(`/api/counting`); - if (res.ok) setCounts(await res.json()); - } catch (e) { - console.error(e); - } - setLoading(false); - }; - fetchHistory(); + const userData = localStorage.getItem('user'); + if (userData) setUser(JSON.parse(userData)); + + fetchData(); }, []); + const fetchData = async () => { + try { + const [histRes, setRes] = await Promise.all([ + fetch('/api/counting'), + fetch('/api/settings') + ]); + + if (histRes.ok) setCounts(await histRes.json()); + if (setRes.ok) setSettings(await setRes.json()); + } catch (e) { + console.error(e); + } finally { + setLoading(false); + } + }; + + const handleEditSubmit = async (id) => { + if (editValue === '') return; + setSaving(true); + try { + const res = await fetch(`/api/counting/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ new_count: editValue, userId: user?.id }) + }); + + if (res.ok) { + setCounts(counts.map(c => c.id === id ? { ...c, new_count: Number(editValue) } : c)); + setEditingId(null); + } else { + alert('خطا در ثبت اصلاحیه'); + } + } catch (e) { + alert('خطای شبکه'); + } finally { + setSaving(false); + } + }; + + const canCorrect = settings?.correction_roles ? hasRole(user?.roles, settings.correction_roles) : false; + return ( -
+
-
+
+ {loading ? ( -
در حال دریافت...
+
+
+

در حال دریافت تاریخچه...

+
) : counts.length === 0 ? ( -
تاریخچه خالی است.
- ) : ( - counts.map(count => ( -
-
-
- {count.product_name} - شمارنده: {count.user?.name} | انبار: {count.warehouse} - قفسه: {count.shelf || 'ثبت نشده'} -
-
- {count.new_count} - موجودی ثبت شده -
-
-
- {new Date(count.createdAt).toLocaleString('fa-IR')} -
+
+
+
- )) +

هیچ رکورد انبارگردانی یافت نشد.

+
+ ) : ( +
+
+

آخرین رکوردهای ثبت شده

+ + {counts.length} رکورد + +
+ + + {counts.map(count => { + const isEditing = editingId === count.id; + + return ( + +
+
+
+ +
+
+ {count.product_name} + کد: {count.product_id} +
+
+ + {!isEditing && ( +
+ {count.new_count} + شمارش شده +
+ )} +
+ +
+
+
+ + قفسه: {count.shelfCode || count.shelf || 'ثبت نشده'} +
+
+ + شمارنده: {count.user?.name || 'نامشخص'} +
+
+ +
+ + {new Date(count.createdAt).toLocaleString('fa-IR')} + + + {canCorrect && !isEditing && ( + + )} +
+
+ + {/* Edit Mode */} + + {isEditing && ( + + setEditValue(e.target.value)} + className="w-20 bg-gray-50 border border-gray-200 rounded-[12px] px-3 py-2 text-center text-sm font-black text-gray-800 focus:outline-none focus:border-indigo-500 transition-colors" + /> + + + + )} + + +
+ ); + })} +
+
)}
diff --git a/src/app/scan/page.js b/src/app/scan/page.js index ada4114..9f84a03 100644 --- a/src/app/scan/page.js +++ b/src/app/scan/page.js @@ -150,7 +150,7 @@ export default function ScanPage() {
- פעال‌سازی دوربین + فعال‌سازی دوربین برای اسکن {mode === 'SHELF' ? 'بارکد قفسه' : 'بارکد کالا'} کلیک کنید