fix: resolve variable declaration order, modal state and lint errors
All checks were successful
Deploy Canina / deploy (push) Successful in 41s
All checks were successful
Deploy Canina / deploy (push) Successful in 41s
This commit is contained in:
parent
fc324aacaf
commit
b2b80849ea
@ -157,12 +157,20 @@ export class AdminController {
|
||||
return { success: true, ...result };
|
||||
}
|
||||
|
||||
@Get('orders/:id')
|
||||
@ApiOperation({ summary: 'دریافت جزئیات کامل یک سفارش با شناسه' })
|
||||
async getOrderById(@Param('id') id: string) {
|
||||
const order = await this.adminService.getOrderById(id);
|
||||
return { success: true, data: order };
|
||||
}
|
||||
|
||||
@Put('orders/:id/status')
|
||||
@ApiOperation({ summary: 'بروزرسانی وضعیت سفارش' })
|
||||
async updateOrderStatus(
|
||||
@Param('id') id: string,
|
||||
@Body('status') status: string,
|
||||
@Body('trackingCode') trackingCode?: string,
|
||||
|
||||
) {
|
||||
const order = await this.adminService.updateOrderStatus(
|
||||
id,
|
||||
|
||||
@ -596,8 +596,31 @@ export class AdminService {
|
||||
};
|
||||
}
|
||||
|
||||
async getOrderById(id: string) {
|
||||
const order = await this.prisma.order.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
user: true,
|
||||
orderItems: {
|
||||
include: {
|
||||
product: true,
|
||||
},
|
||||
},
|
||||
coupon: true,
|
||||
paymentTransactions: {
|
||||
orderBy: { createdAt: 'desc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!order) {
|
||||
throw new NotFoundException(`سفارش با شناسه ${id} یافت نشد`);
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
async updateOrderStatus(id: string, status: string, trackingNumber?: string) {
|
||||
const dataToUpdate: Prisma.OrderUpdateInput = { status };
|
||||
|
||||
if (trackingNumber !== undefined) {
|
||||
dataToUpdate.trackingNumber = trackingNumber;
|
||||
}
|
||||
|
||||
@ -65,6 +65,35 @@ export default function Blogs() {
|
||||
imageUrl: ''
|
||||
});
|
||||
|
||||
const openModal = (blog: BlogPost | null = null) => {
|
||||
if (blog) {
|
||||
setEditingBlog(blog);
|
||||
updateUrlParams({ modal: 'edit', blogId: blog.id });
|
||||
setFormData({
|
||||
title: blog.title,
|
||||
slug: blog.slug,
|
||||
content: blog.content,
|
||||
isPublished: blog.isPublished,
|
||||
metaTitle: blog.metaTitle || '',
|
||||
metaDescription: blog.metaDescription || '',
|
||||
keywords: blog.keywords || '',
|
||||
imageUrl: blog.imageUrl || ''
|
||||
});
|
||||
} else {
|
||||
setEditingBlog(null);
|
||||
updateUrlParams({ modal: 'create', blogId: undefined });
|
||||
setFormData({
|
||||
title: '', slug: '', content: '', isPublished: true, metaTitle: '', metaDescription: '', keywords: '', imageUrl: ''
|
||||
});
|
||||
}
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setIsModalOpen(false);
|
||||
updateUrlParams({ modal: undefined, blogId: undefined });
|
||||
};
|
||||
|
||||
const fetchBlogs = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
@ -129,34 +158,6 @@ export default function Blogs() {
|
||||
}
|
||||
};
|
||||
|
||||
const openModal = (blog: BlogPost | null = null) => {
|
||||
if (blog) {
|
||||
setEditingBlog(blog);
|
||||
updateUrlParams({ modal: 'edit', blogId: blog.id });
|
||||
setFormData({
|
||||
title: blog.title,
|
||||
slug: blog.slug,
|
||||
content: blog.content,
|
||||
isPublished: blog.isPublished,
|
||||
metaTitle: blog.metaTitle || '',
|
||||
metaDescription: blog.metaDescription || '',
|
||||
keywords: blog.keywords || '',
|
||||
imageUrl: blog.imageUrl || ''
|
||||
});
|
||||
} else {
|
||||
setEditingBlog(null);
|
||||
updateUrlParams({ modal: 'create', blogId: undefined });
|
||||
setFormData({
|
||||
title: '', slug: '', content: '', isPublished: true, metaTitle: '', metaDescription: '', keywords: '', imageUrl: ''
|
||||
});
|
||||
}
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setIsModalOpen(false);
|
||||
updateUrlParams({ modal: undefined, blogId: undefined });
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
|
||||
@ -59,6 +59,34 @@ export default function Categories() {
|
||||
imageUrl: ''
|
||||
});
|
||||
|
||||
const openModal = (category: Category | null = null) => {
|
||||
if (category) {
|
||||
setEditingCategory(category);
|
||||
updateUrlParams({ modal: 'edit', categoryId: category.id });
|
||||
setFormData({
|
||||
name: category.name,
|
||||
slug: category.slug,
|
||||
description: category.description || '',
|
||||
metaTitle: category.metaTitle || '',
|
||||
metaDescription: category.metaDescription || '',
|
||||
keywords: category.keywords || '',
|
||||
imageUrl: category.imageUrl || ''
|
||||
});
|
||||
} else {
|
||||
setEditingCategory(null);
|
||||
updateUrlParams({ modal: 'create', categoryId: undefined });
|
||||
setFormData({
|
||||
name: '', slug: '', description: '', metaTitle: '', metaDescription: '', keywords: '', imageUrl: ''
|
||||
});
|
||||
}
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setIsModalOpen(false);
|
||||
updateUrlParams({ modal: undefined, categoryId: undefined });
|
||||
};
|
||||
|
||||
const fetchCategories = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
@ -123,33 +151,6 @@ export default function Categories() {
|
||||
}
|
||||
};
|
||||
|
||||
const openModal = (category: Category | null = null) => {
|
||||
if (category) {
|
||||
setEditingCategory(category);
|
||||
updateUrlParams({ modal: 'edit', categoryId: category.id });
|
||||
setFormData({
|
||||
name: category.name,
|
||||
slug: category.slug,
|
||||
description: category.description || '',
|
||||
metaTitle: category.metaTitle || '',
|
||||
metaDescription: category.metaDescription || '',
|
||||
keywords: category.keywords || '',
|
||||
imageUrl: category.imageUrl || ''
|
||||
});
|
||||
} else {
|
||||
setEditingCategory(null);
|
||||
updateUrlParams({ modal: 'create', categoryId: undefined });
|
||||
setFormData({
|
||||
name: '', slug: '', description: '', metaTitle: '', metaDescription: '', keywords: '', imageUrl: ''
|
||||
});
|
||||
}
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setIsModalOpen(false);
|
||||
updateUrlParams({ modal: undefined, categoryId: undefined });
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
|
||||
@ -72,6 +72,36 @@ export default function Coupons() {
|
||||
setSearchParams(cleaned, { replace: true });
|
||||
};
|
||||
|
||||
const openModal = (coupon: Coupon | null = null) => {
|
||||
if (coupon) {
|
||||
setEditingCoupon(coupon);
|
||||
updateUrlParams({ modal: 'edit', couponId: coupon.id });
|
||||
setFormData({
|
||||
code: coupon.code,
|
||||
type: coupon.type,
|
||||
value: Number(coupon.value),
|
||||
minCartValue: coupon.minCartValue ? String(coupon.minCartValue) : '',
|
||||
maxCartValue: coupon.maxCartValue ? String(coupon.maxCartValue) : '',
|
||||
maxUses: coupon.maxUses ? String(coupon.maxUses) : '',
|
||||
expiresAt: coupon.expiresAt ? new Date(coupon.expiresAt).toISOString().split('T')[0] : '',
|
||||
isActive: coupon.isActive,
|
||||
targets: coupon.targets || []
|
||||
});
|
||||
} else {
|
||||
setEditingCoupon(null);
|
||||
updateUrlParams({ modal: 'create', couponId: undefined });
|
||||
setFormData({
|
||||
code: '', type: 'percent', value: 0, minCartValue: '', maxCartValue: '', maxUses: '', expiresAt: '', isActive: true, targets: []
|
||||
});
|
||||
}
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setIsModalOpen(false);
|
||||
updateUrlParams({ modal: undefined, couponId: undefined });
|
||||
};
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
@ -102,35 +132,6 @@ export default function Coupons() {
|
||||
return () => clearTimeout(timer);
|
||||
}, [fetchData]);
|
||||
|
||||
const openModal = (coupon: Coupon | null = null) => {
|
||||
if (coupon) {
|
||||
setEditingCoupon(coupon);
|
||||
updateUrlParams({ modal: 'edit', couponId: coupon.id });
|
||||
setFormData({
|
||||
code: coupon.code,
|
||||
type: coupon.type,
|
||||
value: Number(coupon.value),
|
||||
minCartValue: coupon.minCartValue ? String(coupon.minCartValue) : '',
|
||||
maxCartValue: coupon.maxCartValue ? String(coupon.maxCartValue) : '',
|
||||
maxUses: coupon.maxUses ? String(coupon.maxUses) : '',
|
||||
expiresAt: coupon.expiresAt ? new Date(coupon.expiresAt).toISOString().split('T')[0] : '',
|
||||
isActive: coupon.isActive,
|
||||
targets: coupon.targets || []
|
||||
});
|
||||
} else {
|
||||
setEditingCoupon(null);
|
||||
updateUrlParams({ modal: 'create', couponId: undefined });
|
||||
setFormData({
|
||||
code: '', type: 'percent', value: 0, minCartValue: '', maxCartValue: '', maxUses: '', expiresAt: '', isActive: true, targets: []
|
||||
});
|
||||
}
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setIsModalOpen(false);
|
||||
updateUrlParams({ modal: undefined, couponId: undefined });
|
||||
};
|
||||
|
||||
|
||||
const handleSave = async (e: React.FormEvent) => {
|
||||
|
||||
@ -75,33 +75,6 @@ export default function MenuManager() {
|
||||
badge: '',
|
||||
});
|
||||
|
||||
const fetchAll = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await api.get('/menu/admin');
|
||||
const data: MenuItem[] = Array.isArray(res.data) ? res.data : (res.data?.data || []);
|
||||
setAllItems(data);
|
||||
|
||||
const modalParam = searchParams.get('modal');
|
||||
const itemIdParam = searchParams.get('itemId');
|
||||
if (modalParam === 'create') {
|
||||
openModal(null);
|
||||
} else if (modalParam === 'edit' && itemIdParam) {
|
||||
const found = data.find((it) => it.id === itemIdParam);
|
||||
if (found) openModal(found);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch menu items:', err);
|
||||
toast.error('خطا در دریافت آیتمهای منو');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAll();
|
||||
}, [fetchAll]);
|
||||
|
||||
const currentItems = allItems.filter(
|
||||
(item) => item.menuType === activeTab && item.parentId === null
|
||||
);
|
||||
@ -144,6 +117,34 @@ export default function MenuManager() {
|
||||
updateUrlState({ modal: null, itemId: null });
|
||||
};
|
||||
|
||||
const fetchAll = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await api.get('/menu/admin');
|
||||
const data: MenuItem[] = Array.isArray(res.data) ? res.data : (res.data?.data || []);
|
||||
setAllItems(data);
|
||||
|
||||
const modalParam = searchParams.get('modal');
|
||||
const itemIdParam = searchParams.get('itemId');
|
||||
if (modalParam === 'create') {
|
||||
openModal(null);
|
||||
} else if (modalParam === 'edit' && itemIdParam) {
|
||||
const found = data.find((it) => it.id === itemIdParam);
|
||||
if (found) openModal(found);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch menu items:', err);
|
||||
toast.error('خطا در دریافت آیتمهای منو');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAll();
|
||||
}, [fetchAll]);
|
||||
|
||||
|
||||
|
||||
const handleSave = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
@ -212,6 +212,40 @@ export default function Orders() {
|
||||
const [modalStatus, setModalStatus] = useState('');
|
||||
const [isSavingTracking, setIsSavingTracking] = useState(false);
|
||||
|
||||
// Refund Order State & Handlers
|
||||
const [refundModalOrder, setRefundModalOrder] = useState<Order | null>(null);
|
||||
const [refundTarget, setRefundTarget] = useState<'wallet' | 'zibal'>('wallet');
|
||||
const [refundAmount, setRefundAmount] = useState<string>('');
|
||||
const [refundReason, setRefundReason] = useState<string>('');
|
||||
const [isProcessingRefund, setIsProcessingRefund] = useState(false);
|
||||
|
||||
const openOrderModal = (order: Order) => {
|
||||
setSelectedOrder(order);
|
||||
setModalTrackingCode(order.trackingNumber || '');
|
||||
setModalStatus(order.status || 'processing');
|
||||
updateUrlParams({ modal: 'details', orderId: order.id });
|
||||
};
|
||||
|
||||
const closeOrderModal = () => {
|
||||
setSelectedOrder(null);
|
||||
updateUrlParams({ modal: undefined, orderId: undefined });
|
||||
};
|
||||
|
||||
const openOrderRefundModal = (order: Order) => {
|
||||
setRefundModalOrder(order);
|
||||
const amt = Number(order.totalAmount || order.total || 0);
|
||||
setRefundAmount(String(amt));
|
||||
// If order was paid by wallet, default to wallet. If online, let admin choose.
|
||||
setRefundTarget(order.paymentMethod === 'wallet' ? 'wallet' : 'zibal');
|
||||
setRefundReason(`استرداد سفارش #${order.trackingNumber || order.id.slice(0, 8)}`);
|
||||
updateUrlParams({ modal: 'refund', orderId: order.id });
|
||||
};
|
||||
|
||||
const closeOrderRefundModal = () => {
|
||||
setRefundModalOrder(null);
|
||||
updateUrlParams({ modal: undefined, orderId: undefined });
|
||||
};
|
||||
|
||||
const fetchOrders = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
@ -238,7 +272,18 @@ export default function Orders() {
|
||||
const modalType = searchParams.get('modal');
|
||||
const orderIdParam = searchParams.get('orderId');
|
||||
if (modalType && orderIdParam) {
|
||||
const foundOrder = orderList.find((o: Order) => o.id === orderIdParam);
|
||||
let foundOrder = orderList.find((o: Order) => o.id === orderIdParam);
|
||||
if (!foundOrder) {
|
||||
try {
|
||||
const singleRes = await api.get(`/admin/orders/${orderIdParam}`);
|
||||
if (singleRes.data?.data) {
|
||||
foundOrder = singleRes.data.data;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Could not fetch single order by ID', err);
|
||||
}
|
||||
}
|
||||
|
||||
if (foundOrder) {
|
||||
if (modalType === 'details') {
|
||||
setSelectedOrder(foundOrder);
|
||||
@ -284,18 +329,6 @@ export default function Orders() {
|
||||
}
|
||||
};
|
||||
|
||||
const openOrderModal = (order: Order) => {
|
||||
setSelectedOrder(order);
|
||||
setModalTrackingCode(order.trackingNumber || '');
|
||||
setModalStatus(order.status || 'processing');
|
||||
updateUrlParams({ modal: 'details', orderId: order.id });
|
||||
};
|
||||
|
||||
const closeOrderModal = () => {
|
||||
setSelectedOrder(null);
|
||||
updateUrlParams({ modal: undefined, orderId: undefined });
|
||||
};
|
||||
|
||||
const handleSaveModalChanges = async () => {
|
||||
if (!selectedOrder) return;
|
||||
try {
|
||||
@ -314,27 +347,6 @@ export default function Orders() {
|
||||
}
|
||||
};
|
||||
|
||||
// Refund Order State & Handlers
|
||||
const [refundModalOrder, setRefundModalOrder] = useState<Order | null>(null);
|
||||
const [refundTarget, setRefundTarget] = useState<'wallet' | 'zibal'>('wallet');
|
||||
const [refundAmount, setRefundAmount] = useState<string>('');
|
||||
const [refundReason, setRefundReason] = useState<string>('');
|
||||
const [isProcessingRefund, setIsProcessingRefund] = useState(false);
|
||||
|
||||
const openOrderRefundModal = (order: Order) => {
|
||||
setRefundModalOrder(order);
|
||||
const amt = Number(order.totalAmount || order.total || 0);
|
||||
setRefundAmount(String(amt));
|
||||
// If order was paid by wallet, default to wallet. If online, let admin choose.
|
||||
setRefundTarget(order.paymentMethod === 'wallet' ? 'wallet' : 'zibal');
|
||||
setRefundReason(`استرداد سفارش #${order.trackingNumber || order.id.slice(0, 8)}`);
|
||||
updateUrlParams({ modal: 'refund', orderId: order.id });
|
||||
};
|
||||
|
||||
const closeOrderRefundModal = () => {
|
||||
setRefundModalOrder(null);
|
||||
updateUrlParams({ modal: undefined, orderId: undefined });
|
||||
};
|
||||
|
||||
|
||||
const handleExecuteOrderRefund = async () => {
|
||||
@ -848,15 +860,20 @@ export default function Orders() {
|
||||
<div>
|
||||
<div className="text-xs font-bold text-gray-400">زمان ثبت سفارش:</div>
|
||||
<div className="text-sm font-black text-gray-900">
|
||||
{toPersianDigits(new Date(selectedOrder.createdAt).toLocaleDateString('fa-IR', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}))}
|
||||
{selectedOrder.createdAt
|
||||
? toPersianDigits(
|
||||
new Date(selectedOrder.createdAt).toLocaleDateString('fa-IR', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
)
|
||||
: '-'}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -195,68 +195,6 @@ export default function Products() {
|
||||
preorderDeposit: '' as number | string
|
||||
});
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
// Fetch products with full query params
|
||||
const prodRes = await api.get('/admin/products', {
|
||||
params: {
|
||||
page,
|
||||
limit,
|
||||
search: search || undefined,
|
||||
categoryId: categoryFilter || undefined,
|
||||
suitableFor: suitableForFilter || undefined,
|
||||
sortBy,
|
||||
sortOrder
|
||||
}
|
||||
});
|
||||
if (prodRes.data?.data) {
|
||||
const prodList = prodRes.data.data;
|
||||
setProducts(prodList);
|
||||
setTotalPages(prodRes.data.meta?.lastPage || 1);
|
||||
|
||||
// Auto-open modal if URL specifies modal
|
||||
const modalParam = searchParams.get('modal');
|
||||
const prodIdParam = searchParams.get('productId');
|
||||
const tabParam = searchParams.get('tab');
|
||||
if (modalParam === 'create') {
|
||||
openModal(null);
|
||||
if (tabParam) setActiveTab(tabParam);
|
||||
} else if (modalParam === 'edit' && prodIdParam) {
|
||||
const found = prodList.find((p: Product) => p.id === prodIdParam);
|
||||
if (found) {
|
||||
openModal(found);
|
||||
if (tabParam) setActiveTab(tabParam);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch categories separately
|
||||
try {
|
||||
const catRes = await api.get('/admin/categories');
|
||||
if (catRes.data?.data) {
|
||||
setCategories(catRes.data.data);
|
||||
} else if (Array.isArray(catRes.data)) {
|
||||
setCategories(catRes.data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Could not fetch categories', err);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
console.error(err);
|
||||
toast.error('خطا در دریافت لیست محصولات');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [page, search, categoryFilter, suitableForFilter, sortBy, sortOrder, searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
fetchData();
|
||||
}, 400);
|
||||
return () => clearTimeout(timer);
|
||||
}, [fetchData]);
|
||||
|
||||
const openModal = (product: Product | null = null) => {
|
||||
setMediaImageError(false);
|
||||
if (product) {
|
||||
@ -338,6 +276,69 @@ export default function Products() {
|
||||
updateUrlParams({ tab: newTab });
|
||||
};
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
// Fetch products with full query params
|
||||
const prodRes = await api.get('/admin/products', {
|
||||
params: {
|
||||
page,
|
||||
limit,
|
||||
search: search || undefined,
|
||||
categoryId: categoryFilter || undefined,
|
||||
suitableFor: suitableForFilter || undefined,
|
||||
sortBy,
|
||||
sortOrder
|
||||
}
|
||||
});
|
||||
if (prodRes.data?.data) {
|
||||
const prodList = prodRes.data.data;
|
||||
setProducts(prodList);
|
||||
setTotalPages(prodRes.data.meta?.lastPage || 1);
|
||||
|
||||
// Auto-open modal if URL specifies modal
|
||||
const modalParam = searchParams.get('modal');
|
||||
const prodIdParam = searchParams.get('productId');
|
||||
const tabParam = searchParams.get('tab');
|
||||
if (modalParam === 'create') {
|
||||
openModal(null);
|
||||
if (tabParam) setActiveTab(tabParam);
|
||||
} else if (modalParam === 'edit' && prodIdParam) {
|
||||
const found = prodList.find((p: Product) => p.id === prodIdParam);
|
||||
if (found) {
|
||||
openModal(found);
|
||||
if (tabParam) setActiveTab(tabParam);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch categories separately
|
||||
try {
|
||||
const catRes = await api.get('/admin/categories');
|
||||
if (catRes.data?.data) {
|
||||
setCategories(catRes.data.data);
|
||||
} else if (Array.isArray(catRes.data)) {
|
||||
setCategories(catRes.data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Could not fetch categories', err);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
console.error(err);
|
||||
toast.error('خطا در دریافت لیست محصولات');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [page, search, categoryFilter, suitableForFilter, sortBy, sortOrder, searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
fetchData();
|
||||
}, 400);
|
||||
return () => clearTimeout(timer);
|
||||
}, [fetchData]);
|
||||
|
||||
|
||||
|
||||
const handleSave = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
@ -148,8 +148,7 @@ export default function Transactions() {
|
||||
const [isSubmittingRefund, setIsSubmittingRefund] = useState(false);
|
||||
|
||||
const openReceiptModal = (tx: Transaction) => {
|
||||
setParam('modal', 'receipt');
|
||||
setParam('txId', tx.id);
|
||||
setMultipleParams({ modal: 'receipt', txId: tx.id }, false);
|
||||
setReceiptData({
|
||||
trackId: tx.trackId || '-',
|
||||
refNumber: tx.refNumber || '-',
|
||||
@ -167,6 +166,7 @@ export default function Transactions() {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}),
|
||||
fee: 0,
|
||||
mobile: tx.user?.mobile || '-',
|
||||
description: tx.description || 'پرداخت سفارش آنلاین کنینا',
|
||||
psp: tx.gateway === 'zibal' ? 'زیبال / بهپرداخت ملت' : tx.gateway,
|
||||
@ -182,8 +182,7 @@ export default function Transactions() {
|
||||
};
|
||||
|
||||
const openDetailsModal = (tx: Transaction) => {
|
||||
setParam('modal', 'details');
|
||||
setParam('txId', tx.id);
|
||||
setMultipleParams({ modal: 'details', txId: tx.id }, false);
|
||||
setSelectedTx(tx);
|
||||
setLiveInquiryData(null);
|
||||
setShowRawLogs(false);
|
||||
@ -217,8 +216,7 @@ export default function Transactions() {
|
||||
};
|
||||
|
||||
const openRefundModal = (tx: Transaction) => {
|
||||
setParam('modal', 'refund');
|
||||
setParam('txId', tx.id);
|
||||
setMultipleParams({ modal: 'refund', txId: tx.id }, false);
|
||||
setRefundModalTx(tx);
|
||||
setRefundTarget(tx.type === 'WALLET_TOPUP' || !tx.trackId ? 'wallet' : 'wallet');
|
||||
setRefundAmount(String(tx.amount || ''));
|
||||
@ -245,12 +243,13 @@ export default function Transactions() {
|
||||
toast.error('کاربر مرتبط با این تراکنش یافت نشد');
|
||||
return;
|
||||
}
|
||||
await api.post(`/users/${userId}/wallet/adjust`, {
|
||||
await api.post(`/admin/users/${userId}/wallet-adjust`, {
|
||||
amount: parsedAmount,
|
||||
type: 'CREDIT',
|
||||
type: 'refund',
|
||||
description: refundReason || `استرداد وجه تراکنش #${refundModalTx.trackId || refundModalTx.id.slice(0, 8)}`,
|
||||
});
|
||||
toast.success('مبلغ با موفقیت به کیف پول کاربر عودت داده شد');
|
||||
|
||||
} else {
|
||||
if (!refundModalTx.trackId) {
|
||||
toast.error('شناسه تراکنش زیبال (Track ID) برای این پرداخت موجود نیست');
|
||||
@ -466,13 +465,12 @@ export default function Transactions() {
|
||||
|
||||
{/* Summary Stat Cards - 2 cols on mobile (2 rows), 4 cols on desktop */}
|
||||
{stats && (
|
||||
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4">
|
||||
<div className="bg-white p-4 sm:p-5 rounded-2xl border border-gray-200 shadow-xs flex items-center justify-between">
|
||||
<div className="min-w-0">
|
||||
<p className="text-[11px] sm:text-xs font-bold text-gray-500 truncate">حجم کل تراکنشهای موفق</p>
|
||||
<p className="text-base sm:text-xl font-black text-gray-900 mt-1">
|
||||
{Number(stats.totalVolume).toLocaleString('fa-IR')}{' '}
|
||||
{(Number(stats.totalVolume) || 0).toLocaleString('fa-IR')}{' '}
|
||||
<span className="text-[10px] sm:text-xs font-normal text-gray-400">تومان</span>
|
||||
</p>
|
||||
</div>
|
||||
@ -485,7 +483,7 @@ export default function Transactions() {
|
||||
<div className="min-w-0">
|
||||
<p className="text-[11px] sm:text-xs font-bold text-gray-500 truncate">پرداختهای موفق امروز</p>
|
||||
<p className="text-base sm:text-xl font-black text-emerald-600 mt-1">
|
||||
{Number(stats.todayVolume).toLocaleString('fa-IR')}{' '}
|
||||
{(Number(stats.todayVolume) || 0).toLocaleString('fa-IR')}{' '}
|
||||
<span className="text-[10px] sm:text-xs font-normal text-gray-400">تومان</span>
|
||||
</p>
|
||||
</div>
|
||||
@ -498,7 +496,7 @@ export default function Transactions() {
|
||||
<div className="min-w-0">
|
||||
<p className="text-[11px] sm:text-xs font-bold text-gray-500 truncate">نرخ موفقیت پرداختها</p>
|
||||
<p className="text-base sm:text-xl font-black text-indigo-600 mt-1">
|
||||
٪{Number(stats.successRate).toLocaleString('fa-IR')}
|
||||
٪{(Number(stats.successRate) || 0).toLocaleString('fa-IR')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-indigo-50 text-indigo-600 rounded-2xl flex items-center justify-center shrink-0">
|
||||
@ -510,9 +508,9 @@ export default function Transactions() {
|
||||
<div className="min-w-0">
|
||||
<p className="text-[11px] sm:text-xs font-bold text-gray-500 truncate">وضعیت تراکنشها</p>
|
||||
<div className="flex flex-wrap items-center gap-2 mt-1 text-[10px] sm:text-xs font-bold">
|
||||
<span className="text-emerald-600">✓ {stats.verifiedCount}</span>
|
||||
<span className="text-amber-500">⏳ {stats.pendingCount}</span>
|
||||
<span className="text-rose-600">✗ {stats.failedCount}</span>
|
||||
<span className="text-emerald-600">✓ {stats.verifiedCount || 0}</span>
|
||||
<span className="text-amber-500">⏳ {stats.pendingCount || 0}</span>
|
||||
<span className="text-rose-600">✗ {stats.failedCount || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-gray-50 text-gray-600 rounded-2xl flex items-center justify-center shrink-0">
|
||||
@ -522,6 +520,7 @@ export default function Transactions() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* Filter and Search Bar */}
|
||||
<div className="bg-white p-5 rounded-2xl border border-gray-200 shadow-xs space-y-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-3">
|
||||
|
||||
@ -103,50 +103,6 @@ export default function Users() {
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isAdjustingWallet, setIsAdjustingWallet] = useState(false);
|
||||
|
||||
const fetchUsers = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const params = new URLSearchParams();
|
||||
params.append('page', page.toString());
|
||||
params.append('limit', '10');
|
||||
if (search) params.append('search', search);
|
||||
if (role) params.append('role', role);
|
||||
|
||||
const response = await api.get(`/admin/users?${params.toString()}`);
|
||||
if (response.data?.success) {
|
||||
const userList = response.data.data || [];
|
||||
setUsers(userList);
|
||||
setTotalPages(response.data.meta?.lastPage || 1);
|
||||
setTotalCount(response.data.meta?.total || 0);
|
||||
|
||||
const modalParam = searchParams.get('modal');
|
||||
const userId = searchParams.get('userId');
|
||||
if (modalParam === 'create') {
|
||||
handleOpenCreate();
|
||||
} else if (userId && userList.length > 0) {
|
||||
const found = userList.find((u: UserRecord) => u.id === userId);
|
||||
if (found) {
|
||||
if (modalParam === 'view') handleOpenView(found);
|
||||
else if (modalParam === 'edit') handleOpenEdit(found);
|
||||
else if (modalParam === 'wallet') handleOpenWallet(found);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch users', err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [page, search, role, searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
const delayDebounceFn = setTimeout(() => {
|
||||
fetchUsers();
|
||||
}, 400);
|
||||
|
||||
return () => clearTimeout(delayDebounceFn);
|
||||
}, [fetchUsers]);
|
||||
|
||||
// Open View Modal
|
||||
const handleOpenView = (user: UserRecord) => {
|
||||
setViewUser(user);
|
||||
@ -209,7 +165,49 @@ export default function Users() {
|
||||
setUserToDelete(null);
|
||||
};
|
||||
|
||||
const fetchUsers = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const params = new URLSearchParams();
|
||||
params.append('page', page.toString());
|
||||
params.append('limit', '10');
|
||||
if (search) params.append('search', search);
|
||||
if (role) params.append('role', role);
|
||||
|
||||
const response = await api.get(`/admin/users?${params.toString()}`);
|
||||
if (response.data?.success) {
|
||||
const userList = response.data.data || [];
|
||||
setUsers(userList);
|
||||
setTotalPages(response.data.meta?.lastPage || 1);
|
||||
setTotalCount(response.data.meta?.total || 0);
|
||||
|
||||
const modalParam = searchParams.get('modal');
|
||||
const userId = searchParams.get('userId');
|
||||
if (modalParam === 'create') {
|
||||
handleOpenCreate();
|
||||
} else if (userId && userList.length > 0) {
|
||||
const found = userList.find((u: UserRecord) => u.id === userId);
|
||||
if (found) {
|
||||
if (modalParam === 'view') handleOpenView(found);
|
||||
else if (modalParam === 'edit') handleOpenEdit(found);
|
||||
else if (modalParam === 'wallet') handleOpenWallet(found);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch users', err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [page, search, role, searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
const delayDebounceFn = setTimeout(() => {
|
||||
fetchUsers();
|
||||
}, 400);
|
||||
|
||||
return () => clearTimeout(delayDebounceFn);
|
||||
}, [fetchUsers]);
|
||||
|
||||
// Create User
|
||||
const handleCreateUser = async (e: React.FormEvent) => {
|
||||
|
||||
@ -62,6 +62,35 @@ export default function Wiki() {
|
||||
keywords: ''
|
||||
});
|
||||
|
||||
const openModal = (term: WikiTerm | null = null) => {
|
||||
if (term) {
|
||||
setEditingTerm(term);
|
||||
updateUrlParams({ modal: 'edit', termKey: term.key });
|
||||
setFormData({
|
||||
key: term.key,
|
||||
term: term.term,
|
||||
definition: term.definition,
|
||||
relatedProducts: Array.isArray(term.relatedProducts) ? term.relatedProducts : [],
|
||||
wikiId: term.wikiId || 'general',
|
||||
metaTitle: term.metaTitle || '',
|
||||
metaDescription: term.metaDescription || '',
|
||||
keywords: term.keywords || ''
|
||||
});
|
||||
} else {
|
||||
setEditingTerm(null);
|
||||
updateUrlParams({ modal: 'create', termKey: undefined });
|
||||
setFormData({
|
||||
key: '', term: '', definition: '', relatedProducts: [], wikiId: 'general', metaTitle: '', metaDescription: '', keywords: ''
|
||||
});
|
||||
}
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setIsModalOpen(false);
|
||||
updateUrlParams({ modal: undefined, termKey: undefined });
|
||||
};
|
||||
|
||||
const fetchTerms = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
@ -132,34 +161,6 @@ export default function Wiki() {
|
||||
.catch(err => console.error(err));
|
||||
}, []);
|
||||
|
||||
const openModal = (term: WikiTerm | null = null) => {
|
||||
if (term) {
|
||||
setEditingTerm(term);
|
||||
updateUrlParams({ modal: 'edit', termKey: term.key });
|
||||
setFormData({
|
||||
key: term.key,
|
||||
term: term.term,
|
||||
definition: term.definition,
|
||||
relatedProducts: Array.isArray(term.relatedProducts) ? term.relatedProducts : [],
|
||||
wikiId: term.wikiId || 'general',
|
||||
metaTitle: term.metaTitle || '',
|
||||
metaDescription: term.metaDescription || '',
|
||||
keywords: term.keywords || ''
|
||||
});
|
||||
} else {
|
||||
setEditingTerm(null);
|
||||
updateUrlParams({ modal: 'create', termKey: undefined });
|
||||
setFormData({
|
||||
key: '', term: '', definition: '', relatedProducts: [], wikiId: 'general', metaTitle: '', metaDescription: '', keywords: ''
|
||||
});
|
||||
}
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setIsModalOpen(false);
|
||||
updateUrlParams({ modal: undefined, termKey: undefined });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user