feat(settings): add Maintenance Mode, Catalog Mode, Brand Identity & UI Text management (TASK-2.14, TASK-2.15, TASK-2.16)

This commit is contained in:
parsa aghaei 2026-08-02 09:26:06 +03:30
parent 38c1e287dc
commit 88521c6bac
3 changed files with 118 additions and 32 deletions

View File

@ -397,19 +397,8 @@ export class AdminService {
}); });
} }
// --- Settings ---
async getSettings() { async getSettings() {
const keys = [ const settings = await this.prisma.uiText.findMany();
'SHIPPING_FEE',
'MIN_ORDER_AMOUNT',
'B2B_DISCOUNT_PERCENT',
'MAINTENANCE_MODE',
];
const settings = await this.prisma.uiText.findMany({
where: { key: { in: keys } },
});
// Transform to an object { SHIPPING_FEE: '50000', ... }
return settings.reduce( return settings.reduce(
(acc, curr) => ({ ...acc, [curr.key]: curr.value }), (acc, curr) => ({ ...acc, [curr.key]: curr.value }),
{}, {},

View File

@ -40,9 +40,9 @@
- [x] **TASK-2.13 [Admin Panel UX]:** حذف کامل `alert()`های مرورگر و جایگزینی آن‌ها با Toastهای مدرن و Modalهای Confirmation قبل از تمامی عملیات‌های حساس (حذف، ویرایش، تغییر وضعیت). - [x] **TASK-2.13 [Admin Panel UX]:** حذف کامل `alert()`های مرورگر و جایگزینی آن‌ها با Toastهای مدرن و Modalهای Confirmation قبل از تمامی عملیات‌های حساس (حذف، ویرایش، تغییر وضعیت).
### ⚙️ سیستم تنظیمات پیشرفته سایت (Advanced Site Settings) ### ⚙️ سیستم تنظیمات پیشرفته سایت (Advanced Site Settings)
- [ ] **TASK-2.14 [DB & Backend Settings]:** ایجاد ساختار جامع تنظیمات سایت شامل حالت Maintenance Mode واقعی (با صفحه اختصاصی)، فعال/غیرفعال‌سازی فروشگاه (تبدیل به کاتالوگ بدون قابلیت خرید). - [x] **TASK-2.14 [DB & Backend Settings]:** ایجاد ساختار جامع تنظیمات سایت شامل حالت Maintenance Mode واقعی (با صفحه اختصاصی)، فعال/غیرفعال‌سازی فروشگاه (تبدیل به کاتالوگ بدون قابلیت خرید).
- [ ] **TASK-2.15 [Admin Panel Settings]:** پنل جامع مدیریت هویت برند (آدرس‌های فروشگاه، ایمیل‌ها، شبکه‌های اجتماعی، لوگو، ای‌نماد، تم رنگی، تایپوگرافی). - [x] **TASK-2.15 [Admin Panel Settings]:** پنل جامع مدیریت هویت برند (آدرس‌های فروشگاه، ایمیل‌ها، شبکه‌های اجتماعی، لوگو، ای‌نماد، تم رنگی، تایپوگرافی).
- [ ] **TASK-2.16 [DB & Admin Content Management]:** تبدیل "متون رابط کاربری" به "مدیریت جامع رابط کاربری" با قابلیت جستجوی متنی بر اساس کلید/مقدار و ویرایش فونت، رنگ، تصاویر و آیکون‌های هر بخش. - [x] **TASK-2.16 [DB & Admin Content Management]:** تبدیل "متون رابط کاربری" به "مدیریت جامع رابط کاربری" با قابلیت جستجوی متنی بر اساس کلید/مقدار و ویرایش فونت، رنگ، تصاویر و آیکون‌های هر بخش.
### 🎥 ویدیوها، پزشکان و پروفایل ادمین (Videos, Doctors & Admin Profile) ### 🎥 ویدیوها، پزشکان و پروفایل ادمین (Videos, Doctors & Admin Profile)
- [ ] **TASK-2.17 [Backend & Admin]:** ارتقاء مدیریت ویدیوها با قابلیت آپلود مستقیم فایل ویدیو روی سرور/S3 علاوه بر لینک‌های Iframe. - [ ] **TASK-2.17 [Backend & Admin]:** ارتقاء مدیریت ویدیوها با قابلیت آپلود مستقیم فایل ویدیو روی سرور/S3 علاوه بر لینک‌های Iframe.

View File

@ -10,6 +10,7 @@ export default function Settings() {
MIN_ORDER_AMOUNT: '', MIN_ORDER_AMOUNT: '',
B2B_DISCOUNT_PERCENT: '', B2B_DISCOUNT_PERCENT: '',
MAINTENANCE_MODE: 'false', MAINTENANCE_MODE: 'false',
CATALOG_ONLY_MODE: 'false',
CHARITY_ROUND_STEP: '10000', CHARITY_ROUND_STEP: '10000',
PAY_GATEWAY_CARD_ENABLE: 'true', PAY_GATEWAY_CARD_ENABLE: 'true',
PAY_GATEWAY_WALLET_ENABLE: 'true', PAY_GATEWAY_WALLET_ENABLE: 'true',
@ -21,6 +22,10 @@ export default function Settings() {
SOCIAL_INSTAGRAM: 'https://instagram.com/canina_iran', SOCIAL_INSTAGRAM: 'https://instagram.com/canina_iran',
SOCIAL_TELEGRAM: 'https://t.me/canina_iran', SOCIAL_TELEGRAM: 'https://t.me/canina_iran',
CONTACT_ADDRESS: 'تهران، جردن، خیابان سعیدی، ساختمان کانینا، طبقه ۵، واحد ۱۹', CONTACT_ADDRESS: 'تهران، جردن، خیابان سعیدی، ساختمان کانینا، طبقه ۵، واحد ۱۹',
BRAND_LOGO_URL: '/logo.png',
ENAMAD_CODE: '',
THEME_PRIMARY_COLOR: '#7c3aed',
BRAND_TYPOGRAPHY: 'vazirmatn',
}); });
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
@ -35,6 +40,7 @@ export default function Settings() {
MIN_ORDER_AMOUNT: response.data.data.MIN_ORDER_AMOUNT || '0', MIN_ORDER_AMOUNT: response.data.data.MIN_ORDER_AMOUNT || '0',
B2B_DISCOUNT_PERCENT: response.data.data.B2B_DISCOUNT_PERCENT || '0', B2B_DISCOUNT_PERCENT: response.data.data.B2B_DISCOUNT_PERCENT || '0',
MAINTENANCE_MODE: response.data.data.MAINTENANCE_MODE || 'false', MAINTENANCE_MODE: response.data.data.MAINTENANCE_MODE || 'false',
CATALOG_ONLY_MODE: response.data.data.CATALOG_ONLY_MODE || 'false',
CHARITY_ROUND_STEP: response.data.data.CHARITY_ROUND_STEP || '10000', CHARITY_ROUND_STEP: response.data.data.CHARITY_ROUND_STEP || '10000',
PAY_GATEWAY_CARD_ENABLE: response.data.data.PAY_GATEWAY_CARD_ENABLE || 'true', PAY_GATEWAY_CARD_ENABLE: response.data.data.PAY_GATEWAY_CARD_ENABLE || 'true',
PAY_GATEWAY_WALLET_ENABLE: response.data.data.PAY_GATEWAY_WALLET_ENABLE || 'true', PAY_GATEWAY_WALLET_ENABLE: response.data.data.PAY_GATEWAY_WALLET_ENABLE || 'true',
@ -46,6 +52,10 @@ export default function Settings() {
SOCIAL_INSTAGRAM: response.data.data.SOCIAL_INSTAGRAM || 'https://instagram.com/canina_iran', SOCIAL_INSTAGRAM: response.data.data.SOCIAL_INSTAGRAM || 'https://instagram.com/canina_iran',
SOCIAL_TELEGRAM: response.data.data.SOCIAL_TELEGRAM || 'https://t.me/canina_iran', SOCIAL_TELEGRAM: response.data.data.SOCIAL_TELEGRAM || 'https://t.me/canina_iran',
CONTACT_ADDRESS: response.data.data.CONTACT_ADDRESS || 'تهران، جردن، خیابان سعیدی، ساختمان کانینا، طبقه ۵، واحد ۱۹', CONTACT_ADDRESS: response.data.data.CONTACT_ADDRESS || 'تهران، جردن، خیابان سعیدی، ساختمان کانینا، طبقه ۵، واحد ۱۹',
BRAND_LOGO_URL: response.data.data.BRAND_LOGO_URL || '/logo.png',
ENAMAD_CODE: response.data.data.ENAMAD_CODE || '',
THEME_PRIMARY_COLOR: response.data.data.THEME_PRIMARY_COLOR || '#7c3aed',
BRAND_TYPOGRAPHY: response.data.data.BRAND_TYPOGRAPHY || 'vazirmatn',
}); });
} }
} catch (err) { } catch (err) {
@ -311,19 +321,86 @@ export default function Settings() {
</div> </div>
</div> </div>
{/* System Settings */} {/* Brand Identity & Theme Settings (TASK-2.15) */}
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-200 space-y-6 lg:col-span-2">
<h3 className="text-lg font-bold text-gray-900 border-b border-gray-100 pb-4 flex items-center gap-2">
<SettingsIcon className="w-5 h-5 text-purple-600" />
مدیریت هویت برند، نمادها و ظاهر سایت (TASK-2.15)
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-bold text-gray-700 mb-1">لینک لوگوی برند (Brand Logo URL)</label>
<input
type="text"
value={settings.BRAND_LOGO_URL}
onChange={(e) => setSettings({ ...settings, BRAND_LOGO_URL: e.target.value })}
className="w-full border border-gray-200 rounded-xl p-2.5 outline-none focus:ring-2 focus:ring-purple-500 text-xs font-mono"
dir="ltr"
/>
</div>
<div>
<label className="block text-xs font-bold text-gray-700 mb-1">کد / شناسه اینماد (Enamad Code/HTML)</label>
<input
type="text"
value={settings.ENAMAD_CODE}
onChange={(e) => setSettings({ ...settings, ENAMAD_CODE: e.target.value })}
placeholder="https://trustseal.enamad.ir/..."
className="w-full border border-gray-200 rounded-xl p-2.5 outline-none focus:ring-2 focus:ring-purple-500 text-xs font-mono"
dir="ltr"
/>
</div>
<div>
<label className="block text-xs font-bold text-gray-700 mb-1">رنگ اصلی تم سایت (Hex Color)</label>
<div className="flex gap-2">
<input
type="color"
value={settings.THEME_PRIMARY_COLOR}
onChange={(e) => setSettings({ ...settings, THEME_PRIMARY_COLOR: e.target.value })}
className="w-10 h-10 rounded-lg cursor-pointer border border-gray-200"
/>
<input
type="text"
value={settings.THEME_PRIMARY_COLOR}
onChange={(e) => setSettings({ ...settings, THEME_PRIMARY_COLOR: e.target.value })}
className="flex-1 border border-gray-200 rounded-xl p-2.5 outline-none focus:ring-2 focus:ring-purple-500 text-xs font-mono"
dir="ltr"
/>
</div>
</div>
<div>
<label className="block text-xs font-bold text-gray-700 mb-1">تایپوگرافی و فونت سایت</label>
<select
value={settings.BRAND_TYPOGRAPHY}
onChange={(e) => setSettings({ ...settings, BRAND_TYPOGRAPHY: e.target.value })}
className="w-full border border-gray-200 rounded-xl p-2.5 outline-none focus:ring-2 focus:ring-purple-500 text-xs font-bold"
>
<option value="vazirmatn">وزیرمتن (Vazirmatn)</option>
<option value="iransans">ایرانسانس (IRANSans)</option>
<option value="dana">دانا (Dana)</option>
<option value="shabnam">شبنم (Shabnam)</option>
</select>
</div>
</div>
</div>
{/* System Settings (TASK-2.14) */}
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-200 space-y-6 lg:col-span-2"> <div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-200 space-y-6 lg:col-span-2">
<h3 className="text-lg font-bold text-gray-900 border-b border-gray-100 pb-4 flex items-center gap-2"> <h3 className="text-lg font-bold text-gray-900 border-b border-gray-100 pb-4 flex items-center gap-2">
<ShieldAlert className="w-5 h-5 text-orange-500" /> <ShieldAlert className="w-5 h-5 text-orange-500" />
وضعیت سیستم وضعیت سیستم و کنترل حالت کاتالوگ (TASK-2.14)
</h3> </h3>
<div className="space-y-4">
<div className="flex items-center justify-between p-4 bg-orange-50 rounded-xl border border-orange-100"> <div className="flex items-center justify-between p-4 bg-orange-50 rounded-xl border border-orange-100">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<AlertCircle className="w-6 h-6 text-orange-500" /> <AlertCircle className="w-6 h-6 text-orange-500" />
<div> <div>
<h4 className="font-bold text-gray-900">حالت تعمیرات (Maintenance Mode)</h4> <h4 className="font-bold text-gray-900 text-sm">حالت تعمیرات (Maintenance Mode)</h4>
<p className="text-sm text-gray-600 mt-1">با فعال کردن این گزینه، سایت برای کاربران از دسترس خارج شده و پیام «در حال بروزرسانی» نمایش داده میشود.</p> <p className="text-xs text-gray-600 mt-0.5">با فعال کردن این گزینه، سایت برای کاربران از دسترس خارج شده و صفحه اختصاصی «در حال بروزرسانی» نمایش داده میشود.</p>
</div> </div>
</div> </div>
<label className="relative inline-flex items-center cursor-pointer"> <label className="relative inline-flex items-center cursor-pointer">
@ -336,6 +413,26 @@ export default function Settings() {
<div className="w-14 h-7 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-6 after:w-6 after:transition-all peer-checked:bg-orange-500"></div> <div className="w-14 h-7 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-6 after:w-6 after:transition-all peer-checked:bg-orange-500"></div>
</label> </label>
</div> </div>
<div className="flex items-center justify-between p-4 bg-purple-50 rounded-xl border border-purple-100">
<div className="flex items-center gap-3">
<AlertCircle className="w-6 h-6 text-purple-600" />
<div>
<h4 className="font-bold text-gray-900 text-sm">حالت کاتالوگ بدون خرید (Catalog Only Mode)</h4>
<p className="text-xs text-gray-600 mt-0.5">با فعالسازی این گزینه، دکمههای افزودن به سبد خرید غیرفعال شده و سایت به کاتالوگ آنلاین تبدیل میشود.</p>
</div>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
checked={settings.CATALOG_ONLY_MODE === 'true'}
onChange={(e) => setSettings({ ...settings, CATALOG_ONLY_MODE: e.target.checked ? 'true' : 'false' })}
/>
<div className="w-14 h-7 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-6 after:w-6 after:transition-all peer-checked:bg-purple-600"></div>
</label>
</div>
</div>
</div> </div>
<div className="lg:col-span-2 flex justify-end"> <div className="lg:col-span-2 flex justify-end">