feat(admin): update order management and SMS rules settings UI with lint fixes

This commit is contained in:
parsa aghaei 2026-09-05 15:16:12 +03:30
parent 5e3e3958c5
commit 56e8029fff
2 changed files with 615 additions and 618 deletions

View File

@ -102,6 +102,9 @@ export interface Order {
total?: number;
isRefill?: boolean;
shippingAddress?: string;
refundStatus?: string;
refundedAmount?: number;
refundMethod?: string;
}
export const ORDER_STATUS_MAP: Record<string, { label: string; color: string; icon: React.ElementType }> = {
@ -242,8 +245,12 @@ export default function Orders() {
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.
// If order was already refunded to wallet, default/force to gateway
if (order.refundStatus === 'wallet') {
setRefundTarget('zibal');
} else {
setRefundTarget(order.paymentMethod === 'wallet' ? 'wallet' : 'zibal');
}
setRefundReason(`استرداد سفارش #${order.trackingNumber || order.id.slice(0, 8)}`);
updateUrlParams({ modal: 'refund', orderId: order.id });
};
@ -440,16 +447,15 @@ export default function Orders() {
}
const res = await api.post('/payment/admin/refund', {
orderId: refundModalOrder.id,
trackId,
amount: refundAmount ? Number(refundAmount) * 10 : undefined,
tryReverse: true,
description: refundReason || `استرداد وجه سفارش #${refundModalOrder.trackingNumber || refundModalOrder.id.slice(0, 8)}`,
});
if (res.data?.result === 1 || res.data?.status === 1 || res.data?.success) {
toast.success('درخواست استرداد وجه به درگاه زیبال با موفقیت ارسال شد.');
// Also mark order as cancelled
await api.put(`/admin/orders/${refundModalOrder.id}/status`, { status: 'cancelled' });
if (res.data?.result === 1 || res.data?.status === 1 || res.data?.result === 100 || res.data?.success) {
toast.success('درخواست استرداد وجه به درگاه زیبال با موفقیت ارسال شد و وضعیت سفارش به‌روزرسانی شد.');
setRefundModalOrder(null);
await fetchOrders();
} else {
@ -790,6 +796,22 @@ export default function Orders() {
<CreditCard className="w-3 h-3 shrink-0" />
<span className="font-vazir">{payInfo.label}</span>
</span>
{order.refundStatus && (
<span
className={`text-[10px] font-bold font-vazir px-2 py-0.5 rounded-md border flex items-center gap-1 shadow-sm ${
order.refundStatus === 'gateway'
? 'bg-rose-50 text-rose-700 border-rose-200'
: 'bg-amber-50 text-amber-700 border-amber-200'
}`}
>
<Undo2 className="w-3 h-3 shrink-0" />
<span className="font-vazir">
{order.refundStatus === 'gateway'
? 'مسترد به درگاه'
: 'مسترد به کیف‌پول'}
</span>
</span>
)}
{order.isRefill && (
<span className="text-[10px] font-bold font-vazir px-2 py-0.5 rounded-md bg-emerald-50 text-emerald-700 border border-emerald-200 shadow-sm">
تمدید ۵٪-
@ -855,16 +877,16 @@ export default function Orders() {
className="text-gray-600 hover:bg-gray-100"
/>
{(order.status === 'cancelled' || order.status === 'pending_payment') && (
{(order.status === 'cancelled' || order.status === 'pending_payment') && order.refundStatus !== 'gateway' && (
<Button
variant="ghost"
size="xs"
startIcon={Undo2}
onClick={() => openOrderRefundModal(order)}
disabled={isProcessing}
className="text-rose-600 hover:bg-rose-50"
className={`hover:bg-rose-50 ${order.refundStatus === 'wallet' ? 'text-amber-700 hover:bg-amber-50' : 'text-rose-600'}`}
>
استرداد
{order.refundStatus === 'wallet' ? 'استرداد بانکی' : 'استرداد'}
</Button>
)}
</div>
@ -1018,17 +1040,15 @@ export default function Orders() {
{(() => {
let addrText = selectedOrder.address?.fullAddress || selectedOrder.shippingAddress || 'ثبت نشده';
let postCode = selectedOrder.address?.postalCode;
let prov = selectedOrder.address?.province;
let ct = selectedOrder.address?.city;
if (typeof selectedOrder.shippingAddress === 'string' && selectedOrder.shippingAddress.trim().startsWith('{')) {
try {
const parsed = JSON.parse(selectedOrder.shippingAddress);
addrText = parsed.formatted || `${parsed.province || ''}، ${parsed.city || ''}، ${parsed.address || ''}`;
if (parsed.postalCode) postCode = parsed.postalCode;
if (parsed.province) prov = parsed.province;
if (parsed.city) ct = parsed.city;
} catch {}
} catch {
// ignore malformed json
}
}
return (
@ -1115,6 +1135,16 @@ export default function Orders() {
<span>روش پرداخت:</span>
<span className="text-white font-bold">{selectedOrder.paymentMethod === 'wallet' ? 'کیف پول الکترونیک' : 'درگاه پرداخت آنلاین'}</span>
</div>
{selectedOrder.refundStatus && (
<div className="flex justify-between font-bold text-amber-300 pt-1 border-t border-gray-800/80">
<span>وضعیت استرداد وجه:</span>
<span>
{selectedOrder.refundStatus === 'gateway'
? `مسترد به درگاه بانکی (${toPersianDigits(Number(selectedOrder.refundedAmount || selectedOrder.totalAmount || 0).toLocaleString())} تومان)`
: `مسترد به کیف‌پول (${toPersianDigits(Number(selectedOrder.refundedAmount || selectedOrder.totalAmount || 0).toLocaleString())} تومان)`}
</span>
</div>
)}
<div className="flex justify-between text-sm sm:text-base font-black text-amber-400 pt-2 border-t border-gray-800">
<span>مبلغ نهایی پرداخت شده:</span>
<span>{toPersianDigits(Number(selectedOrder.totalAmount || selectedOrder.total || 0).toLocaleString())} تومان</span>
@ -1157,14 +1187,22 @@ export default function Orders() {
{/* Destination selector */}
<div>
<label className="block font-bold text-gray-700 mb-2">روش عودت و بازگشت وجه:</label>
{refundModalOrder.refundStatus === 'wallet' && (
<div className="mb-3 p-3 bg-amber-50 border border-amber-200 rounded-xl text-amber-800 text-[11px] leading-relaxed">
<strong>توجه:</strong> وجه این سفارش قبلاً به کیف پول کاربر عودت داده شده است. در صورت ثبت استرداد درگاه زیبال، مبلغ استرداد به صورت خودکار از موجودی کیف پول کاربر کسر خواهد شد تا از استرداد مضاعف جلوگیری شود.
</div>
)}
<div className="grid grid-cols-2 gap-3">
<button
type="button"
disabled={refundModalOrder.refundStatus === 'wallet'}
onClick={() => setRefundTarget('wallet')}
className={`p-3.5 rounded-2xl border text-right transition-all cursor-pointer flex flex-col gap-1.5 ${
refundTarget === 'wallet'
? 'border-purple-600 bg-purple-50/60 ring-2 ring-purple-600/20'
: 'border-gray-200 hover:border-gray-300'
className={`p-3.5 rounded-2xl border text-right transition-all flex flex-col gap-1.5 ${
refundModalOrder.refundStatus === 'wallet'
? 'opacity-40 cursor-not-allowed bg-gray-50 border-gray-200'
: refundTarget === 'wallet'
? 'border-purple-600 bg-purple-50/60 ring-2 ring-purple-600/20 cursor-pointer'
: 'border-gray-200 hover:border-gray-300 cursor-pointer'
}`}
>
<div className="flex items-center gap-2 font-black text-gray-900">
@ -1172,7 +1210,9 @@ export default function Orders() {
<span>شارژ کیف پول کاربر</span>
</div>
<p className="text-[11px] text-gray-500 font-medium leading-relaxed">
افزایش فوری اعتبار کیف پول جهت خریدهای بعدی مشتری
{refundModalOrder.refundStatus === 'wallet'
? 'قبلاً به کیف پول مسترد شده است'
: 'افزایش فوری اعتبار کیف پول جهت خریدهای بعدی مشتری'}
</p>
</button>

View File

@ -47,6 +47,7 @@ import { useSearchParams } from 'react-router-dom';
import api from '../services/api';
import Spinner from '../components/ui/Spinner';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
export interface SmsRule {
id: string;
@ -462,12 +463,9 @@ export default function SmsSettingsPage() {
description: ruleFormDescription.trim() || undefined,
};
let newRules: SmsRule[] = [];
if (editingRule) {
newRules = (config.rules || []).map((r) => (r.id === editingRule.id ? updatedRule : r));
} else {
newRules = [...(config.rules || []), updatedRule];
}
const newRules: SmsRule[] = editingRule
? (config.rules || []).map((r) => (r.id === editingRule.id ? updatedRule : r))
: [...(config.rules || []), updatedRule];
const newConfig = { ...config, rules: newRules };
setConfig(newConfig);
@ -2092,24 +2090,14 @@ export default function SmsSettingsPage() {
)}
{/* Modal: Add/Edit SMS Automation Rule */}
{isRuleModalOpen && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
<div className="bg-white w-full max-w-2xl rounded-2xl shadow-2xl border border-gray-100 overflow-hidden font-vazir animate-in fade-in zoom-in duration-200 max-h-[90vh] flex flex-col">
<div className="flex items-center justify-between p-5 border-b border-gray-100 shrink-0">
<h3 className="text-base font-bold text-gray-900 flex items-center gap-2">
<Zap className="w-5 h-5 text-amber-500" />
{editingRule ? 'ویرایش سناریوی پیامک' : 'تعریف سناریوی پیامکی جدید'}
</h3>
<button
type="button"
onClick={() => setIsRuleModalOpen(false)}
className="text-gray-400 hover:text-gray-600 p-1 rounded-lg"
<Modal
isOpen={isRuleModalOpen}
onClose={() => setIsRuleModalOpen(false)}
title={editingRule ? 'ویرایش سناریوی پیامک' : 'تعریف سناریوی پیامکی جدید'}
icon={Zap}
maxWidth="2xl"
>
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleSaveRule} className="p-6 space-y-4 overflow-y-auto">
<form onSubmit={handleSaveRule} className="space-y-4">
{/* Rule Title */}
<div>
<label className="block text-xs font-bold text-gray-700 mb-1">
@ -2489,42 +2477,30 @@ export default function SmsSettingsPage() {
<button
type="button"
onClick={() => setIsRuleModalOpen(false)}
className="px-5 py-2.5 rounded-xl border border-gray-200 text-xs font-bold text-gray-600 hover:bg-gray-50"
className="px-5 py-2.5 rounded-xl border border-gray-200 text-xs font-bold text-gray-600 hover:bg-gray-50 cursor-pointer"
>
انصراف
</button>
<button
type="submit"
className="bg-amber-600 hover:bg-amber-700 text-white font-bold px-6 py-2.5 rounded-xl text-xs flex items-center gap-2 shadow-md shadow-amber-200"
className="bg-amber-600 hover:bg-amber-700 text-white font-bold px-6 py-2.5 rounded-xl text-xs flex items-center gap-2 shadow-md shadow-amber-200 cursor-pointer"
>
<Save className="w-4 h-4" />
{editingRule ? 'ذخیره تغییرات سناریو' : 'ثبت و فعال‌سازی سناریو'}
</button>
</div>
</form>
</div>
</div>
)}
</Modal>
{/* Modal: Add New Pattern */}
{isAddModalOpen && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
<div className="bg-white w-full max-w-xl rounded-2xl shadow-2xl border border-gray-100 overflow-hidden font-vazir animate-in fade-in zoom-in duration-200">
<div className="flex items-center justify-between p-5 border-b border-gray-100">
<h3 className="text-base font-bold text-gray-900 flex items-center gap-2">
<Plus className="w-5 h-5 text-purple-600" />
درج الگوی جدید در سامانه ملی پیامک
</h3>
<button
type="button"
onClick={() => setIsAddModalOpen(false)}
className="text-gray-400 hover:text-gray-600 p-1 rounded-lg"
<Modal
isOpen={isAddModalOpen}
onClose={() => setIsAddModalOpen(false)}
title="درج الگوی جدید در سامانه ملی پیامک"
icon={Plus}
maxWidth="xl"
>
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleCreatePattern} className="p-6 space-y-5">
<form onSubmit={handleCreatePattern} className="space-y-4">
<div>
<label className="block text-xs font-bold text-gray-700 mb-1">عنوان الگو (Title) *</label>
<input
@ -2574,46 +2550,37 @@ export default function SmsSettingsPage() {
</div>
<div className="flex justify-end gap-3 pt-3 border-t border-gray-100">
<button
<Button
type="button"
variant="outline"
onClick={() => setIsAddModalOpen(false)}
className="px-5 py-2.5 rounded-xl border border-gray-200 text-xs font-bold text-gray-600 hover:bg-gray-50"
className="px-5 py-2 text-xs font-bold"
>
انصراف
</button>
<button
</Button>
<Button
type="submit"
variant="primary"
disabled={isSubmittingPattern}
className="bg-purple-600 hover:bg-purple-700 text-white font-bold px-6 py-2.5 rounded-xl text-xs flex items-center gap-2 shadow-md shadow-purple-200 disabled:opacity-50"
isLoading={isSubmittingPattern}
className="bg-purple-600 hover:bg-purple-700 text-white font-bold px-6 py-2 text-xs"
>
{isSubmittingPattern ? <Spinner size="sm" /> : <Save className="w-4 h-4" />}
ارسال و ثبت الگو در ملی پیامک
</button>
</Button>
</div>
</form>
</div>
</div>
)}
</Modal>
{/* Modal: Edit Pattern */}
{isEditModalOpen && editingPattern && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
<div className="bg-white w-full max-w-xl rounded-2xl shadow-2xl border border-gray-100 overflow-hidden font-vazir animate-in fade-in zoom-in duration-200">
<div className="flex items-center justify-between p-5 border-b border-gray-100">
<h3 className="text-base font-bold text-gray-900 flex items-center gap-2">
<Edit3 className="w-5 h-5 text-blue-600" />
ویرایش متن الگو (BodyId: {editingPattern.id})
</h3>
<button
type="button"
onClick={() => setIsEditModalOpen(false)}
className="text-gray-400 hover:text-gray-600 p-1 rounded-lg"
<Modal
isOpen={isEditModalOpen && !!editingPattern}
onClose={() => setIsEditModalOpen(false)}
title={`ویرایش متن الگو (BodyId: ${editingPattern?.id || ''})`}
icon={Edit3}
maxWidth="xl"
>
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleEditPattern} className="p-6 space-y-5">
{editingPattern && (
<form onSubmit={handleEditPattern} className="space-y-4">
<div>
<label className="block text-xs font-bold text-gray-700 mb-1">عنوان الگو</label>
<input
@ -2652,46 +2619,47 @@ export default function SmsSettingsPage() {
</div>
<div className="flex justify-end gap-3 pt-3 border-t border-gray-100">
<button
<Button
type="button"
variant="outline"
onClick={() => setIsEditModalOpen(false)}
className="px-5 py-2.5 rounded-xl border border-gray-200 text-xs font-bold text-gray-600 hover:bg-gray-50"
className="px-5 py-2 text-xs font-bold"
>
انصراف
</button>
<button
</Button>
<Button
type="submit"
variant="primary"
disabled={isSubmittingPattern}
className="bg-blue-600 hover:bg-blue-700 text-white font-bold px-6 py-2.5 rounded-xl text-xs flex items-center gap-2 shadow-md shadow-blue-200 disabled:opacity-50"
isLoading={isSubmittingPattern}
className="bg-blue-600 hover:bg-blue-700 text-white font-bold px-6 py-2 text-xs"
>
{isSubmittingPattern ? <Spinner size="sm" /> : <Save className="w-4 h-4" />}
ذخیره ویرایش الگو
</button>
</Button>
</div>
</form>
</div>
</div>
)}
</Modal>
{/* Modal: Log Details */}
{isLogDetailModalOpen && selectedLog && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
<div className="bg-white w-full max-w-lg rounded-2xl shadow-2xl border border-gray-100 overflow-hidden font-vazir animate-in fade-in zoom-in duration-200">
<div className="flex items-center justify-between p-5 border-b border-gray-100">
<h3 className="text-base font-bold text-gray-900 flex items-center gap-2">
<Info className="w-5 h-5 text-purple-600" />
جزییات پیامک ارسالی
</h3>
<button
type="button"
<Modal
isOpen={isLogDetailModalOpen && !!selectedLog}
onClose={() => setIsLogDetailModalOpen(false)}
title="جزییات پیامک ارسالی"
icon={Info}
maxWidth="2xl"
footer={
<Button
variant="outline"
onClick={() => setIsLogDetailModalOpen(false)}
className="text-gray-400 hover:text-gray-600 p-1 rounded-lg"
className="px-5 py-2 text-xs font-bold"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="p-6 space-y-4 text-xs">
بستن
</Button>
}
>
{selectedLog && (
<div className="space-y-4 text-xs">
<div className="grid grid-cols-2 gap-3 bg-gray-50 p-4 rounded-xl border border-gray-100">
<div>
<span className="text-gray-400 font-medium">شماره گیرنده:</span>
@ -2749,12 +2717,12 @@ export default function SmsSettingsPage() {
}
return (
<div className="space-y-1">
<div className="space-y-1.5">
<span className="font-bold text-gray-800 flex items-center gap-1.5">
<FileText className="w-3.5 h-3.5 text-purple-600" />
<span>متن کامل پیامک ارسالی:</span>
</span>
<div className="bg-purple-50/50 p-3.5 rounded-xl border border-purple-200/70 font-sans text-xs text-gray-800 leading-relaxed whitespace-pre-wrap select-text">
<div className="bg-purple-50/50 p-4 rounded-xl border border-purple-200/70 font-sans text-xs text-gray-800 leading-relaxed whitespace-pre-wrap select-text max-h-60 overflow-y-auto">
{fullText || (selectedLog.args && selectedLog.args.length > 0 ? selectedLog.args.join(' ') : 'متن ثبت نشده است.')}
</div>
</div>
@ -2762,9 +2730,9 @@ export default function SmsSettingsPage() {
})()}
{selectedLog.args && selectedLog.args.length > 0 && (
<div className="space-y-1">
<div className="space-y-1.5">
<span className="font-bold text-gray-700">آرگومانها و مقادیر ارسالی به پترن:</span>
<div className="bg-slate-50 p-3 rounded-xl border border-slate-200 font-mono text-[11px] space-y-1">
<div className="bg-slate-50 p-3.5 rounded-xl border border-slate-200 font-mono text-[11px] space-y-1.5 max-h-48 overflow-y-auto">
{selectedLog.args.map((arg, idx) => (
<div key={idx} className="flex items-center gap-2">
<span className="text-purple-600 font-bold">{`{${idx}}`}:</span>
@ -2776,27 +2744,16 @@ export default function SmsSettingsPage() {
)}
{selectedLog.errorMessage && (
<div className="space-y-1">
<div className="space-y-1.5">
<span className="font-bold text-rose-700">توضیحات خطا / پاسخ درگاه:</span>
<div className="bg-rose-50 text-rose-800 p-3 rounded-xl border border-rose-200 text-[11px] leading-relaxed">
<div className="bg-rose-50 text-rose-800 p-3.5 rounded-xl border border-rose-200 text-[11px] leading-relaxed max-h-36 overflow-y-auto">
{selectedLog.errorMessage}
</div>
</div>
)}
<div className="flex justify-end gap-2 pt-3 border-t border-gray-100">
<button
type="button"
onClick={() => setIsLogDetailModalOpen(false)}
className="px-5 py-2.5 rounded-xl border border-gray-200 text-xs font-bold text-gray-600 hover:bg-gray-50"
>
بستن
</button>
</div>
</div>
</div>
</div>
)}
</Modal>
</div>
);
}