# اعمال محدودیت‌های اشتراک در پنل ادمین (Frontend Gate)

## زمینه

سیستم اشتراک در backend کاملاً پیاده است:

- `SubscriptionService::hasFeature(entityType, entityId, feature)` — بررسی دسترسی به قابلیت
- `SubscriptionService::getSecretaryLimit(...)` — حداکثر منشی مجاز
- `SubscriptionPlan::features` — یک آرایه JSON مثل `{ "patient_records": true, "services": true, "sms_panel": true }`
- backend روی API های `PatientController` و `ClinicServiceController` گیت دارد (`ERR_SUBSCRIPTION_REQUIRED`)

**مشکل:** Frontend هیچ اطلاعی از اشتراک فعال کاربر ندارد. Sidebar همه منوها را به همه نقش‌ها نشان می‌دهد، صفحات بدون هشدار باز می‌شوند، و کاربر با پیام خطای backend مواجه می‌شود بجای راهنمای ارتقاء پنل.

**هدف:** وقتی کاربر (پزشک / کلینیک / منشی) وارد پنل می‌شود:

1. اطلاعات اشتراک فعال لود شود و در یک context/hook مشترک در دسترس باشد
2. آیتم‌های Sidebar که نیاز به feature دارند — اگر feature فعال نیست — با آیکون قفل نمایش داده شوند یا پنهان شوند
3. صفحاتی که feature ندارند، یک بنر «برای استفاده از این قابلیت پنل خود را ارتقاء دهید» نمایش دهند

---

## مشکل / هدف

### قابلیت‌های محدودشده توسط اشتراک

| Feature Key       | صفحه/منو                        | نقش مرتبط                 |
| ----------------- | ------------------------------- | ------------------------- |
| `patient_records` | `/admin/my-patients`            | doctor, clinic, secretary |
| `services`        | `/admin/clinic-services`        | doctor, clinic            |
| `sms_panel`       | `/admin/sms-wallet`             | doctor, clinic            |
| `max_secretaries` | `/admin/my-secretaries` (تعداد) | doctor, clinic            |

**نکته منشی:** منشی اشتراک مستقل ندارد — اشتراک entity ای که به آن متصل است (دکتر یا کلینیک) اعمال می‌شود. endpoint `/api/v1/subscription/my` برای منشی null برمی‌گرداند (چون `resolveEntity` در `SubscriptionController` فقط ROLE_DOCTOR و ROLE_CLINIC را handle می‌کند).

---

## فایل‌های مرتبط

| فایل                                                     | نقش                                                           |
| -------------------------------------------------------- | ------------------------------------------------------------- |
| `src/Subscription/Controller/SubscriptionController.php` | endpoint `GET /api/v1/subscription/my` — برای doctor و clinic |
| `src/Subscription/Service/SubscriptionService.php`       | `hasFeature()`, `getSecretaryLimit()`                         |
| `assets/admin/stores/authStore.ts`                       | state مرکزی auth — باید subscription هم اینجا باشد            |
| `assets/admin/components/layout/Sidebar.tsx`             | `buildSections()` — منوها بر اساس `primaryRole`               |
| `assets/admin/App.tsx`                                   | `RoleRoute` — گیت نقش‌ها، باید feature gate هم اضافه شود      |
| `assets/admin/pages/MyPatientsPage.tsx`                  | نیاز به `patient_records`                                     |
| `assets/admin/pages/ClinicServicesPage.tsx`              | نیاز به `services`                                            |
| `assets/admin/pages/SmsWalletPage.tsx`                   | نیاز به `sms_panel`                                           |
| `assets/admin/pages/MySecretariesPage.tsx`               | نیاز به `max_secretaries` بیش از ۱                            |
| `assets/admin/pages/SubscriptionPage.tsx`                | صفحه ارتقاء پنل — مقصد CTA ها                                 |

---

## وضعیت فعلی

### Backend — endpoint اشتراک

