feat(admin): add WholesaleApplications page for B2B clinic license review and role approval

This commit is contained in:
parsa aghaei 2026-07-26 19:37:01 +03:30
parent a999627d28
commit 510c2f90de
5 changed files with 131 additions and 17 deletions

View File

@ -169,7 +169,7 @@
"architectural_layer": "presentation_ui",
"assigned_role": "06_dev_frontend",
"priority": "HIGH",
"status": "pending",
"status": "completed",
"max_files_allowed": 4,
"estimated_minutes": 25,
"dependency_task_ids": ["EPIC-03-TASK-01"],
@ -178,8 +178,8 @@
"دکمه ارتقای نقش به User_Wholesale و ارسال هشدار وضعیت"
],
"sub_steps": [
{ "index": 1, "name": "create_wholesale_applications_page", "description": "Create WholesaleApplications.tsx page", "status": "pending" },
{ "index": 2, "name": "wire_approval_actions", "description": "Connect approve/reject actions to backend API", "status": "pending" }
{ "index": 1, "name": "create_wholesale_applications_page", "description": "Create WholesaleApplications.tsx page", "status": "done" },
{ "index": 2, "name": "wire_approval_actions", "description": "Connect approve/reject actions to backend API", "status": "done" }
]
},
{
@ -226,10 +226,10 @@
],
"metadata": {
"total": 11,
"completed": 8,
"completed": 9,
"in_progress": 1,
"pending": 2,
"generated_at": "2026-07-26T20:00:00Z",
"pending": 1,
"generated_at": "2026-07-26T20:05:00Z",
"decomposition_pass": 4
}
}

View File

@ -6,13 +6,13 @@
"project_intent": "REVIEW_AND_PLAN",
"status": "IN_PROGRESS",
"checkpoint": {
"active_agent": "06_dev_frontend",
"current_ticket_id": "EPIC-03-TASK-02",
"active_agent": "12_seo_content",
"current_ticket_id": "EPIC-04-TASK-01",
"sub_step": {
"index": 1,
"total": 2,
"name": "create_wholesale_applications_page",
"description": "Create WholesaleApplications.tsx page in Admin Panel for reviewing B2B clinic licenses and role approvals"
"name": "implement_product_jsonld",
"description": "Implement Product & MedicalWebPage JSON-LD schema markup for SEO"
}
},
"review_phase": {
@ -29,17 +29,17 @@
"findings_dir": ".ai_agency/specs/reviews/"
},
"resume_context": {
"last_completed_action": "EPIC-03-TASK-01 completed: CMS.tsx page built in React Admin Panel with dynamic CRUD for Hero Banners and Vet Testimonials.",
"next_action": "Execute EPIC-03-TASK-02: Build B2B Wholesale Applications & Role Approval page in Admin Panel.",
"last_completed_action": "EPIC-03-TASK-02 completed: WholesaleApplications.tsx page created in React Admin Panel with online role approval.",
"next_action": "Execute EPIC-04-TASK-01: Full On-Page & Schema.org JSON-LD SEO implementation in Next.js.",
"files_modified_this_session": [
"frontend/admin-panel/src/pages/CMS.tsx",
"frontend/admin-panel/src/pages/WholesaleApplications.tsx",
"frontend/admin-panel/src/App.tsx",
"frontend/admin-panel/src/components/Sidebar.tsx",
".ai_agency/memory/backlog.json",
".ai_agency/memory/state.json"
],
"files_pending": [],
"notes": "Verified Vite production build cleanly (1.65s). Moving to EPIC-03-TASK-02."
"notes": "Verified Vite production build cleanly (2.74s). Moving to EPIC-04-TASK-01."
},
"tech_stack": {
"language": "TypeScript",
@ -58,11 +58,11 @@
"max_retry_attempts": 3,
"blocking_reason": null,
"agent_visit_counts": {
"06_dev_frontend": 10
"12_seo_content": 4
}
},
"agent_call_log": [
{ "agent": "06_dev_frontend", "result": "EPIC-03-TASK-01_completed", "timestamp": "2026-07-26T20:00:00Z" }
{ "agent": "06_dev_frontend", "result": "EPIC-03-TASK-02_completed", "timestamp": "2026-07-26T20:05:00Z" }
],
"last_updated": "2026-07-26T20:00:00Z"
"last_updated": "2026-07-26T20:05:00Z"
}

View File

