-
-
قفسه {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 && (
+
)}
+
+
+
+
+
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.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' ? 'بارکد قفسه' : 'بارکد کالا'} کلیک کنید