diff --git a/public/manifest.json b/public/manifest.json index bbc4bfb..d90565b 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,7 +1,7 @@ { - "name": "پردیس رایانه - سیستم انبارگردانی", - "short_name": "انبارگردانی", - "description": "سیستم جامع مدیریت قفسه‌ها و انبارگردانی", + "name": "سیستم انبارداری", + "short_name": "انبارداری", + "description": "سیستم جامع مدیریت قفسه‌ها و انبارداری", "start_url": "/", "display": "standalone", "background_color": "#ffffff", diff --git a/src/app/admin/locations/page.js b/src/app/admin/locations/page.js index 73b6d37..6b48bc0 100644 --- a/src/app/admin/locations/page.js +++ b/src/app/admin/locations/page.js @@ -2,6 +2,8 @@ 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'; const Scanner = dynamic(() => import('@yudiel/react-qr-scanner').then(mod => mod.Scanner), { ssr: false }); export default function AdminLocations() { @@ -9,9 +11,18 @@ export default function AdminLocations() { const [newCode, setNewCode] = useState(''); const [loading, setLoading] = useState(false); const [cameraEnabled, setCameraEnabled] = useState(false); - 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 }); + + const showToast = (message, isError = false) => { + setToast({ show: true, message, isError }); + setTimeout(() => setToast({ show: false, message: '', isError: false }), 3000); + }; useEffect(() => { fetchLocations(); @@ -28,125 +39,173 @@ export default function AdminLocations() { } }; - const handleAddLocation = async (codeValue) => { - const targetCode = codeValue || newCode; + const handleAddOrEdit = async (codeValue) => { + const targetCode = editingId ? editCode : (codeValue || newCode); if (!targetCode) return; setLoading(true); try { - const res = await fetch('/api/locations', { - method: 'POST', + const method = editingId ? 'PUT' : 'POST'; + const url = editingId ? `/api/locations/${editingId}` : '/api/locations'; + + const res = await fetch(url, { + method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code: targetCode }) }); const data = await res.json(); if (res.ok) { - alert('قفسه با موفقیت ثبت شد!'); + showToast(editingId ? 'قفسه با موفقیت ویرایش شد' : 'قفسه با موفقیت ثبت شد!'); setNewCode(''); + setEditingId(null); + setEditCode(''); fetchLocations(); } else { - alert(data.error || 'خطا در ثبت قفسه'); + showToast(data.error || 'خطا در ثبت قفسه', true); } } catch (e) { - alert('خطای شبکه'); + showToast('خطای شبکه', true); } finally { setLoading(false); setCameraEnabled(false); } }; + const handleDelete = async (id) => { + if (!confirm('آیا از حذف این قفسه اطمینان دارید؟')) return; + try { + const res = await fetch(`/api/locations/${id}`, { method: 'DELETE' }); + const data = await res.json(); + if (res.ok) { + showToast('قفسه حذف شد'); + fetchLocations(); + } else { + showToast(data.error || 'خطا در حذف', true); + } + } catch (e) { + showToast('خطای شبکه', true); + } + }; + const handleScan = (detectedCodes) => { if (detectedCodes && detectedCodes.length > 0) { const scannedValue = detectedCodes[0].rawValue; - handleAddLocation(scannedValue); + handleAddOrEdit(scannedValue); } }; const handleError = (error) => { - console.error(error); const msg = error?.message || error?.name || ''; - if (msg.includes('Requested device not found') || msg.includes('NotFoundError') || msg.includes('device not found')) { - setCamError('هیچ دوربینی روی این دستگاه یافت نشد. لطفاً از لپ‌تاپ یا موبایل استفاده کنید.'); + if (msg.includes('Requested device not found')) { + setCamError('دوربینی یافت نشد.'); } else { - setCamError(msg || 'خطا در دسترسی به دوربین. آیا از HTTPS یا localhost استفاده میکنید؟'); + setCamError(msg || 'خطا در دسترسی به دوربین.'); } }; - // استخراج طبقات یکتا برای ساخت دکمه‌های فیلتر const floors = [...new Set(locations.map(loc => loc.floor))].sort(); - - // اعمال فیلتر روی لیست قفسه‌ها const filteredLocations = activeFilter === 'all' ? locations : locations.filter(loc => loc.floor === activeFilter); return ( -
-
+
+
-
+
-
-

ثبت قفسه جدید

-

- فرمت استاندارد شامل حروف و اعداد است. - مثال: C2F2 (طبقه C، منطقه 2، قطاع F، ردیف 2) + {/* Header Title */} +

+

تنظیمات قفسه‌ها

+

+ ثبت، ویرایش و مدیریت قفسه‌های انبار

+
-
+ {/* Action Box */} +
+ +
+ {editingId ? 'ویرایش قفسه' : 'ثبت قفسه جدید'} + {editingId && ( + + )} +
+ +
setNewCode(e.target.value)} + value={editingId ? editCode : newCode} + onChange={(e) => editingId ? setEditCode(e.target.value) : setNewCode(e.target.value)} placeholder="مثال: C2F2" - className="w-full border p-2 rounded text-center uppercase font-bold" + 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" /> - + {loading ?
: (editingId ? : )} +
-
- {cameraEnabled ? ( -
- {camError ? ( -
{camError}
- ) : ( - - )} - -
- ) : ( -
+ ) : ( +
+ +
+ )} + + + )} + + + {!cameraEnabled && !editingId && ( + setCameraEnabled(true)} - className="w-full bg-blue-500 text-white font-bold py-2 rounded flex justify-center items-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" > + اسکن بارکد قفسه - + )}
-
-

