در پنل ادمین، قابلیت بررسی مدارک شرکت و تایید/رد شرکت برای نمایش تیک آبی را پیاده کن.
هدف:
ادمین بتواند مدارک شرکت را ببیند، مدارک لازم را بررسی کند، وضعیت هر فایل را تایید/رد کند و در نهایت اگر مدارک لازم کامل و تایید شده بود، شرکت را verified کند تا در کلاینت با تیک آبی نمایش داده شود.
دریافت شرکت:
GET /panel/v1/Company/id/:id
Auth: Admin token
دریافت مدارک شرکت:
GET /panel/v1/Company/:companyId/gallery
Auth: Admin token
Queryهای کاربردی:
GET /panel/v1/Company/:companyId/gallery?placement=identity&type=document
GET /panel/v1/Company/:companyId/gallery?placement=contract&type=document
GET /panel/v1/Company/:companyId/gallery?placement=agreement&type=document
ویرایش وضعیت یک مدرک:
PATCH /panel/v1/Company/gallery/:id
Auth: Admin token
Body نمونه برای تایید مدرک:
{
"isAccepted": true,
"declinedReason": ""
}
Body نمونه برای رد مدرک:
{
"isAccepted": false,
"declinedReason": "تصویر قرارداد خوانا نیست"
}
تایید نهایی شرکت و دادن تیک آبی:
PATCH /panel/v1/Company/:id/verification
Auth: Admin token
Body نمونه تایید:
{
"isVerified": true,
"verificationNote": "مدارک بررسی و تایید شد",
"requiredPlacements": ["identity", "contract", "agreement"]
}
Body نمونه رد/برداشتن تیک آبی:
{
"isVerified": false,
"verificationStatus": "rejected",
"verificationNote": "مدارک قرارداد ناقص است"
}
رفتار بکاند:
- برای تایید نهایی، بکاند بررسی میکند که برای همه requiredPlacements فایل document تاییدشده وجود داشته باشد.
- اگر requiredPlacements ارسال نشود، پیشفرض اینها بررسی میشوند: identity، contract، agreement.
- اگر مدرکی ناقص باشد، API خطا میدهد و missingPlacements را برمیگرداند.
- response شرکت شامل فیلدهای isVerified، verificationStatus، verificationNote، verifiedBy و verifiedAt است.
رفتار UI:
- در صفحه جزئیات شرکت، یک کارت «بررسی مدارک و تیک آبی» اضافه کن.
- مدارک را در تبهای جدا نمایش بده: مدارک هویتی، قراردادها، توافقها، مجوزها.
- برای هر فایل preview/download، caption، type، placement، isAccepted و declinedReason را نمایش بده.
- دکمه تایید/رد برای هر مدرک داشته باش.
- دکمه «تایید شرکت و دادن تیک آبی» فقط وقتی فعال شود که مدارک لازم تایید شده باشند.
- اگر API missingPlacements داد، همان placementها را در UI هایلایت کن.
- وضعیت شرکت را با badge نمایش بده:
- pending: در انتظار بررسی
- accepted: تایید شده / تیک آبی
- rejected: رد شده
- بعد از هر تغییر مدرک یا وضعیت شرکت، دیتای شرکت و گالری را refresh کن.
- همه پیامهای خطا را فارسی و کنار بخش مربوط نمایش بده.
راهنمای پرامپتهای Codex
هر پرامپت آمادهای که برای Codex لازم داشته باشیم از این به بعد اینجا اضافه میشود.
Admin
14 پرامپتمشکل auth/cache داریم: بعد از logout یا login با اکانت جدید، پنل هنوز اطلاعات اکانت قبلی را در header/profile نمایش میدهد. لطفاً جریان AuthProvider/useAuth/AppShell/HeaderNavbar را کامل بررسی و اصلاح کن. موارد الزامی: 1. هنگام logout همه stateهای مربوط به admin/user و token و cache پاک شود. 2. هنگام login موفق، اطلاعات کاربر جدید از پاسخ login یا endpoint current user دوباره set شود. 3. اگر token عوض شد، admin قدیمی حتی موقت نمایش داده نشود. 4. localStorage/sessionStorage keys مربوط به auth بررسی شوند و با token فعلی sync بمانند. 5. HeaderNavbar فقط از admin فعلی context بخواند و stale state یا state محلی جداگانه نداشته باشد. جزئیات فنی مورد انتظار: - AuthProvider باید یک source of truth واضح برای token و admin داشته باشد. - در logout، token، admin، headers/interceptors مرتبط، cacheهای in-memory و کلیدهای storage مرتبط پاک شوند. - در login، اول token جدید ذخیره و روی http client set شود، سپس admin جدید از login response یا endpoint current user وارد context شود. - اگر endpoint current user fail شد، admin قبلی نباید نمایش داده شود و کاربر باید به login یا حالت unauthenticated برگردد. - اگر پروژه از axios interceptor استفاده میکند، مطمئن شو Authorization header بعد از logout حذف و بعد از login با token جدید جایگزین شود. - اگر چند key قدیمی مثل token/admin/adminToken/user در storage وجود دارد، migration/cleanup سبک انجام بده تا داده قبلی باقی نماند. - AppShell و HeaderNavbar نباید admin را یک بار در state محلی کپی کنند مگر اینکه با context sync شود؛ ترجیحاً مستقیم از useAuth بخوانند. - در loading اولیه auth، header نباید اطلاعات admin قبلی را نشان دهد. تست دستی: 1. با اکانت Admin A لاگین کن و نام/اطلاعات header را بررسی کن. 2. logout کن و مطمئن شو header/profile خالی یا صفحه login میشود. 3. بدون refresh، با اکانت Admin B لاگین کن. 4. مطمئن شو header/profile فقط اطلاعات Admin B را نشان میدهد. 5. صفحه را refresh کن و بررسی کن همچنان Admin B نمایش داده شود. 6. دوباره logout کن و localStorage/sessionStorage را بررسی کن که داده Admin B باقی نمانده باشد.
در پنل ادمین، یک بخش مدیریت «بازه تعداد کارکنان» بساز تا ادمین بتواند گزینههای قابل انتخاب در فرم ساخت شرکت ساپلایر را مدیریت کند.
هدف:
ادمین بتواند لیست بازههای تعداد کارکنان را initialize، مشاهده، ایجاد، ویرایش، فعال/غیرفعال و حذف کند. این گزینهها بعداً در پنل ساپلایر برای فیلد teamSizeRange استفاده میشوند، اما داخل Company مقدار title به صورت string ذخیره میشود نه id.
APIها:
Initialize:
POST /panel/v1/TeamSizeRange/initialize
Auth: Admin token
لیست:
GET /panel/v1/TeamSizeRange
Auth: Admin token
جزئیات:
GET /panel/v1/TeamSizeRange/id/:id
Auth: Admin token
ایجاد:
POST /panel/v1/TeamSizeRange
Auth: Admin token
Body نمونه:
{
"title": "۱۱ تا ۵۰ نفر",
"key": "team_11_50",
"min": 11,
"max": 50,
"sort": 2,
"isActive": true
}
ویرایش:
PUT /panel/v1/TeamSizeRange/:id
Auth: Admin token
حذف:
DELETE /panel/v1/TeamSizeRange/:id
Auth: Admin token
گزینههای initialize پیشفرض:
- ۱ تا ۱۰ نفر
- ۱۱ تا ۵۰ نفر
- ۵۱ تا ۱۰۰ نفر
- ۱۰۱ تا ۵۰۰ نفر
- بیش از ۵۰۰ نفر
رفتار UI:
- صفحه مدیریت را با جدول مرتب بر اساس sort بساز.
- ستونها: عنوان، key، min، max، sort، وضعیت، تاریخ ایجاد، عملیات.
- دکمه «initialize گزینههای پیشفرض» داشته باشد و بعد از موفقیت لیست refresh شود.
- فرم ایجاد/ویرایش باید title و key را الزامی کند.
- key بهتر است انگلیسی، یکتا و بدون فاصله باشد.
- min و max عددی باشند؛ برای «بیش از ۵۰۰ نفر» max میتواند خالی/null باشد.
- وضعیت isActive با switch کنترل شود.
- delete باید confirm داشته باشد.
- خطاهای API کنار فیلد یا در toast فارسی نمایش داده شوند.
نکته مهم:
- این لیست فقط برای مدیریت optionهاست.
- هنگام ثبت شرکت، اگر ساپلایر id/key/title یکی از این گزینهها را بفرستد، بکاند title را پیدا میکند و همان title فارسی را داخل Company.teamSizeRange به صورت string ذخیره میکند.
در پنل ادمین، یک بخش مدیریت «بازه حداقل سرمایه» بساز تا ادمین بتواند گزینههای قابل انتخاب در فرم ساخت/ویرایش آگهی را مدیریت کند.
هدف:
ادمین بتواند لیست بازههای حداقل سرمایه را initialize، مشاهده، ایجاد، ویرایش، فعال/غیرفعال و حذف کند. این گزینهها بعداً در پنل ساپلایر و پنل ادمین برای فیلد minimumCapital آگهی استفاده میشوند، اما داخل Advertise مقدار title به صورت string ذخیره میشود نه id.
APIها:
Initialize:
POST /panel/v1/MinimumCapitalRange/initialize
Auth: Admin token
لیست:
GET /panel/v1/MinimumCapitalRange
Auth: Admin token
جزئیات:
GET /panel/v1/MinimumCapitalRange/id/:id
Auth: Admin token
ایجاد:
POST /panel/v1/MinimumCapitalRange
Auth: Admin token
Body نمونه:
{
"title": "۵۰۰ میلیون تا ۱ میلیارد",
"key": "capital_500m_1b",
"min": 500000000,
"max": 1000000000,
"sort": 2,
"isActive": true
}
ویرایش:
PUT /panel/v1/MinimumCapitalRange/:id
Auth: Admin token
حذف:
DELETE /panel/v1/MinimumCapitalRange/:id
Auth: Admin token
گزینههای initialize پیشفرض:
- کمتر از ۵۰۰ میلیون
- ۵۰۰ میلیون تا ۱ میلیارد
- ۱ تا ۲ میلیارد
- ۲ تا ۵ میلیارد
- بیش از ۵ میلیارد
رفتار UI:
- صفحه مدیریت را با جدول مرتب بر اساس sort بساز.
- ستونها: عنوان، key، min، max، sort، وضعیت، تاریخ ایجاد، عملیات.
- دکمه «initialize گزینههای پیشفرض» داشته باشد و بعد از موفقیت لیست refresh شود.
- فرم ایجاد/ویرایش باید title و key را الزامی کند.
- min و max عددی باشند و با جداکننده خوانا نمایش داده شوند.
- برای «بیش از ۵ میلیارد» max میتواند خالی/null باشد.
- وضعیت isActive با switch کنترل شود.
- delete باید confirm داشته باشد.
- خطاهای API کنار فیلد یا در toast فارسی نمایش داده شوند.
نکته مهم:
- این لیست فقط برای مدیریت optionهاست.
- هنگام ثبت آگهی، اگر پنل یا ساپلایر id/key/title یکی از این گزینهها را بفرستد، بکاند title را پیدا میکند و همان title فارسی را داخل Advertise.minimumCapital به صورت string ذخیره میکند.
برای پنل ادمین، فرم افزودن/ویرایش آگهی را مثل پنل supplier چندمرحلهای پیاده کن.
APIهای جدید پنل ادمین:
1. Step 1 - اطلاعات اصلی آگهی
POST /panel/v1/Advertise/step1
Body:
{
"company": "companyId",
"advertiseId": "optional for edit",
"title": "عنوان آگهی",
"category": "categoryId",
"dealershipTypes": ["dealershipTypeId"],
"space": "فضای مورد نیاز",
"minimumExperience": "حداقل سابقه",
"minimumCapital": "حداقل سرمایه"
}
2. Step 2 - توضیحات
POST /panel/v1/Advertise/step2
Body:
{
"advertiseId": "advertiseId",
"description": "توضیحات",
"videoLink": "optional"
}
3. Step 3 - افزودن محصول نمونه
POST /panel/v1/Advertise/step3
Content-Type: multipart/form-data
Fields:
- advertiseId
- title optional
- file
4. Step 3 update - ویرایش/حذف محصول نمونه
PUT /panel/v1/Advertise/step3/:advertiseId
Body:
{
"productsSample": [
{
"_id": "sampleId",
"title": "عنوان جدید",
"image": "imageUrl"
}
],
"removePicUrl": ["imageUrl"]
}
5. Step 4 - مزایا و شهرها
POST /panel/v1/Advertise/step4
Body:
{
"advertiseId": "advertiseId",
"benefits": ["مزیت اول", "مزیت دوم"],
"city": ["cityId"]
}
6. Step 5 - پلان، کاور و فعالسازی
POST /panel/v1/Advertise/step5
Content-Type: multipart/form-data
Fields:
- advertiseId
- advertiseType
- statusId optional
- cover optional
نکات:
- همه درخواستها باید با توکن ادمین ارسال شوند.
- در Step1 انتخاب company الزامی است.
- در Step1 انتخاب category برای ساخت آگهی الزامی است.
- category باید از بین دستهبندیهای همان شرکت انتخاب شود؛ بعد از انتخاب company، دستهبندیهای مجاز را از company.category نمایش بده.
- در حالت ویرایش، category برگشتی آگهی را prefill کن و اگر ادمین company را تغییر داد، لیست category را بر اساس شرکت جدید refresh کن.
- اگر category قبلی داخل دستهبندیهای شرکت جدید نبود، فیلد category را خالی کن و از ادمین انتخاب جدید بخواه.
- اگر بکاند خطای «دسته بندی آگهی باید از بین دسته بندی های شرکت انتخاب شود» برگرداند، پیام را کنار فیلد category نمایش بده.
- بعد از Step1، id آگهی برگشتی را برای Stepهای بعدی نگه دار.
- اگر Step5 بدون statusId ارسال شود، بکاند وضعیت paid را پیشفرض میگذارد.
- اگر پلان امکان cover نداشته باشد و cover ارسال شود، بکاند خطا میدهد.
- هر شرکت میتواند چند آگهی فعال داشته باشد؛ محدودیت activeAdvertiseId از بکاند برداشته شده است.
- UI و UX فرم را مثل فرم supplier نگه دار، فقط انتخاب شرکت/ساپلایر مخصوص ادمین را اضافه کن.
برای پنل ادمین، امکان دریافت لیست آگهیها بر اساس آیدی شرکت یا آیدی ساپلایر را اضافه کن. API: GET /panel/v1/Advertise/by-company-or-supplier Query params: - companyId optional - supplierId optional - page optional, default 1 - limit optional, default 10, max 100 - sort optional, default -createdAt نمونه با آیدی شرکت: GET /panel/v1/Advertise/by-company-or-supplier?companyId=COMPANY_ID&page=1&limit=10&sort=-createdAt نمونه با آیدی ساپلایر: GET /panel/v1/Advertise/by-company-or-supplier?supplierId=SUPPLIER_ID&page=1&limit=10&sort=-createdAt نکات: - درخواست باید با توکن ادمین ارسال شود. - اگر companyId ارسال شود، آگهیهای همان شرکت برگردانده شود. - اگر supplierId ارسال شود، ابتدا شرکتهای متعلق به ساپلایر پیدا شود و بعد آگهیهای آن شرکتها برگردانده شود. - خروجی مثل لیست آگهیهای پنل transform و pagination داشته باشد. - اگر هیچ آگهیای پیدا نشد، empty state مناسب نمایش بده.
برای پنل ادمین، صفحه مدیریت گزینههای «امکانات موجود» در پروفایل کاربر را پیاده کن.
هدف:
ادمین بتواند گزینههایی مثل مغازه، دفتر کار، مجوز فعالیت، ماشین پخش و ... را ایجاد، ویرایش، فعال/غیرفعال و حذف کند. کلاینت هم همین گزینههای فعال را در فرم «توانمندیها و تجربه» نمایش میدهد.
APIهای پنل ادمین:
1. ساخت گزینههای پیشفرض
POST /panel/v1/UserFacility/initialize
Auth: Admin token
این موارد پیشفرض را میسازد:
- مغازه
- دفتر کار
- مجوز فعالیت
- ماشین پخش
- نیروی بازاریابی
- تیم فنی
- فعالیت آنلاین
- چک و وثیقه
2. لیست همه امکانات
GET /panel/v1/UserFacility
Auth: Admin token
Response:
{
"data": [
{
"id": "...",
"title": "مغازه",
"key": "store",
"iconName": "store",
"sort": 10,
"isActive": true
}
]
}
3. دریافت یک مورد
GET /panel/v1/UserFacility/id/:id
Auth: Admin token
4. ایجاد امکان جدید
POST /panel/v1/UserFacility
Auth: Admin token
Body:
{
"title": "نمایندگی فعال",
"key": "active-dealership",
"iconName": "store",
"sort": 90,
"isActive": true
}
5. ویرایش امکان
PUT /panel/v1/UserFacility/:id
Auth: Admin token
Body:
{
"title": "عنوان جدید",
"key": "new-key",
"iconName": "truck",
"sort": 100,
"isActive": false
}
6. حذف امکان
DELETE /panel/v1/UserFacility/:id
Auth: Admin token
API کلاینت برای فرم پروفایل:
GET /client/v1/UserFacility
این endpoint فقط امکانات فعال را برمیگرداند و نیاز به توکن ندارد.
نحوه اتصال به فرم پروفایل کاربر:
- در کارت «توانمندیها و تجربه»، گزینههای امکانات را از GET /client/v1/UserFacility بگیر.
- مقدار انتخابشده را در submit پروفایل با key یا id ارسال کن:
PUT /client/v1/User/profile/capabilities
Body:
{
"facilities": ["store", "office", "delivery-vehicle"],
"workExperience": "متن تجربه کاری...",
"interestCategories": ["categoryId"]
}
نکات UI پنل:
- یک جدول یا لیست مدیریتی برای امکانات بساز.
- ستونها: عنوان، key، آیکن، ترتیب نمایش، وضعیت فعال/غیرفعال، عملیات.
- فرم ایجاد/ویرایش شامل title، key، iconName، sort و isActive باشد.
- دکمه «ساخت گزینههای پیشفرض» برای endpoint initialize داشته باش.
- حذف با confirm انجام شود.
- loading، empty state و پیام خطای API هندل شود.
- بعد از create/update/delete/initialize لیست را refresh کن.
- ظاهر صفحه با طراحی فعلی پنل ادمین هماهنگ باشد.
برای پنل ادمین، صفحه مدیریت گزینههای «دستهبندیهای مورد علاقه کاربر» را پیاده کن.
هدف:
ادمین بتواند گزینههای علاقهمندی کاربر را ایجاد، ویرایش، فعال/غیرفعال و حذف کند. هر علاقهمندی باید با فیلد relatedId به یک CategoryFranchise وصل باشد تا وقتی کاربر گزینهای را انتخاب میکند، id دستهبندی مرتبط برای ذخیره پروفایل استفاده شود.
APIهای پنل ادمین:
1. ساخت گزینههای پیشفرض
POST /panel/v1/UserInterest/initialize
Auth: Admin token
این موارد پیشفرض را میسازد و relatedId هرکدام را به دستهبندی متناظر در CategoryFranchise وصل میکند:
- آرایشی و سلامت
- فناوری
- خودرو
- مد و پوشاک
- غذا و نوشیدنی
- خدمات
2. لیست همه علاقهمندیها
GET /panel/v1/UserInterest
Auth: Admin token
Response:
{
"data": [
{
"id": "...",
"title": "فناوری",
"key": "technology",
"relatedId": {
"id": "categoryId",
"title": "فناوری و دیجیتال",
"key": "technology_digital"
},
"sort": 20,
"isActive": true
}
]
}
3. دریافت یک مورد
GET /panel/v1/UserInterest/id/:id
Auth: Admin token
4. ایجاد علاقهمندی جدید
POST /panel/v1/UserInterest
Auth: Admin token
Body:
{
"title": "لوازم خانگی",
"key": "home-appliances",
"relatedId": "categoryId",
"sort": 70,
"isActive": true
}
5. ویرایش علاقهمندی
PUT /panel/v1/UserInterest/:id
Auth: Admin token
Body:
{
"title": "عنوان جدید",
"key": "new-key",
"relatedId": "categoryId",
"sort": 80,
"isActive": false
}
6. حذف علاقهمندی
DELETE /panel/v1/UserInterest/:id
Auth: Admin token
نکات UI پنل:
- یک جدول یا لیست مدیریتی برای علاقهمندیها بساز.
- ستونها: عنوان، key، دستهبندی مرتبط، ترتیب نمایش، وضعیت فعال/غیرفعال، عملیات.
- فرم ایجاد/ویرایش شامل title، key، relatedId، sort و isActive باشد.
- برای انتخاب relatedId، لیست دستهبندیها را از GET /panel/v1/CategoryFranchise بگیر و به صورت select/searchable select نمایش بده.
- دکمه «ساخت علاقهمندیهای پیشفرض» برای endpoint initialize داشته باش.
- حذف با confirm انجام شود.
- loading، empty state و پیام خطای API هندل شود.
- بعد از create/update/delete/initialize لیست را refresh کن.
- ظاهر صفحه با طراحی فعلی پنل ادمین هماهنگ باشد.
برای پنل ادمین، صفحه مدیریت گزینههای «مزایای کلیدی» آگهی را پیاده کن.
هدف:
ادمین بتواند گزینههای پیشنهادی مزایای کلیدی را ایجاد، ویرایش، فعال/غیرفعال و حذف کند. این گزینهها در فرم ساخت/ویرایش آگهی، بخش «مزایای کلیدی»، به عنوان پیشنهاد نمایش داده میشوند و کاربر/ساپلایر همچنان بتواند مزیت سفارشی هم اضافه کند.
APIهای پنل ادمین:
1. ساخت گزینههای پیشفرض
POST /panel/v1/KeyBenefit/initialize
Auth: Admin token
این موارد پیشفرض را میسازد:
- کیفیت تضمینشده
- ارسال سریع
- پشتیبانی سریع
- قیمت رقابتی
- تیم متخصص
- تحویل بهموقع
- پرداخت امن
- رضایت مشتریان
- خدمات پس از فروش
- همکاری قابل اعتماد
2. لیست همه مزایای کلیدی
GET /panel/v1/KeyBenefit
Auth: Admin token
Response:
{
"data": [
{
"id": "...",
"title": "کیفیت تضمینشده",
"key": "guaranteed-quality",
"iconName": "verified",
"sort": 10,
"isActive": true
}
]
}
3. دریافت یک مورد
GET /panel/v1/KeyBenefit/id/:id
Auth: Admin token
4. ایجاد مزیت جدید
POST /panel/v1/KeyBenefit
Auth: Admin token
Body:
{
"title": "ارسال رایگان تجهیزات",
"key": "free-equipment-shipping",
"iconName": "local_shipping",
"sort": 90,
"isActive": true
}
5. ویرایش مزیت
PUT /panel/v1/KeyBenefit/:id
Auth: Admin token
Body:
{
"title": "عنوان جدید",
"key": "new-key",
"iconName": "verified",
"sort": 100,
"isActive": false
}
6. حذف مزیت
DELETE /panel/v1/KeyBenefit/:id
Auth: Admin token
APIهای خواندن برای فرم آگهی:
GET /client/v1/KeyBenefit
GET /supplier/v1/KeyBenefit
این endpointها فقط مزایای فعال را برمیگردانند و نیاز به توکن ندارند.
نکات UI پنل:
- یک جدول یا لیست مدیریتی برای مزایای کلیدی بساز.
- ستونها: عنوان، key، آیکن، ترتیب نمایش، وضعیت فعال/غیرفعال، عملیات.
- فرم ایجاد/ویرایش شامل title، key، iconName، sort و isActive باشد.
- دکمه «ساخت مزایای پیشفرض» برای endpoint initialize داشته باش.
- حذف با confirm انجام شود.
- loading، empty state و پیام خطای API هندل شود.
- بعد از create/update/delete/initialize لیست را refresh کن.
- ظاهر صفحه با طراحی فعلی پنل ادمین هماهنگ باشد.
در پنل ادمین، صفحه «مدیریت ساپلایرها» را طوری تغییر بده که لیست اصلی از شرکتها گرفته شود، نه از لیست خام ساپلایرها.
منطق مورد نظر:
در این صفحه هدف مدیریت شرکتهای متصل به ساپلایر است. بنابراین جدول باید شرکتها را نمایش بدهد و اطلاعات ساپلایر از فیلد owner/populate شده شرکت خوانده شود. اگر شرکتی owner نداشت، همان ردیف با وضعیت «بدون ساپلایر» نمایش داده شود.
API پیشنهادی:
POST /panel/v1/Company/filter
Auth: Admin token
Body نمونه:
{
"page": 1,
"limit": 50,
"q": "نام شرکت یا موبایل یا ایمیل",
"supplierType": "supplierTypeId",
"isLegal": true,
"owner": "supplierId"
}
اگر بکاند فعلی فیلترهای supplierType و isLegal را روی Company/filter پشتیبانی نمیکند، همان route را تکمیل کن:
- فیلتر q روی companyName، phoneNumber، email، slug و همچنین اطلاعات owner مثل businessName، firstName، lastName، phoneNumber، email اعمال شود.
- فیلتر supplierType روی company.supplierType اعمال شود.
- فیلتر isLegal روی owner.isLegal اعمال شود.
- خروجی pagination داشته باشد.
- خروجی شرکتها باید populate owner داشته باشد:
- businessName
- firstName
- lastName
- email
- phoneNumber
- status
- isLegal
Response مورد انتظار:
{
"data": [
{
"id": "companyId",
"companyName": "نام شرکت",
"phone": "...",
"email": "...",
"supplierType": [],
"owner": {
"id": "supplierId",
"businessName": "نام ساپلایر",
"email": "...",
"phoneNumber": "...",
"isLegal": false,
"status": "active"
},
"createdAt": "...",
"updatedAt": "..."
}
],
"metaData": {
"page": 1,
"limit": 50,
"totalPage": 1,
"totalCount": 1
}
}
تغییرات فرانت:
- در صفحه /suppliers، به جای fetchSuppliers از fetchCompanies استفاده کن.
- نام صفحه میتواند همان «مدیریت ساپلایرها» بماند، اما description توضیح بدهد که لیست بر اساس شرکتهای ثبتشده است.
- جدول را بر اساس company render کن:
- نام تجاری: company.companyName
- راه ارتباط: company.phone یا company.email، و اگر نبود owner.phoneNumber یا owner.email
- نوع حساب: owner.isLegal ? حقوقی : حقیقی
- حوزه فعالیت: company.supplierType
- شرکت: company.companyName
- تاریخ ثبت: company.createdAt
- دکمه مشاهده اگر owner وجود داشت به /suppliers/:ownerId برود، و اگر owner نبود disabled یا به جزئیات شرکت برود.
- دکمه «ویرایش یا ایجاد شرکت» برای هر ردیف:
- اگر company id دارد به /companies/:companyId برود.
- اگر فقط owner وجود داشت و شرکت نبود، به /companies/new?supplierId=ownerId برود.
فیلترها:
- q باید server-side ارسال شود و فقط client-side نباشد.
- supplierType روی شرکت فیلتر شود.
- isLegal روی owner فیلتر شود.
- pagination از metaData بکاند استفاده کند.
نکات:
- ساختار UI و استایل فعلی صفحه حفظ شود.
- loading، empty state و error state حفظ شود.
- اگر API 204 برگشت، لیست خالی نشان بده.
- abstraction جدید نساز اگر api/companies.js و fetchCompanies وجود دارد؛ همان را توسعه بده.
- مراقب باش تغییر صفحه /suppliers مسیرهای جزئیات /suppliers/:id و ساخت ساپلایر جدید را نشکند.
برای پنل ادمین، امکان حذف شرکت را به UI مدیریت شرکتها/مدیریت ساپلایرها اضافه کن.
API بکاند:
DELETE /panel/v1/Company/:id
Auth: Admin token
نمونه:
DELETE /panel/v1/Company/COMPANY_ID
Response موفق:
{
"data": {
"id": "companyId",
"companyName": "نام شرکت",
"isDeleted": true,
"deletedBy": "admin"
}
}
تغییرات فرانت:
- در api/companies.js یک helper با نام deleteCompany داشته باش:
export async function deleteCompany(id) {
const response = await http.delete(`/panel/v1/Company/${id}`);
return response.data;
}
- در صفحه لیست شرکتها و اگر صفحه /suppliers بر اساس company render میشود، کنار اکشنهای هر ردیف دکمه حذف شرکت اضافه کن.
- قبل از حذف confirm/modal بگیر:
«آیا از حذف این شرکت مطمئن هستید؟»
- بعد از حذف موفق:
- notification موفق نمایش بده.
- لیست را refresh کن یا همان ردیف را از state حذف کن.
- اگر شرکت مربوط به ساپلایر در صفحه /suppliers حذف شد، ردیف از لیست حذف شود.
- اگر حذف ناموفق بود، پیام API را با notification خطا نمایش بده.
نکات UI/UX:
- دکمه حذف destructive باشد و از رنگ قرمز/خطر سیستم طراحی استفاده کند.
- هنگام درخواست حذف، دکمه همان ردیف loading/disabled شود.
- حذف باید soft delete بکاند باشد؛ هیچ فایل یا دیتای وابسته را سمت فرانت پاک نکن.
- اگر id شرکت وجود نداشت، دکمه حذف نمایش داده نشود یا disabled باشد.
- مسیر جزئیات شرکت بعد از حذف اگر روی همان صفحه بود، کاربر را به /companies یا /suppliers برگردان.
برای پنل ادمین، دریافت و نمایش نوتیفهای داخلی پنل و تست Push ادمینها را پیاده کن.
هدف:
- ادمین بتواند notificationهای پنل را در هدر/منو یا صفحه اعلانها ببیند.
- تعداد اعلانهای خواندهنشده نمایش داده شود.
- ادمین بتواند یک اعلان را read کند یا همه را read کند.
- route تست پنل باید یک notification داخلی بسازد و همزمان به ادمینهایی که FCM token فعال دارند Push واقعی بفرستد.
پیشنیاز Push برای ادمین:
فرانت پنل باید بعد از گرفتن FCM token مرورگر ادمین، آن را در بکاند ثبت کند:
POST /panel/v1/Notification/push-token
Auth: Admin token
Body:
{
"token": "FCM_ADMIN_BROWSER_TOKEN",
"platform": "web",
"userAgent": "navigator.userAgent"
}
APIها:
1. لیست اعلانهای پنل
GET /panel/v1/Notification
Auth: Admin token
Query params:
- page optional, default 1
- limit optional, default 20, max 100
- sort optional, default -createdAt
- isRead optional: true یا false
- type optional
Response موفق:
{
"data": [
{
"id": "notificationId",
"title": "آگهی جدید ثبت شد",
"message": "آگهی ... توسط ساپلایر ثبت شد.",
"type": "advertise-created",
"targetType": "Advertise",
"targetId": "advertiseId",
"link": "/advertises/advertiseId",
"data": {},
"isRead": false,
"createdAt": "..."
}
],
"unreadCount": 3,
"metaData": {}
}
2. تعداد خواندهنشدهها
GET /panel/v1/Notification/unread-count
Auth: Admin token
Response:
{
"data": {
"unreadCount": 3
}
}
3. ساخت اعلان تست پنل و ارسال Push واقعی به ادمینهای دارای token فعال
POST /panel/v1/Notification/test
Auth: Admin token
Body:
{
"title": "اعلان تست پنل",
"message": "این اعلان برای تست پنل ادمین ساخته شد",
"link": "/notifications",
"data": {
"type": "test"
}
}
Response موفق با Push ارسالشده:
{
"data": {
"notification": {
"id": "notificationId",
"title": "اعلان تست پنل",
"message": "این اعلان برای تست پنل ادمین ساخته شد",
"type": "test",
"link": "/notifications",
"isRead": false
},
"pushPreview": {
"activeAdminPushReceivers": 1,
"message": "این اعلان برای 1 ادمین دارای توکن فعال ارسال شد/تلاش شد."
},
"pushResult": {
"sent": true,
"successCount": 1,
"failureCount": 0,
"invalidTokens": []
}
},
"message": "اعلان تست ساخته شد و برای 1 ادمین ارسال شد."
}
اگر Push ارسال نشد، pushResult.sent برابر false میشود و message علت را نشان میدهد؛ مثلاً Firebase تنظیم نشده یا token فعالی وجود ندارد.
4. خواندن یک اعلان
PUT /panel/v1/Notification/:id/read
Auth: Admin token
5. خواندن همه اعلانها
PUT /panel/v1/Notification/read-all
Auth: Admin token
رفتار UI:
- در هدر پنل، آیکن زنگ اعلان اضافه کن.
- کنار آیکن زنگ، badge تعداد unreadCount را نمایش بده.
- هنگام mount پنل یا ورود ادمین، GET /panel/v1/Notification/unread-count را بزن.
- با کلیک روی زنگ، dropdown آخرین اعلانها را از GET /panel/v1/Notification?limit=10 بگیر.
- هر آیتم شامل title، message کوتاه، زمان نسبی، وضعیت خواندهنشده و لینک باشد.
- با کلیک روی آیتم:
- PUT /panel/v1/Notification/:id/read را بزن.
- اگر link دارد، به همان مسیر برو.
- unreadCount را کم کن یا دوباره از API بگیر.
- در صفحه کامل اعلانها، pagination، filter isRead و دکمه «خواندن همه» داشته باش.
- دکمه «ساخت اعلان تست و ارسال Push» برای POST /panel/v1/Notification/test داشته باش.
- بعد از تست، pushPreview.activeAdminPushReceivers و pushResult.successCount/failureCount را نمایش بده.
نکات UI/UX:
- طراحی RTL و هماهنگ با پنل باشد.
- اگر unreadCount صفر بود badge نمایش داده نشود یا خاکستری باشد.
- اگر API 204 برگشت، empty state نمایش بده.
- loading و error state برای dropdown و صفحه کامل داشته باش.
- برای «خواندن همه» confirm لازم نیست، ولی loading داشته باشد.
- اگر notification link نداشت، فقط آن را read کن و در dropdown بمان.
- برای تست Push، اگر pushResult.sent false بود، پیام علت را واضح نمایش بده.
نکات فنی:
- /panel/v1/Notification/test هم notification داخلی میسازد و هم Push واقعی به ادمینهای دارای token فعال ارسال میکند.
- Push Firebase برای ادمینها نیاز دارد فرانت پنل FCM token ادمین را در POST /panel/v1/Notification/push-token ثبت کرده باشد.
- برای ارسال Push سفارشی بدون ساخت notification تست، از POST /panel/v1/Notification/push/admins استفاده کن.
- اگر successCount مثبت است ولی مرورگر اعلان نشان نمیدهد، مشکل از permission/service worker/foreground handling فرانت است.
برای پنل ادمین، صفحه ارسال Push Notification با Firebase Cloud Messaging بساز.
هدف:
- ادمین بتواند یک اعلان تستی یا عمومی برای کاربران ارسال کند.
- ادمین بتواند عنوان، متن، لینک مقصد و نوع مخاطب را تعیین کند.
- ارسال فقط از بکاند انجام شود؛ فرانت پنل ادمین نباید service account یا private key داشته باشد.
- کاربران فقط وقتی اعلان مرورگر را فعال کردهاند و FCM token فعال دارند notification بگیرند.
مسیر پیشنهادی UI:
/notifications
یا
/settings/notifications
APIهای بکاند:
1. ثبت FCM token مرورگر ادمین
POST /panel/v1/Notification/push-token
Auth: Admin token
Body:
{
"token": "FCM_ADMIN_BROWSER_TOKEN",
"platform": "web",
"userAgent": "navigator.userAgent"
}
2. حذف/غیرفعالسازی FCM token ادمین
DELETE /panel/v1/Notification/push-token
Auth: Admin token
Body:
{
"token": "FCM_ADMIN_BROWSER_TOKEN"
}
3. ارسال Push به همه ادمینهایی که token فعال دارند
POST /panel/v1/Notification/push/admins
Auth: Admin token
Body:
{
"title": "اعلان پنل",
"body": "یک آگهی جدید ثبت شد",
"url": "/notifications",
"data": {
"type": "advertise-created"
}
}
4. ارسال Push به یک کاربر مشخص
POST /panel/v1/Notification/push/user/:userId
Auth: Admin token
Body:
{
"title": "عنوان اعلان",
"body": "متن اعلان",
"url": "/dashboard/profile",
"data": {
"type": "admin-message"
}
}
5. ارسال Push عمومی به همه کاربران دارای token فعال
POST /panel/v1/Notification/push/broadcast
Auth: Admin token
Body:
{
"title": "عنوان اعلان",
"body": "متن اعلان",
"url": "/dashboard/profile",
"data": {
"type": "broadcast"
}
}
6. ساخت اعلان تست پنل و ارسال Push واقعی به ادمینهای دارای token فعال
POST /panel/v1/Notification/test
Auth: Admin token
Body:
{
"title": "اعلان تست پنل",
"message": "این اعلان برای تست پنل ادمین ساخته شد",
"link": "/notifications",
"data": {
"type": "test"
}
}
Response مهم تست:
{
"data": {
"notification": {},
"pushPreview": {
"activeAdminPushReceivers": 1
},
"pushResult": {
"sent": true,
"successCount": 1,
"failureCount": 0,
"invalidTokens": []
}
}
}
UI صفحه پنل:
- یک فرم ارسال اعلان بساز با فیلدهای:
- عنوان اعلان: title، required
- متن اعلان: body/message، required
- لینک مقصد: url/link، optional
- مخاطب:
- ادمینهای پنل
- یک کاربر مشخص
- همه کاربران
- انتخاب کاربر: searchable select، فقط برای حالت «یک کاربر مشخص»
- دکمه ارسال باید loading و disabled state داشته باشد.
- بعد از ارسال موفق، آمار response را نمایش بده:
- sent
- successCount
- failureCount
- invalidTokens count
- اگر token فعالی نبود، warning/empty state نمایش بده.
رفتار مورد انتظار:
- برای مخاطب «ادمینهای پنل»، route /push/admins را صدا بزن.
- برای مخاطب «یک کاربر مشخص»، route /push/user/:userId را صدا بزن.
- برای مخاطب «همه کاربران»، route /push/broadcast را صدا بزن.
- برای تست پنل، route /test را صدا بزن و pushResult را نمایش بده.
نکات امنیتی:
- هیچ مقدار FIREBASE_SERVICE_ACCOUNT_JSON، private key یا server credential را در فرانت پنل قرار نده.
- ارسال push فقط از API بکاند انجام شود.
- همه endpointهای panel باید با Admin token صدا زده شوند.
نکات UI/UX:
- صفحه RTL و هماهنگ با طراحی فعلی پنل باشد.
- قبل از broadcast به همه کاربران confirm modal بگیر.
- اگر ارسال طولانی شد، دکمه loading بماند و دوبار ارسال نشود.
- خطای API واضح نمایش داده شود.
- بعد از ارسال موفق، فرم را reset نکن مگر کاربر خودش بخواهد.
برای پنل ادمین و بخشهای مرتبط، مدیریت درخواستهای آگهی AdvertiseRequest را پیاده کن.
هدف:
کاربر بتواند برای یک آگهی درخواست همکاری ثبت کند، تامینکننده بتواند درخواستهای آگهیهای شرکت خودش را ببیند و با کاربر چت کند، و ادمین بتواند درخواستها را فیلتر، بررسی، تغییر وضعیت و حذف کند.
APIهای Client:
1. ثبت درخواست برای آگهی
POST /client/v1/AdvertiseRequest
Auth: User token
Body:
{
"advertiseId": "ADVERTISE_ID",
"hasRequiredCapital": true,
"capitalAmount": "۵۰۰ میلیون تا ۱ میلیارد",
"messageToCompany": "متن پیام کاربر برای شرکت",
"relatedExperience": "تجربه مرتبط کاربر",
"facilities": "business-license, office, cheque-guarantee",
"consentToShareInfo": true,
"requestArea": [
{ "city": "CITY_ID" },
{ "description": "شهر یا محدوده دیگر" }
]
}
نکات ثبت:
- advertiseId الزامی است.
- consentToShareInfo باید true باشد.
- حداقل یک مورد requestArea لازم است.
- اگر برای همان advertise و user قبلا درخواست ثبت شده باشد، API با 409 همان درخواست قبلی را برمیگرداند.
- facilities ممکن است string با کامای فارسی یا انگلیسی باشد. در response باید به آبجکتهای UserFacility تبدیل و iconName برگردد.
2. درخواستهای من
GET /client/v1/AdvertiseRequest/my
Auth: User token
Query:
- page optional
- limit optional
- sort optional, default -createdAt
- q optional
3. پیامهای چت درخواست
GET /client/v1/AdvertiseRequest/:id/chat/messages
POST /client/v1/AdvertiseRequest/:id/chat/messages
Auth: User token
Body ارسال پیام:
{
"message": "متن پیام"
}
APIهای Supplier:
1. لیست درخواستهای آگهیهای شرکت تامینکننده
GET /supplier/v1/AdvertiseRequest
Auth: Supplier token
Query:
- page optional
- limit optional
- sort optional, default -createdAt
- advertiseId optional
- statusId optional
- isViewed optional
- q optional
2. دریافت یک درخواست
GET /supplier/v1/AdvertiseRequest/:id
Auth: Supplier token
Response سمت supplier:
- فیلد adminNote نباید نمایش داده شود.
- اگر یادداشت ادمین لازم بود، با نام supplierNote نمایش داده شود.
- facilities باید آبجکتهای فارسی UserFacility و iconName داشته باشد.
- advertiseExpireAt، isAdvertiseExpired، hasMatchingAdvertiseCity و matchedRequestAreas را نمایش بده.
3. چت تامینکننده با کاربر
GET /supplier/v1/AdvertiseRequest/:id/chat/messages
POST /supplier/v1/AdvertiseRequest/:id/chat/messages
Auth: Supplier token
Body ارسال پیام:
{
"message": "متن پیام"
}
APIهای Panel:
1. لیست همه درخواستها
GET /panel/v1/AdvertiseRequest
Auth: Admin token
Query:
- page optional
- limit optional
- sort optional, default -createdAt
2. فیلتر درخواستها
POST /panel/v1/AdvertiseRequest/filter
Auth: Admin token
Body:
{
"page": 1,
"limit": 10,
"sort": "-createdAt",
"q": "متن جستجو",
"advertise": "ADVERTISE_ID",
"company": "COMPANY_ID",
"user": "USER_ID",
"statusId": "STATUS_ID",
"isViewed": true
}
3. دریافت یک درخواست
GET /panel/v1/AdvertiseRequest/id/:id
Auth: Admin token
4. ویرایش درخواست
PUT /panel/v1/AdvertiseRequest/:id
Auth: Admin token
Body:
{
"statusId": "STATUS_ID",
"statusDescription": "توضیح وضعیت",
"adminNote": "یادداشت ادمین",
"hasRequiredCapital": true,
"capitalAmount": "مقدار سرمایه",
"messageToCompany": "پیام",
"relatedExperience": "تجربه",
"facilities": "business-license, office",
"consentToShareInfo": true,
"isViewed": true,
"requestArea": [
{ "city": "CITY_ID" }
]
}
5. بررسی و تغییر وضعیت سریع
PATCH /panel/v1/AdvertiseRequest/review/:id
Auth: Admin token
Body:
{
"statusId": "STATUS_ID",
"statusDescription": "توضیح وضعیت",
"adminNote": "یادداشت ادمین"
}
وقتی statusId تغییر میکند، بکاند این فیلدها را ذخیره میکند:
- adminNote
- reviewedBy
- reviewedAt
- statusHistory
6. چت پنل روی درخواست
GET /panel/v1/AdvertiseRequest/id/:id/chat/messages
POST /panel/v1/AdvertiseRequest/id/:id/chat/messages
Auth: Admin token
Body ارسال پیام:
{
"message": "متن پیام"
}
7. حذف درخواست
DELETE /panel/v1/AdvertiseRequest/:id
Auth: Admin token
UI مورد انتظار:
- صفحه لیست با فیلترهای وضعیت، آگهی، شرکت، کاربر، دیدهشده/دیدهنشده و جستجو بساز.
- در جدول این موارد را نشان بده:
- عنوان آگهی
- شرکت
- کاربر
- وضعیت
- شهرهای درخواستشده
- سرمایه
- تاریخ ثبت
- وضعیت انقضای آگهی
- صفحه جزئیات درخواست داشته باش:
- اطلاعات آگهی و شرکت
- اطلاعات کاربر
- messageToCompany و relatedExperience
- facilities با آیکون و عنوان فارسی
- requestArea
- statusHistory
- فرم تغییر وضعیت و adminNote
- بخش چت
- بعد از تغییر وضعیت، دیتای جزئیات را دوباره fetch کن.
- خطاهای 400، 404، 409 و 500 را با پیام API نمایش بده.
- همه فرمها RTL و هماهنگ با طراحی فعلی پنل باشند.
Client
11 پرامپتدر سمت کلاینت، تیک آبی شرکتهای تاییدشده را در صفحه شرکت، کارت شرکت و کارت/جزئیات آگهی نمایش بده.
دادههای بکاند:
در response شرکت و company داخل آگهی این فیلدها برمیگردند:
{
"isVerified": true,
"verificationStatus": "accepted",
"verificationNote": "...",
"verifiedAt": "..."
}
APIهای مهم:
GET /client/v1/Company/slug/:slug
GET /client/v1/Company/id/:id
POST /client/v1/Company
GET /client/v1/Advertise/slug/:slug
GET /client/v1/Advertise/id/:id
POST /client/v1/Advertise
رفتار UI:
- اگر isVerified true و verificationStatus برابر accepted بود، کنار نام شرکت badge تیک آبی نمایش بده.
- در کارتهای آگهی، اگر advertise.company.isVerified true بود، کنار نام شرکت تیک آبی نمایش بده.
- در صفحه جزئیات آگهی، کنار نام شرکت و در کارت پروفایل شرکت تیک آبی نمایش بده.
- در صفحه شرکت، کنار companyName تیک آبی نمایش بده.
- برای tooltip متن کوتاه بگذار: «شرکت توسط کارپک تایید شده است».
- اگر isVerified false یا verificationStatus غیر از accepted بود، هیچ badge تاییدی نمایش نده.
نکات طراحی:
- badge کوچک و واضح باشد و layout را جابهجا نکند.
- از آیکون check/verified موجود در icon library پروژه استفاده کن.
- رنگ badge باید آبی/اعتمادساز باشد اما با طراحی فعلی هماهنگ بماند.
- در SSR/SEO هم badge بر اساس داده اولیه نمایش داده شود، نه فقط بعد از fetch سمت کلاینت.
- روی موبایل، badge نباید نام شرکت را از کادر بیرون بزند.
در پروژه کلاینت Next.js، صفحات عمومی و SEO-sensitive را طوری پیاده کن که دادههای بدون توکن قبل از لود شدن صفحه روی سرور fetch شوند تا گوگل محتوای صفحه را ببیند.
هدف:
برای صفحات عمومی مثل لیست آگهیها، جزئیات آگهی، صفحه شرکت، دستهبندیها و لیست شرکتها از getInitialProps یا getServerSideProps استفاده کن و داده را server-side بگیر. برای این کار فقط از APIهایی استفاده کن که توکن نمیخواهند.
APIهای مجاز بدون توکن برای SSR:
آگهیها:
POST /client/v1/Advertise
POST /client/v1/Advertise/vip
GET /client/v1/Advertise/id/:id
GET /client/v1/Advertise/slug/:slug
GET /client/v1/Advertise/supplier/:supplierId
GET /client/v1/Advertise/advertiseRequest/id/:id
شرکتها:
POST /client/v1/Company
GET /client/v1/Company/id/:id
GET /client/v1/Company/slug/:slug
GET /client/v1/Company/gallery/id/:id
این دو API شرکت توکن میخواهند و نباید در SSR عمومی استفاده شوند:
GET /client/v1/Company/contact/id/:id
GET /client/v1/Company/contact/slug/:slug
دستهبندی و فیلترهای عمومی:
GET /client/v1/CategoryFranchise
GET /client/v1/CategoryFranchise/id/:id
POST /client/v1/CategoryFranchise/filter
GET /client/v1/AdvertiseType
GET /client/v1/DealershipType
GET /client/v1/DealershipTypes
GET /client/v1/SupplierType
GET /client/v1/KeyBenefit
شهر و استان:
GET /client/v1/Province
GET /client/v1/Province/id/:id
POST /client/v1/Province/filter
GET /client/v1/City
GET /client/v1/City/all
GET /client/v1/City/tree
GET /client/v1/City/province/:provinceId
POST /client/v1/City/filter
POST /client/v1/City/distinct
دادههای عمومی فرم:
GET /client/v1/UserFacility
GET /client/v1/UserInterest
GET /client/v1/UserInterest/id/:id
صفحات پیشنهادی برای SSR:
- صفحه لیست آگهیها: POST /client/v1/Advertise
- صفحه جزئیات آگهی: GET /client/v1/Advertise/slug/:slug
- صفحه شرکت: GET /client/v1/Company/slug/:slug
- صفحه دستهبندیها: GET /client/v1/CategoryFranchise
- صفحه لیست شرکتها: POST /client/v1/Company
رفتار مورد انتظار:
- در صفحات عمومی، داده اصلی صفحه باید داخل getServerSideProps یا getInitialProps گرفته شود و به کامپوننت به عنوان initialData برسد.
- بعد از hydrate شدن صفحه، اگر نیاز به فیلتر/صفحهبندی سمت کاربر بود، از همان APIها client-side دوباره fetch کن.
- هیچ token، cookie خصوصی یا Authorization header را برای این صفحات عمومی لازم نکن.
- اگر API 204 داد، صفحه باید empty state قابل crawl داشته باشد، نه فقط spinner.
- اگر API خطا داد، status مناسب Next.js برگردان؛ برای جزئیات آگهی/شرکت اگر داده نبود notFound یا صفحه 404 بده.
- title، meta description، canonical و OpenGraph را از داده SSR شده بساز.
- برای صفحه جزئیات آگهی، title/description/company/category/city/publishedAt را در meta و structured data استفاده کن.
- برای صفحه شرکت، companyName/aboutCompany/category/supplierType/city/logo را در meta و structured data استفاده کن.
- loading اولیه صفحه نباید جای محتوای اصلی را بگیرد؛ محتوای SSR شده باید در HTML اولیه وجود داشته باشد.
نمونه الگو:
export async function getServerSideProps(ctx) {
const { slug } = ctx.params;
const res = await api.get(`/client/v1/Advertise/slug/${slug}`);
if (!res?.data?.data) {
return { notFound: true };
}
return {
props: {
initialAdvertise: res.data.data,
},
};
}
نکات مهم:
- routeهای supplier/v1 و panel/v1 را برای SEO عمومی استفاده نکن.
- endpointهایی که UserToken یا SupplierToken میخواهند نباید داخل SSR عمومی گوگل قرار بگیرند.
- اگر پروژه از App Router استفاده میکند، همین منطق را با server component، generateMetadata و fetch server-side پیاده کن.
در سمت کلاینت، لیست و جستجوی آگهیها را به فیلتر دستهبندی وصل کن.
هدف:
کاربر بتواند آگهیها را با دستهبندی جستجو و فیلتر کند و این فیلتر همراه با سرچ متنی، pagination و sort کار کند.
API:
POST /client/v1/Advertise
Body نمونه:
{
"category": "CATEGORY_ID",
"q": "متن جستجو",
"page": 1,
"limit": 10,
"sort": "-createdAt"
}
نکات پیادهسازی:
- لیست دستهبندیها را از API دستهبندیهای پروژه بگیر و در UI به صورت select یا searchable select نمایش بده.
- مقدار ارسالی category باید ObjectId دستهبندی باشد.
- اگر کاربر دستهبندی را پاک کرد، category را از body حذف کن تا همه آگهیها نمایش داده شوند.
- فیلتر category باید با q همزمان کار کند؛ یعنی کاربر بتواند داخل یک دستهبندی سرچ متنی انجام دهد.
- هنگام تغییر category، page را به 1 برگردان.
- category برگشتی در response آگهی را در کارت یا جزئیات آگهی نمایش بده.
- empty state مناسب برای وقتی هیچ آگهی در آن دستهبندی پیدا نشد نمایش بده.
- query state را اگر پروژه pattern دارد در URL یا state مدیریت کن تا برگشت کاربر از جزئیات آگهی فیلترها را از بین نبرد.
در سمت کلاینت، فرم ثبت درخواست برای یک آگهی را به API محدوده قابل انتخاب آگهی وصل کن.
هدف:
وقتی کاربر میخواهد برای یک آگهی درخواست ثبت کند، فقط شهرها یا استانهای مجاز همان آگهی را برای انتخاب محدوده درخواست ببیند. اگر آگهی روی «کل ایران» یا «کل کشور» تعریف شده بود، لیست همه استانها نمایش داده شود.
API:
GET /client/v1/Advertise/advertiseRequest/id/:id
Response نمونه:
{
"data": {
"provinces": [],
"cities": [],
"hasAllIranLocation": false
}
}
رفتار UI:
- هنگام باز شدن فرم ثبت درخواست آگهی، این API را با id آگهی صدا بزن.
- اگر hasAllIranLocation برابر true بود، selector استان را از data.provinces پر کن و selector شهر را تا زمان انتخاب استان خالی/غیرفعال نگه دار.
- اگر hasAllIranLocation برابر false بود و data.cities مقدار داشت، selector شهر را از data.cities پر کن.
- اگر hasAllIranLocation برابر false بود و فقط data.provinces مقدار داشت، selector استان را نمایش بده.
- اگر فقط یک شهر یا استان وجود داشت، همان مقدار را به صورت پیشفرض انتخاب کن ولی همچنان امکان دیدن مقدار انتخابشده در UI مشخص باشد.
- خروجی انتخاب کاربر باید با ساختار فعلی ثبت درخواست هماهنگ باشد؛ یعنی در POST /client/v1/AdvertiseRequest مقدار requestArea یا city/cityId طبق قرارداد بکاند ارسال شود.
نکات مهم:
- این API فقط برای گرفتن محدوده شهر/استان آگهی است و نباید اطلاعات خود آگهی را از آن انتظار داشته باشی.
- برای اطلاعات جزئیات آگهی از API جزئیات آگهی استفاده کن.
- برای حالت empty یا خطای 204، پیام مناسب نمایش بده که محدودهای برای این آگهی ثبت نشده است.
- loading، disabled state و retry را برای selector محدوده پیاده کن.
- UI RTL باشد و با طراحی فعلی کلاینت هماهنگ بماند.
در پنل کاربر، بخش «کسبوکارهای من» را طبق UI تصویر پیاده کن.
API بکاند:
Base:
/client/v1/UserBusiness
همه درخواستها نیاز به توکن کاربر دارند.
1. دریافت لیست کسبوکارهای من
GET /client/v1/UserBusiness/my
Response:
{
"data": [
{
"id": "...",
"name": "یدککار مرکزی",
"logo": "imageUrl",
"description": "توزیعکننده تخصصی قطعات...",
"phone": "021...",
"address": "تهران، خیابان...",
"isActive": true,
"createdAt": "...",
"updatedAt": "..."
}
]
}
2. افزودن کسبوکار
POST /client/v1/UserBusiness
Content-Type: multipart/form-data
Fields:
- name: string, required
- logo: file, optional
- description: string, optional
- phone: string, optional
- address: string, optional
- isActive: boolean, optional, default true
3. ویرایش کسبوکار
PUT /client/v1/UserBusiness/:id
Content-Type: multipart/form-data
Fields:
- name: string, optional
- logo: file, optional
- description: string, optional
- phone: string, optional
- address: string, optional
- isActive: boolean, optional
4. حذف کسبوکار
DELETE /client/v1/UserBusiness/:id
UI:
- در صفحه پروفایل/داشبورد کاربر یک کارت با عنوان «کسبوکارهای من» بساز.
- لیست کسبوکارها را بالا نمایش بده.
- هر آیتم شامل لوگو/آیکن، نام، توضیح کوتاه، وضعیت فعال/غیرفعال و دکمه حذف/ویرایش باشد.
- پایین لیست فرم «افزودن کسبوکار جدید» قرار بده.
- فیلدها:
- تصویر کسبوکار/لوگو با upload box
- نام کسبوکار
- توضیحات
- شماره تماس
- آدرس دقیق
- سوییچ یا checkbox با متن «این کسبوکار فعال است»
- فرم باید RTL باشد و از استایل فعلی پنل کاربر پیروی کند.
- بعد از ثبت موفق، لیست را refresh کن و فرم را reset کن.
- هنگام ویرایش، اطلاعات آیتم داخل فرم بنشیند و دکمه فرم به «ذخیره تغییرات» تغییر کند.
- هنگام حذف، confirm بگیر.
- loading، empty state و خطاهای API را هندل کن.
- برای ارسال فرم حتما از FormData استفاده کن و logo را با key `logo` بفرست.
- مقدار `isActive` را به صورت `true` یا `false` داخل FormData ارسال کن.
Firebase Cloud Messaging را در فرانت پنل کاربر پیاده کن و به بکاند Karpack وصل کن.
هدف:
- وقتی کاربر گزینه «مرورگر» را در تنظیمات اعلانها فعال میکند، permission مرورگر گرفته شود.
- FCM token مرورگر با Firebase دریافت شود.
- token به بکاند ارسال و ذخیره شود.
- وقتی کاربر گزینه «مرورگر» را خاموش میکند، token در بکاند غیرفعال شود.
- Push notification فقط برای کاربر لاگینشده فعال باشد.
Firebase config پروژه:
const firebaseConfig = {
apiKey: "AIzaSyDrh2YhFRfyzlsTXWHMfa_uwJZFg4KhuDA",
authDomain: "karpack-73532.firebaseapp.com",
projectId: "karpack-73532",
storageBucket: "karpack-73532.firebasestorage.app",
messagingSenderId: "1018999335326",
appId: "1:1018999335326:web:640e624eeb3a61c9570e8f",
measurementId: "G-N940P3FEC5"
};
این config عمومی است، ولی بهتر است در env فرانت قرار بگیرد.
envهای پیشنهادی فرانت:
VITE_FIREBASE_API_KEY=AIzaSyDrh2YhFRfyzlsTXWHMfa_uwJZFg4KhuDA
VITE_FIREBASE_AUTH_DOMAIN=karpack-73532.firebaseapp.com
VITE_FIREBASE_PROJECT_ID=karpack-73532
VITE_FIREBASE_STORAGE_BUCKET=karpack-73532.firebasestorage.app
VITE_FIREBASE_MESSAGING_SENDER_ID=1018999335326
VITE_FIREBASE_APP_ID=1:1018999335326:web:640e624eeb3a61c9570e8f
VITE_FIREBASE_MEASUREMENT_ID=G-N940P3FEC5
VITE_FIREBASE_VAPID_KEY=از Firebase Console بگیر
VAPID key را از این مسیر بگیر:
Firebase Console → Project Settings → Cloud Messaging → Web Push certificates → Generate key pair
وابستگی فرانت:
اگر firebase نصب نیست:
npm install firebase
فایلهای پیشنهادی:
- src/lib/firebase.ts یا src/lib/firebase.js
- src/services/pushNotification.ts
- public/firebase-messaging-sw.js
نمونه src/lib/firebase.js:
import { initializeApp } from "firebase/app";
import { getMessaging, isSupported } from "firebase/messaging";
const firebaseConfig = {
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
appId: import.meta.env.VITE_FIREBASE_APP_ID,
measurementId: import.meta.env.VITE_FIREBASE_MEASUREMENT_ID,
};
export const firebaseApp = initializeApp(firebaseConfig);
export async function getFirebaseMessaging() {
const supported = await isSupported();
if (!supported) return null;
return getMessaging(firebaseApp);
}
نمونه گرفتن token و ثبت در بکاند:
import { getToken, deleteToken } from "firebase/messaging";
import { getFirebaseMessaging } from "@/lib/firebase";
export async function registerBrowserPush(api) {
if (!("Notification" in window)) {
throw new Error("این مرورگر اعلان را پشتیبانی نمیکند");
}
const permission = await Notification.requestPermission();
if (permission !== "granted") {
throw new Error("اجازه اعلان مرورگر داده نشد");
}
const messaging = await getFirebaseMessaging();
if (!messaging) {
throw new Error("Firebase Messaging در این مرورگر پشتیبانی نمیشود");
}
const token = await getToken(messaging, {
vapidKey: import.meta.env.VITE_FIREBASE_VAPID_KEY,
});
await api.post("/client/v1/User/push-token", {
token,
platform: "web",
userAgent: navigator.userAgent,
});
return token;
}
export async function unregisterBrowserPush(api, token) {
if (token) {
await api.delete("/client/v1/User/push-token", {
data: { token },
});
}
const messaging = await getFirebaseMessaging();
if (messaging) {
await deleteToken(messaging).catch(() => null);
}
}
APIهای آماده بکاند:
1. ثبت FCM token
POST /client/v1/User/push-token
Auth: Bearer user token
Body:
{
"token": "fcm-token",
"platform": "web",
"deviceId": "optional-stable-device-id",
"userAgent": "navigator.userAgent"
}
2. غیرفعالسازی FCM token
DELETE /client/v1/User/push-token
Auth: Bearer user token
Body:
{
"token": "fcm-token"
}
3. ارسال اعلان تستی از بکاند
POST /client/v1/User/push-token/test
Auth: Bearer user token
Body:
{
"title": "Karpack",
"body": "اعلان آزمایشی",
"url": "/dashboard/profile"
}
اتصال به تنظیمات اعلانها:
- داده اولیه کاربر را از GET /client/v1/User/me بگیر.
- اگر user.notificationSettings.channels.browser برابر true بود و permission قبلاً granted بود، token را silent refresh کن و به بکاند بفرست.
- وقتی کاربر چکباکس «مرورگر» را روشن کرد:
1. permission بگیر.
2. FCM token بگیر.
3. POST /client/v1/User/push-token را بزن.
4. PUT /client/v1/User/notification-settings را با browser: true بزن.
- اگر permission denied شد، browser را true نکن و پیام راهنما نمایش بده.
- وقتی کاربر چکباکس «مرورگر» را خاموش کرد:
1. DELETE /client/v1/User/push-token را بزن.
2. PUT /client/v1/User/notification-settings را با browser: false بزن.
Service worker:
فایل public/firebase-messaging-sw.js را اضافه کن.
در service worker نمیتوان مستقیم از import.meta.env استفاده کرد؛ مقدارهای Firebase config را یا hardcode کن چون عمومی هستند، یا هنگام build inject کن.
نمونه public/firebase-messaging-sw.js:
importScripts("https://www.gstatic.com/firebasejs/10.13.2/firebase-app-compat.js");
importScripts("https://www.gstatic.com/firebasejs/10.13.2/firebase-messaging-compat.js");
firebase.initializeApp({
apiKey: "AIzaSyDrh2YhFRfyzlsTXWHMfa_uwJZFg4KhuDA",
authDomain: "karpack-73532.firebaseapp.com",
projectId: "karpack-73532",
storageBucket: "karpack-73532.firebasestorage.app",
messagingSenderId: "1018999335326",
appId: "1:1018999335326:web:640e624eeb3a61c9570e8f",
measurementId: "G-N940P3FEC5"
});
const messaging = firebase.messaging();
messaging.onBackgroundMessage((payload) => {
const title = payload.notification?.title || "Karpack";
const options = {
body: payload.notification?.body || "اعلان جدید دارید",
icon: "/karpack-logo.png",
data: {
url: payload.data?.url || "/dashboard/profile",
},
};
self.registration.showNotification(title, options);
});
self.addEventListener("notificationclick", (event) => {
event.notification.close();
const url = event.notification.data?.url || "/dashboard/profile";
event.waitUntil(clients.openWindow(url));
});
نکات UI/UX:
- درخواست permission را هنگام ورود صفحه نمایش نده؛ فقط بعد از کلیک مستقیم کاربر روی فعالسازی «مرورگر».
- اگر مرورگر FCM را پشتیبانی نکرد، گزینه «مرورگر» disabled شود و پیام کوتاه بده.
- اگر permission denied بود، راهنمای فعالسازی از تنظیمات مرورگر نمایش بده.
- loading فقط روی کنترل «مرورگر» یا دکمه ذخیره همین بخش باشد.
- طراحی RTL و ظاهر کارت اعلانها حفظ شود.
تست دستی:
- با کاربر لاگینشده «مرورگر» را فعال کن و مطمئن شو POST /push-token موفق است.
- صفحه را refresh کن و مطمئن شو browser=true و token از دست نمیرود.
- POST /client/v1/User/push-token/test را بزن و دریافت notification را تست کن.
- «مرورگر» را خاموش کن و مطمئن شو DELETE /push-token و browser=false انجام میشود.
- permission denied و مرورگر unsupported را هم تست کن.
در سمت کلاینت/ساپلایر، گزینههای پیشنهادی «مزایای کلیدی» در فرم ساخت/ویرایش آگهی را از بکاند دریافت و نمایش بده.
API:
GET /client/v1/KeyBenefit
برای پنل ساپلایر:
GET /supplier/v1/KeyBenefit
Auth:
این endpointها نیاز به توکن ندارند و فقط مزایای فعال را برمیگردانند.
Response:
{
"data": [
{
"id": "...",
"title": "کیفیت تضمینشده",
"key": "guaranteed-quality",
"iconName": "verified",
"sort": 10,
"isActive": true
}
]
}
رفتار مورد انتظار در فرم آگهی:
- هنگام mount شدن مرحله «مزایا و شهرها» یا بخش «مزایای کلیدی»، لیست را از API بگیر.
- گزینههای hardcode شده قبلی را حذف کن.
- گزینهها را به صورت chip/button پیشنهادی یا dropdown قابل انتخاب نمایش بده.
- label هر گزینه از title باشد.
- value انتخابشده برای submit بهتر است title باشد، چون فیلد فعلی advertise.benefits در بکاند آرایه string است.
- اگر UI پروژه با key کار میکند، قبل از submit key را به title تبدیل کن یا مطمئن شو بکاند و نمایش جزئیات آگهی همان value را درست نمایش میدهند.
- ترتیب نمایش همان ترتیب response باشد.
- iconName را اگر icon map موجود است به آیکن وصل کن؛ اگر نبود فقط title را نمایش بده.
اتصال به ذخیره آگهی:
در مرحله مزایا، مقدار benefits را مثل قبل به API ساخت/ویرایش آگهی بفرست:
POST /supplier/v1/Advertise/step4
یا
POST /panel/v1/Advertise/step4
Body نمونه:
{
"advertiseId": "advertiseId",
"benefits": ["کیفیت تضمینشده", "ارسال سریع"],
"city": ["cityId1", "cityId2"]
}
UI/UX:
- متن راهنما حفظ شود: «حداکثر ۵ مزیت کلیدی همکاری با شما را اضافه کنید.»
- کاربر بتواند از پیشنهادها انتخاب کند و همچنین مزیت سفارشی تایپ کند.
- تعداد مزایا را حداکثر ۵ نگه دار.
- از اضافه شدن مقدار تکراری جلوگیری کن.
- loading state برای دریافت پیشنهادها داشته باش.
- اگر API 204 یا data خالی برگشت، بخش تایپ دستی مزایا همچنان کار کند.
- در صورت خطا، پیام کوتاه نمایش بده ولی فرم را بلاک نکن.
- بعد از انتخاب یک پیشنهاد، آن را به لیست benefits اضافه کن و input را پاک کن.
- طراحی RTL و استایل فعلی کارت «مزایای کلیدی» را حفظ کن.
در سمت کلاینت، گزینههای «دستهبندیهای مورد علاقه» در فرم ثبتنام/تکمیل پروفایل کاربر را از بکاند دریافت و نمایش بده.
API:
GET /client/v1/UserInterest
Auth:
این endpoint نیاز به توکن ندارد.
Response:
{
"data": [
{
"id": "...",
"title": "فناوری",
"key": "technology",
"relatedId": {
"id": "categoryId",
"title": "فناوری و دیجیتال",
"key": "technology_digital"
},
"sort": 20,
"isActive": true
}
]
}
رفتار مورد انتظار:
- هنگام mount شدن فرم ثبتنام/تکمیل پروفایل، لیست علاقهمندیها را از GET /client/v1/UserInterest بگیر.
- گزینههای hardcode شده قبلی مثل آرایشی و سلامت، فناوری، خودرو، مد و پوشاک، غذا و نوشیدنی و خدمات را حذف کن.
- گزینهها را به صورت checkbox/chip چندانتخابی نمایش بده، مشابه UI فعلی تصویر.
- label هر گزینه از title باشد.
- value انتخابشده برای submit باید relatedId.id باشد، چون بکاند پروفایل کاربر category/interestCategories را با id دستهبندی ذخیره میکند.
- ترتیب نمایش همان ترتیب response باشد.
- اگر relatedId خالی یا نامعتبر بود، آن گزینه را disabled نمایش بده یا در submit ارسال نکن.
اتصال به ثبتنام یا تکمیل پروفایل:
در submit، علاقهمندیهای انتخابشده را با یکی از keyهای مورد قبول بکاند ارسال کن. ترجیح با interestCategories است.
Body نمونه:
{
"interestCategories": ["categoryId1", "categoryId2"]
}
اگر فرم ثبتنام همین حالا فیلد category میفرستد، مقدار آن را از selected interests بساز:
{
"category": ["categoryId1", "categoryId2"]
}
Prefill:
- اگر صفحه پروفایل است، داده اولیه کاربر را از GET /client/v1/User/me بگیر.
- اگر user.category شامل id دستهبندیها بود، گزینههایی را selected کن که relatedId.id آنها داخل user.category است.
- اگر مقدار قدیمی وجود داشت که در لیست علاقهمندیها نبود، آن را حذف نکن؛ فقط در UI به شکل گزینه ناشناخته/غیرفعال نمایش بده تا دیتای کاربر گم نشود.
UI/UX:
- loading state برای دریافت علاقهمندیها داشته باش.
- اگر API 204 یا data خالی برگشت، empty state کوتاه نمایش بده و فرم همچنان قابل ارسال باشد.
- در صورت خطا، پیام مناسب نمایش بده و از ارسال مقدار اشتباه جلوگیری کن.
- طراحی RTL، فاصلهها و استایل checkbox/chip فعلی فرم را حفظ کن.
- متن کمکی فعلی «این انتخابها فعلا فقط برای تکمیل اولیه پروفایل ثبت میشوند.» حفظ شود مگر اینکه در طراحی جدید جای بهتری داشته باشد.
در سمت کلاینت، گزینههای «امکانات موجود» در کارت «توانمندیها و تجربه» را از بکاند دریافت و نمایش بده.
API:
GET /client/v1/UserFacility
Auth:
این endpoint نیاز به توکن ندارد.
Response:
{
"data": [
{
"id": "...",
"title": "مغازه",
"key": "store",
"iconName": "store",
"sort": 10,
"isActive": true
}
]
}
رفتار مورد انتظار:
- هنگام mount شدن صفحه /dashboard/profile، لیست امکانات را از GET /client/v1/UserFacility بگیر.
- فقط دادههای response را نمایش بده؛ گزینههای hardcode شده قبلی را حذف کن.
- گزینهها را به صورت chip/button چندانتخابی نمایش بده.
- label هر chip از title باشد.
- value ذخیرهشده برای submit ترجیحاً key باشد.
- iconName را اگر icon map در پروژه وجود دارد به آیکن مناسب وصل کن؛ اگر آیکن موجود نبود فقط title را نمایش بده.
- ترتیب نمایش همان ترتیب response باشد.
اتصال به ذخیره پروفایل:
بعد از انتخاب امکانات، در submit پروفایل این مسیر را صدا بزن:
PUT /client/v1/User/profile/capabilities
Body:
{
"facilities": ["store", "office"],
"workExperience": "متن تجربه کاری...",
"interestCategories": ["categoryId"]
}
Prefill:
- داده اولیه کاربر را از GET /client/v1/User/me بگیر.
- اگر user.facilities شامل key گزینهها بود، همان chipها را selected کن.
- اگر مقدار custom یا قدیمی وجود داشت که در لیست امکانات نبود، آن را هم به عنوان chip انتخابشده نمایش بده تا دیتای کاربر گم نشود.
UI/UX:
- loading state برای دریافت امکانات داشته باش.
- اگر API 204 یا data خالی برگشت، empty state کوتاه نمایش بده.
- در صورت خطا، پیام مناسب نمایش بده و فرم تجربه کاری همچنان قابل استفاده باشد.
- فیلد «افزودن امکانات دیگر» حفظ شود؛ مقدار تایپشده بعد از زدن + به لیست انتخابشده facilities اضافه شود.
- طراحی RTL و استایل کارت فعلی صفحه پروفایل را حفظ کن.
در داشبورد کاربر، داخل صفحه «پروفایل من»، کارت «توانمندیها و تجربه» را به بکاند وصل کن.
مسیر صفحه:
/dashboard/profile
API بکاند:
PUT /client/v1/User/profile/capabilities
Auth:
همه درخواستها باید با توکن کاربر ارسال شوند.
Body:
{
"facilities": ["مغازه", "دفتر کار", "ماشین پخش"],
"workExperience": "خلاصهای از سوابق کاری کاربر...",
"interestCategories": ["categoryId1", "categoryId2"]
}
نامهای جایگزین قابل قبول:
- facilities یا availableFacilities
- interestCategories یا interests یا category
Validation:
- facilities الزامی است و باید حداقل یک مورد داشته باشد.
- workExperience اختیاری است.
- interestCategories اختیاری و چندانتخابی است.
Response موفق:
{
"data": {
"id": "...",
"fullName": "...",
"email": "...",
"phoneNumber": "...",
"category": ["categoryId"],
"facilities": ["مغازه"],
"workExperience": "..."
},
"message": "اطلاعات با موفقیت ثبت شد"
}
دریافت داده اولیه:
GET /client/v1/User/me
از response همین مسیر مقدارهای category، facilities و workExperience را برای prefill فرم استفاده کن.
UI:
- همین کارت موجود با عنوان «توانمندیها و تجربه» را نگه دار.
- امکانات موجود به صورت chip/button چندانتخابی باشد.
- گزینههای پیشفرض:
- مغازه
- دفتر کار
- مجوز فعالیت
- ماشین پخش
- نیروی بازاریابی
- تیم فنی
- فعالیت آنلاین
- چک و وثیقه
- فیلد «افزودن امکانات دیگر» داشته باشد؛ با دکمه + مقدار تایپشده به facilities اضافه شود.
- textarea تجربه کاری به workExperience وصل شود.
- select/dropdown «حوزههای علاقه» چندانتخابی باشد و مقدار آن با interestCategories ارسال شود.
- بعد از ثبت موفق toast/success message نمایش بده.
- هنگام خطا پیام API را نمایش بده.
- loading button و disabled state هنگام submit داشته باش.
- بعد از ذخیره موفق، state محلی را با data برگشتی sync کن.
نکات فنی:
- درخواست را JSON بفرست، نه FormData.
- نمونه request:
await api.put("/client/v1/User/profile/capabilities", {
facilities,
workExperience,
interestCategories
});
- اگر پروژه service/hook آماده برای user profile دارد، از همان استفاده کن و abstraction جدید نساز.
- استایل RTL، spacing و ظاهر کارت را دقیقاً مثل باقی کارتهای صفحه پروفایل نگه دار.
در داشبورد کاربر، کارت تنظیمات حساب را به بکاند وصل کن تا هم تنظیمات اعلانها و هم انتخاب دارک/لایت مود ذخیره و بازیابی شوند.
مسیر صفحه:
/dashboard/profile یا صفحه تنظیمات حساب کاربر
Auth:
همه درخواستها باید با توکن کاربر ارسال شوند.
دریافت داده اولیه:
GET /client/v1/User/me
از response کاربر مقدارهای زیر را برای prefill استفاده کن:
{
"themeMode": "light",
"notificationSettings": {
"types": {
"messagesAndRequests": true,
"specialOffers": true
},
"channels": {
"email": true,
"mobile": true,
"browser": false
}
}
}
پیشفرضها اگر response مقدار نداشت:
- themeMode: light
- messagesAndRequests: true
- specialOffers: true
- email: true
- mobile: true
- browser: false
1. ذخیره تنظیمات اعلانها
PUT /client/v1/User/notification-settings
Content-Type: application/json
Body:
{
"notificationSettings": {
"types": {
"messagesAndRequests": true,
"specialOffers": true
},
"channels": {
"email": true,
"mobile": true,
"browser": false
}
}
}
Response موفق:
{
"data": {
"id": "...",
"notificationSettings": {
"types": {
"messagesAndRequests": true,
"specialOffers": true
},
"channels": {
"email": true,
"mobile": true,
"browser": false
}
}
},
"message": "تنظیمات اعلانها با موفقیت ثبت شد"
}
نگاشت UI اعلانها:
- چکباکس «پیامها و درخواستها» به notificationSettings.types.messagesAndRequests وصل شود.
- چکباکس «پیشنهادات ویژه» به notificationSettings.types.specialOffers وصل شود.
- چکباکس «ایمیل» به notificationSettings.channels.email وصل شود.
- چکباکس «موبایل» به notificationSettings.channels.mobile وصل شود.
- چکباکس «مرورگر» به notificationSettings.channels.browser وصل شود.
2. ذخیره حالت نمایش دارک/لایت
PUT /client/v1/User/theme-mode
Content-Type: application/json
Body:
{
"themeMode": "dark"
}
مقادیر مجاز themeMode:
- light
- dark
Response موفق:
{
"data": {
"id": "...",
"themeMode": "dark"
},
"message": "تنظیمات نمایش با موفقیت ثبت شد"
}
رفتار مورد انتظار:
- هنگام mount صفحه، از GET /client/v1/User/me مقدارهای فعلی را بخوان و UI را prefill کن.
- UI تم را فوراً بر اساس themeMode ذخیرهشده اعمال کن.
- بعد از تغییر هر چکباکس اعلان، میتوانی auto-save بزنی یا از دکمه «ذخیره تغییرات» همان بخش استفاده کنی.
- وقتی کاربر بین دارک و لایت تغییر داد، مقدار جدید را با PUT /client/v1/User/theme-mode ذخیره کن.
- بعد از ذخیره موفق، state محلی user/theme/notification را با data برگشتی sync کن.
- هنگام ذخیره، فقط کنترلهای همان بخش disabled/loading شوند.
- در صورت خطا، پیام API را نمایش بده و مقدار قبلی را از دست نده.
نکات فنی:
- درخواستها را JSON بفرست، نه FormData.
- برای تم فقط یکی از مقدارهای light یا dark را ارسال کن.
- بکاند aliasهای theme یا mode را هم میپذیرد، ولی در فرانت از themeMode استفاده کن.
- اگر پروژه context/store برای theme دارد، همین endpoint را داخل همان flow وصل کن و state موازی جدید نساز.
- اگر کاربر لاگین نبود، میتوانی تم را موقتاً در localStorage نگه داری؛ بعد از لاگین مقدار بکاند اولویت داشته باشد.
- اگر مرورگر/Push notification در فرانت هنوز پیاده نشده، فقط مقدار browser را ذخیره کن و درخواست permission مرورگر را در این task اضافه نکن.
نکات UI/UX:
- طراحی RTL، آیکنها، کارتها، spacing و ظاهر فعلی تنظیمات حفظ شود.
- چکباکسهای اعلان باید مثل UI فعلی تصویر داخل کارتهای قابل کلیک باشند.
- تغییر تم باید بدون reload صفحه اعمال شود.
- کنترل تم میتواند switch، segmented control یا گزینه داخل تنظیمات حساب باشد.
- label فارسی پیشنهادی برای تم:
- حالت روشن
- حالت تاریک
Supplier
11 پرامپتدر پنل ساپلایر، وضعیت تایید شرکت و مسیر گرفتن تیک آبی را نمایش بده و ساپلایر را برای تکمیل مدارک راهنمایی کن.
دریافت وضعیت شرکت:
GET /supplier/v1/Company/myCompany
Auth: Supplier token
فیلدهای مهم response:
{
"isVerified": false,
"verificationStatus": "pending",
"verificationNote": "...",
"verifiedAt": "..."
}
دریافت مدارک شرکت به صورت group by:
GET /supplier/v1/Company/gallery
Auth: Supplier token
کلیدهای مهم مدارک:
- data.identity: مدارک هویتی
- data.contract: قراردادها
- data.agreement: توافقها
- data.permit: مجوزها
آپلود مدرک:
POST /supplier/v1/Company/step3
Auth: Supplier token
Content-Type: multipart/form-data
Body نمونه مدارک هویتی:
{
"placement": "identity",
"type": "document",
"caption": "کارت ملی مدیرعامل",
"file": "..."
}
Body نمونه قرارداد:
{
"placement": "contract",
"type": "document",
"caption": "قرارداد همکاری",
"file": "..."
}
Body نمونه توافق:
{
"placement": "agreement",
"type": "document",
"caption": "توافقنامه",
"file": "..."
}
رفتار UI:
- در داشبورد/پروفایل شرکت یک کارت «وضعیت تایید شرکت» نمایش بده.
- اگر isVerified true و verificationStatus برابر accepted بود، badge «تایید شده / تیک آبی» نمایش بده.
- اگر pending بود، پیام «مدارک شما در انتظار بررسی است» نمایش بده.
- اگر rejected بود، verificationNote را نمایش بده و CTA برای اصلاح مدارک بده.
- در بخش مدارک، سه سکشن اصلی بساز: مدارک هویتی، قراردادها، توافقها.
- برای هر فایل وضعیت isAccepted و declinedReason را نمایش بده.
- اگر یک سکشن خالی بود، empty state و دکمه آپلود همان placement را نشان بده.
- بعد از آپلود/ویرایش/حذف فایل، دوباره GET /supplier/v1/Company/gallery و GET /supplier/v1/Company/myCompany را صدا بزن.
- ساپلایر نباید خودش بتواند isAccepted یا isVerified را تغییر دهد؛ اینها فقط خواندنی هستند.
- همه پیامها RTL و فارسی باشند و با طراحی فعلی پنل ساپلایر هماهنگ بمانند.
در پنل ساپلایر، flow ثبت و تکمیل پروفایل شرکت را به بکاند وصل کن و UI را چندمرحلهای، واضح و قابل ویرایش بساز.
داده اولیه:
GET /supplier/v1/Company/myCompany
Response باید برای prefill استفاده شود و این فیلدهای مهم را نمایش بده:
- companyName
- phone و phoneInside
- foundedYear
- teamSizeRange
- province و city
- addressDetails
- category
- supplierType
- membershipDate یا owner.createdAt به عنوان تاریخ عضویت
- createdAt به عنوان تاریخ ایجاد شرکت
- updatedAt به عنوان آخرین بروزرسانی شرکت
- logo و headerCover
- aboutCompany و missionStatement
- benefits
- phoneNumber و email و website
- products
- completionProgress
مرحله 1 - اطلاعات پایه شرکت:
POST /supplier/v1/Company/step1
Body نمونه:
{
"companyName": "نام شرکت",
"phone": "021...",
"phoneInside": "123",
"foundedYear": 1400,
"teamSizeRange": "10-50",
"province": "PROVINCE_ID",
"city": "CITY_ID",
"addressDetails": "آدرس کامل",
"category": ["CATEGORY_ID"],
"supplierType": ["SUPPLIER_TYPE_ID"]
}
رفتار مورد انتظار:
- category و supplierType را از APIهای مرجع پنل/کلاینت بگیر و به صورت multi select یا searchable multi select نمایش بده.
- مقدار ارسالی category و supplierType باید آرایه ObjectId باشد.
- وقتی province تغییر کرد، cityهای همان استان را نمایش بده یا اگر API پروژه cityها را یکجا میدهد، client-side فیلتر کن.
- اگر response شامل membershipDate یا owner.createdAt بود، در کارت خلاصه شرکت با عنوان «تاریخ عضویت» نمایش بده.
- اگر response شامل createdAt بود، آن را با عنوان «تاریخ ایجاد شرکت» نمایش بده.
- اگر company موجود بود، فرم را prefill کن و دکمه را «ذخیره تغییرات» نشان بده.
- اگر company هنوز ساخته نشده بود، فرم خالی و دکمه «ثبت شرکت» نمایش داده شود.
- completionProgress را به صورت progress/badge نمایش بده.
- بعد از ذخیره موفق، دوباره GET /supplier/v1/Company/myCompany را بزن تا category، supplierType، city و province به صورت populate شده در UI بنشیند.
مرحله 2 - معرفی و رسانه:
POST /supplier/v1/Company/step2
Content-Type: multipart/form-data
Fields:
- aboutCompany
- missionStatement
- promoVideoUrl
- benefits
- headerCoverType
- coverColor
- logo optional
- cover optional
مرحله 3 - گالری و مدارک:
POST /supplier/v1/Company/step3
PATCH /supplier/v1/Company/step3/:id
DELETE /supplier/v1/Company/step3/:id
مرحله 4 - راههای ارتباط و محصولات:
POST /supplier/v1/Company/step4
Body نمونه:
{
"phoneNumber": "09...",
"email": "info@example.com",
"website": "https://example.com",
"products": ["محصول اول", "محصول دوم"]
}
نکات UI/UX:
- فرم RTL، مرتب و مرحلهای باشد.
- خطاهای API کنار فیلد مربوط نمایش داده شوند.
- در حالت loading فقط همان بخش فرم disabled شود.
- تصاویر logo/headerCover با preview نمایش داده شوند.
- category، supplierType و تاریخ عضویت در خلاصه پروفایل شرکت همیشه قابل مشاهده باشند.
در پنل ساپلایر، هنگام ساخت یا ویرایش آگهی، فیلد «حداقل سرمایه» را به لیست بازههای قابل انتخاب بکاند وصل کن.
هدف:
ساپلایر در Step1 ساخت آگهی، حداقل سرمایه موردنیاز را از بین گزینههایی انتخاب کند که ادمین در پنل مدیریت کرده است. این فیلد در Advertise به صورت string ذخیره میشود، نه ObjectId.
API دریافت گزینهها:
GET /supplier/v1/MinimumCapitalRange
Response نمونه:
{
"data": [
{
"id": "MINIMUM_CAPITAL_RANGE_ID",
"title": "۵۰۰ میلیون تا ۱ میلیارد",
"name": "۵۰۰ میلیون تا ۱ میلیارد",
"key": "capital_500m_1b",
"min": 500000000,
"max": 1000000000,
"sort": 2,
"isActive": true
}
]
}
API ذخیره Step1 آگهی:
POST /supplier/v1/Advertise/step1
Auth: Supplier token
Body نمونه:
{
"advertiseId": "ADVERTISE_ID_FOR_EDIT",
"title": "عنوان آگهی",
"category": "CATEGORY_ID",
"dealershipTypes": ["DEALERSHIP_TYPE_ID"],
"space": "حداقل فضا",
"minimumExperience": "حداقل سابقه",
"minimumCapital": "MINIMUM_CAPITAL_RANGE_ID"
}
رفتار مهم بکاند:
- minimumCapital داخل Advertise به صورت string ذخیره میشود.
- فرانت میتواند id یا key یا title گزینه را ارسال کند.
- بکاند گزینه را پیدا میکند و title فارسی آن را داخل Advertise.minimumCapital ذخیره میکند.
- در response آگهی، minimumCapital همان string ذخیرهشده است، مثلا: "۵۰۰ میلیون تا ۱ میلیارد".
رفتار UI:
- هنگام mount شدن Step1 آگهی، GET /supplier/v1/MinimumCapitalRange را صدا بزن.
- گزینهها را با label برابر title/name نمایش بده.
- مقدار ارسالی بهتر است id گزینه باشد تا بکاند title را resolve کند.
- در حالت edit، اگر response آگهی شامل minimumCapital بود، select را بر اساس title یا id متناظر prefill کن.
- اگر گزینهای در لیست نبود یا API 204 داد، فیلد را disabled کن و پیام «بازه حداقل سرمایه برای انتخاب موجود نیست» نمایش بده.
- اگر فقط یک گزینه وجود داشت، همان را پیشفرض انتخاب کن.
- بعد از ذخیره موفق Step1، داده آگهی را refresh کن تا مقدار string ذخیرهشده نمایش داده شود.
نکات UI/UX:
- فیلد در کنار minimumExperience و space در Step1 آگهی قرار بگیرد.
- loading، error و retry برای دریافت لیست پیاده شود.
- خطای «بازه حداقل سرمایه پیدا نشد» کنار فیلد minimumCapital نمایش داده شود.
- UI RTL و هماهنگ با فرم ساخت آگهی فعلی باشد.
در پنل ساپلایر، هنگام ساخت یا ویرایش شرکت، فیلد «تعداد کارکنان» را به لیست بازههای قابل انتخاب بکاند وصل کن.
هدف:
ساپلایر در Step1 ساخت شرکت، تعداد کارکنان شرکت را از بین گزینههایی انتخاب کند که ادمین در پنل مدیریت کرده است. این فیلد در Company به صورت string ذخیره میشود، نه ObjectId.
API دریافت گزینهها:
GET /supplier/v1/TeamSizeRange
Response نمونه:
{
"data": [
{
"id": "TEAM_SIZE_RANGE_ID",
"title": "۱۱ تا ۵۰ نفر",
"name": "۱۱ تا ۵۰ نفر",
"key": "team_11_50",
"min": 11,
"max": 50,
"sort": 2,
"isActive": true
}
]
}
API ذخیره شرکت:
POST /supplier/v1/Company/step1
Auth: Supplier token
Body نمونه:
{
"companyName": "نام شرکت",
"phone": "021...",
"phoneInside": "123",
"foundedYear": 1400,
"teamSizeRange": "TEAM_SIZE_RANGE_ID",
"province": "PROVINCE_ID",
"city": "CITY_ID",
"addressDetails": "آدرس کامل",
"category": ["CATEGORY_ID"],
"supplierType": ["SUPPLIER_TYPE_ID"]
}
رفتار مهم بکاند:
- teamSizeRange داخل Company به صورت string ذخیره میشود.
- فرانت میتواند id یا key یا title گزینه را ارسال کند.
- بکاند گزینه را پیدا میکند و title فارسی آن را داخل Company.teamSizeRange ذخیره میکند.
- در response شرکت، teamSizeRange همان string ذخیرهشده است، مثلا: "۱۱ تا ۵۰ نفر".
رفتار UI:
- هنگام mount شدن Step1، GET /supplier/v1/TeamSizeRange را صدا بزن.
- گزینهها را با label برابر title/name نمایش بده.
- مقدار ارسالی میتواند id گزینه باشد تا بکاند title را resolve کند.
- اگر response شرکت از GET /supplier/v1/Company/myCompany شامل teamSizeRange بود، select را بر اساس title یا id متناظر prefill کن.
- اگر گزینهای در لیست نبود یا API 204 داد، فیلد را disabled کن و پیام «بازه تعداد کارکنان برای انتخاب موجود نیست» نمایش بده.
- اگر فقط یک گزینه وجود داشت، همان را پیشفرض انتخاب کن.
- بعد از ذخیره موفق Step1، دوباره GET /supplier/v1/Company/myCompany را بزن تا مقدار string ذخیرهشده در خلاصه شرکت نمایش داده شود.
نکات UI/UX:
- فیلد در کنار foundedYear و اطلاعات پایه شرکت قرار بگیرد.
- loading، error و retry برای دریافت لیست پیاده شود.
- خطای «بازه تعداد کارکنان پیدا نشد» کنار فیلد teamSizeRange نمایش داده شود.
- UI RTL و هماهنگ با فرم ساخت شرکت فعلی باشد.
در پنل ساپلایر، هنگام ساخت یا ویرایش شرکت، فیلد «نوع شرکت / نوع تامینکننده» را به بکاند وصل کن.
هدف:
ساپلایر موقع ساخت شرکت بتواند supplierTypeهای مجاز را ببیند و یک یا چند مورد را برای شرکتش انتخاب کند. مقدار انتخابشده باید در Step1 شرکت ذخیره و در حالت ویرایش دوباره prefill شود.
API دریافت گزینهها:
GET /supplier/v1/SupplierType
Response نمونه:
{
"data": [
{
"id": "SUPPLIER_TYPE_ID",
"name": "تولیدکننده",
"key": "manufacturer",
"sort": 1
}
]
}
API ذخیره در شرکت:
POST /supplier/v1/Company/step1
Body نمونه:
{
"companyName": "نام شرکت",
"phone": "021...",
"phoneInside": "123",
"foundedYear": 1400,
"teamSizeRange": "10-50",
"province": "PROVINCE_ID",
"city": "CITY_ID",
"addressDetails": "آدرس کامل",
"category": ["CATEGORY_ID"],
"supplierType": ["SUPPLIER_TYPE_ID"]
}
رفتار UI:
- هنگام mount شدن مرحله اول ساخت شرکت، GET /supplier/v1/SupplierType را صدا بزن.
- گزینهها را با label فارسی name/title و مقدار id نمایش بده.
- اگر چند انتخاب مجاز است، از multi select یا checkbox group استفاده کن.
- اگر فقط یک انتخاب مد نظر طراحی است، قبل از ارسال آن را همچنان به صورت آرایه supplierType: ["id"] بفرست تا با بکاند هماهنگ باشد.
- در حالت ویرایش، مقدار supplierType برگشتی از GET /supplier/v1/Company/myCompany را prefill کن.
- اگر supplierType خالی بود، پیام اعتبارسنجی کنار فیلد نمایش بده و اجازه ذخیره نده.
- اگر API گزینهای برنگرداند یا 204 شد، فیلد را disabled کن و پیام «نوع شرکت برای انتخاب موجود نیست» نمایش بده.
- بعد از ذخیره موفق Step1، دوباره GET /supplier/v1/Company/myCompany را بزن تا supplierType به صورت populate شده در خلاصه شرکت نمایش داده شود.
نکات مهم:
- supplierType باید آرایه ObjectId باشد.
- مقدار name/title فقط برای نمایش است و نباید به جای id ارسال شود.
- loading، error و retry برای دریافت لیست supplierType پیاده شود.
- UI باید RTL و هماهنگ با فرم ساخت شرکت فعلی باشد.
در پنل ساپلایر، بخش «گالری شرکت» را به بکاند وصل کن و فایلهای شرکت ساپلایر را نمایش بده.
API:
GET /supplier/v1/Company/gallery
Auth:
نیاز به توکن ساپلایر دارد.
Queryهای اختیاری:
- placement
- type
نمونه:
GET /supplier/v1/Company/gallery
GET /supplier/v1/Company/gallery?placement=gallery
GET /supplier/v1/Company/gallery?type=image
GET /supplier/v1/Company/gallery?placement=permit&type=document
GET /supplier/v1/Company/gallery?placement=identity&type=document
GET /supplier/v1/Company/gallery?placement=contract&type=document
GET /supplier/v1/Company/gallery?placement=agreement&type=document
رفتار بکاند:
- فقط گالری شرکت متعلق به ساپلایر لاگینشده برمیگردد.
- اگر ساپلایر هنوز شرکت نساخته باشد، پیام «شما مستقیم نمی توانید وارد مرحله سوم شوید» برمیگردد.
- اگر placement معتبر نباشد، پیام «جایگاه فایل معتبر نیست» برمیگردد.
- اگر دادهای نبود، response با status 204 و data خالی برمیگردد.
- response موفق به صورت group by بر اساس placement برمیگردد، نه آرایه ساده.
Response نمونه:
{
"data": {
"gallery": [
{
"id": "GALLERY_ID",
"caption": "تصویر شوروم",
"type": "image",
"placement": "gallery",
"hidden": false,
"isAccepted": true,
"declinedReason": "",
"url": "https://..."
}
],
"identity": [],
"contract": [],
"agreement": []
}
}
نکته:
- فقط placementهایی که داده دارند ممکن است در response بیایند؛ پس UI باید نبودن یک کلید را مثل آرایه خالی در نظر بگیرد.
رفتار مورد انتظار در UI:
- هنگام ورود به تب/مرحله گالری، این endpoint را صدا بزن.
- داده را بر اساس کلیدهای data.gallery، data.honor، data.permit، data.identity، data.contract و data.agreement بخوان.
- هر گروه را در تب/سکشن جدا نمایش بده و لیست فایلها را به صورت کارت یا grid نشان بده.
- هر آیتم باید preview، caption، type، placement، وضعیت hidden/isAccepted و تاریخ ثبت را اگر در response بود نمایش بدهد.
- placementهای مجاز: gallery، honor، permit، identity، contract، agreement.
- برای مدارک هویتی از placement=identity استفاده کن.
- برای قراردادها از placement=contract استفاده کن.
- برای توافقها از placement=agreement استفاده کن.
- برای image، preview تصویر نشان بده.
- برای document/PDF، کارت فایل با آیکن document و لینک مشاهده/دانلود نشان بده.
- فیلترهای placement و type را در UI قرار بده و با تغییر هر فیلتر دوباره API را صدا بزن.
- اگر response 204 بود، empty state با پیام «هنوز فایلی ثبت نشده است» نشان بده.
- خطاهای API را با پیام فارسی خود API نمایش بده.
اتصال با عملیات گالری:
- افزودن فایل از مسیر POST /supplier/v1/Company/step3 انجام شود.
- ویرایش فایل از مسیر PATCH /supplier/v1/Company/step3/:id انجام شود.
- حذف فایل از مسیر DELETE /supplier/v1/Company/step3/:id انجام شود.
- بعد از هر create/update/delete موفق، دوباره GET /supplier/v1/Company/gallery را برای refresh لیست صدا بزن.
در پنل ساپلایر، فرم ساخت/ویرایش آگهی را طوری تغییر بده که ساپلایر بتواند برای آگهی فقط یکی از دستهبندیهای شرکت خودش را انتخاب کند.
منبع دستهبندیها:
- اطلاعات شرکت ساپلایر را از API فعلی شرکت بگیر.
- دستهبندیهای مجاز همان company.category هستند.
- در UI فقط همین دستهبندیها نمایش داده شوند.
اتصال به ذخیره آگهی:
POST /supplier/v1/Advertise/step1
Body نمونه:
{
"advertiseId": "ADVERTISE_ID_FOR_EDIT",
"title": "عنوان آگهی",
"category": "CATEGORY_ID",
"dealershipTypes": ["DEALERSHIP_TYPE_ID"],
"space": "حداقل فضا",
"minimumExperience": "حداقل سابقه",
"minimumCapital": "حداقل سرمایه"
}
رفتار مورد انتظار:
- در حالت create، category الزامی است.
- در حالت edit، اگر category ارسال شود باید مقدار جدید در فرم انتخاب شود.
- اگر شرکت category ندارد، select را disabled کن و پیام مناسب بده که ابتدا دستهبندی شرکت باید تکمیل شود.
- مقدار ارسالی category باید ObjectId یکی از آیتمهای company.category باشد.
- اگر API خطای «دسته بندی آگهی باید از بین دسته بندی های شرکت انتخاب شود» برگرداند، همان پیام را کنار فیلد category نمایش بده.
- category برگشتی در response آگهی را برای prefill فرم و نمایش جزئیات استفاده کن.
- در لیست آگهیهای ساپلایر، امکان فیلتر با category را اضافه کن:
GET /supplier/v1/Advertise/getMyAdvertises?category=CATEGORY_ID
- هنگام تغییر category در فیلتر لیست، page را به 1 برگردان و اگر category پاک شد query param را حذف کن.
در پنل ساپلایر، گزینههای پیشنهادی «مزایای کلیدی» در فرم ساخت/ویرایش آگهی را از بکاند دریافت و نمایش بده.
API:
GET /supplier/v1/KeyBenefit
Auth:
این endpoint نیاز به توکن ندارد و فقط مزایای فعال را برمیگرداند.
Response:
{
"data": [
{
"id": "...",
"title": "کیفیت تضمینشده",
"key": "guaranteed-quality",
"iconName": "verified",
"sort": 10,
"isActive": true
}
]
}
رفتار مورد انتظار:
- هنگام mount شدن مرحله «مزایا و شهرها» یا بخش «مزایای کلیدی»، لیست را از GET /supplier/v1/KeyBenefit بگیر.
- گزینههای hardcode شده قبلی را حذف کن.
- گزینهها را به صورت chip/button پیشنهادی یا dropdown قابل انتخاب نمایش بده.
- label هر گزینه از title باشد.
- value انتخابشده برای submit بهتر است title باشد، چون فیلد فعلی advertise.benefits در بکاند آرایه string است.
- ترتیب نمایش همان ترتیب response باشد.
- iconName را اگر icon map موجود است به آیکن وصل کن؛ اگر نبود فقط title را نمایش بده.
اتصال به ذخیره آگهی:
در مرحله مزایا، مقدار benefits را مثل قبل به API ساخت/ویرایش آگهی بفرست:
POST /supplier/v1/Advertise/step4
Body نمونه:
{
"advertiseId": "advertiseId",
"benefits": ["کیفیت تضمینشده", "ارسال سریع"],
"city": ["cityId1", "cityId2"]
}
UI/UX:
- متن راهنما حفظ شود: «حداکثر ۵ مزیت کلیدی همکاری با شما را اضافه کنید.»
- ساپلایر بتواند از پیشنهادها انتخاب کند و همچنین مزیت سفارشی تایپ کند.
- تعداد مزایا را حداکثر ۵ نگه دار.
- از اضافه شدن مقدار تکراری جلوگیری کن.
- loading state برای دریافت پیشنهادها داشته باش.
- اگر API 204 یا data خالی برگشت، بخش تایپ دستی مزایا همچنان کار کند.
- در صورت خطا، پیام کوتاه نمایش بده ولی فرم را بلاک نکن.
- بعد از انتخاب یک پیشنهاد، آن را به لیست benefits اضافه کن و input را پاک کن.
- طراحی RTL و استایل فعلی کارت «مزایای کلیدی» را حفظ کن.
وضعیتهای درخواست آگهی AdvertiseRequestStatus را کامل پیاده کن؛ طوری که seed/initialize، APIهای پنل و ساپلایر، populate در AdvertiseRequest و UI تغییر وضعیت همگی از یک ساختار مشترک استفاده کنند.
هدف:
فرانت نباید وضعیتها، متنهای پیشنهادی، دلایل رد، رنگها یا نوع کنترل فرم را hardcode کند. همه این اطلاعات باید از AdvertiseRequestStatus برگردد تا صفحه جزئیات درخواست بتواند برای هر status کنترل درست را بسازد:
- تایید: متنهای پیشنهادی به صورت radio button
- رد: دلایل رد به صورت checkbox
- تماس/بستهشدن: متن پیشنهادی یا textarea
- توضیحات اختصاصی: همیشه قابل وارد کردن باشد
Backend:
فایلهای مرتبط:
- src/model/v1/AdvertiseRequestStatus.js
- src/service/v1/AdvertiseRequestStatus.js
- src/view/panel/v1/AdvertiseRequestStatus.js
- src/controller/panel/v1/AdvertiseRequestStatus/index.js
- src/controller/supplier/v1/AdvertiseRequestStatus/index.js
- src/routes/supplier/v1/AdvertiseRequestStatus/index.js
- src/view/supplier/v1/AdvertiseRequestStatus.js
- src/service/v1/AdvertiseRequest.js
مدل AdvertiseRequestStatus را با این فیلدها کامل کن:
- key
- fa_name
- sort
- color_text
- color_back
- inputType: یکی از radio، checkbox، textarea، none
- suggestedTexts: آرایه آبجکت شامل key، text، sort، isDefault
- reasons: آرایه آبجکت شامل key، title، sort
initialize:
- initialize نباید رکورد تکراری بسازد.
- اگر key از قبل وجود داشت، رکورد یا رکوردهای همان key با دادههای seed آپدیت شوند.
- اگر key وجود نداشت، رکورد جدید ساخته شود.
- اگر احتمال duplicate قدیمی وجود دارد، اول updateMany({ key }, { $set: ... }) بزن و فقط وقتی matchedCount صفر بود create کن.
- اگر مطمئن هستی key یکتا است، updateOne({ key }, { $set: ... }, { upsert: true }) قابل قبول است.
- روی key ایندکس unique یا حداقل migration/cleanup برای duplicateهای قدیمی را در نظر بگیر.
- اگر رکورد قبلا soft delete شده بود، در initialize دوباره isDeleted=false و deletedBy=null شود.
- initialize را طوری بنویس که چندبار اجرا شدنش خروجی دیتابیس را خراب نکند.
Seed پیشنهادی:
- pending:
- fa_name: در انتظار بررسی
- inputType: none
- رنگ amber
- approved:
- fa_name: تایید شده
- inputType: radio
- suggestedTexts:
- درخواست شما تایید شد. لطفا برای هماهنگی مراحل بعدی با تامینکننده تماس بگیرید.
- درخواست شما تایید شد. تیم تامینکننده برای ادامه همکاری با شما تماس خواهد گرفت.
- rejected:
- fa_name: رد شده
- inputType: checkbox
- suggestedTexts:
- درخواست شما در حال حاضر مورد تایید قرار نگرفت.
- reasons:
- عدم کفایت فضا
- کمبود تجربه کاری مرتبط
- عدم تطابق بودجه با شرایط همکاری
- contacted:
- fa_name: تماس گرفته شد
- inputType: radio
- suggestedTexts مناسب پیگیری تماس
- closed:
- fa_name: بسته شد
- inputType: textarea
- suggestedTexts متن بسته شدن درخواست
Panel API:
- initialize موجود پنل باید seed بالا را اجرا کند.
- create و update پنل این فیلدها را بپذیرند:
- id
- key
- fa_name
- sort
- color_text
- color_back
- inputType
- suggestedTexts
- reasons
- هنگام update جزئی، فیلدهای قبلی مثل suggestedTexts و reasons حذف نشوند.
- خروجی view پنل همه فیلدهای بالا را برگرداند.
Supplier API:
مسیر supplier بساز:
GET /supplier/v1/AdvertiseRequestStatus
Auth:
این endpoint باید با SupplierToken محافظت شود.
رفتار endpoint ساپلایر:
- فقط رکوردهای حذفنشده AdvertiseRequestStatus را برگردان.
- خروجی با sort صعودی و سپس createdAt صعودی مرتب شود.
- response از view مخصوص supplier عبور کند.
- view ساپلایر هم همه فیلدهای UI config را برگرداند: key، fa_name، sort، color_text، color_back، inputType، suggestedTexts، reasons.
- اگر دادهای نبود، مثل الگوی پروژه status 204 با data خالی برگردان.
- خطاها در LogProgrammer با sectionName برابر supplier و className برابر AdvertiseRequestStatus ذخیره شوند.
- route را در src/routes/supplier/v1/index.js با مسیر /AdvertiseRequestStatus رجیستر کن.
AdvertiseRequest populate:
- populate وضعیت داخل AdvertiseRequest هم کامل شود تا statusId و statusHistory.statusId همین فیلدهای UI config را داشته باشند.
- این موضوع برای responseهای supplier و panel مهم است، چون صفحه جزئیات درخواست از statusId تصمیم میگیرد radio/checkbox/textarea نشان بدهد.
Frontend:
- در صفحه مدیریت/جزئیات درخواست آگهی ساپلایر، هنگام mount شدن صفحه GET /supplier/v1/AdvertiseRequestStatus را صدا بزن.
- select فیلتر statusId و select فرم تغییر وضعیت را از response بساز.
- label گزینهها fa_name و value گزینهها id باشد.
- وضعیت فعلی هر درخواست از statusId.fa_name نمایش داده شود.
- کنترل فرم تغییر وضعیت را از inputType بساز.
- اگر inputType برابر radio بود، suggestedTexts را به صورت radio نمایش بده و متن انتخابشده را داخل note/statusDescription بفرست.
- اگر inputType برابر checkbox بود، reasons را checkbox نمایش بده و دلایل انتخابشده را به متن توضیحات اضافه یا به فیلد مناسب ارسال کن.
- textarea توضیحات اختصاصی همیشه قابل استفاده باشد.
- رنگ دکمه ثبت و کارت وضعیت از color_text/color_back یا mapping نزدیک به آن خوانده شود.
- statusId انتخابشده در فیلتر لیست با query param statusId به GET /supplier/v1/AdvertiseRequest ارسال شود.
- statusId انتخابشده در فرم تغییر وضعیت با PATCH /supplier/v1/AdvertiseRequest/review/:id ارسال شود.
- اگر endpoint مقدار 204 یا data خالی داد، select وضعیت را disabled کن و پیام کوتاه «وضعیتی برای نمایش وجود ندارد» نشان بده.
- اگر خطا رخ داد، صفحه درخواستها را کامل بلاک نکن؛ فقط فیلتر و فرم وضعیت را با پیام خطا غیرفعال کن.
Response نمونه:
{
"data": [
{
"id": "STATUS_ID",
"key": "approved",
"fa_name": "تایید شده",
"sort": 3,
"color_text": "text-emerald-600",
"color_back": "bg-emerald-50",
"inputType": "radio",
"suggestedTexts": [
{
"key": "approved_supplier_contact",
"text": "درخواست شما تایید شد. لطفا برای هماهنگی مراحل بعدی با تامینکننده تماس بگیرید.",
"sort": 1,
"isDefault": true
}
],
"reasons": []
},
{
"id": "STATUS_ID",
"key": "rejected",
"fa_name": "رد شده",
"sort": 4,
"color_text": "text-red-600",
"color_back": "bg-red-50",
"inputType": "checkbox",
"suggestedTexts": [
{
"key": "rejected_default",
"text": "درخواست شما در حال حاضر مورد تایید قرار نگرفت.",
"sort": 1,
"isDefault": true
}
],
"reasons": [
{ "key": "insufficient_space", "title": "عدم کفایت فضا", "sort": 1 },
{ "key": "low_related_experience", "title": "کمبود تجربه کاری مرتبط", "sort": 2 },
{ "key": "budget_mismatch", "title": "عدم تطابق بودجه با شرایط همکاری", "sort": 3 }
]
}
]
}
Verification:
- دوبار initialize را اجرا کن و مطمئن شو رکورد تکراری ساخته نمیشود.
- یکی از رکوردهای موجود را با همان key تغییر بده و دوباره initialize کن؛ باید با seed آپدیت شود.
- GET /supplier/v1/AdvertiseRequestStatus باید فیلدهای inputType، suggestedTexts و reasons را برگرداند.
- response جزئیات AdvertiseRequest باید statusId و statusHistory.statusId کامل داشته باشد.
- فرم فرانت برای approved حالت radio و برای rejected حالت checkbox نشان بدهد.
- PATCH تغییر وضعیت همچنان statusId، supplierNote/statusDescription را درست ذخیره کند و statusHistory بسازد.
در داشبورد ساپلایر، صفحه مدیریت درخواستهای آگهی را پیاده کن.
هدف:
ساپلایر بتواند درخواستهایی را که کاربران برای آگهیهای شرکت او ثبت کردهاند ببیند، فیلتر کند، جزئیات هر درخواست را باز کند و با کاربر چت کند.
Auth:
همه endpointها باید با توکن ساپلایر صدا زده شوند.
APIها:
1. لیست درخواستها
GET /supplier/v1/AdvertiseRequest
Query params:
- page optional, default 1
- limit optional, default 10, max 100
- sort optional, default -createdAt
- advertiseId optional
- statusId optional
- isViewed optional
- q optional
نمونه:
GET /supplier/v1/AdvertiseRequest?page=1&limit=10&sort=-createdAt&advertiseId=ADVERTISE_ID&statusId=STATUS_ID&isViewed=false&q=متن
2. دریافت جزئیات یک درخواست
GET /supplier/v1/AdvertiseRequest/:id
3. دریافت پیامهای چت درخواست
GET /supplier/v1/AdvertiseRequest/:id/chat/messages
4. ارسال پیام چت
POST /supplier/v1/AdvertiseRequest/:id/chat/messages
Content-Type: application/json
Body:
{
"message": "متن پیام"
}
5. تغییر وضعیت درخواست و ثبت یادداشت ساپلایر
PATCH /supplier/v1/AdvertiseRequest/review/:id
Content-Type: application/json
Body:
{
"statusId": "STATUS_ID",
"supplierNote": "یادداشت ساپلایر برای این درخواست",
"statusDescription": "توضیح وضعیت"
}
Response موفق:
{
"data": {
"id": "...",
"statusId": {},
"statusDescription": "توضیح وضعیت",
"supplierNote": "یادداشت ساپلایر برای این درخواست",
"supplierReviewedBy": {},
"supplierReviewedAt": "...",
"statusHistory": []
},
"message": "نام وضعیت"
}
نکات تغییر وضعیت:
- statusId الزامی است و باید از وضعیتهای AdvertiseRequestStatus باشد.
- supplierNote اختیاری است ولی در UI یک textarea برای آن بگذار.
- بعد از تغییر وضعیت، جزئیات درخواست را دوباره fetch کن یا response را در state جایگزین کن.
- بکاند فقط اجازه تغییر درخواستهای شرکت همان ساپلایر را میدهد؛ اگر 404 برگشت یعنی درخواست متعلق به این ساپلایر نیست یا وجود ندارد.
- تغییر وضعیت، supplierReviewedBy، supplierReviewedAt و statusHistory را هم بهروزرسانی میکند.
Response مهم درخواست:
{
"data": {
"id": "...",
"advertise": {
"_id": "...",
"title": "عنوان آگهی",
"slug": "ad-12345",
"minimumCapital": "...",
"city": [],
"statusId": {},
"expireAt": "..."
},
"company": {},
"user": {
"_id": "...",
"fullName": "نام کاربر",
"phoneNumber": "..."
},
"statusId": {},
"statusDescription": "...",
"supplierNote": "یادداشت قابل نمایش برای ساپلایر",
"supplierReviewedBy": {},
"supplierReviewedAt": "...",
"statusHistory": [],
"isViewed": true,
"viewedAt": "...",
"hasRequiredCapital": true,
"capitalAmount": "۵۰۰ میلیون تا ۱ میلیارد",
"messageToCompany": "متن پیام کاربر",
"relatedExperience": "تجربه کاربر",
"facilities": [
{
"id": "...",
"key": "business-license",
"title": "جواز کسب",
"iconName": "badge"
}
],
"consentToShareInfo": true,
"requestArea": [],
"advertiseExpireAt": "...",
"isAdvertiseExpired": false,
"hasMatchingAdvertiseCity": true,
"matchedAdvertiseCities": [],
"matchedRequestAreas": [],
"createdAt": "...",
"updatedAt": "..."
}
}
نکات response:
- سمت ساپلایر فیلد adminNote نمایش داده نمیشود؛ اگر یادداشت وجود داشته باشد با نام supplierNote برمیگردد.
- facilities باید به صورت آبجکت با title فارسی و iconName نمایش داده شود، نه string خام.
- اگر آگهی منقضی شده باشد isAdvertiseExpired برابر true است و باید badge هشدار نمایش داده شود.
- hasMatchingAdvertiseCity و matchedRequestAreas برای نمایش تطابق شهر درخواست با شهرهای آگهی استفاده شود.
UI لیست:
- جدول یا لیست کارتمحور RTL بساز.
- ستونها/اطلاعات اصلی:
- نام کاربر
- عنوان آگهی
- وضعیت درخواست
- سرمایه اعلامشده
- شهر/محدوده درخواست
- وضعیت دیدهشدن
- تاریخ ثبت
- وضعیت انقضای آگهی
- فیلترها:
- انتخاب آگهی
- انتخاب وضعیت
- دیدهشده/دیدهنشده
- جستجو q
- مرتبسازی جدیدترین/قدیمیترین
- pagination را از metaData پاسخ API بخوان.
- اگر API 204 داد، empty state مناسب نشان بده.
UI جزئیات:
- کارت اطلاعات آگهی و شرکت
- کارت اطلاعات کاربر
- پیام کاربر به شرکت messageToCompany
- تجربه مرتبط relatedExperience
- سرمایه capitalAmount و hasRequiredCapital
- امکانات facilities با آیکن
- محدودههای درخواست requestArea
- وضعیت تطابق شهرها matchedRequestAreas
- supplierNote اگر وجود داشت
- تاریخچه وضعیت statusHistory اگر در response بود
- فرم تغییر وضعیت:
- select وضعیت با مقدار statusId
- textarea یادداشت با مقدار supplierNote
- input/textarea توضیح وضعیت با مقدار statusDescription
- دکمه ذخیره که PATCH /supplier/v1/AdvertiseRequest/review/:id را صدا بزند
- بخش چت پایین صفحه یا در تب جدا
رفتار چت:
- پیامها را با GET /supplier/v1/AdvertiseRequest/:id/chat/messages بگیر.
- ارسال پیام با POST انجام شود.
- بعد از ارسال موفق، پیام جدید را به لیست اضافه کن یا دوباره fetch کن.
- input ارسال پیام loading و disabled state داشته باشد.
- پیام خالی ارسال نشود.
نکات امنیتی و دسترسی:
- ساپلایر فقط درخواستهای مربوط به آگهیهای شرکت خودش را میبیند؛ اگر 404 گرفت، پیام «درخواست پیدا نشد» نشان بده.
- اطلاعات تماس کاربر فقط همان چیزی که API برگردانده نمایش داده شود.
نکات UI/UX:
- طراحی با داشبورد فعلی ساپلایر هماهنگ باشد.
- badge وضعیتها از statusId.fa_name استفاده کند.
- برای facilities اگر iconName در icon map نبود، fallback icon نمایش بده.
- برای آگهی منقضیشده badge واضح بگذار.
- خطاهای API با پیام برگشتی نمایش داده شود.
- فرمها و لیستها روی موبایل هم بدون overflow باشند.
در داشبورد ساپلایر، کارت تنظیمات حساب را به بکاند وصل کن تا هم تنظیمات اعلانها و هم انتخاب دارک/لایت مود ذخیره و بازیابی شوند.
مسیر صفحه:
/supplier/dashboard/profile یا صفحه تنظیمات حساب ساپلایر
Auth:
همه درخواستها باید با توکن ساپلایر ارسال شوند.
دریافت داده اولیه:
GET /supplier/v1/Supplier/me
از response ساپلایر مقدارهای زیر را برای prefill استفاده کن:
{
"themeMode": "light",
"notificationSettings": {
"types": {
"messagesAndRequests": true,
"specialOffers": true
},
"channels": {
"email": true,
"mobile": true,
"browser": false
}
}
}
پیشفرضها اگر response مقدار نداشت:
- themeMode: light
- messagesAndRequests: true
- specialOffers: true
- email: true
- mobile: true
- browser: false
1. ذخیره تنظیمات اعلانها
PUT /supplier/v1/Supplier/notification-settings
Content-Type: application/json
Body:
{
"notificationSettings": {
"types": {
"messagesAndRequests": true,
"specialOffers": true
},
"channels": {
"email": true,
"mobile": true,
"browser": false
}
}
}
Response موفق:
{
"data": {
"id": "...",
"notificationSettings": {
"types": {
"messagesAndRequests": true,
"specialOffers": true
},
"channels": {
"email": true,
"mobile": true,
"browser": false
}
}
},
"message": "تنظیمات اعلانها با موفقیت ثبت شد"
}
نگاشت UI اعلانها:
- چکباکس «پیامها و درخواستها» به notificationSettings.types.messagesAndRequests وصل شود.
- چکباکس «پیشنهادات ویژه» به notificationSettings.types.specialOffers وصل شود.
- چکباکس «ایمیل» به notificationSettings.channels.email وصل شود.
- چکباکس «موبایل» به notificationSettings.channels.mobile وصل شود.
- چکباکس «مرورگر» به notificationSettings.channels.browser وصل شود.
2. ذخیره حالت نمایش دارک/لایت
PUT /supplier/v1/Supplier/theme-mode
Content-Type: application/json
Body:
{
"themeMode": "dark"
}
مقادیر مجاز themeMode:
- light
- dark
Response موفق:
{
"data": {
"id": "...",
"themeMode": "dark"
},
"message": "تنظیمات نمایش با موفقیت ثبت شد"
}
رفتار مورد انتظار:
- هنگام mount صفحه، از GET /supplier/v1/Supplier/me مقدارهای فعلی را بخوان و UI را prefill کن.
- UI تم را فوراً بر اساس themeMode ذخیرهشده اعمال کن.
- بعد از تغییر هر چکباکس اعلان، میتوانی auto-save بزنی یا از دکمه «ذخیره تغییرات» همان بخش استفاده کنی.
- وقتی ساپلایر بین دارک و لایت تغییر داد، مقدار جدید را با PUT /supplier/v1/Supplier/theme-mode ذخیره کن.
- بعد از ذخیره موفق، state محلی supplier/theme/notification را با data برگشتی sync کن.
- هنگام ذخیره، فقط کنترلهای همان بخش disabled/loading شوند.
- در صورت خطا، پیام API را نمایش بده و مقدار قبلی را از دست نده.
نکات فنی:
- درخواستها را JSON بفرست، نه FormData.
- برای تم فقط یکی از مقدارهای light یا dark را ارسال کن.
- اگر پروژه context/store برای theme دارد، همین endpoint را داخل همان flow وصل کن و state موازی جدید نساز.
- اگر ساپلایر لاگین نبود، میتوانی تم را موقتاً در localStorage نگه داری؛ بعد از لاگین مقدار بکاند اولویت داشته باشد.
- اگر مرورگر/Push notification در فرانت هنوز پیاده نشده، فقط مقدار browser را ذخیره کن و درخواست permission مرورگر را در این task اضافه نکن.
نکات UI/UX:
- طراحی RTL، آیکنها، کارتها، spacing و ظاهر فعلی تنظیمات ساپلایر حفظ شود.
- چکباکسهای اعلان باید داخل کارتهای قابل کلیک باشند.
- تغییر تم باید بدون reload صفحه اعمال شود.
- کنترل تم میتواند switch، segmented control یا گزینه داخل تنظیمات حساب باشد.
- label فارسی پیشنهادی برای تم:
- حالت روشن
- حالت تاریک
Template
1 قالبعنوان: هدف: مسیرها/APIها: رفتار مورد انتظار: نکات UI/UX: نکات فنی: موارد خطا و loading: خروجی نهایی: