# تاریخچه‌ی کامل پورت — 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 در قالب‌ها باقی ماند. بعد به‌جای رفع ریشه‌ای، همان چند کلاس را **۱۲۴ بار در `