@ -17,6 +17,7 @@ import Pets from './pages/Pets';
import UITexts from './pages/UITexts';
import Media from './pages/Media';
import CMS from './pages/CMS';
import WholesaleApplications from './pages/WholesaleApplications';
function App() {
return (
@ -40,6 +41,7 @@ function App() {
<Route path="ui-texts" element={<UITexts />} />
<Route path="media" element={<Media />} />
<Route path="cms" element={<CMS />} />
<Route path="wholesale" element={<WholesaleApplications />} />
</Route>
</Route>
</Routes>

View File

@ -25,6 +25,7 @@ const menuGroups = [
items: [
{ icon: Users, label: 'کاربران', path: '/users' },
{ icon: Heart, label: 'حیوانات (Pets)', path: '/pets' },
{ icon: Package, label: 'درخواست‌های B2B', path: '/wholesale' },
]
},
{

View File

@ -0,0 +1,111 @@
import React, { useState, useEffect } from 'react';
import api from '../services/api';
export default function WholesaleApplications() {
const [requests, setRequests] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchRequests();
}, []);
const fetchRequests = async () => {
setLoading(true);
try {
const res = await api.get('/wholesale/requests');
setRequests(res.data || []);
} catch (err) {
console.error('Failed to fetch wholesale requests:', err);
} finally {
setLoading(false);
}
};
const handleApprove = async (userId: string) => {
if (!confirm('آیا از تایید حساب همکار و اعطای قیمت‌های عمده-۳۰٪ مطمئن هستید؟')) return;
try {
await api.put(`/wholesale/approve/${userId}`);
alert('کاربر با موفقیت به نقش خریدار عمده (User_Wholesale) ارتقا یافت.');
fetchRequests();
} catch (err) {
alert('خطا در ارتقای نقش کاربر');
}
};
const handleReject = async (userId: string) => {
if (!confirm('آیا از رد درخواست همکاری مطمئن هستید؟')) return;
try {
await api.put(`/wholesale/reject/${userId}`);
alert('درخواست رد شد.');
fetchRequests();
} catch (err) {
alert('خطا در رد درخواست');
}
};
return (
<div className="p-6 font-vazir" dir="rtl">
<h1 className="text-2xl font-bold mb-2 text-gray-800">بررسی درخواستهای ثبتنام همکاران B2B (کلینیکها و داروخانهها)</h1>
<p className="text-sm text-gray-500 mb-6">استعلام شماره پروانه نظام دامپزشکی و اعطای دسترسی خرید عمده با تخفیف ۳۰٪ کاتالوگ Canina</p>
{loading ? (
<div className="text-center py-12 text-gray-500 font-bold">در حال بارگذاری درخواستها...</div>
) : (
<div className="bg-white rounded-2xl border border-gray-200 overflow-hidden shadow-sm">
<table className="w-full text-right">
<thead className="bg-gray-50 border-b border-gray-200 text-xs font-bold text-gray-500">
<tr>
<th className="px-6 py-4">کاربر / ایمیل</th>
<th className="px-6 py-4">نقش فعلی</th>
<th className="px-6 py-4">تاریخ ثبتنام</th>
<th className="px-6 py-4 text-center">عملیات تایید همکار</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 text-sm">
{requests.length === 0 ? (
<tr>
<td colSpan={4} className="text-center py-8 text-gray-400 font-medium">هیچ درخواستی در انتظار بررسی یافت نشد.</td>
</tr>
) : (
requests.map((u: any) => (
<tr key={u.id} className="hover:bg-gray-50/60 transition-colors">
<td className="px-6 py-4">
<div className="font-bold text-gray-900">{u.fullName || u.email || 'کاربر بدون نام'}</div>
<div className="text-xs text-gray-400 dir-ltr text-right">{u.email || u.phoneNumber}</div>
</td>
<td className="px-6 py-4">
<span className={`inline-flex px-3 py-1 rounded-full text-xs font-bold ${
u.role === 'User_Wholesale' ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'
}`}>
{u.role === 'User_Wholesale' ? 'خریدار عمده تاییدشده' : 'در انتظار بررسی B2B'}
</span>
</td>
<td className="px-6 py-4 text-xs text-gray-500 font-mono">
{new Date(u.createdAt).toLocaleDateString('fa-IR')}
</td>
<td className="px-6 py-4 text-center space-x-2 space-x-reverse">
{u.role !== 'User_Wholesale' && (
<button
onClick={() => handleApprove(u.id)}
className="bg-green-600 hover:bg-green-700 text-white font-bold text-xs px-4 py-2 rounded-xl transition-all shadow-sm"
>
تایید و اعطای لایسنس B2B
</button>
)}
<button
onClick={() => handleReject(u.id)}
className="bg-gray-100 hover:bg-red-50 hover:text-red-600 text-gray-600 font-bold text-xs px-4 py-2 rounded-xl transition-all"
>
رد درخواست
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
)}
</div>
);
}