Files
approagency-admin-panel/app/admin/products/page.tsx
T

248 lines
10 KiB
TypeScript

'use client';
import { useState, useEffect, useCallback, useRef } from 'react';
import { useAuth } from '@/contexts/AuthContext';
import { packagesApi } from '@/lib/api/packages';
import { productsApi } from '@/lib/api/products';
import ProductList from '@/components/admin/products/ProductList';
import ProductForm from '@/components/admin/products/ProductForm';
import PageHeader from '@/components/admin/PageHeader';
import ConfirmDialog from '@/components/admin/ConfirmDialog';
import { showToast } from '@/components/Toast';
import { PackageName } from '@/types/package';
import { Product, CreateProductData, UpdateProductData } from '@/types/product';
import { PlusIcon } from '@heroicons/react/24/outline';
export default function ProductsPage() {
const { token } = useAuth();
const [packages, setPackages] = useState<PackageName[]>([]);
const [selectedPackage, setSelectedPackage] = useState<PackageName | null>(null);
const [products, setProducts] = useState<Product[]>([]);
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [isLoadingPackages, setIsLoadingPackages] = useState(false);
const [isFormVisible, setIsFormVisible] = useState(false);
const [error, setError] = useState<string | null>(null);
const [deleteTarget, setDeleteTarget] = useState<Product | null>(null);
const initialLoadRef = useRef(false);
const prevPackageRef = useRef<string | null>(null);
useEffect(() => {
if (token && !initialLoadRef.current) {
initialLoadRef.current = true;
loadPackages();
}
}, [token]);
const loadPackages = async () => {
setIsLoadingPackages(true);
setError(null);
try {
const data = await packagesApi.getAllPackages(token!);
setPackages(data);
if (data.length > 0 && !selectedPackage) {
setSelectedPackage(data[0]);
}
} catch (err) {
setError('خطا در دریافت لیست پکیج‌ها');
} finally {
setIsLoadingPackages(false);
}
};
const loadProducts = useCallback(async (packageName: string, force = false) => {
if (!force && prevPackageRef.current === packageName) {
return;
}
setIsLoading(true);
setError(null);
prevPackageRef.current = packageName;
try {
const data = await productsApi.getProducts(packageName, token!);
setProducts(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'خطا در دریافت محصولات');
} finally {
setIsLoading(false);
}
}, [token]);
useEffect(() => {
if (selectedPackage?.name && token) {
loadProducts(selectedPackage.name);
} else {
setProducts([]);
}
}, [selectedPackage?.name, token, loadProducts]);
const handlePackageChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const pkg = packages.find(p => p.name === e.target.value);
setSelectedPackage(pkg || null);
setIsFormVisible(false);
setSelectedProduct(null);
setError(null);
};
const handleCreate = () => {
setSelectedProduct(null);
setIsFormVisible(true);
};
const handleEdit = (product: Product) => {
setSelectedProduct(product);
setIsFormVisible(true);
};
const handleDelete = (product: Product) => {
setDeleteTarget(product);
};
const confirmDelete = async () => {
if (!selectedPackage || !deleteTarget) return;
const product = deleteTarget;
setIsLoading(true);
setError(null);
try {
await productsApi.deleteProduct(selectedPackage.name!, product.id, token!);
showToast('success', 'محصول با موفقیت حذف شد');
if (selectedPackage.name) {
prevPackageRef.current = null;
await loadProducts(selectedPackage.name, true);
}
} catch (err) {
setError(err instanceof Error ? err.message : 'خطا در حذف محصول');
} finally {
setIsLoading(false);
setDeleteTarget(null);
}
};
const handleSubmit = async (data: CreateProductData | UpdateProductData) => {
if (!selectedPackage) return;
setIsLoading(true);
setError(null);
try {
if (selectedProduct) {
await productsApi.updateProduct(selectedPackage.name!, selectedProduct.id, data, token!);
showToast('success', 'محصول با موفقیت به‌روزرسانی شد');
} else {
await productsApi.createProduct(selectedPackage.name!, data as CreateProductData, token!);
showToast('success', 'محصول با موفقیت ایجاد شد');
}
if (selectedPackage.name) {
prevPackageRef.current = null;
await loadProducts(selectedPackage.name, true);
}
setIsFormVisible(false);
setSelectedProduct(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'خطا در ذخیره محصول');
} finally {
setIsLoading(false);
}
};
const handleCancel = () => {
setIsFormVisible(false);
setSelectedProduct(null);
setError(null);
};
return (
<div className="p-4 sm:p-8">
<div className="max-w-7xl mx-auto">
<PageHeader title="مدیریت محصولات" />
{/* Package Selector */}
<div className="bg-white dark:bg-gray-900 p-4 rounded-2xl ring-1 ring-gray-200 dark:ring-gray-800 shadow-sm mb-6">
<label htmlFor="package" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
انتخاب پکیج
</label>
<select
id="package"
value={selectedPackage?.name || ''}
onChange={handlePackageChange}
disabled={isLoadingPackages}
className="block w-full px-3 py-2.5 text-gray-900 dark:text-gray-100 border border-gray-200 dark:border-gray-700 rounded-xl shadow-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent sm:text-sm disabled:opacity-50 dark:bg-gray-800/80 transition-colors"
>
{isLoadingPackages ? (
<option>در حال بارگذاری پکیج‌ها...</option>
) : (
packages.map((pkg) => (
<option key={pkg.id} value={pkg.name || ''}>
{pkg.title} ({pkg.name})
</option>
))
)}
</select>
</div>
{error && (
<div className="mb-4 rounded-xl bg-red-50 dark:bg-red-950/40 ring-1 ring-red-200 dark:ring-red-800/60 p-4 flex items-center gap-3">
<p className="text-sm text-red-700 dark:text-red-300">{error}</p>
<button onClick={() => setError(null)} className="mr-auto text-red-400 hover:text-red-600">
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
</div>
)}
{selectedPackage && (
<>
{!isFormVisible && (
<div className="mb-4 flex justify-end">
<button
onClick={handleCreate}
disabled={isLoading}
className="inline-flex items-center gap-2 px-4 py-2.5 bg-indigo-600 text-white rounded-xl text-sm font-medium hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-gray-900 focus:ring-indigo-500 disabled:opacity-50 transition-colors shadow-sm shadow-indigo-500/20"
>
<PlusIcon className="h-4 w-4" />
محصول جدید
</button>
</div>
)}
{isFormVisible ? (
<ProductForm
product={selectedProduct}
packageName={selectedPackage.name!}
onSubmit={handleSubmit}
onCancel={handleCancel}
isLoading={isLoading}
/>
) : (
<ProductList
products={products}
packageName={selectedPackage.name!}
onEdit={handleEdit}
onDelete={handleDelete}
isLoading={isLoading}
/>
)}
</>
)}
<ConfirmDialog
open={!!deleteTarget}
title="حذف محصول"
message={`آیا از حذف محصول «${deleteTarget?.title || ''}» اطمینان دارید؟`}
confirmLabel="حذف"
isLoading={isLoading}
onConfirm={confirmDelete}
onCancel={() => setDeleteTarget(null)}
/>
</div>
</div>
);
}