+
+
تنظیمات اصلی
+
مدیریت قوانین انبارگردانی و دسترسیها
+
+
+
+
+ {/* Blind Counting Setting */}
+
+
+
+
+
+
+
شمارش کور (Blind Counting)
+
+
+ در صورت فعال بودن، انبارگردانها موجودی فعلی سیستم را نمیبینند و مجبورند به جای تایید کورکورانه، کالاها را به صورت واقعی بشمارند.
+
+
+
+ {/* Elegant Switch Button */}
+
+
+
+ {/* Correction Roles Setting */}
-
-
+
+
-
شمارش کور (Blind Counting)
+
دسترسی ثبت اصلاحیه
-
- در صورت فعال بودن، انبارگردانها موجودی فعلی سیستم را نمیبینند و مجبورند به جای تایید کورکورانه، کالاها را به صورت واقعی بشمارند.
+
+ وقتی انبارگردانی یک قفسه بسته میشود، چه نقشهایی اجازه دارند درخواست اصلاحیه ثبت کنند؟
-
-
-
-
- {/* Correction Roles Setting */}
-
-
-
-
-
-
دسترسی ثبت اصلاحیه
-
-
- وقتی انبارگردانی یک قفسه بسته میشود، چه نقشهایی اجازه دارند درخواست اصلاحیه ثبت کنند؟ (حالت چندانتخابی)
-
-
-
- {availableRoles.map(role => {
- const isSelected = settings.correction_roles?.includes(role.id);
- return (
-
- );
- })}
-
-
-
- {/* Warehouses Setting */}
-
-
-
- WH
-
-
مدیریت انبارهای حسابفا
-
-
- لیست انبارهایی که انبارداران مجاز به انبارگردانی آنها هستند. کد انبار باید دقیقاً با کد حسابفا مطابقت داشته باشد.
-
-
-
- {settings.warehouses?.map((wh) => (
-
-
-
کد: {wh.id}
-
{wh.name}
-
-
-
- ))}
-
-
-
-
+
+ {availableRoles.map(role => {
+ const isSelected = settings.correction_roles?.includes(role.id);
+ return (
+
toggleRole(role.id)}
+ className={`flex items-center justify-between p-3.5 rounded-[16px] border text-xs font-bold transition-all ${
+ isSelected
+ ? 'border-indigo-500 bg-indigo-50 text-indigo-700 shadow-sm'
+ : 'border-gray-200 bg-white text-gray-600 hover:border-gray-300 hover:bg-gray-50'
+ }`}
+ >
+ {role.label}
+
+
+
+
+ );
+ })}
-
-
-
{saving ? (
- ) : saved ? (
- <>
-
- ذخیره شد
- >
) : (
<>
-
+
ذخیره تغییرات
>
)}
+
+ {toast.show && (
+
+ {toast.isError ? : }
+ {toast.message}
+
+ )}
+
);
}
diff --git a/src/app/admin/users/page.js b/src/app/admin/users/page.js
new file mode 100644
index 0000000..25184dc
--- /dev/null
+++ b/src/app/admin/users/page.js
@@ -0,0 +1,205 @@
+'use client';
+import { useState, useEffect } from 'react';
+import Header from '@/components/Header';
+import { motion, AnimatePresence } from 'framer-motion';
+import { Users, Edit2, Check, Shield, User as UserIcon, X } from 'lucide-react';
+
+export default function UsersPage() {
+ const [users, setUsers] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [editingUser, setEditingUser] = useState(null);
+ const [saving, setSaving] = useState(false);
+
+ const availableRoles = [
+ { id: 'ADMIN', label: 'مدیر کل' },
+ { id: 'SUPERVISOR', label: 'سرپرست انبار' },
+ { id: 'ACCOUNTANT', label: 'حسابدار' },
+ { id: 'COUNTER', label: 'انبارگردان' }
+ ];
+
+ useEffect(() => {
+ fetchUsers();
+ }, []);
+
+ const fetchUsers = async () => {
+ try {
+ const res = await fetch('/api/admin/users');
+ if (res.ok) {
+ setUsers(await res.json());
+ }
+ } catch (e) {
+ console.error(e);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleSaveRoles = async () => {
+ if (!editingUser) return;
+ setSaving(true);
+ try {
+ const res = await fetch(`/api/admin/users/${editingUser.id}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ roles: editingUser.roles })
+ });
+ if (res.ok) {
+ fetchUsers();
+ setEditingUser(null);
+ }
+ } catch (e) {
+ console.error(e);
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ const toggleRole = (roleId) => {
+ setEditingUser(prev => {
+ const roles = prev.roles || [];
+ if (roles.includes(roleId)) {
+ return { ...prev, roles: roles.filter(r => r !== roleId) };
+ } else {
+ return { ...prev, roles: [...roles, roleId] };
+ }
+ });
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
لیست کاربران سیستم
+
مدیریت دسترسیها و بررسی فعالیت کارکنان
+
+
+
+ {users.map(user => {
+ const userRoles = user.roles || [];
+ return (
+
+
+
+
+
+
+
+ {user.name || user.username}
+ {user.username}
+
+
+
+
+
+
+
+ {userRoles.map(r => {
+ const label = availableRoles.find(ar => ar.id === r)?.label || r;
+ return {label};
+ })}
+ {userRoles.length === 0 && بدون نقش}
+
+
+ {user._count?.countings || 0} شمارش
+
+
+
+ );
+ })}
+
+
+
+ {/* Edit Modal */}
+
+ {editingUser && (
+
+
setEditingUser(null)}
+ className="absolute inset-0 bg-gray-900/40 backdrop-blur-sm"
+ />
+
+
+
+
+ ویرایش دسترسیها
+
+
+
+
+
+
+ {editingUser.name || editingUser.username}
+
+
+
+ {availableRoles.map(role => {
+ const isSelected = editingUser.roles?.includes(role.id);
+ return (
+
toggleRole(role.id)}
+ className={`flex items-center justify-between p-3 rounded-[16px] border text-xs font-bold transition-all ${
+ isSelected
+ ? 'border-indigo-500 bg-indigo-50 text-indigo-700'
+ : 'border-gray-200 bg-white text-gray-600'
+ }`}
+ >
+ {role.label}
+
+
+
+
+ );
+ })}
+
+
+
+ {saving ? : 'ذخیره دسترسیها'}
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/src/app/admin/warehouses/page.js b/src/app/admin/warehouses/page.js
new file mode 100644
index 0000000..2a81569
--- /dev/null
+++ b/src/app/admin/warehouses/page.js
@@ -0,0 +1,210 @@
+'use client';
+import { useState, useEffect } from 'react';
+import Header from '@/components/Header';
+import { motion, AnimatePresence } from 'framer-motion';
+import { Plus, Trash2, Building2, AlertCircle, XCircle, Check } from 'lucide-react';
+
+export default function WarehousesPage() {
+ const [warehouses, setWarehouses] = useState([]);
+ const [newCode, setNewCode] = useState('');
+ const [newName, setNewName] = useState('');
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ 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(() => {
+ fetchSettings();
+ }, []);
+
+ const fetchSettings = async () => {
+ try {
+ const res = await fetch('/api/settings');
+ if (res.ok) {
+ const data = await res.json();
+ setWarehouses(data.warehouses || []);
+ }
+ } catch (error) {
+ console.error(error);
+ showToast('خطا در دریافت لیست انبارها', true);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const saveWarehouses = async (newWarehouses) => {
+ setSaving(true);
+ try {
+ const res = await fetch('/api/settings', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ warehouses: newWarehouses })
+ });
+ if (res.ok) {
+ setWarehouses(newWarehouses);
+ showToast('انبارها با موفقیت ذخیره شدند');
+ } else {
+ showToast('خطا در ذخیره انبار', true);
+ }
+ } catch (error) {
+ console.error(error);
+ showToast('خطای شبکه', true);
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ const handleAdd = () => {
+ if (!newCode || !newName) {
+ showToast('لطفاً کد و نام انبار را وارد کنید', true);
+ return;
+ }
+
+ // Convert to number strictly if Hesabfa requires int, but string is safer and compatible
+ const codeId = parseInt(newCode);
+ if(isNaN(codeId)) {
+ showToast('کد انبار باید عدد باشد', true);
+ return;
+ }
+
+ if (warehouses.find(w => parseInt(w.id) === codeId)) {
+ showToast('انباری با این کد قبلاً ثبت شده است', true);
+ return;
+ }
+
+ const updated = [...warehouses, { id: codeId.toString(), name: newName }];
+ saveWarehouses(updated);
+ setNewCode('');
+ setNewName('');
+ };
+
+ const handleDelete = (id) => {
+ if (!confirm('آیا از حذف این انبار اطمینان دارید؟')) return;
+ const updated = warehouses.filter(w => w.id !== id);
+ saveWarehouses(updated);
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
انبارهای حسابفا
+
مدیریت لیست انبارهایی که سیستم استعلام موجودی میگیرد
+
+
+
+
+
+
+
+
+
+ ثبت انبار جدید
+
+
+
+
+
+ setNewCode(e.target.value)}
+ placeholder="مثال: 11"
+ className="w-full bg-white border border-gray-200 rounded-[12px] px-4 py-3 text-sm focus:outline-none focus:border-orange-500 transition-colors"
+ />
+
+
+
+ setNewName(e.target.value)}
+ placeholder="مثال: انبار مرکزی"
+ className="w-full bg-white border border-gray-200 rounded-[12px] px-4 py-3 text-sm focus:outline-none focus:border-orange-500 transition-colors"
+ />
+
+
+ {saving ? : }
+ افزودن به لیست
+
+
+
+
+
+
لیست انبارهای فعال
+
+ {warehouses.length === 0 ? (
+
+
هیچ انباری ثبت نشده است.
+
+ ) : (
+
+
+ {warehouses.map((wh) => (
+
+
+
+ {wh.id}
+
+
{wh.name}
+
+
+
+ ))}
+
+
+ )}
+
+
+
+
+
+
+ {toast.show && (
+
+ {toast.isError ? : }
+ {toast.message}
+
+ )}
+
+
+ );
+}
diff --git a/src/app/api/admin/users/[id]/route.js b/src/app/api/admin/users/[id]/route.js
new file mode 100644
index 0000000..9df947e
--- /dev/null
+++ b/src/app/api/admin/users/[id]/route.js
@@ -0,0 +1,18 @@
+import prisma from '@/lib/prisma';
+import { NextResponse } from 'next/server';
+
+export async function PUT(req, { params }) {
+ try {
+ const { id: idParam } = await params;
+ const id = parseInt(idParam, 10);
+ const { roles } = await req.json();
+
+ const updated = await prisma.user.update({
+ where: { id },
+ data: { roles }
+ });
+ return NextResponse.json({ success: true, user: updated });
+ } catch (error) {
+ return NextResponse.json({ error: 'خطا در ویرایش کاربر' }, { status: 500 });
+ }
+}
diff --git a/src/app/api/admin/users/route.js b/src/app/api/admin/users/route.js
new file mode 100644
index 0000000..8947473
--- /dev/null
+++ b/src/app/api/admin/users/route.js
@@ -0,0 +1,18 @@
+import prisma from '@/lib/prisma';
+import { NextResponse } from 'next/server';
+
+export async function GET() {
+ try {
+ const users = await prisma.user.findMany({
+ include: {
+ _count: {
+ select: { countings: true }
+ }
+ },
+ orderBy: { createdAt: 'desc' }
+ });
+ return NextResponse.json(users);
+ } catch (error) {
+ return NextResponse.json({ error: 'خطا در دریافت کاربران' }, { status: 500 });
+ }
+}
diff --git a/src/app/api/hesabfa/route.js b/src/app/api/hesabfa/route.js
index 0840f6f..469125e 100644
--- a/src/app/api/hesabfa/route.js
+++ b/src/app/api/hesabfa/route.js
@@ -26,6 +26,16 @@ export async function POST(req) {
return NextResponse.json(res.data);
}
+ if (type === 'all') {
+ const res = await axios.post('https://api.hesabfa.com/v1/item/getitems', {
+ apiKey: HESABFA_API_KEY,
+ loginToken: HESABFA_TOKEN,
+ queryInfo: { Take: 2000, Skip: 0 },
+ type: 0
+ });
+ return NextResponse.json(res.data);
+ }
+
return NextResponse.json({ error: 'نوع درخواست نامعتبر است' }, { status: 400 });
} catch (error) {
return NextResponse.json({ error: 'خطا در ارتباط با حسابفا' }, { status: 500 });
diff --git a/src/app/api/locations/[id]/route.js b/src/app/api/locations/[id]/route.js
index 3d11fa0..b801fba 100644
--- a/src/app/api/locations/[id]/route.js
+++ b/src/app/api/locations/[id]/route.js
@@ -26,7 +26,7 @@ export async function PUT(req, { params }) {
try {
const { id: idParam } = await params;
const id = parseInt(idParam, 10);
- const { code } = await req.json();
+ const { code, warehouse } = await req.json();
const location = await prisma.location.findUnique({
where: { id },
@@ -45,10 +45,11 @@ export async function PUT(req, { params }) {
const [, floor, regionStr, sector, rowStr] = match;
const region = parseInt(regionStr, 10);
const row = parseInt(rowStr, 10);
+ const warehouseId = warehouse ? parseInt(warehouse, 10) : null;
const updated = await prisma.location.update({
where: { id },
- data: { code: code.toUpperCase(), floor, region, sector, row }
+ data: { code: code.toUpperCase(), floor, region, sector, row, warehouse: warehouseId }
});
return NextResponse.json({ success: true, location: updated });
diff --git a/src/app/api/locations/route.js b/src/app/api/locations/route.js
index 6cf0cfa..b5163b6 100644
--- a/src/app/api/locations/route.js
+++ b/src/app/api/locations/route.js
@@ -15,7 +15,7 @@ export async function GET() {
export async function POST(req) {
try {
- const { code } = await req.json(); // "C2F2"
+ const { code, warehouse } = await req.json(); // "C2F2", 11
// Pattern validation (e.g., C2F2 -> 1 Char, 1 Num, 1 Char, 1 Num)
const regex = /^([A-Za-z]+)(\d+)([A-Za-z]+)(\d+)$/;
@@ -28,16 +28,18 @@ export async function POST(req) {
const [, floor, regionStr, sector, rowStr] = match;
const region = parseInt(regionStr, 10);
const row = parseInt(rowStr, 10);
+ const warehouseId = warehouse ? parseInt(warehouse, 10) : null;
const location = await prisma.location.upsert({
where: { code: code.toUpperCase() },
- update: {}, // if exists, do nothing or update timestamps
+ update: { warehouse: warehouseId }, // if exists, do nothing or update timestamps
create: {
code: code.toUpperCase(),
floor,
region,
sector,
- row
+ row,
+ warehouse: warehouseId
}
});