feat: complete admin panel and backend admin api migration
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,291 @@
|
||||
# تاریخچهی کامل پورت — henza_admin → gold-banking-monorepo
|
||||
|
||||
> این سند خلاصهی کامل و دقیق تمام کارهایی است که تا این لحظه روی پروژه انجام شده،
|
||||
> از شروع پروژه تا آخرین commit. برای جزئیات ریزتر batch به batch به
|
||||
> [`ADMIN-PORT-PLAN.md`](./ADMIN-PORT-PLAN.md) مراجعه کنید — آن سند مرجع فنی
|
||||
> زنده است، این سند خلاصهی روایی و تاریخی.
|
||||
|
||||
**تاریخ نگارش:** ۱۳ شهریور ۱۴۰۵ (۲۰۲۶-۰۹-۱۳)
|
||||
|
||||
---
|
||||
|
||||
## ۱. هدف پروژه
|
||||
|
||||
پورت کامل و بدون تغییر پنل ادمین legacy جواهرات/طلا (`henza_admin`، Vue 2 +
|
||||
Vuesax + قالب Vuexy) به `apps/admin-panel` در مونوریپوی
|
||||
`gold-banking-monorepo` (Vue 3 + Vite + Pinia + ant-design-vue)، همراه با
|
||||
ساخت بکاند NestJS متناظر برای هر بخش.
|
||||
|
||||
### قوانین صریح کارفرما (که در تمام مسیر رعایت شدند)
|
||||
|
||||
- «همه رو با دقت و بدون دست خوردن میخوام» — پورت وفادار، بدون تغییر منطق
|
||||
- باگهای legacy **نباید ساکت اصلاح شوند** — باید عیناً پورت و **جداگانه گزارش**
|
||||
شوند
|
||||
- حفظ دقیق قرارداد اعداد فارسی/لاتین و رفتار RTL همانطور که در legacy بود
|
||||
- **`henza_server/` و `henza_pwa/` (و بعداً `henza_admin/`) فقط خواندنی —
|
||||
هرگز تغییر داده نشوند**
|
||||
- اول پلن، بعد اجرا batch به batch
|
||||
- پشتیبانی از هر دو حالت ادمین و شعبه (`isBranch()`)
|
||||
- ACL «دقیقا پایبند به ساختار» — وفاداری کامل به مدل دانهریز
|
||||
`permission(rid, ut)` در legacy
|
||||
- لاگین واقعی بکاند، نه mock
|
||||
- حذف ~۳۹۴ فایل دمو الگوی Vuexy
|
||||
- «بکاند همراه هر بخش» — هر batch با endpoint های خودش میآید
|
||||
- «REST تمیز + لایهی نگاشت» در فرانتاند
|
||||
|
||||
---
|
||||
|
||||
## ۲. مراحل اصلی پروژه (Phase 0 تا ممیزی نهایی)
|
||||
|
||||
### Phase 0-2: زیرساخت
|
||||
- راهاندازی مونوریپو با pnpm workspaces + Turborepo
|
||||
- طراحی schema اولیهی Prisma (اکنون **۳۵ مدل**)
|
||||
- ساخت لایهی auth واقعی: `@nestjs/jwt@12` + `@nestjs/passport@10.0.3`
|
||||
(pinشده برای سازگاری با NestJS 10) + bcryptjs
|
||||
- تبدیل مکانیکی روترهای legacy (`router/admin.js` + `router/branch.js`) به
|
||||
TypeScript، با حفظ ۱۰۰٪ مسیرها، نامها، `meta.role` و breadcrumb ها
|
||||
- تبدیل مکانیکی `permissionConst.ts` — تمام ۱۸۱ برچسب فارسی بایتبهبایت
|
||||
یکسان (با diff تأیید شد)
|
||||
|
||||
### Batch های ۳ تا ۱۵: پورت view به view
|
||||
پورت کامل **۶۹ view** به همراه بکاند متناظر هر batch. جزئیات کامل هر batch
|
||||
(فایلها، تصمیمها، مشکلات) در `ADMIN-PORT-PLAN.md` ثبت شده.
|
||||
|
||||
### تصمیمهای کلیدی کارفرما در طول مسیر
|
||||
1. **کامپوننت مشترک بهجای کپی فایلبهفایل** برای ۵ لیست عمدهی تقریباً
|
||||
یکسان و ۴ fork از XTable
|
||||
2. **توکنهای طراحی از خودِ legacy ادمین** (`secondary #836447`، فونت
|
||||
`p-yek`) — نه هماهنگ با اپلیکیشن کاربر که قبلاً ساخته شده بود
|
||||
|
||||
### ممیزی نهایی (Phase 16)
|
||||
پس از تکمیل ۶۹/۶۹ view، یک ممیزی جامع اجرا شد که یافتههای معماری مهمی را
|
||||
آشکار کرد:
|
||||
- «فاکتور» در legacy در واقع تخصیص موجودی شعبه است، نه فاکتور فروش
|
||||
- ردیفهای «صندوق» در legacy همان `LedgerEntry` هستند
|
||||
- هر دو داشبورد عمدتاً کامنتشدهی دمو Vuexy بودند
|
||||
- ApexCharts import شده ولی هرگز در markup زنده استفاده نشده
|
||||
- **۳۶ باگ/کوییرک legacy** ثبت و مستند شد
|
||||
- **۶ رفتار مخرب دادهی legacy عمداً پورت نشدند** (حذف سند تاییدشده، ویرایش
|
||||
مبلغ تاییدشده، حذف دستهبندی دارای وابسته، ایجاد چرخه در دستهبندی، یتیمکردن
|
||||
ردیابی ممیزی) — هرکدام با پیام ۴۰۰ فارسی جایگزین شد
|
||||
|
||||
---
|
||||
|
||||
## ۳. بستن ۷ شکاف schema (جلسهی اخیر)
|
||||
|
||||
پس از ممیزی نهایی، ۷ شکاف باقیمانده در schema شناسایی و به ترتیب بسته شد.
|
||||
برای هر شکاف، منطق مستقیماً از **سرور legacy** (`henza_server`) خوانده شد،
|
||||
نه فقط از view ها — این روش یک فرض غلط قبلی را هم آشکار کرد.
|
||||
|
||||
| # | شکاف | راهحل |
|
||||
|---|---|---|
|
||||
| ۱ | تفکیک بنکدار/مغازهدار | enum `CustomerKind` با **۵** مقدار (`Customer::$TYPES` سرور) |
|
||||
| ۲ | تفکیک سفارش تک/عمده | enum `OrderChannel` (`RETAIL` \| `WHOLESALE`) |
|
||||
| ۳ | ستون بایگانی مجزا | `Customer.isArchived` |
|
||||
| ۴ | دو سوییچ مجزای مشتری | `Customer.canPlaceOrders` جدا از `isActive` |
|
||||
| ۵ | ستونهای تجاری مشتری | `phone`, `address`, `storeName`, `pasazhName`, `zone`, `memberCode` |
|
||||
| ۶ | endpoint آپلود فایل | مدل `UploadedFile` + `POST admin/files/create` + سرو استاتیک `/uploads/` |
|
||||
| ۷ | آواتار کارشناس فروش | `Customer.avatarFileId` (رابطهی نامدار `CustomerAvatar` به `UploadedFile`) |
|
||||
|
||||
### یافتههای مهم از خواندن مستقیم سرور legacy
|
||||
|
||||
**منبع:** `AdminController::getBanker/getShopkeeper/getCustomer` و
|
||||
`MajorCustomerController::indexArchive/indexVerify` در `henza_server`.
|
||||
|
||||
- `banker` (بنکدار) → `type=4` · `shopkeeper` (حضوری) → `type=3` **فقط** ·
|
||||
لیست اصلی عمده (اپلیکیشن) → `type=2`
|
||||
- **اصلاح یک فرض قبلی:** پیشتر نوشته شده بود که لیست اصلی عمده «بدون فیلتر»
|
||||
است. این درست نبود؛ خوانده و تصحیح شد.
|
||||
- هر سه لیست نوع، فیلتر `is_archive=0` هم دارند — یعنی بایگانیکردن یک مشتری
|
||||
او را از **همهی** لیستهای نوع حذف میکند، نه فقط از یکی.
|
||||
- `Customer::$TYPES` سرور یک مقدار پنجم دارد (`۵ سرپرست کارگاه`) که فرم
|
||||
`bulkCustomer.vue` هرگز نشان نمیداد. enum جدید هر پنج مقدار را دارد، اما
|
||||
فرم پورتشده عمداً همان چهارتای legacy را نشان میدهد (برای وفاداری کامل
|
||||
به رفتار دیدهشده در UI).
|
||||
- سفارش تک و عمده در legacy در **دو جدول کاملاً جدا** بودند (`orders` و
|
||||
`orders_orders`)؛ هیچکدام از دو کنترلر فیلتر نوع نداشتند چون **خود جدول**
|
||||
تفکیککننده بود، پس دو لیست هرگز همپوشانی نداشتند. `Order.channel` همان
|
||||
نقش را در یک مدل واحد بازی میکند.
|
||||
- آواتار در legacy یک **FK** بود (`image_id`) نه یک URL ساده، و در پاسخ بهشکل
|
||||
تودرتوی `image: { id, image }` برمیگشت — هر دو نیمهی این قرارداد عیناً
|
||||
حفظ شد.
|
||||
|
||||
### دو رفتار legacy که عمداً پورت **نشدند**
|
||||
|
||||
۱. **آپلود بدون احراز هویت.** در legacy، `file/create` هیچ middleware نداشت
|
||||
و گروه `auth:api` هم در `routes/admin.php:12` کامنت بود — یعنی آپلود
|
||||
ناشناس میپذیرفت. این یک باگ امنیتی بود، نه قابلیت؛ در پورت، ادمینِ
|
||||
واردشده الزامی است. rid اختصاصی هم ساخته نشد چون واژگان ACL موجود در
|
||||
legacy ورودی media/file ندارد.
|
||||
۲. **ارسال پیامک درون `toggleActive`.** legacy هنگام فعالسازی مستقیم SMS
|
||||
میفرستاد. این به ماژول اعلانها موکول شد (درگاه پیامک هنوز واقعاً وصل
|
||||
نیست، طبق تصمیم قبلی).
|
||||
۳. **باگ در بارگذاری آواتار.** مسیر لود legacy در
|
||||
`ReagentCustomer.vue:280` بدون محافظ به `data.image.id` دست میزد، پس باز
|
||||
کردن یک کارشناس فروش بدون عکس throw میکرد (که خودِ `catch` میبلعید و
|
||||
فرم نیمهپر میماند). در پورت این خواندن guard شده است.
|
||||
|
||||
### تأیید اجرایی این بخش
|
||||
|
||||
```
|
||||
لیستها: عمده(t2)=۱ · بنکدار(t4)=۲ · حضوری(t3)=۲ · بایگانی=۱ · تاییدنشده=۲
|
||||
سوییچها: toggle-sts فقط sts را جابهجا کرد؛ toggle-active فقط active
|
||||
بایگانی: ردیف از لیست نوع خارج و به بایگانی رفت، بدون دستخوردن active
|
||||
سفارش: خردهفروشی=۳ · عمده=۱
|
||||
آپلود: ۲۰۰ + فایل از URL خودش بایتبهبایت برگشت؛ mime نامعتبر/حجم
|
||||
زیاد/بدون فایل → ۴۰۰ فارسی؛ درخواست ناشناس → ۴۰۱
|
||||
آواتار: آپلود → ساخت با image_id → خواندن image:{id,image} → پیشنمایش
|
||||
۲۰۰ → حذف → هر دو null؛ مشتری بدون آواتار → null (نه خطا)
|
||||
ممیزی: ۵۹/۵۹ endpoint خواندنی = ۲۰۰ · ACL ۱۴۴/۱۴۴ رید · ۱۵۴ مسیر ادمین
|
||||
بیلد: api ✓ · admin ✓ · branch ✓ · هر دو typecheck ✓
|
||||
```
|
||||
|
||||
تستهای نوشتن فقط روی مشتریهای تستیای انجام شد که در همان جلسه ساخته و
|
||||
بعد حذف شدند — دادهی seed اصلی هرگز دستخورد نکرد.
|
||||
|
||||
---
|
||||
|
||||
## ۴. لاگین تستی حالت توسعه (جلسهی جاری)
|
||||
|
||||
طبق درخواست صریح، شمارهی **۰۹۱۲۸۰۸۱۳۹۲** برای ورود آزمایشی در **هر دو**
|
||||
اپلیکیشن مجاز شد — فقط برای محیط توسعه:
|
||||
|
||||
- **پنل ادمین:** یک `AdminUser` با نقش super admin (`userTypeId: 85`) ساخته
|
||||
شد؛ رمز عبور دقیقاً برابر خودِ شماره تلفن است.
|
||||
- **اپلیکیشن کاربر (user-app):** همان شماره بهعنوان یک `Customer` تاییدشده
|
||||
(`VERIFIED`) اضافه شد. جریان OTP در `LoginView.vue` از قبل stub بود
|
||||
(هر کد ۴ رقمی برای یک مشتریِ واقعیِ منطبق پذیرفته میشود)، پس این مشتری
|
||||
بهمحض ورود شماره در گام اول شناسایی و قابل ورود است.
|
||||
- هر دو ردیف داخل خودِ `prisma/seed.ts` با کامنت **DEV-ONLY** مشخص شدهاند
|
||||
و هرگز در استقرار واقعی اجرا نمیشوند — این فایل فقط با اجرای دستیِ
|
||||
`npx prisma db seed` روی دیتابیس محلی اجرا میشود.
|
||||
|
||||
**✅ فعال و تأییدشده.** `npx prisma db seed` اجرا شد و هر دو ردیف در
|
||||
دیتابیس محلی ساخته شدند. لاگین بهصورت زنده تست شد:
|
||||
|
||||
```
|
||||
POST /api/admin/auth/login { phone: '09128081392', password: '09128081392' }
|
||||
→ 200، super admin، ۱۴۴ rid، JWT صادر شد
|
||||
|
||||
GET /api/customers
|
||||
→ مشتری «کاربر تست توسعه» با mobile=09128081392 در لیست موجود و قابل
|
||||
شناسایی توسط گام اول OTP در LoginView.vue
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ۵. نقد پورت و رفع مشکلات پنل ادمین
|
||||
|
||||
کارفرما گزارش داد که پنل ادمین «هم دیزاین هم کارکرد» مشکل دارد و خواست که
|
||||
**خودم پورت خودم را نقد کنم**. دو ممیزی مستقل (ظاهر + کارکرد) اجرا شد و
|
||||
یافتههای کلیدیشان را با خواندن مستقیم فایلها تأیید کردم.
|
||||
|
||||
### سه اشتباه اصلی که مرتکب شده بودم
|
||||
|
||||
**۱. فرض کردم کلاسهای Tailwind کار میکنند.** legacy یک بیلد کامل Tailwind
|
||||
داشت (`tailwind.config.js`، ~۲۸KB) با مقیاس سفارشی `mb-base: 2.2rem`. من
|
||||
`tailwindcss` را در `apps/admin-panel` نصب نکردم و `postcss.config` هم
|
||||
نگذاشتم، ولی ~۱۸۵ کلاس Tailwind در قالبها باقی ماند. بعد بهجای رفع ریشهای،
|
||||
همان چند کلاس را **۱۲۴ بار در `<style scoped>` فایلها کپی کردم** — و در این
|
||||
کپیکاری `mb-base` را `1.5rem` نوشتم (۳۲٪ تنگتر از legacy).
|
||||
|
||||
**۲. مرز CSS scoped را نادیده گرفتم.** ۱۷ مورد `icon-class="w-5 h-5 text-primary"`
|
||||
به `TooltipBtn` پاس داده میشد، ولی استایل scoped والد هرگز به فرزند نمیرسد و
|
||||
هیچ `:deep()` در پروژه نبود. `h-5` هم اصلاً هیچجا تعریف نشده بود. یعنی آن
|
||||
۱۷ آیکون **بیرنگ و بیاندازه** رندر میشدند.
|
||||
|
||||
**۳. لایهی نگاشت را ناقص گذاشتم.** ۱۸ مسیر فرانت به بکاند نمیرسید — در حالی
|
||||
که **مسیر بکاند هر ۱۸ مورد از قبل وجود داشت**. فقط ورودی مپ را ننوشته بودم.
|
||||
|
||||
### بدترین مورد: تخریب داده
|
||||
|
||||
در `bulkCustomer.vue` (و `ReagentCustomer.vue`) هنگام ویرایش، `Object.assign`
|
||||
فقط ۹ فیلد را برمیگرداند و `store_name`/`pasazh_name`/`mcode`/`phone` را جا
|
||||
میانداخت؛ آنها `''` میماندند، فرم کل `...form` را spread میکرد، و بکاند
|
||||
`if (data.x !== undefined)` دارد — پس **هر بار ذخیرهی یک مشتری موجود، این
|
||||
چهار ستون را پاک میکرد**.
|
||||
|
||||
### آنچه انجام شد
|
||||
|
||||
| مرحله | کار |
|
||||
|---|---|
|
||||
| ۱ | رفع تخریب داده: پرکردن کامل فرم روی ویرایش + ارسال **تفاضلی** (فقط فیلدهای تغییرکرده) در هر دو فرم |
|
||||
| ۲ | ۹ الگو + ۲ ورودی دقیق به `endpoints.ts` → هر ۱۸ endpoint مرده زنده شد |
|
||||
| ۳ | `assets/styles/utilities.less` سراسری با **مقادیر legacy**؛ حذف جراحی ۱۲۴ کپی تکراری از ۳۹ فایل |
|
||||
| ۴ | `<a-config-provider :locale="faIR" direction="rtl">` در `App.vue` |
|
||||
| ۵ | `reagent_id` و `password` (با bcrypt) در بکاند ذخیره میشوند |
|
||||
| ۶ | دکمهی «افزودن کاربر»، رفع تایپوی `color-success`، صادقسازی `mainLayoutType` |
|
||||
|
||||
### نکات فنی مهم
|
||||
|
||||
- **گریدها عمداً محلی ماندند.** مقادیر `minmax` در `.grid-2/3/4` بین فایلها
|
||||
فرق دارد (۲۰۰ تا ۳۲۰ پیکسل)؛ یکسانسازیشان چیدمان ~۲۶ فرم را بیصدا عوض
|
||||
میکرد. فقط قواعدی حذف شدند که بدنهشان **دقیقاً** با نسخهی سراسری یکی بود.
|
||||
- **سه قاعده RTL-flipped هستند و همانطور ماندند:** `.ml-1`/`.ml-3` در پورت
|
||||
`margin-right` میدهند و `.mr-4` میدهد `margin-left` — چون پنل RTL است.
|
||||
در فایل سراسری هم عیناً حفظ شد و کامنتگذاری شد که «اصلاح» نشود.
|
||||
- **`percent` عمداً دستنخورده ماند.** `Customer` اصلاً ستون `percent` ندارد؛
|
||||
این عدد از `AdminUser.wagePercent` کارشناس معرف میآید. یعنی ویرایشش از فرم
|
||||
مشتری، کمیسیون آن کارشناس را برای **همهی** مشتریانش عوض میکرد. نیاز به
|
||||
تصمیم شما دارد.
|
||||
- **چیدمان افقی حالت شعبه:** `themeConfig` مقدار `horizontal` میداد ولی
|
||||
`Main.vue` همیشه منوی عمودی رندر میکرد. کامپوننتهای `HorizontalNavMenu`
|
||||
(۴ فایل) هرگز پورت نشدهاند، پس بهجای ساختن بیسروصدای آنها، نوع به
|
||||
`'vertical'` محدود شد تا کد ادعای دروغ نکند. پورت چیدمان افقی کار جداست.
|
||||
|
||||
### تأیید اجرایی
|
||||
|
||||
```
|
||||
دادهرفتگی: ویرایش فقط-نام → ۰ فیلد از دست رفت (قبلاً هر ۴ پاک میشد)
|
||||
Stage 5: reagent ذخیره شد · password یک hash واقعی bcrypt شد
|
||||
endpointها: هر ۱۰ شکل بازیابیشده → ۲۰۰ (قبلاً ۴۰۴)
|
||||
typecheck: admin ✓ · api ✓
|
||||
بیلد: admin ✓ · branch ✓
|
||||
```
|
||||
|
||||
> تستهای نوشتن فقط روی مشتریای انجام شد که خودم ساختم و بعد حذف کردم.
|
||||
|
||||
---
|
||||
|
||||
## ۶. ابعاد نهایی کد (تا این لحظه)
|
||||
|
||||
| معیار | مقدار |
|
||||
|---|---|
|
||||
| فایلهای `.vue` پورتشده (views) | ۸۰ |
|
||||
| ماژولهای بکاند NestJS | ۲۵ |
|
||||
| مدلهای Prisma | ۳۵ |
|
||||
| ورودیهای دقیق لایهی نگاشت (endpoints.ts) | ۷۷ + ۲۶ الگو |
|
||||
| باگ/کوییرک legacy ثبتشده | ۳۶ |
|
||||
| مسیرهای ادمین نگاشتشده | ۱۵۴ |
|
||||
| پوشش ACL (rid) | ۱۴۴ / ۱۴۴ |
|
||||
| کاهش حجم با کامپوننت مشترک (۵ لیست عمده + ۲ جزئیات مشتری + ۲ فرم تخفیف) | ۴٬۶۷۶ خط → ۱٬۱۳۹ خط |
|
||||
|
||||
---
|
||||
|
||||
## ۷. آنچه این تاریخچه **نمیتواند** تأیید کند
|
||||
|
||||
⚠️ **ظاهر رندرشده در مرورگر — مهمتر از همیشه.** هیچ view ای چشمی دیده نشده؛
|
||||
همهی تأییدها API و بیلد و typecheck بوده. دو تغییر اخیر بیشترین ریسک بصری را
|
||||
دارند و **حتماً** باید چشمی دیده شوند:
|
||||
|
||||
- **`direction="rtl"` روی ConfigProvider.** `ant.less` دستی برای ant **۱.x**
|
||||
نوشته شده؛ حالا ant ۳ هم RTL خودش را اعمال میکند. احتمال تداخل یا
|
||||
دوباراعمالشدن هست و بیلد آن را نمیگیرد.
|
||||
- **`mb-base` از `1.5rem` به `2.2rem` رفت** (مقدار واقعی legacy). فاصلهی
|
||||
بین کارتها در ۱۳ view عمداً بازتر میشود — این تغییر خواستهشده است، نه باگ.
|
||||
|
||||
بهعلاوه آواتار، اسلایدر، و فرم دستهبندی که uploader واقعی گرفتند.
|
||||
|
||||
⚠️ **مسیرهای نوشتن با نقشهای کمدسترسی.** دودتست فقط روی مسیرهای خواندنی
|
||||
انجام شد تا داده خراب نشود؛ مسیرهای نوشتن در همان batch خودشان تست شدند اما
|
||||
نه بهصورت ماتریسی روی همهی نقشها.
|
||||
|
||||
⚠️ **درگاه پیامک واقعی وصل نیست** — پیام ثبت میشود اما ارسال نمیشود.
|
||||
|
||||
---
|
||||
|
||||
## مرجع
|
||||
|
||||
برای جزئیات فنی کامل هر batch — شامل کد دقیق تغییرات، دلایل هر تصمیم
|
||||
معماری، و متن کامل دیالوگ تصمیمگیری — به
|
||||
[`docs/ADMIN-PORT-PLAN.md`](./ADMIN-PORT-PLAN.md) مراجعه کنید.
|
||||
Reference in New Issue
Block a user