diff --git a/.claude/prompt/admin-pwa.md b/.claude/prompt/admin-pwa.md new file mode 100644 index 00000000..efa9c12d --- /dev/null +++ b/.claude/prompt/admin-pwa.md @@ -0,0 +1,200 @@ +# Admin Panel PWA + +پنل ادمین (`/admin`) را به یک Progressive Web App تبدیل کن تا کاربران بتوانند آن را روی دستگاه خود نصب کنند و آفلاین هم shell اولیه را ببینند. + +--- + +## وضعیت فعلی پروژه + +- **Twig template**: `templates/admin/index.html.twig` — فایل HTML ورودی پنل ادمین +- **Webpack Encore** با `addEntry('admin', './assets/admin/index.tsx')` +- **Output path**: `public/build/` +- **Public path**: `/build` +- آیکونهای موجود: `public/favicon.ico` و `public/favicon.png` +- دامنه local: `https://clinic-pro.ddev.site` + +**قابلیتهای ۱ تا ۴ قبلاً اجرا شدهاند:** +- `public/manifest.json` ✅ موجود است +- `public/sw.js` ✅ موجود است +- ثبت SW در `assets/admin/index.tsx` ✅ انجام شده +- `assets/admin/components/ui/PwaInstallBanner.tsx` ✅ موجود است (برای کاربران logged-in) + +--- + +## قابلیتهای مورد نیاز + +### قابلیت ۱ — Web App Manifest + +فایل `public/manifest.json` بساز با این مشخصات: + +```json +{ + "name": "ClinicPro Admin", + "short_name": "ClinicPro", + "description": "پنل مدیریت کلینیک پرو", + "start_url": "/admin", + "scope": "/admin", + "display": "standalone", + "orientation": "portrait-primary", + "theme_color": "#6366f1", + "background_color": "#f8fafc", + "lang": "fa", + "dir": "rtl", + "icons": [ + { "src": "/favicon.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" }, + { "src": "/favicon.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" } + ] +} +``` + +- رنگ `theme_color` را از CSS variable `--primary` پروژه بگیر (مقدار واقعی را از `assets/admin/styles.css` بخوان) +- در `templates/admin/index.html.twig` این تگها را به `
` اضافه کن: + ```html + + + + + + + + ``` + +--- + +### قابلیت ۲ — Service Worker (Cache Shell) + +یک service worker در `public/sw.js` بنویس. **بدون workbox** — native Service Worker API: + +**استراتژی:** +- `install` event: کش کردن shell (فایلهای ضروری برای render اولیه) +- `activate` event: پاک کردن کشهای قدیمی +- `fetch` event: + - درخواستهای API (`/api/`) → **فقط Network** (هرگز کش نکن، اگر آفلاین بود خطا بده) + - فایلهای build (`/build/`) → **Cache First** (سریعتر، stale-while-revalidate) + - navigation (`/admin*`) → **Network First با fallback** به shell کششده + +**فایلهایی که در install کش میشوند:** +``` +/admin +/manifest.json +/favicon.png +``` +فایلهای `/build/` را در install کش **نکن** (hash در نام آنهاست و هر deploy عوض میشود) — آنها را در fetch با Cache First مدیریت کن. + +**Cache name** شامل version باشد تا activate بتواند قدیمیها را پاک کند: +```js +const CACHE_NAME = 'clinicpro-admin-v1'; +const BUILD_CACHE = 'clinicpro-build-v1'; +``` + +--- + +### قابلیت ۳ — ثبت Service Worker در React + +در `assets/admin/index.tsx` (فایل entry point) بعد از `ReactDOM.render` / `createRoot`: + +```typescript +if ('serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker.register('/sw.js') + .catch(() => { /* silent fail in dev */ }); + }); +} +``` + +**نکته:** registration باید فقط در production یا بهصورت همیشگی باشد — در هر دو حالت کار میکند چون sw.js در `/public` است و مستقیم serve میشود. + +--- + +### قابلیت ۴ — Install Prompt Banner (اختیاری اما مطلوب) + +یک کامپوننت کوچک React در `assets/admin/components/ui/PwaInstallBanner.tsx` بساز: + +- `beforeinstallprompt` event را listen میکند +- اگر prompt در دسترس بود یک banner کوچک در پایین صفحه نشان میدهد: + ``` + [آیکون] ClinicPro را نصب کنید — دسترسی سریعتر [نصب] [×] + ``` +- بعد از dismiss در `localStorage` ذخیره کن (`pwa-dismissed`) تا دوباره نشان داده نشود +- در `assets/admin/App.tsx` یا `AdminLayout` این کامپوننت را اضافه کن + +**طراحی**: از CSS variables پروژه استفاده کن (`--surface`, `--border`, `--primary`, `--text`). RTL رعایت شود. + +--- + +## تست پس از هر قابلیت + +```bash +# Build +ddev exec yarn dev + +# بررسی manifest +curl https://clinic-pro.ddev.site/manifest.json + +# بررسی sw.js +curl https://clinic-pro.ddev.site/sw.js | head -5 + +# بررسی Twig +curl https://clinic-pro.ddev.site/admin | grep manifest +``` + +**تست PWA در Chrome:** +1. مرورگر Chrome → `https://clinic-pro.ddev.site/admin` +2. DevTools → Application → Service Workers → باید registered باشد +3. DevTools → Application → Manifest → باید parse شده و installable باشد +4. آیکون نصب در address bar باید ظاهر شود + +--- + +### قابلیت ۵ — نمایش پرامپت نصب در صفحه لاگین + +در صفحه لاگین (`assets/admin/pages/LoginPage.tsx`) یک بخش نصب اپلیکیشن اضافه کن که **همیشه قابل مشاهده** باشد — نه فقط وقتی `beforeinstallprompt` فایر شده. + +**منطق نمایش:** + +- اگر app قبلاً نصب شده (`window.matchMedia('(display-mode: standalone)').matches`) → این بخش را نشان **نده** +- اگر `pwa-dismissed` در localStorage بود → این بخش را نشان **نده** +- در غیر این صورت همیشه نشان بده + +**دو حالت:** + +۱. **وقتی `beforeinstallprompt` در دسترس است** (Chrome/Edge desktop و Android): + - دکمه «نصب اپلیکیشن» نشان بده + - با کلیک روی دکمه، native install prompt مرورگر را باز کن + +۲. **وقتی `beforeinstallprompt` در دسترس نیست** (Safari iOS، Firefox، یا قبل از fire شدن event): + - یک راهنمای متنی نشان بده: + - iOS: «در Safari: دکمه Share را بزن، سپس «Add to Home Screen» را انتخاب کن» + - سایر: «در منوی مرورگر گزینه «Install app» یا «Add to Home Screen» را انتخاب کن» + +**طراحی:** + +یک card زیبا زیر فرم لاگین (یا بالای آن در موبایل) با این ظاهر: + +``` +┌─────────────────────────────────────────┐ +│ [آیکون ClinicPro] │ +│ ClinicPro را نصب کنید │ +│ دسترسی سریعتر — بدون نیاز به مرورگر │ +│ │ +│ [دکمه نصب / راهنما] [بعداً] │ +└─────────────────────────────────────────┘ +``` + +- از CSS variables پروژه استفاده کن (`--primary-soft`, `--primary-700`, `--surface`, `--border`) +- RTL رعایت شود +- دکمه «بعداً» مثل قابلیت ۴ عمل کند: `localStorage.setItem('pwa-dismissed', '1')` و بخش را مخفی کند +- کامپوننت جداگانه بساز: `assets/admin/components/ui/PwaLoginCard.tsx` +- در `LoginPage.tsx` این کامپوننت را import و زیر card اصلی فرم لاگین قرار بده + +**نکته مهم:** این کامپوننت مستقل از `PwaInstallBanner` است — banner برای کاربران logged-in است، این card برای صفحه لاگین است. منطق `beforeinstallprompt` را در یک custom hook مشترک (`assets/admin/hooks/usePwaInstall.ts`) بگذار تا هر دو کامپوننت از آن استفاده کنند و event را دو بار capture نکنند. + +--- + +## محدودیتها و نکات + +- **workbox اضافه نکن** — native API کافی است و dependency غیرضروری اضافه نمیکند +- **Webpack Encore را تغییر نده** — sw.js مستقیم در `public/` مینشیند و نیازی به bundle ندارد +- **DDEV HTTPS**: mkcert باید قبلاً نصب شده باشد (`mkcert -install`)؛ اگر نبود service worker register نمیشود +- **Scope**: `/admin` — service worker فقط این path را intercept میکند، `/api/` را تحت تأثیر قرار نمیدهد مگر از داخل scope فراخوانی شود +- **آیکونها**: اگر `favicon.png` کوچک بود (زیر 192px)، یک آیکون مناسب در `public/icons/` بساز یا از همان استفاده کن — Chrome حتی با آیکون کوچک هم install prompt نشان میدهد diff --git a/assets/admin/App.tsx b/assets/admin/App.tsx index 11c87a1f..4bf6c640 100644 --- a/assets/admin/App.tsx +++ b/assets/admin/App.tsx @@ -33,6 +33,7 @@ import MyPatientsPage from './pages/MyPatientsPage'; import MyFinancialPage from './pages/MyFinancialPage'; import ClinicFormPage from './pages/ClinicFormPage'; import PreRegistrationsPage from './pages/PreRegistrationsPage'; +import PwaInstallBanner from './components/ui/PwaInstallBanner'; // ── Guards ────────────────────────────────────────────────────────────────── @@ -77,6 +78,7 @@ function RoleRoute({ roles, children }: { roles: string[]; children: React.React export default function App() { return ( + <>نصب ClinicPro
+دسترسی سریعتر از روی صفحه اصلی
+ClinicPro
+پنل مدیریت کلینیک
++ اپلیکیشن را روی دستگاه خود نصب کنید و بدون باز کردن مرورگر وارد شوید. +
+ ++ مراحل نصب: +
+{step}
+{inv.clinic.name}
+ {inv.invited_specialty && ( +تخصص: {inv.invited_specialty}
+ )} ++ {isExpired ? 'منقضی شده' : `انقضا: ${new Date(inv.expires_at * 1000).toLocaleDateString('fa-IR')}`} +
+