```php
// SubscriptionController.php — GET /api/v1/subscription/my
// فقط doctor و clinic را handle می‌کند
private function resolveEntity(User $user): array
{
    if ($user->hasRole('ROLE_DOCTOR')) {
        $doctor = $this->doctorRepo->findByUser($user);
        return $doctor !== null ? ['doctor', $doctor->getId()] : ['doctor', null];
    }
    if ($user->hasRole('ROLE_CLINIC')) {
        $clinic = $this->clinicRepo->findByUser($user);
        return $clinic !== null ? ['clinic', $clinic->getId()] : ['clinic', null];
    }
    return ['unknown', null]; // منشی → 403
}
```

پاسخ:

```json
{
    "success": true,
    "data": {
        "subscription": {
            "uuid": "...",
            "plan": {
                "name": "basic",
                "level": 1,
                "features": {
                    "patient_records": true,
                    "services": true,
                    "sms_panel": false
                },
                "max_secretaries": 3
            },
            "is_trial": false,
            "expires_at": 1750000000,
            "days_remaining": 45
        },
        "used_trial": false
    }
}
```

اگر اشتراک فعال نداشته باشد: `"subscription": null`

### Frontend — authStore

```ts
// authStore.ts — فعلاً subscription اصلاً در store نیست
interface AuthState {
    primaryRole: "admin" | "clinic" | "doctor" | "secretary" | "user" | null;
    dbUuid: string | null;
    // ... subscription وجود ندارد
}
```

### Frontend — Sidebar

```tsx
// Sidebar.tsx — همه آیتم‌ها را بدون بررسی feature نمایش می‌دهد
if (primaryRole === "doctor") {
    return [
        {
            label: "مدیریت",
            items: [
                {
                    to: "/admin/my-patients",
                    icon: FolderOpenIcon,
                    label: "پرونده بیماران",
                },
                {
                    to: "/admin/clinic-services",
                    icon: WrenchScrewdriverIcon,
                    label: "سرویس‌ها",
                },
                {
                    to: "/admin/sms-wallet",
                    icon: DevicePhoneMobileIcon,
                    label: "کیف پول پیامک",
                },
            ],
        },
    ];
}
```

### Frontend — App.tsx

```tsx
// فقط نقش‌ها چک می‌شوند، feature gate وجود ندارد
<Route path="my-patients" element={<RoleRoute roles={['doctor', 'secretary', 'clinic']}><MyPatientsPage /></RoleRoute>} />
<Route path="clinic-services" element={<RoleRoute roles={['doctor', 'clinic']}><ClinicServicesPage /></RoleRoute>} />
<Route path="sms-wallet" element={<RoleRoute roles={['doctor', 'clinic']}><SmsWalletPage /></RoleRoute>} />
```

---

## وظایف

### ۱. Backend — منشی می‌تواند اشتراک entity خود را ببیند

در `SubscriptionController::resolveEntity()` حالت `ROLE_SECRETARY` را اضافه کن:

```php
if ($user->hasRole('ROLE_SECRETARY')) {
    $dbUuid = $this->contextRepo->findByUser($user)?->getDbUuid();
    if ($dbUuid) {
        $clinic = $this->clinicRepo->findByUuid($dbUuid);
        if ($clinic) return ['clinic', $clinic->getId()];
        $doctor = $this->doctorRepo->findByUuid($dbUuid);
        if ($doctor) return ['doctor', $doctor->getId()];
    }
}
```

نیاز به inject کردن `UserActiveContextRepository` و `ClinicRepository` و `DoctorRepository` به `SubscriptionController` دارد.

### ۲. Frontend — hook مشترک `useSubscription`

یک فایل `assets/admin/hooks/useSubscription.ts` بساز:

```ts
import { useQuery } from "@tanstack/react-query";
import { api } from "../lib/api";
import { useAuthStore } from "../stores/authStore";
import type { ApiResponse, MySubscriptionData } from "../types";

export function useSubscription() {
    const primaryRole = useAuthStore((s) => s.primaryRole);
    const enabled =
        primaryRole === "doctor" ||
        primaryRole === "clinic" ||
        primaryRole === "secretary";

    const { data } = useQuery<ApiResponse<MySubscriptionData>>({
        queryKey: ["subscription-my"],
        queryFn: () => api.get("/api/v1/subscription/my"),
        enabled,
        staleTime: 2 * 60 * 1000,
    });

    const sub = data?.data?.subscription ?? null;
    const features: Record<string, boolean> = sub?.plan?.features ?? {};
    const maxSecretaries: number = sub?.plan?.max_secretaries ?? 1;
    const hasPlan = sub !== null;

    return {
        subscription: sub,
        hasFeature: (key: string) => hasPlan && (features[key] ?? false),
        maxSecretaries,
        hasPlan,
        isExpiringSoon:
            (sub?.days_remaining ?? 0) > 0 && (sub?.days_remaining ?? 0) <= 7,
    };
}
```

