first commit
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
'use client';
|
||||
import { useState, useEffect } from 'react';
|
||||
import Header from '@/components/Header';
|
||||
import dynamic from 'next/dynamic';
|
||||
const Scanner = dynamic(() => import('@yudiel/react-qr-scanner').then(mod => mod.Scanner), { ssr: false });
|
||||
|
||||
export default function AdminLocations() {
|
||||
const [locations, setLocations] = useState([]);
|
||||
const [newCode, setNewCode] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [cameraEnabled, setCameraEnabled] = useState(false);
|
||||
|
||||
const [camError, setCamError] = useState('');
|
||||
const [activeFilter, setActiveFilter] = useState('all');
|
||||
|
||||
useEffect(() => {
|
||||
fetchLocations();
|
||||
}, []);
|
||||
|
||||
const fetchLocations = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/locations');
|
||||
if (res.ok) {
|
||||
setLocations(await res.json());
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddLocation = async (codeValue) => {
|
||||
const targetCode = codeValue || newCode;
|
||||
if (!targetCode) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch('/api/locations', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ code: targetCode })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
alert('قفسه با موفقیت ثبت شد!');
|
||||
setNewCode('');
|
||||
fetchLocations();
|
||||
} else {
|
||||
alert(data.error || 'خطا در ثبت قفسه');
|
||||
}
|
||||
} catch (e) {
|
||||
alert('خطای شبکه');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setCameraEnabled(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleScan = (detectedCodes) => {
|
||||
if (detectedCodes && detectedCodes.length > 0) {
|
||||
const scannedValue = detectedCodes[0].rawValue;
|
||||
handleAddLocation(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('هیچ دوربینی روی این دستگاه یافت نشد. لطفاً از لپتاپ یا موبایل استفاده کنید.');
|
||||
} else {
|
||||
setCamError(msg || 'خطا در دسترسی به دوربین. آیا از HTTPS یا localhost استفاده میکنید؟');
|
||||
}
|
||||
};
|
||||
|
||||
// استخراج طبقات یکتا برای ساخت دکمههای فیلتر
|
||||
const floors = [...new Set(locations.map(loc => loc.floor))].sort();
|
||||
|
||||
// اعمال فیلتر روی لیست قفسهها
|
||||
const filteredLocations = activeFilter === 'all'
|
||||
? locations
|
||||
: locations.filter(loc => loc.floor === activeFilter);
|
||||
|
||||
return (
|
||||
<div className="w-full min-h-screen bg-gray-50 flex flex-col pb-20">
|
||||
<Header title="مدیریت قفسه ها (ادمین)" showBack={true} />
|
||||
|
||||
<div className="p-4 flex flex-col gap-6 items-center">
|
||||
|
||||
<div className="w-full bg-white p-4 rounded shadow border border-gray-200">
|
||||
<h2 className="font-bold mb-2">ثبت قفسه جدید</h2>
|
||||
<p className="text-xs text-gray-500 mb-4">
|
||||
فرمت استاندارد شامل حروف و اعداد است.
|
||||
مثال: C2F2 (طبقه C، منطقه 2، قطاع F، ردیف 2)
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<input
|
||||
type="text"
|
||||
dir="ltr"
|
||||
value={newCode}
|
||||
onChange={(e) => setNewCode(e.target.value)}
|
||||
placeholder="مثال: C2F2"
|
||||
className="w-full border p-2 rounded text-center uppercase font-bold"
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleAddLocation(newCode)}
|
||||
disabled={loading}
|
||||
className="bg-purple-600 text-white font-bold py-2 rounded"
|
||||
>
|
||||
ثبت دستی
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 border-t pt-4">
|
||||
{cameraEnabled ? (
|
||||
<div className="w-full aspect-video relative flex flex-col items-center justify-center bg-gray-100 rounded">
|
||||
{camError ? (
|
||||
<div className="text-red-500 text-xs p-4 text-center">{camError}</div>
|
||||
) : (
|
||||
<Scanner onScan={handleScan} onError={handleError} />
|
||||
)}
|
||||
<button
|
||||
onClick={() => { setCameraEnabled(false); setCamError(''); }}
|
||||
className="absolute bottom-2 right-2 bg-red-500 text-white text-xs px-2 py-1 rounded z-10"
|
||||
>
|
||||
بستن دوربین
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setCameraEnabled(true)}
|
||||
className="w-full bg-blue-500 text-white font-bold py-2 rounded flex justify-center items-center gap-2"
|
||||
>
|
||||
اسکن بارکد قفسه
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full bg-white p-4 rounded shadow border border-gray-200">
|
||||
<h2 className="font-bold mb-4">قفسه های ثبت شده ({filteredLocations.length})</h2>
|
||||
|
||||
{/* فیلترهای تگ (اسکرول افقی) */}
|
||||
{floors.length > 0 && (
|
||||
<div className="flex gap-2 overflow-x-auto pb-2 mb-3 scrollbar-hide" style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}>
|
||||
<button
|
||||
onClick={() => setActiveFilter('all')}
|
||||
className={`px-4 py-1.5 rounded-full text-xs font-bold whitespace-nowrap transition-colors border ${activeFilter === 'all' ? 'bg-purple-600 text-white border-purple-600' : 'bg-gray-50 text-gray-600 border-gray-200'}`}
|
||||
>
|
||||
همه طبقات
|
||||
</button>
|
||||
{floors.map(floor => (
|
||||
<button
|
||||
key={floor}
|
||||
onClick={() => setActiveFilter(floor)}
|
||||
className={`px-4 py-1.5 rounded-full text-xs font-bold whitespace-nowrap transition-colors border ${activeFilter === floor ? 'bg-purple-600 text-white border-purple-600' : 'bg-gray-50 text-gray-600 border-gray-200'}`}
|
||||
>
|
||||
طبقه {floor}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-2 max-h-[300px] overflow-y-auto pr-1">
|
||||
{filteredLocations.map((loc) => (
|
||||
<div key={loc.id} className="flex justify-between items-center bg-gray-50 p-2 rounded border border-gray-100 text-sm">
|
||||
<span className="font-bold text-lg text-purple-700">{loc.code}</span>
|
||||
<span className="text-gray-500 text-xs">طبقه {loc.floor} | منطقه {loc.region} | قطاع {loc.sector} | ردیف {loc.row}</span>
|
||||
</div>
|
||||
))}
|
||||
{filteredLocations.length === 0 && <span className="text-center text-sm text-gray-400 py-4">موردی یافت نشد.</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import prisma from '@/lib/prisma';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
// Get all countings grouped by floor using the relation
|
||||
const countings = await prisma.counting.findMany({
|
||||
include: {
|
||||
location: true
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate stats per floor
|
||||
const stats = countings.reduce((acc, curr) => {
|
||||
const floor = curr.location?.floor || 'بدون قفسه';
|
||||
|
||||
if (!acc[floor]) {
|
||||
acc[floor] = { floor, totalCountings: 0, items: [] };
|
||||
}
|
||||
|
||||
acc[floor].totalCountings += 1;
|
||||
acc[floor].items.push(curr);
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return NextResponse.json(Object.values(stats));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ error: 'خطا در دریافت آمار' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import prisma from '@/lib/prisma';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { signToken } from '@/lib/auth';
|
||||
|
||||
export async function POST(req) {
|
||||
try {
|
||||
const { username, password } = await req.json();
|
||||
|
||||
if (!username || !password) {
|
||||
return Response.json({ error: 'نام کاربری و رمز عبور الزامی است.' }, { status: 400 });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { username }
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return Response.json({ error: 'کاربر یافت نشد.' }, { status: 404 });
|
||||
}
|
||||
|
||||
const isMatch = await bcrypt.compare(password, user.password);
|
||||
|
||||
if (!isMatch) {
|
||||
return Response.json({ error: 'رمز عبور اشتباه است.' }, { status: 401 });
|
||||
}
|
||||
|
||||
const token = signToken({ id: user.id, username: user.username, name: user.name, orgId: user.orgId, role: user.role });
|
||||
|
||||
return Response.json({ message: 'با موفقیت وارد شدید', token, user: { id: user.id, name: user.name, orgId: user.orgId, role: user.role } });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return Response.json({ error: 'خطای سرور رخ داد.' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import prisma from '@/lib/prisma';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function POST(req) {
|
||||
try {
|
||||
const data = await req.json();
|
||||
|
||||
const count = await prisma.counting.create({
|
||||
data: {
|
||||
product_id: Number(data.product_id),
|
||||
product_name: data.product_name,
|
||||
warehouse: Number(data.warehouse),
|
||||
shelfCode: data.shelfCode || null,
|
||||
old_count: Number(data.old_count),
|
||||
new_count: Number(data.new_count),
|
||||
user_id: Number(data.user_id)
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, count });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ error: 'خطا در ثبت اطلاعات' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(req) {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const productId = searchParams.get('product_id');
|
||||
const warehouse = searchParams.get('warehouse');
|
||||
const userId = searchParams.get('user_id');
|
||||
|
||||
try {
|
||||
if (productId && warehouse) {
|
||||
const lastCount = await prisma.counting.findFirst({
|
||||
where: { product_id: Number(productId), warehouse: Number(warehouse) },
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
return NextResponse.json(lastCount || { message: -1 });
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
const counts = await prisma.counting.findMany({
|
||||
where: { user_id: Number(userId) },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: { location: true }
|
||||
});
|
||||
return NextResponse.json(counts);
|
||||
}
|
||||
|
||||
const allCounts = await prisma.counting.findMany({
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 50,
|
||||
include: { user: { select: { name: true } }, location: true }
|
||||
});
|
||||
return NextResponse.json(allCounts);
|
||||
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'خطا در دریافت اطلاعات' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import axios from 'axios';
|
||||
|
||||
const HESABFA_API_KEY = 'NCuDX3bksHlhXWGIqTvatvme3YTplxdF';
|
||||
const HESABFA_TOKEN = '4ddb2fc517f6f6fe6d4b9bdd08fa0df31a564a62e12c4353eb9533ae63447b57ca87c479beb7f02b276929c861dad779';
|
||||
|
||||
export async function POST(req) {
|
||||
try {
|
||||
const { code, type } = await req.json();
|
||||
|
||||
if (type === 'name') {
|
||||
const res = await axios.post('https://api.hesabfa.com/v1/item/get', {
|
||||
apiKey: HESABFA_API_KEY,
|
||||
loginToken: HESABFA_TOKEN,
|
||||
code: Number(code)
|
||||
});
|
||||
return NextResponse.json(res.data);
|
||||
}
|
||||
|
||||
if (type === 'quantity') {
|
||||
const res = await axios.post('https://api.hesabfa.com/v1/item/GetQuantity2', {
|
||||
apiKey: HESABFA_API_KEY,
|
||||
loginToken: HESABFA_TOKEN,
|
||||
codes: [Number(code)]
|
||||
});
|
||||
return NextResponse.json(res.data);
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'نوع درخواست نامعتبر است' }, { status: 400 });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'خطا در ارتباط با حسابفا' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import prisma from '@/lib/prisma';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const locations = await prisma.location.findMany({
|
||||
orderBy: [{ floor: 'asc' }, { region: 'asc' }, { sector: 'asc' }, { row: 'asc' }]
|
||||
});
|
||||
return NextResponse.json(locations);
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'خطا در دریافت لیست انبارها' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req) {
|
||||
try {
|
||||
const { code } = await req.json(); // "C2F2"
|
||||
|
||||
// Pattern validation (e.g., C2F2 -> 1 Char, 1 Num, 1 Char, 1 Num)
|
||||
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 location = await prisma.location.upsert({
|
||||
where: { code: code.toUpperCase() },
|
||||
update: {}, // if exists, do nothing or update timestamps
|
||||
create: {
|
||||
code: code.toUpperCase(),
|
||||
floor,
|
||||
region,
|
||||
sector,
|
||||
row
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, location });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ error: 'خطا در ثبت قفسه' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
'use client';
|
||||
import { useState, useEffect, Suspense } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import Header from '@/components/Header';
|
||||
|
||||
function CountingContent() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const code = searchParams.get('code');
|
||||
const warehouse = searchParams.get('warehouse');
|
||||
|
||||
const [productName, setProductName] = useState('');
|
||||
const [oldCount, setOldCount] = useState(null);
|
||||
const [newCount, setNewCount] = useState('');
|
||||
const [shelf, setShelf] = useState('');
|
||||
const [lastCount, setLastCount] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [user, setUser] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const userData = localStorage.getItem('user');
|
||||
if (userData) {
|
||||
setUser(JSON.parse(userData));
|
||||
}
|
||||
|
||||
if (code && warehouse) {
|
||||
fetchData();
|
||||
}
|
||||
}, [code, warehouse]);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
// Fetch Product Name
|
||||
const nameRes = await fetch('/api/hesabfa', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ code, type: 'name' })
|
||||
});
|
||||
const nameData = await nameRes.json();
|
||||
setProductName(nameData?.Result?.Name || 'نامشخص');
|
||||
|
||||
// Fetch Product Quantity
|
||||
const qRes = await fetch('/api/hesabfa', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ code, type: 'quantity' })
|
||||
});
|
||||
const qData = await qRes.json();
|
||||
const productInfo = qData?.Result?.[0];
|
||||
const wInfo = productInfo?.Warehouse?.find(w => w.Code === Number(warehouse));
|
||||
setOldCount(wInfo?.Quantity ?? 0);
|
||||
|
||||
// Fetch Last Count
|
||||
const lastRes = await fetch(`/api/counting?product_id=${code}&warehouse=${warehouse}`);
|
||||
const lastData = await lastRes.json();
|
||||
if (lastData.message !== -1) {
|
||||
setLastCount(lastData);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (isConfirm) => {
|
||||
const finalCount = isConfirm ? oldCount : newCount;
|
||||
if (finalCount === '' || finalCount === null) {
|
||||
alert('لطفا تعداد را وارد کنید.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/counting', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
product_id: code,
|
||||
product_name: productName,
|
||||
warehouse,
|
||||
shelfCode: shelf.toUpperCase(),
|
||||
old_count: oldCount,
|
||||
new_count: finalCount,
|
||||
user_id: user?.id
|
||||
})
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
alert('شمارش با موفقیت ثبت شد!');
|
||||
router.push('/dashboard');
|
||||
} else {
|
||||
alert('خطا در ثبت شمارش');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full min-h-screen bg-gray-50 flex flex-col">
|
||||
<Header title="ثبت شمارش" showBack={true} />
|
||||
|
||||
<div className="p-4 flex flex-col gap-4">
|
||||
{loading ? (
|
||||
<div className="text-center p-10 animate-pulse">در حال دریافت اطلاعات...</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="bg-white p-4 rounded-lg shadow border border-gray-200">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs text-gray-500">کد: {code}</span>
|
||||
<span className="font-bold">{productName}</span>
|
||||
</div>
|
||||
<div className="bg-gray-100 p-2 rounded flex flex-col items-center">
|
||||
<span className="font-bold text-lg">{oldCount}</span>
|
||||
<span className="text-xs text-gray-500">موجودی سیستم</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-3 rounded-lg shadow text-sm border border-gray-200">
|
||||
<span className="font-bold">آخرین شمارش: </span>
|
||||
{lastCount ? (
|
||||
<span>{lastCount.new_count} عدد (توسط کاربر {lastCount.user_id})</span>
|
||||
) : (
|
||||
<span className="text-gray-500">ندارد</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col bg-white p-4 rounded-lg shadow gap-3">
|
||||
<label className="text-sm font-bold">اطلاعات فیزیکی</label>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
dir="ltr"
|
||||
placeholder="قفسه (مثال: C2F2)"
|
||||
value={shelf}
|
||||
onChange={(e) => setShelf(e.target.value)}
|
||||
className="w-full border rounded p-2 text-center uppercase"
|
||||
/>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
dir="ltr"
|
||||
placeholder="تعداد شمارش شده"
|
||||
value={newCount}
|
||||
onChange={(e) => setNewCount(e.target.value)}
|
||||
className="w-full border-2 border-purple-200 focus:border-purple-500 rounded p-4 text-center text-2xl font-bold"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 mt-2">
|
||||
<button
|
||||
onClick={() => handleSubmit(true)}
|
||||
className="flex-1 py-3 bg-green-500 hover:bg-green-600 text-white font-bold rounded shadow"
|
||||
>
|
||||
تایید موجودی سیستم
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => handleSubmit(false)}
|
||||
className="w-full py-4 bg-purple-600 hover:bg-purple-700 text-white font-bold rounded shadow mt-2"
|
||||
>
|
||||
ثبت مغایرت / موجودی جدید
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CountingPage() {
|
||||
return (
|
||||
<Suspense fallback={<div>در حال بارگذاری...</div>}>
|
||||
<CountingContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
'use client';
|
||||
import Header from '@/components/Header';
|
||||
import Link from 'next/link';
|
||||
import { motion } from 'framer-motion';
|
||||
import { History, ScanLine, ListChecks } from 'lucide-react';
|
||||
|
||||
export default function Dashboard() {
|
||||
|
||||
const container = {
|
||||
hidden: { opacity: 0 },
|
||||
show: {
|
||||
opacity: 1,
|
||||
transition: { staggerChildren: 0.1 }
|
||||
}
|
||||
};
|
||||
|
||||
const item = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
show: { opacity: 1, y: 0, transition: { type: 'spring', stiffness: 300, damping: 24 } }
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full min-h-screen bg-gray-50 flex flex-col">
|
||||
<Header title="داشبورد" />
|
||||
|
||||
<motion.div
|
||||
variants={container}
|
||||
initial="hidden"
|
||||
animate="show"
|
||||
className="p-5 flex flex-col gap-4"
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<motion.div variants={item}>
|
||||
<Link href="/history" className="bg-white/80 backdrop-blur-sm border border-gray-100 rounded-3xl shadow-[0_4px_20px_rgb(0,0,0,0.03)] p-6 flex flex-col items-center justify-center aspect-square gap-3 hover:bg-white hover:scale-[1.02] active:scale-95 transition-all">
|
||||
<div className="w-12 h-12 bg-blue-50 text-blue-500 rounded-2xl flex items-center justify-center mb-1">
|
||||
<History strokeWidth={1.5} size={24} />
|
||||
</div>
|
||||
<span className="font-extrabold text-xs text-gray-700">تاریخچه شمارش</span>
|
||||
</Link>
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={item}>
|
||||
<Link href="/scan" className="bg-white/80 backdrop-blur-sm border border-gray-100 rounded-3xl shadow-[0_4px_20px_rgb(0,0,0,0.03)] p-6 flex flex-col items-center justify-center aspect-square gap-3 hover:bg-white hover:scale-[1.02] active:scale-95 transition-all">
|
||||
<div className="w-12 h-12 bg-purple-50 text-purple-600 rounded-2xl flex items-center justify-center mb-1">
|
||||
<ScanLine strokeWidth={1.5} size={24} />
|
||||
</div>
|
||||
<span className="font-extrabold text-xs text-gray-700">اسکن کالا</span>
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<motion.div variants={item}>
|
||||
<Link href="/my-counts" className="bg-white/80 backdrop-blur-sm border border-gray-100 rounded-3xl shadow-[0_4px_20px_rgb(0,0,0,0.03)] p-5 flex items-center justify-between hover:bg-white hover:scale-[1.01] active:scale-[0.99] transition-all">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 bg-green-50 text-green-600 rounded-xl flex items-center justify-center">
|
||||
<ListChecks strokeWidth={1.5} size={20} />
|
||||
</div>
|
||||
<span className="font-extrabold text-sm text-gray-700">شمارشهای من</span>
|
||||
</div>
|
||||
<div className="w-8 h-8 bg-gray-50 rounded-full flex items-center justify-center">
|
||||
<svg className="w-4 h-4 text-gray-400 rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7" /></svg>
|
||||
</div>
|
||||
</Link>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@font-face {
|
||||
font-family: "IRANSans";
|
||||
src: url("/fonts/iran.woff2") format("woff2"),
|
||||
url("/fonts/iran.woff") format("woff");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@theme {
|
||||
--font-iran: "IRANSans", sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
direction: rtl;
|
||||
background-color: #f8fafc; /* Lighter modern gray */
|
||||
font-family: var(--font-iran);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for horizontal scroll areas but keep functionality */
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
'use client';
|
||||
import { useState, useEffect } from 'react';
|
||||
import Header from '@/components/Header';
|
||||
|
||||
export default function HistoryPage() {
|
||||
const [counts, setCounts] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
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();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="w-full min-h-screen bg-gray-50 flex flex-col pb-20">
|
||||
<Header title="تاریخچه کل شمارشها" showBack={true} />
|
||||
|
||||
<div className="p-4 flex flex-col gap-4">
|
||||
{loading ? (
|
||||
<div className="text-center p-10">در حال دریافت...</div>
|
||||
) : counts.length === 0 ? (
|
||||
<div className="text-center text-gray-500 p-10">تاریخچه خالی است.</div>
|
||||
) : (
|
||||
counts.map(count => (
|
||||
<div key={count.id} className="bg-white p-4 rounded shadow border border-gray-200">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold text-sm">{count.product_name}</span>
|
||||
<span className="text-xs text-gray-500 mt-1">شمارنده: {count.user?.name} | انبار: {count.warehouse}</span>
|
||||
<span className="text-xs text-gray-500 mt-1">قفسه: {count.shelf || 'ثبت نشده'}</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center bg-green-50 p-2 rounded border border-green-200">
|
||||
<span className="font-bold text-green-700">{count.new_count}</span>
|
||||
<span className="text-[10px] text-green-600">موجودی ثبت شده</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 text-[10px] text-gray-400 text-left">
|
||||
{new Date(count.createdAt).toLocaleString('fa-IR')}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import './globals.css'
|
||||
|
||||
export const metadata = {
|
||||
title: 'Pardis Counting',
|
||||
description: 'اپلیکیشن انبارگردانی پردیس',
|
||||
}
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html lang="fa" dir="rtl">
|
||||
<body>
|
||||
<main className="w-full min-h-screen flex flex-col justify-start items-center bg-gray-100">
|
||||
<div className="w-full max-w-md bg-white min-h-screen shadow-lg relative pb-16">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
'use client';
|
||||
import { useState, useEffect } from 'react';
|
||||
import Header from '@/components/Header';
|
||||
|
||||
export default function MyCountsPage() {
|
||||
const [counts, setCounts] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMyCounts = async () => {
|
||||
const userData = localStorage.getItem('user');
|
||||
if (userData) {
|
||||
const user = JSON.parse(userData);
|
||||
try {
|
||||
const res = await fetch(`/api/counting?user_id=${user.id}`);
|
||||
if (res.ok) setCounts(await res.json());
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
fetchMyCounts();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="w-full min-h-screen bg-gray-50 flex flex-col pb-20">
|
||||
<Header title="شمارش های من" showBack={true} />
|
||||
|
||||
<div className="p-4 flex flex-col gap-4">
|
||||
{loading ? (
|
||||
<div className="text-center p-10">در حال دریافت...</div>
|
||||
) : counts.length === 0 ? (
|
||||
<div className="text-center text-gray-500 p-10">هنوز شمارشی ثبت نکردهاید.</div>
|
||||
) : (
|
||||
counts.map(count => (
|
||||
<div key={count.id} className="bg-white p-4 rounded shadow border border-gray-200">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold text-sm">{count.product_name}</span>
|
||||
<span className="text-xs text-gray-500 mt-1">کد: {count.product_id} | انبار: {count.warehouse}</span>
|
||||
<span className="text-xs text-gray-500 mt-1">قفسه: {count.shelf || 'ثبت نشده'}</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center bg-gray-100 p-2 rounded">
|
||||
<span className="font-bold">{count.new_count}</span>
|
||||
<span className="text-[10px]">شمارش شما</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 text-[10px] text-gray-400 text-left">
|
||||
{new Date(count.createdAt).toLocaleString('fa-IR')}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
'use client';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export default function LoginPage() {
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
router.push('/dashboard');
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
const handleLogin = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
localStorage.setItem('token', data.token);
|
||||
localStorage.setItem('user', JSON.stringify(data.user));
|
||||
router.push('/dashboard');
|
||||
} else {
|
||||
setError(data.error);
|
||||
}
|
||||
} catch (err) {
|
||||
setError('خطا در برقراری ارتباط با سرور');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-screen flex flex-col justify-center items-center bg-gradient-to-br from-gray-50 to-gray-100 p-4 relative overflow-hidden">
|
||||
|
||||
{/* Decorative Blur Backgrounds */}
|
||||
<div className="absolute top-[-10%] left-[-10%] w-64 h-64 bg-purple-400/20 rounded-full blur-3xl"></div>
|
||||
<div className="absolute bottom-[-10%] right-[-10%] w-64 h-64 bg-blue-400/20 rounded-full blur-3xl"></div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
|
||||
className="w-full max-w-sm p-8 bg-white/60 backdrop-blur-2xl rounded-3xl shadow-[0_8px_30px_rgb(0,0,0,0.04)] border border-white/50 z-10"
|
||||
>
|
||||
<div className="flex flex-col items-center mb-8">
|
||||
<div className="w-16 h-16 bg-gradient-to-tr from-purple-600 to-blue-500 rounded-2xl shadow-lg shadow-purple-500/30 flex items-center justify-center mb-4 text-white font-extrabold text-2xl">
|
||||
P
|
||||
</div>
|
||||
<h1 className="text-xl font-extrabold text-gray-800 tracking-tight">پردیس رایانه</h1>
|
||||
<p className="text-xs text-gray-500 mt-1 font-medium">اپلیکیشن مدیریت انبار</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<motion.div initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} className="mb-6 p-3 bg-red-50/80 border border-red-100 text-red-600 text-xs rounded-xl text-center">
|
||||
{error}
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleLogin} className="flex flex-col gap-5">
|
||||
<div className="flex flex-col">
|
||||
<label className="text-xs font-bold text-gray-600 mb-1.5 ml-1">شماره موبایل</label>
|
||||
<input
|
||||
type="text"
|
||||
dir="ltr"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="px-4 py-3 bg-white/50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500 focus:bg-white transition-all text-sm"
|
||||
placeholder="09xxxxxxxxx"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<label className="text-xs font-bold text-gray-600 mb-1.5 ml-1">رمز عبور</label>
|
||||
<input
|
||||
type="password"
|
||||
dir="ltr"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="px-4 py-3 bg-white/50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500 focus:bg-white transition-all text-sm"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.01 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full py-3.5 bg-gray-900 hover:bg-black text-white text-sm font-bold rounded-xl transition-colors mt-2 shadow-lg shadow-gray-900/20 disabled:opacity-70"
|
||||
>
|
||||
{loading ? 'در حال ورود...' : 'ورود به سیستم'}
|
||||
</motion.button>
|
||||
</form>
|
||||
<p className="text-[10px] text-center text-gray-400 mt-6 font-medium">رمز عبور پیشفرض: 123456</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
'use client';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Header from '@/components/Header';
|
||||
import dynamic from 'next/dynamic';
|
||||
const Scanner = dynamic(() => import('@yudiel/react-qr-scanner').then(mod => mod.Scanner), { ssr: false });
|
||||
|
||||
export default function ScanPage() {
|
||||
const [code, setCode] = useState('');
|
||||
const [warehouse, setWarehouse] = useState('11');
|
||||
const [cameraEnabled, setCameraEnabled] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const handleGoToCounting = () => {
|
||||
if (code) {
|
||||
router.push(`/counting?code=${code}&warehouse=${warehouse}`);
|
||||
}
|
||||
};
|
||||
|
||||
const [camError, setCamError] = useState('');
|
||||
|
||||
const handleScan = (detectedCodes) => {
|
||||
if (detectedCodes && detectedCodes.length > 0) {
|
||||
const scannedValue = detectedCodes[0].rawValue;
|
||||
setCode(scannedValue);
|
||||
setCameraEnabled(false);
|
||||
setTimeout(() => {
|
||||
router.push(`/counting?code=${scannedValue}&warehouse=${warehouse}`);
|
||||
}, 1500);
|
||||
}
|
||||
};
|
||||
|
||||
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('هیچ دوربینی روی این دستگاه یافت نشد. لطفاً از لپتاپ یا موبایل استفاده کنید.');
|
||||
} else {
|
||||
setCamError(msg || 'خطا در دسترسی به دوربین. آیا از HTTPS یا localhost استفاده میکنید؟');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full min-h-screen bg-gray-50 flex flex-col pb-20">
|
||||
<Header title="اسکن کد کالا" showBack={true} />
|
||||
|
||||
<div className="p-4 flex flex-col gap-6 items-center mt-4">
|
||||
|
||||
<div className="w-full max-w-sm aspect-video border-2 border-dashed border-gray-300 rounded-lg overflow-hidden flex flex-col items-center justify-center bg-gray-100 relative">
|
||||
{cameraEnabled ? (
|
||||
<div className="w-full h-full relative">
|
||||
{camError ? (
|
||||
<div className="text-red-500 text-xs p-4 text-center">{camError}</div>
|
||||
) : (
|
||||
<Scanner
|
||||
onScan={handleScan}
|
||||
onError={handleError}
|
||||
formats={['qr_code', 'code_128', 'ean_13']}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
onClick={() => { setCameraEnabled(false); setCamError(''); }}
|
||||
className="absolute bottom-2 right-2 bg-red-500 text-white text-xs px-2 py-1 rounded z-10"
|
||||
>
|
||||
بستن دوربین
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setCameraEnabled(true)}
|
||||
className="text-blue-500 font-bold px-4 py-2"
|
||||
>
|
||||
فعال کردن دوربین برای اسکن
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-500">کد کالا را اسکن یا وارد کنید</p>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
dir="ltr"
|
||||
value={code}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
|
||||
<select
|
||||
value={warehouse}
|
||||
onChange={(e) => setWarehouse(e.target.value)}
|
||||
className="w-full max-w-xs text-center text-lg p-3 border rounded-lg focus:ring-2 focus:ring-purple-500 outline-none"
|
||||
>
|
||||
<option value="11">مرکزی</option>
|
||||
<option value="13">انبار فروشگاه</option>
|
||||
<option value="14">انبار کارگاه شارژ</option>
|
||||
<option value="15">انبار کارگاه تعمیرات</option>
|
||||
</select>
|
||||
|
||||
<button
|
||||
onClick={handleGoToCounting}
|
||||
disabled={!code}
|
||||
className="w-full max-w-xs py-4 bg-green-500 hover:bg-green-600 disabled:bg-gray-300 text-white font-bold text-xl rounded-lg transition-colors"
|
||||
>
|
||||
شمارش
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
'use client';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { motion } from 'framer-motion';
|
||||
import { LogOut, ChevronRight, Wifi, WifiOff } from 'lucide-react';
|
||||
|
||||
export default function Header({ title = 'داشبورد', showBack = false }) {
|
||||
const [user, setUser] = useState(null);
|
||||
const [isOnline, setIsOnline] = useState(true);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const userData = localStorage.getItem('user');
|
||||
if (userData) {
|
||||
setUser(JSON.parse(userData));
|
||||
} else {
|
||||
router.push('/');
|
||||
}
|
||||
|
||||
setIsOnline(navigator.onLine);
|
||||
|
||||
const handleOnline = () => setIsOnline(true);
|
||||
const handleOffline = () => setIsOnline(false);
|
||||
|
||||
window.addEventListener('online', handleOnline);
|
||||
window.addEventListener('offline', handleOffline);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('online', handleOnline);
|
||||
window.removeEventListener('offline', handleOffline);
|
||||
};
|
||||
}, [router]);
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
router.push('/');
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.header
|
||||
initial={{ y: -50, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ duration: 0.4, ease: [0.22, 1, 0.36, 1] }}
|
||||
className="sticky top-0 z-50 w-full h-16 bg-white/70 backdrop-blur-xl border-b border-gray-200/50 flex items-center justify-between px-4 text-gray-800"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{showBack ? (
|
||||
<button onClick={() => router.back()} className="p-2 hover:bg-gray-100 rounded-full transition-colors">
|
||||
<ChevronRight size={20} className="text-gray-600" />
|
||||
</button>
|
||||
) : (
|
||||
<button onClick={handleLogout} className="p-2 hover:bg-red-50 text-red-500 rounded-full transition-colors flex items-center justify-center">
|
||||
<LogOut size={18} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="font-extrabold text-sm tracking-tight text-gray-800">
|
||||
{title}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col items-end">
|
||||
<span className="text-xs font-bold text-gray-700">{user?.name || 'کاربر'}</span>
|
||||
{user?.role === 'ADMIN' && <Link href="/admin/locations" className="text-[10px] text-purple-600 font-bold hover:underline">مدیریت قفسهها</Link>}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 bg-gray-50/50 p-1.5 rounded-full border border-gray-100">
|
||||
{isOnline ? (
|
||||
<Wifi size={14} className="text-green-500" />
|
||||
) : (
|
||||
<WifiOff size={14} className="text-red-500" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
export function signToken(payload) {
|
||||
return jwt.sign(payload, process.env.JWT_SECRET || 'secret', { expiresIn: '1d' });
|
||||
}
|
||||
|
||||
export function verifyToken(token) {
|
||||
try {
|
||||
return jwt.verify(token, process.env.JWT_SECRET || 'secret');
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const prismaClientSingleton = () => {
|
||||
return new PrismaClient()
|
||||
}
|
||||
|
||||
const globalForPrisma = globalThis
|
||||
|
||||
const prisma = globalForPrisma.prisma ?? prismaClientSingleton()
|
||||
|
||||
export default prisma
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
|
||||
Reference in New Issue
Block a user