292 lines
19 KiB
Markdown
292 lines
19 KiB
Markdown
# تاریخچهی کامل پورت — 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) مراجعه کنید.
|