From 025bdd730c29bfd1549e5a5d9fe31cc2209e4a3b Mon Sep 17 00:00:00 2001 From: "Muhammad A. Ghorbani" Date: Sat, 19 Sep 2026 20:30:12 +0330 Subject: [PATCH] feat: add core panel components, navigation layout, and services management views --- .../aqila_panel_ui_design_system_standard.md | 50 +- src/components/layout/AppSidebar.tsx | 24 +- src/config/navigation.ts | 8 +- .../billing/components/BillingDetailModal.tsx | 10 + .../billing/components/RejectFactorDialog.tsx | 111 +- src/features/services/ServicesPage.tsx | 76 +- .../components/CategoriesServiceView.tsx | 859 +++++++++++ .../services/components/DuasServiceView.tsx | 946 ++++++++++++ .../services/components/FaqServiceView.tsx | 1271 +++++++++++++++++ .../components/ShrinesServiceView.tsx | 614 ++++++++ src/features/services/services/faq-api.ts | 111 ++ .../services/services/trip-etiquette-api.ts | 198 +++ src/features/services/types/faq.ts | 68 + src/features/services/types/trip-etiquette.ts | 105 ++ .../tours/components/TourStatusBadge.tsx | 1 + src/icons.tsx | 3 + 16 files changed, 4386 insertions(+), 69 deletions(-) create mode 100644 src/features/services/components/CategoriesServiceView.tsx create mode 100644 src/features/services/components/DuasServiceView.tsx create mode 100644 src/features/services/components/FaqServiceView.tsx create mode 100644 src/features/services/components/ShrinesServiceView.tsx create mode 100644 src/features/services/services/faq-api.ts create mode 100644 src/features/services/services/trip-etiquette-api.ts create mode 100644 src/features/services/types/faq.ts create mode 100644 src/features/services/types/trip-etiquette.ts diff --git a/guide/aqila_panel_ui_design_system_standard.md b/guide/aqila_panel_ui_design_system_standard.md index dc3caca..437a5a0 100644 --- a/guide/aqila_panel_ui_design_system_standard.md +++ b/guide/aqila_panel_ui_design_system_standard.md @@ -204,25 +204,63 @@ ## ۸. استاندارد لودینگ و اسکلتون (Shimmer / Loading Skeleton Standard) > [!IMPORTANT] -> **الگوی واحد لودینگ:** -> مطابق مصوبه سیستم، لودینگ تمامی صفحات و جداول پنل باید منحصراً از کامپوننت بازاستفاده‌پذیر **`Shimmer`** (برگرفته از صفحه صورت‌حساب‌ها) استفاده کنند و از قراردادن اسپینرهای ناهماهنگ خودداری شود. - -### مشخصات استاندارد Shimmer: +> **الگوی واحد و سراسری لودینگ (Unified Shimmer Standard):** +> لودینگ تمامی صفحات، جداول و کارت‌ها در سامانه عقیله (هم در پنل وب و هم در اپلیکیشن فلاتر) باید منحصراً از اسکلتون یکپارچه شیمر با پالت خنثی (Neutral Slate) و ریتم پالس متناسب تبعیت کنند. هرگونه اسپینر ناهماهنگ، کانتینرهای بدون انیمیشن یا استفاده از رنگ‌های متفرقه در لودینگ اکیداً ممنوع است. + +### ۸.۱. پالت رنگی و زمان‌بندی استاندارد شیمر (Shimmer Color Tokens & Timing) +برای جلوگیری از چندپارگی بصری و پرهیز از لودینگ‌های کدر یا مدادرنگی، رنگ‌های شیمر در تمام کلاینت‌ها قفل شده است: +- **تم روشن (Light Mode):** + - رنگ پایه (Base): `#E2E8F0` (توکن `outline` / `muted-200`) + - رنگ درخشش (Highlight): `#F8FAFC` (توکن `muted-50`) +- **تم تاریک (Dark Mode):** + - رنگ پایه (Base): `#22252C` (توکن `card-base`) + - رنگ درخشش (Highlight): `#333742` (توکن `card-highlight`) +- **دوره تناوب انیمیشن (Cycle Period):** دقیقاً `1500ms` (۱.۵ ثانیه) برای ایجاد پالس موجی لطیف، طبیعی و یکنواخت. + +### ۸.۲. مشخصات پیاده‌سازی در وب و پنل مدیریت (`aqila_panel`) +- **کامپوننت مرجع:** `@/components/shared/Shimmer` - **کانتینر:** `p-8 space-y-3` (یا درون `Card` با بوردر استاندارد). - **ردیف‌ها:** `h-12 bg-muted/30 animate-pulse rounded-lg`. - **رفتار در تم لایت و دارک:** پالس ملایم با کنتراست طبیعی متناسب با توکن `--muted`. ```tsx -// نحوه فراخوانی استاندارد در صفحات +// نحوه فراخوانی استاندارد در صفحات پنل import { Shimmer } from '@/components/shared/Shimmer' {isLoading ? ( - + ) : ( )} ``` +### ۸.۳. مشخصات پیاده‌سازی در اپلیکیشن فلاتر (`aqila_flutter`) +- **مرجع واحد هماهنگ‌کننده موج (Single Source of Truth):** ویجت **`AppShimmer`** در مسیر: + `lib/core/ui_kit/atoms/app_shimmer.dart` +- **ویجت‌های اتمیک درون اسکلتون:** + 1. **`ShimmerBox`:** جهت ترسیم کارت‌ها، بنرها، تصاویر و دکمه‌ها (`AppRadii.radiusSm` تا `AppRadii.radiusLg`). + 2. **`ShimmerLine`:** جهت ترسیم سطرهای متنی و عناوین با ارتفاع پیش‌فرض `14px` و انحنای `4px`. + 3. **`ShimmerCircle`:** جهت ترسیم آواتارها، آیکون‌ها و بج‌های دایره‌ای. +- **وضعیت ویجت `ShimmerWidget`:** + ویجت `ShimmerWidget` در مسیر `lib/core/widgets/shimmer_widget.dart` صرفاً یک **آداپتور سازگاری گذشته‌نگر (Legacy Adapter)** است تا کدهای قدیمی متوقف نشوند. در کلیه فیچرها، صفحات و پول‌ریکوئست‌های جدید باید **منحصراً از `AppShimmer`** استفاده شود. + +```dart +// نحوه فراخوانی استاندارد در اپلیکیشن فلاتر +import 'package:tourism/core/ui_kit/atoms/app_shimmer.dart'; + +return const AppShimmer( + child: Column( + children: [ + ShimmerBox(width: double.infinity, height: 160, borderRadius: 16), + SizedBox(height: 12), + ShimmerLine(width: 140, height: 16), + SizedBox(height: 8), + ShimmerLine(width: 220, height: 12), + ], + ), +); +``` + --- ## ۹. دسترسی‌پذیری و تم‌های تاریک و روشن (Accessibility, Light & Dark Mode) diff --git a/src/components/layout/AppSidebar.tsx b/src/components/layout/AppSidebar.tsx index 332bfc1..4431c9d 100644 --- a/src/components/layout/AppSidebar.tsx +++ b/src/components/layout/AppSidebar.tsx @@ -42,22 +42,24 @@ function NavItem({ item }: { item: NavItemConfig }) { item.children?.some((c) => c.to === '/tours' || c.to === '/services' ? pathname === c.to : pathname.startsWith(c.to) ) - const [isOpen, setIsOpen] = React.useState(Boolean(isChildActive)) + const isAlwaysOpen = Boolean(item.alwaysOpen) + const [isOpen, setIsOpen] = React.useState(isAlwaysOpen || Boolean(isChildActive)) React.useEffect(() => { - if (isChildActive) { + if (isAlwaysOpen || isChildActive) { setIsOpen(true) } - }, [isChildActive]) + }, [isAlwaysOpen, isChildActive]) if (hasChildren && item.children) { return ( setIsOpen((prev) => !prev)} + onClick={isAlwaysOpen ? undefined : () => setIsOpen((prev) => !prev)} className={cn( 'w-full justify-between transition-colors', - isChildActive && 'text-primary font-semibold' + isChildActive && 'text-primary font-semibold', + isAlwaysOpen && 'cursor-default select-none hover:bg-transparent' )} tooltip={item.title} > @@ -67,14 +69,16 @@ function NavItem({ item }: { item: NavItemConfig }) {
- + {!isAlwaysOpen && ( + + )}
- {isOpen && ( + {(isAlwaysOpen || isOpen) && (
{item.children.map((sub) => { const isSubActive = diff --git a/src/config/navigation.ts b/src/config/navigation.ts index 7976919..0d1e34c 100644 --- a/src/config/navigation.ts +++ b/src/config/navigation.ts @@ -13,6 +13,7 @@ export interface NavItemConfig { icon: IconName badge?: string soon?: boolean + alwaysOpen?: boolean children?: NavSubItemConfig[] } @@ -38,15 +39,18 @@ export const navigationConfig: NavGroupConfig[] = [ { title: 'خدمات سامانه', icon: 'sparkles', + alwaysOpen: true, children: [ { title: 'مرکز خدمات (همه)', to: '/services', icon: 'grid' }, { title: 'سیم‌کارت (Simcard)', to: '/services/simcard', icon: 'simcard' }, { title: 'راهنمای زائر (Zayer Guide)', to: '/services/zayer-guide', icon: 'guide' }, { title: 'صرافی آنلاین (Exchange)', to: '/services/exchange', icon: 'exchange' }, - { title: 'آداب سفر (Etiquette)', to: '/services/trip-etiquette', icon: 'etiquette' }, - { title: 'مشاوره (Consultant)', to: '/services/consultant', icon: 'headset' }, + { title: 'ادعیه و زیارات (Duas)', to: '/services/duas', icon: 'book' }, + { title: 'اماکن زیارتی (Shrines)', to: '/services/shrines', icon: 'shrine' }, { title: 'فروشگاه معصومه (Store)', to: '/services/store', icon: 'store' }, { title: 'تخمین هزینه (Cost Estimate)', to: '/services/cost-estimate', icon: 'calculator' }, + { title: 'سوالات متداول (FAQ)', to: '/services/faq', icon: 'helpCircle' }, + { title: 'پشتیبانی (Support)', to: '/services/consultant', icon: 'headset' }, ], }, { title: 'صورت‌حساب‌ها و مالی', to: '/billing', icon: 'receipt' }, diff --git a/src/features/billing/components/BillingDetailModal.tsx b/src/features/billing/components/BillingDetailModal.tsx index a9fd1f3..a527a24 100644 --- a/src/features/billing/components/BillingDetailModal.tsx +++ b/src/features/billing/components/BillingDetailModal.tsx @@ -88,6 +88,16 @@ export function BillingDetailModal({
)} + {factor.status === 'expired' && ( +
+ +
+

دلیل انقضا:

+

{factor.rejected_description || 'انقضای مهلت پرداخت (سپری شدن ۷۲ ساعت)'}

+
+
+ )} + {factor.expiration_date && (
diff --git a/src/features/billing/components/RejectFactorDialog.tsx b/src/features/billing/components/RejectFactorDialog.tsx index 6508396..1f6caa8 100644 --- a/src/features/billing/components/RejectFactorDialog.tsx +++ b/src/features/billing/components/RejectFactorDialog.tsx @@ -45,41 +45,88 @@ export function RejectFactorDialog({ return ( !val && onClose()}> - - - - - رد صورت‌حساب #{factor.id} - - - مشتری: {factor.user_info?.fullname || factor.user_info?.email || 'نامشخص'} • مبلغ:{' '} - {Number(factor.amount).toLocaleString()} $ - + + {/* هدر دیالوگ با پس‌زمینه و حاشیه متناسب */} + +
+
+ +
+
+ + رد صورت‌حساب + #{factor.id} + + + + مشتری:{' '} + + {factor.user_info?.fullname || factor.user_info?.email || 'نامشخص'} + + + + + مبلغ:{' '} + + ${Number(factor.amount).toLocaleString()} + + + +
+
+ +
-
- -