**نکته:** query key `['subscription-my']` همان key ی است که `SubscriptionPage` هم استفاده می‌کند — cache مشترک، یک بار fetch.

### ۳. Frontend — Sidebar با قفل feature

در `Sidebar.tsx`:

```tsx
import { useSubscription } from "../../hooks/useSubscription";
// ...

export default function Sidebar() {
    const primaryRole = useAuthStore((s) => s.primaryRole);
    const dbUuid = useAuthStore((s) => s.dbUuid);
    const { hasFeature } = useSubscription();
    // ...
}
```

نوع `SectionItem` را گسترش بده:

```ts
type SectionItem = {
    to: string;
    icon: React.ElementType;
    label: string;
    feature?: string; // اگر تعریف شد، بدون آن feature → آیکون قفل
};
```

آیتم‌های محدود را با `feature` mark کن:

```tsx
{ to: '/admin/my-patients',    icon: FolderOpenIcon,       label: 'پرونده بیماران', feature: 'patient_records' },
{ to: '/admin/clinic-services',icon: WrenchScrewdriverIcon, label: 'سرویس‌ها',       feature: 'services' },
{ to: '/admin/sms-wallet',     icon: DevicePhoneMobileIcon, label: 'کیف پول پیامک',  feature: 'sms_panel' },
```

در render هر آیتم:

```tsx
const isLocked = item.feature ? !hasFeature(item.feature) : false;

<NavLink
    to={isLocked ? "/admin/subscription" : item.to}
    style={isLocked ? { opacity: 0.5 } : {}}
    title={isLocked ? "نیاز به ارتقاء پنل" : undefined}
>
    <item.icon />
    {item.label}
    {isLocked && <LockClosedIcon style={{ width: 12, marginRight: "auto" }} />}
</NavLink>;
```

import کن: `import { LockClosedIcon } from '@heroicons/react/24/outline';`

### ۴. Frontend — FeatureGate component

یک component کوچک `assets/admin/components/ui/FeatureGate.tsx` بساز:

```tsx
import React from "react";
import { Link } from "react-router-dom";
import { LockClosedIcon } from "@heroicons/react/24/outline";
import { useSubscription } from "../../hooks/useSubscription";

interface FeatureGateProps {
    feature: string;
    children: React.ReactNode;
}

export default function FeatureGate({ feature, children }: FeatureGateProps) {
    const { hasFeature, hasPlan } = useSubscription();

    if (hasFeature(feature)) return <>{children}</>;

    return (
        <div
            style={{
                display: "flex",
                flexDirection: "column",
                alignItems: "center",
                justifyContent: "center",
                minHeight: 320,
                gap: 16,
                padding: 40,
                textAlign: "center",
            }}
        >
            <div
                style={{
                    width: 64,
                    height: 64,
                    borderRadius: "50%",
                    background: "oklch(0.97 0.01 256)",
                    display: "grid",
                    placeItems: "center",
                }}
            >
                <LockClosedIcon style={{ width: 28, color: "var(--text-3)" }} />
            </div>
            <div>
                <div style={{ fontWeight: 700, fontSize: 16, marginBottom: 6 }}>
                    این قابلیت در پنل فعلی شما فعال نیست
                </div>
                <div
                    style={{
                        color: "var(--text-3)",
                        fontSize: 13.5,
                        maxWidth: 360,
                    }}
                >
                    {hasPlan
                        ? "برای استفاده از این قابلیت پنل خود را ارتقاء دهید"
                        : "برای استفاده از این قابلیت یک پنل اشتراکی فعال کنید"}
                </div>
            </div>
            <Link
                to="/admin/subscription"
                className="btn primary sm"
                style={{ textDecoration: "none" }}
            >
                {hasPlan ? "ارتقاء پنل" : "مشاهده پنل‌های اشتراکی"}
            </Link>
        </div>
    );
}
```

