From 56e8029fff065c1139410ad280d4fcccbd1dadcf Mon Sep 17 00:00:00 2001 From: parsa aghaei Date: Sat, 5 Sep 2026 15:16:12 +0330 Subject: [PATCH] feat(admin): update order management and SMS rules settings UI with lint fixes --- frontend/admin-panel/src/pages/Orders.tsx | 78 +- .../admin-panel/src/pages/SmsSettingsPage.tsx | 1155 ++++++++--------- 2 files changed, 615 insertions(+), 618 deletions(-) diff --git a/frontend/admin-panel/src/pages/Orders.tsx b/frontend/admin-panel/src/pages/Orders.tsx index 352a298..1be649a 100644 --- a/frontend/admin-panel/src/pages/Orders.tsx +++ b/frontend/admin-panel/src/pages/Orders.tsx @@ -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 = { @@ -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. - setRefundTarget(order.paymentMethod === 'wallet' ? 'wallet' : 'zibal'); + // 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() { {payInfo.label} + {order.refundStatus && ( + + + + {order.refundStatus === 'gateway' + ? 'مسترد به درگاه' + : 'مسترد به کیف‌پول'} + + + )} {order.isRefill && ( تمدید ۵٪- @@ -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' && ( )} @@ -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() { روش پرداخت: {selectedOrder.paymentMethod === 'wallet' ? 'کیف پول الکترونیک' : 'درگاه پرداخت آنلاین'} + {selectedOrder.refundStatus && ( +
+ وضعیت استرداد وجه: + + {selectedOrder.refundStatus === 'gateway' + ? `مسترد به درگاه بانکی (${toPersianDigits(Number(selectedOrder.refundedAmount || selectedOrder.totalAmount || 0).toLocaleString())} تومان)` + : `مسترد به کیف‌پول (${toPersianDigits(Number(selectedOrder.refundedAmount || selectedOrder.totalAmount || 0).toLocaleString())} تومان)`} + +
+ )}
مبلغ نهایی پرداخت شده: {toPersianDigits(Number(selectedOrder.totalAmount || selectedOrder.total || 0).toLocaleString())} تومان @@ -1157,14 +1187,22 @@ export default function Orders() { {/* Destination selector */}
+ {refundModalOrder.refundStatus === 'wallet' && ( +
+ توجه: وجه این سفارش قبلاً به کیف پول کاربر عودت داده شده است. در صورت ثبت استرداد درگاه زیبال، مبلغ استرداد به صورت خودکار از موجودی کیف پول کاربر کسر خواهد شد تا از استرداد مضاعف جلوگیری شود. +
+ )}
diff --git a/frontend/admin-panel/src/pages/SmsSettingsPage.tsx b/frontend/admin-panel/src/pages/SmsSettingsPage.tsx index be30a1b..c594a2c 100644 --- a/frontend/admin-panel/src/pages/SmsSettingsPage.tsx +++ b/frontend/admin-panel/src/pages/SmsSettingsPage.tsx @@ -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,350 +2090,340 @@ export default function SmsSettingsPage() { )} {/* Modal: Add/Edit SMS Automation Rule */} - {isRuleModalOpen && ( -
-
-
-

- - {editingRule ? 'ویرایش سناریوی پیامک' : 'تعریف سناریوی پیامکی جدید'} -

- +
+ + {/* Dropdown list */} + {isUserDropdownOpen && ( +
+
+ انتخاب کاربر برای اضافه شدن به لیست شماره‌ها: + +
+ {isLoadingUsers ? ( +
در حال بارگذاری کاربران...
+ ) : systemUsers.length === 0 ? ( +
کاربری یافت نشد
+ ) : ( + systemUsers.map((u) => { + const uName = (u.firstName ? `${u.firstName} ${u.lastName || ''}` : 'بدون نام').trim(); + const uPhone = u.mobile || ''; + const isAlreadySelected = uPhone && ruleFormCustomPhone.includes(uPhone); + const roleBadge = u.role === 'Admin' ? 'مدیر' : u.role === 'Vet' ? 'دامپزشک' : 'کاربر'; + + return ( + + ); + }) + )} +
+ )} +
+ +
setRuleFormTitle(e.target.value)} - placeholder="مثلاً: ارسال مشخصات گیرنده به شرکت پست / انبار" - className="w-full border border-gray-200 rounded-xl p-3 outline-none focus:ring-2 focus:ring-amber-500 text-sm font-bold" + value={ruleFormCustomPhone} + onChange={(e) => setRuleFormCustomPhone(e.target.value)} + placeholder="مثلاً: 09123456789 (برای چند شماره با کاما یا ویرگول جدا کنید)" + className="w-full border border-amber-300 rounded-xl p-2.5 outline-none focus:ring-2 focus:ring-amber-500 text-xs font-mono font-bold bg-white" + dir="ltr" />
- {/* Event & Recipient Grid */} -
- {/* Trigger Event */} -
- - -
+

+ 💡 می‌توانید از جستجوی بالا مدیر یا کاربر مدنظرتان را انتخاب کنید یا شماره‌ها را مستقیماً تایپ نمایید. +

+
+ )} - {/* Recipient Type */} -
- - -
+ {/* Pattern ID Selection */} +
+
+ + انتخاب از الگوها یا درج دستی +
+ +
+ {/* Select from registered patterns */} +
+
- {/* Custom Phone Number Input / User Picker if custom */} - {ruleFormRecipientType === 'custom' && ( -
-
- - - انتخاب از کاربران/مدیران سیستم یا درج مستقیم - -
+ {/* Or Manual Body ID Input */} +
+ setRuleFormPatternId(e.target.value)} + placeholder="یا وارد کردن شناسه پترن جدید (مثلاً: 508081)" + className="w-full border border-gray-200 rounded-xl p-2.5 outline-none focus:ring-2 focus:ring-amber-500 text-xs font-mono font-bold bg-white" + dir="ltr" + /> +
+
- {/* Searchable User Selector */} -
-
-
- - { - const q = e.target.value; - setUserSearchQuery(q); - setIsUserDropdownOpen(true); - fetchSystemUsers(q); - }} - onFocus={() => { - setIsUserDropdownOpen(true); - if (systemUsers.length === 0) fetchSystemUsers(); - }} - placeholder="جستجوی کاربر یا مدیر در سیستم (نام، شماره تماس)..." - className="w-full pr-8 pl-3 py-2 bg-white border border-amber-300 rounded-xl text-xs outline-none focus:ring-2 focus:ring-amber-500" - /> -
+ {/* Show Preview of Selected Pattern Text if found */} + {(() => { + const selPat = patterns.find((p) => String(p.id) === String(ruleFormPatternId)); + if (!selPat) return null; + return ( +
+ متن الگو: + {selPat.body} +
+ ); + })()} +
+ + {/* Dynamic Variables Mapper */} +
+
+ + + کلیک روی متغیرها برای افزودن یا حذف + +
+ + {/* Current mapped variables */} +
+ {ruleFormVariables.length === 0 ? ( + + هیچ متغیری انتخاب نشده است. از لیست زیر متغیرهای مورد نظر را اضافه کنید. + + ) : ( + ruleFormVariables.map((vKey, idx) => { + const curEventDef = eventDefinitions.find((ed) => ed.event === ruleFormEvent); + const varMeta = curEventDef?.variables.find((v) => v.key === vKey); + return ( +
+ {`{${idx}}`}: + {varMeta ? varMeta.label : vKey}
- - {/* Dropdown list */} - {isUserDropdownOpen && ( -
-
- انتخاب کاربر برای اضافه شدن به لیست شماره‌ها: - -
- {isLoadingUsers ? ( -
در حال بارگذاری کاربران...
- ) : systemUsers.length === 0 ? ( -
کاربری یافت نشد
- ) : ( - systemUsers.map((u) => { - const uName = (u.firstName ? `${u.firstName} ${u.lastName || ''}` : 'بدون نام').trim(); - const uPhone = u.mobile || ''; - const isAlreadySelected = uPhone && ruleFormCustomPhone.includes(uPhone); - const roleBadge = u.role === 'Admin' ? 'مدیر' : u.role === 'Vet' ? 'دامپزشک' : 'کاربر'; - - return ( - - ); - }) - )} -
- )} -
- -
- setRuleFormCustomPhone(e.target.value)} - placeholder="مثلاً: 09123456789 (برای چند شماره با کاما یا ویرگول جدا کنید)" - className="w-full border border-amber-300 rounded-xl p-2.5 outline-none focus:ring-2 focus:ring-amber-500 text-xs font-mono font-bold bg-white" - dir="ltr" - /> -
- -

- 💡 می‌توانید از جستجوی بالا مدیر یا کاربر مدنظرتان را انتخاب کنید یا شماره‌ها را مستقیماً تایپ نمایید. -

-
- )} - - {/* Pattern ID Selection */} -
-
- - انتخاب از الگوها یا درج دستی -
- -
- {/* Select from registered patterns */} -
- -
- - {/* Or Manual Body ID Input */} -
- setRuleFormPatternId(e.target.value)} - placeholder="یا وارد کردن شناسه پترن جدید (مثلاً: 508081)" - className="w-full border border-gray-200 rounded-xl p-2.5 outline-none focus:ring-2 focus:ring-amber-500 text-xs font-mono font-bold bg-white" - dir="ltr" - /> -
-
- - {/* Show Preview of Selected Pattern Text if found */} - {(() => { - const selPat = patterns.find((p) => String(p.id) === String(ruleFormPatternId)); - if (!selPat) return null; - return ( -
- متن الگو: - {selPat.body} -
); - })()} -
+ }) + )} +
- {/* Dynamic Variables Mapper */} -
-
- - - کلیک روی متغیرها برای افزودن یا حذف + {/* Available Variables for the selected event */} + {(() => { + const curEventDef = eventDefinitions.find((ed) => ed.event === ruleFormEvent); + if (!curEventDef || curEventDef.variables.length === 0) return null; + + return ( +
+ + متغیرهای در دسترس برای رویداد «{curEventDef.label}»: -
- - {/* Current mapped variables */} -
- {ruleFormVariables.length === 0 ? ( - - هیچ متغیری انتخاب نشده است. از لیست زیر متغیرهای مورد نظر را اضافه کنید. - - ) : ( - ruleFormVariables.map((vKey, idx) => { - const curEventDef = eventDefinitions.find((ed) => ed.event === ruleFormEvent); - const varMeta = curEventDef?.variables.find((v) => v.key === vKey); - return ( -
- {`{${idx}}`}: - {varMeta ? varMeta.label : vKey} - -
- ); - }) - )} -
- - {/* Available Variables for the selected event */} - {(() => { - const curEventDef = eventDefinitions.find((ed) => ed.event === ruleFormEvent); - if (!curEventDef || curEventDef.variables.length === 0) return null; - - return ( -
- - متغیرهای در دسترس برای رویداد «{curEventDef.label}»: - -
+
{curEventDef.variables.map((v) => (
-
-
- )} + {/* Modal: Add New Pattern */} - {isAddModalOpen && ( -
-
-
-

- - درج الگوی جدید در سامانه ملی پیامک -

- + setIsAddModalOpen(false)} + title="درج الگوی جدید در سامانه ملی پیامک" + icon={Plus} + maxWidth="xl" + > +
+
+ + setNewPatternTitle(e.target.value)} + placeholder="مثلاً: تایید سفارش کنینا" + className="w-full border border-gray-200 rounded-xl p-3 outline-none focus:ring-2 focus:ring-purple-500 text-sm font-bold" + /> +
+ +
+
+ +
+ درج سریع متغیر: + {['{0}', '{1}', '{2}'].map((v) => ( + + ))} +
- -
- - setNewPatternTitle(e.target.value)} - placeholder="مثلاً: تایید سفارش کنینا" - className="w-full border border-gray-200 rounded-xl p-3 outline-none focus:ring-2 focus:ring-purple-500 text-sm font-bold" - /> -
+