قفسه های ثبت شده ({filteredLocations.length})

+ {/* List Section */} +
- {/* فیلترهای تگ (اسکرول افقی) */} +
+

لیست قفسه‌ها ({filteredLocations.length})

+
+ + {/* Tag Filters */} {floors.length > 0 && ( -
+
@@ -154,7 +213,7 @@ export default function AdminLocations() { @@ -162,18 +221,83 @@ export default function AdminLocations() {
)} -
- {filteredLocations.map((loc) => ( -
- {loc.code} - طبقه {loc.floor} | منطقه {loc.region} | قطاع {loc.sector} | ردیف {loc.row} + {/* Cards */} +
+ {filteredLocations.map((loc) => { + const hasData = loc._count?.countings > 0; + return ( + +
+
+ +
+
+ {loc.code} + + طبقه {loc.floor} • منطقه {loc.region} • قطاع {loc.sector} + +
+
+ +
+ {!hasData ? ( + <> + + + + ) : ( +
+ + {loc._count.countings} کالا +
+ )} +
+
+ ); + })} + + {filteredLocations.length === 0 && ( +
+
+ +
+ هیچ قفسه‌ای یافت نشد + با فرم بالا یک قفسه جدید ثبت کنید
- ))} - {filteredLocations.length === 0 && موردی یافت نشد.} + )}
-
+ + {/* Minimal Toast Notification */} + + {toast.show && ( + + {toast.isError ? : } + {toast.message} + + )} +
); } diff --git a/src/app/api/locations/[id]/route.js b/src/app/api/locations/[id]/route.js new file mode 100644 index 0000000..3d11fa0 --- /dev/null +++ b/src/app/api/locations/[id]/route.js @@ -0,0 +1,59 @@ +import prisma from '@/lib/prisma'; +import { NextResponse } from 'next/server'; + +export async function DELETE(req, { params }) { + try { + const { id: idParam } = await params; + const id = parseInt(idParam, 10); + const location = await prisma.location.findUnique({ + where: { id }, + include: { _count: { select: { countings: true } } } + }); + + if (!location) return NextResponse.json({ error: 'قفسه یافت نشد' }, { status: 404 }); + if (location._count.countings > 0) { + return NextResponse.json({ error: 'این قفسه دارای کالای شمرده شده است و قابل حذف نیست.' }, { status: 400 }); + } + + await prisma.location.delete({ where: { id } }); + return NextResponse.json({ success: true }); + } catch (error) { + return NextResponse.json({ error: 'خطا در حذف قفسه' }, { status: 500 }); + } +} + +export async function PUT(req, { params }) { + try { + const { id: idParam } = await params; + const id = parseInt(idParam, 10); + const { code } = await req.json(); + + const location = await prisma.location.findUnique({ + where: { id }, + include: { _count: { select: { countings: true } } } + }); + + if (!location) return NextResponse.json({ error: 'قفسه یافت نشد' }, { status: 404 }); + if (location._count.countings > 0) { + return NextResponse.json({ error: 'این قفسه دارای کالا است و قابل ویرایش نیست.' }, { status: 400 }); + } + + const regex = /^([A-Za-z]+)(\d+)([A-Za-z]+)(\d+)$/; + const match = code.toUpperCase().match(regex); + if (!match) return NextResponse.json({ error: 'فرمت کد نامعتبر است. مثال: C2F2' }, { status: 400 }); + + const [, floor, regionStr, sector, rowStr] = match; + const region = parseInt(regionStr, 10); + const row = parseInt(rowStr, 10); + + const updated = await prisma.location.update({ + where: { id }, + data: { code: code.toUpperCase(), floor, region, sector, row } + }); + + return NextResponse.json({ success: true, location: updated }); + } catch (error) { + if (error.code === 'P2002') return NextResponse.json({ error: 'این کد قفسه از قبل وجود دارد' }, { status: 400 }); + return NextResponse.json({ error: 'خطا در ویرایش قفسه' }, { status: 500 }); + } +} diff --git a/src/app/api/locations/route.js b/src/app/api/locations/route.js index 1687cc0..6cf0cfa 100644 --- a/src/app/api/locations/route.js +++ b/src/app/api/locations/route.js @@ -4,6 +4,7 @@ import { NextResponse } from 'next/server'; export async function GET() { try { const locations = await prisma.location.findMany({ + include: { _count: { select: { countings: true } } }, orderBy: [{ floor: 'asc' }, { region: 'asc' }, { sector: 'asc' }, { row: 'asc' }] }); return NextResponse.json(locations); diff --git a/src/app/layout.js b/src/app/layout.js index 50bbf84..309d466 100644 --- a/src/app/layout.js +++ b/src/app/layout.js @@ -9,8 +9,8 @@ export const viewport = { } export const metadata = { - title: 'پردیس رایانه - انبارگردانی', - description: 'اپلیکیشن انبارگردانی پردیس', + title: 'سیستم انبارداری', + description: 'اپلیکیشن مدیریت انبار', manifest: "/manifest.json", appleWebApp: { capable: true, diff --git a/src/app/page.js b/src/app/page.js index d9d897f..d840bcd 100644 --- a/src/app/page.js +++ b/src/app/page.js @@ -96,56 +96,13 @@ export default function EntryPage() { className="absolute inset-0 z-50 flex flex-col items-center justify-center bg-gray-50/50 backdrop-blur-md" > - - پردیس رایانه - - - - انبارگردانی هوشمند - - - - مدیریت حرفه‌ای انبار و قفسه‌ها - + انبارداری - - -
- - H U K A - - طراحی توسط هوکا -
-
- - ) : showInstall ? ( -
- پردیس رایانه +
+ انبارداری

@@ -235,7 +192,7 @@ export default function EntryPage() { className="flex flex-col items-center justify-center flex-1 pt-10" >
- پردیس رایانه + انبارداری

خوش آمدید diff --git a/src/app/scan/page.js b/src/app/scan/page.js index bee7aa5..de353c8 100644 --- a/src/app/scan/page.js +++ b/src/app/scan/page.js @@ -1,8 +1,10 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useState } from 'react'; import { useRouter } from 'next/navigation'; import Header from '@/components/Header'; import dynamic from 'next/dynamic'; +import { motion } from 'framer-motion'; +import { ScanLine, Search, Box, X, ChevronDown, CheckCircle2 } from 'lucide-react'; const Scanner = dynamic(() => import('@yudiel/react-qr-scanner').then(mod => mod.Scanner), { ssr: false }); export default function ScanPage() { @@ -26,7 +28,7 @@ export default function ScanPage() { setCameraEnabled(false); setTimeout(() => { router.push(`/counting?code=${scannedValue}&warehouse=${warehouse}`); - }, 1500); + }, 500); } }; @@ -34,76 +36,113 @@ export default function ScanPage() { console.error(error); const msg = error?.message || error?.name || ''; if (msg.includes('Requested device not found') || msg.includes('NotFoundError') || msg.includes('device not found')) { - setCamError('هیچ دوربینی روی این دستگاه یافت نشد. لطفاً از لپ‌تاپ یا موبایل استفاده کنید.'); + setCamError('دوربینی روی این دستگاه یافت نشد. لطفاً از لپ‌تاپ یا موبایل استفاده کنید.'); } else { - setCamError(msg || 'خطا در دسترسی به دوربین. آیا از HTTPS یا localhost استفاده میکنید؟'); + setCamError(msg || 'خطا در دسترسی به دوربین.'); } }; return ( -
-
+
+
-
+
-
- {cameraEnabled ? ( -
- {camError ? ( -
{camError}
- ) : ( - - )} - +
+ ) : ( + setCameraEnabled(true)} + className="w-full h-full flex flex-col items-center justify-center gap-4 py-12" > - بستن دوربین - -
- ) : ( - - )} +
+ +
+
+ فعال‌سازی دوربین + برای اسکن بارکد کالا کلیک کنید +
+ + )} +
-

کد کالا را اسکن یا وارد کنید

- - setCode(e.target.value)} - placeholder="ورود دستی کد" - className="w-full max-w-xs text-center text-3xl p-4 border rounded-lg focus:ring-2 focus:ring-purple-500 outline-none" - /> - - - - + {/* Manual Input */} +
+
+
+ +
+ setCode(e.target.value)} + placeholder="ورود دستی کد کالا..." + className="w-full pl-12 pr-5 py-4 bg-white border border-gray-200 rounded-[24px] focus:outline-none focus:border-indigo-500 focus:ring-4 focus:ring-indigo-50 transition-all text-left text-lg font-bold text-gray-800 placeholder-gray-300 shadow-sm" + /> +
+ +
+
+ +
+ +
+ + + {code ? : } + شروع شمارش کالا + +
+
);