### ۵. Frontend — استفاده از FeatureGate در صفحات

در **`MyPatientsPage.tsx`** محتوای اصلی را wrap کن:

```tsx
import FeatureGate from "../components/ui/FeatureGate";
// ...
return (
    <FeatureGate feature="patient_records">{/* کد فعلی صفحه */}</FeatureGate>
);
```

در **`ClinicServicesPage.tsx`**:

```tsx
return <FeatureGate feature="services">{/* کد فعلی */}</FeatureGate>;
```

در **`SmsWalletPage.tsx`**:

```tsx
return <FeatureGate feature="sms_panel">{/* کد فعلی */}</FeatureGate>;
```

### ۶. Frontend — محدودیت تعداد منشی در MySecretariesPage

در **`MySecretariesPage.tsx`** وقتی کاربر می‌خواهد منشی جدید اضافه کند:

```tsx
import { useSubscription } from "../hooks/useSubscription";
// ...
const { maxSecretaries } = useSubscription();
// تعداد فعلی منشی ها از data موجود
const activeCount = secretaries.filter((s) => s.is_active).length;
const isAtLimit = activeCount >= maxSecretaries;

// روی دکمه «افزودن منشی»:
<button
    className="btn primary sm"
    onClick={() => setAddOpen(true)}
    disabled={isAtLimit}
    title={
        isAtLimit
            ? `حداکثر ${maxSecretaries} منشی در پنل فعلی مجاز است`
            : undefined
    }
>
    افزودن منشی
</button>;
{
    isAtLimit && (
        <div style={{ fontSize: 12, color: "var(--text-3)", marginTop: 4 }}>
            برای افزودن منشی بیشتر{" "}
            <Link to="/admin/subscription">پنل خود را ارتقاء دهید</Link>
        </div>
    );
}
```

---

## نکات مهم

- **admin نقش اشتراک ندارد:** `useSubscription` فقط برای doctor/clinic/secretary فعال است. در Sidebar ادمین هیچ feature gate نداشته باشد.
- **secretary اشتراک entity خود را می‌بیند:** بعد از task 1، `/api/v1/subscription/my` برای secretary هم کار می‌کند. اگر db_uuid هنوز set نشده (قبل از انتخاب context)، `subscription: null` برمی‌گردد — `useSubscription` به‌درستی `hasPlan: false` برمی‌گرداند.
- **Cache مشترک:** SubscriptionPage هم از `queryKey: ['subscription-my']` استفاده می‌کند — تغییر ندهید تا cache reuse شود.
- **پلن free:** در DB ممکن است اصلاً subscription نداشته باشند (`subscription: null`). این حالت را با `hasPlan: false` handle کن. فرض نکن که "free" یک plan object است.
- **LockClosedIcon** را import کن از `@heroicons/react/24/outline` — باید در heroicons وجود داشته باشد.
- **migration لازم نیست** — فقط backend method و frontend تغییر می‌کند.
- **مستندات:** بعد از تغییر `SubscriptionController`، فایل `docs/api/subscription.md` باید به‌روز شود تا `GET /api/v1/subscription/my` برای secretary هم مستند شود.

---

## ترتیب اجرا

1. Backend: `SubscriptionController::resolveEntity()` — حالت secretary
2. تست route: `ddev exec php bin/console cache:clear`
3. Frontend: `assets/admin/hooks/useSubscription.ts` — hook جدید
4. Frontend: `assets/admin/components/ui/FeatureGate.tsx` — component جدید
5. Frontend: `Sidebar.tsx` — اضافه کردن `feature` به آیتم‌ها و render قفل
6. Frontend: `MyPatientsPage.tsx`، `ClinicServicesPage.tsx`، `SmsWalletPage.tsx` — wrap با FeatureGate
7. Frontend: `MySecretariesPage.tsx` — محدودیت تعداد منشی
8. Build: `ddev exec yarn dev`
9. TypeScript: `ddev exec npx tsc --noEmit`
10. مستندات: `docs/api/subscription.md`
