diff --git a/.agents/skills/caveman/README.md b/.agents/skills/caveman/README.md new file mode 100644 index 00000000..d749b834 --- /dev/null +++ b/.agents/skills/caveman/README.md @@ -0,0 +1,48 @@ +# caveman + +Talk like smart caveman. Same brain, fewer tokens. + +## What it does + +Compress every model response to caveman-style prose. Drops articles, filler, pleasantries, and hedging. Keeps every technical detail, code block, error string, and symbol exact. Cuts ~65-75% of output tokens with full accuracy preserved. Mode persists for the whole session until changed or stopped. + +Six intensity levels: + +| Level | What change | +|-------|-------------| +| `lite` | Drop filler/hedging. Sentences stay full. Professional but tight. | +| `full` | Default. Drop articles, fragments OK, short synonyms. | +| `ultra` | Bare fragments. Abbreviations (DB, auth, fn). Arrows for causality. | +| `wenyan-lite` | Classical Chinese register, light compression. | +| `wenyan-full` | Maximum 文言文. 80-90% character reduction. | +| `wenyan-ultra` | Extreme classical compression. | + +Auto-clarity rule: caveman drops to normal prose for security warnings, irreversible-action confirmations, multi-step sequences where fragment ambiguity risks misread, and when user repeats a question. Resumes after the clear part. + +## How to invoke + +``` +/caveman # full mode (default) +/caveman lite # lighter compression +/caveman ultra # extreme compression +/caveman wenyan # classical Chinese +stop caveman # back to normal prose +``` + +## Example output + +Question: "Why does my React component re-render?" + +Normal prose: +> Your component re-renders because you create a new object reference each render. Wrapping it in `useMemo` will fix the issue. + +Caveman (full): +> New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`. + +Caveman (ultra): +> Inline obj prop → new ref → re-render. `useMemo`. + +## See also + +- [`SKILL.md`](./SKILL.md) — full LLM-facing instructions +- [Caveman README](../../README.md) — repo overview, install, benchmarks diff --git a/.agents/skills/caveman/SKILL.md b/.agents/skills/caveman/SKILL.md new file mode 100644 index 00000000..8792c1ab --- /dev/null +++ b/.agents/skills/caveman/SKILL.md @@ -0,0 +1,78 @@ +--- +name: caveman +description: > + Ultra-compressed communication mode. Cuts token usage ~75% by speaking like caveman + while keeping full technical accuracy. Supports intensity levels: lite, full (default), ultra, + wenyan-lite, wenyan-full, wenyan-ultra. + Use when user says "caveman mode", "talk like caveman", "use caveman", "less tokens", + "be brief", or invokes /caveman. Also auto-triggers when token efficiency is requested. +--- + +Respond terse like smart caveman. All technical substance stay. Only fluff die. + +## Persistence + +ACTIVE EVERY RESPONSE. No revert after many turns. No filler drift. Still active if unsure. Off only: "stop caveman" / "normal mode". + +Default: **full**. Switch: `/caveman lite|full|ultra`. + +## Rules + +Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). No tool-call narration, no decorative tables/emoji, no dumping long raw error logs unless asked — quote shortest decisive line. Standard well-known tech acronyms OK (DB/API/HTTP); never invent new abbreviations reader can't decode. Technical terms exact. Code blocks unchanged. Errors quoted exact. + +Preserve user's dominant language. User write Portuguese → reply Portuguese caveman. User write Spanish → reply Spanish caveman. Compress the style, not the language. No forced English openings or status phrases. ALWAYS keep technical terms, code, API names, CLI commands, commit-type keywords (feat/fix/...), and exact error strings verbatim — unless user explicitly ask for translation. + +No self-reference. Never name or announce the style. No "caveman mode on", "me caveman think", no third-person caveman tags. Output caveman-only — never normal answer plus "Caveman:" recap. Exception: user explicitly ask what the mode is. + +Pattern: `[thing] [action] [reason]. [next step].` + +Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..." +Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:" + +## Intensity + +| Level | What change | +|-------|------------| +| **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight | +| **full** | Drop articles, fragments OK, short synonyms. Classic caveman. No tool-call narration, no decorative tables/emoji, no long raw error-log dumps unless asked. Standard acronyms OK; no invented abbreviations | +| **ultra** | Abbreviate prose words (DB/auth/config/req/res/fn/impl) — prose words only, never real code symbols/function names. Strip conjunctions, arrows for causality (X → Y), one word when one word enough. Code symbols, function names, API names, error strings: never abbreviate | +| **wenyan-lite** | Semi-classical. Drop filler/hedging but keep grammar structure, classical register | +| **wenyan-full** | Maximum classical terseness. Fully 文言文. 80-90% character reduction. Classical sentence patterns, verbs precede objects, subjects often omitted, classical particles (之/乃/為/其) | +| **wenyan-ultra** | Extreme abbreviation while keeping classical Chinese feel. Maximum compression, ultra terse | + +Example — "Why React component re-render?" +- lite: "Your component re-renders because you create a new object reference each render. Wrap it in `useMemo`." +- full: "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`." +- ultra: "Inline obj prop → new ref → re-render. `useMemo`." +- wenyan-lite: "組件頻重繪,以每繪新生對象參照故。以 useMemo 包之。" +- wenyan-full: "每繪新生對象參照,故重繪;以 useMemo 包之則免。" +- wenyan-ultra: "新參照→重繪。useMemo Wrap。" + +Example — "Explain database connection pooling." +- lite: "Connection pooling reuses open connections instead of creating new ones per request. Avoids repeated handshake overhead." +- full: "Pool reuse open DB connections. No new connection per request. Skip handshake overhead." +- ultra: "Pool = reuse DB conn. Skip handshake → fast under load." +- wenyan-full: "池reuse open connection。不每req新開。skip handshake overhead。" +- wenyan-ultra: "池reuse conn。skip handshake → fast。" + +## Auto-Clarity + +Drop caveman when: +- Security warnings +- Irreversible action confirmations +- Multi-step sequences where fragment order or omitted conjunctions risk misread +- Compression itself creates technical ambiguity (e.g., `"migrate table drop column backup first"` — order unclear without articles/conjunctions) +- User asks to clarify or repeats question + +Resume caveman after clear part done. + +Example — destructive op: +> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone. +> ```sql +> DROP TABLE users; +> ``` +> Caveman resume. Verify backup exist first. + +## Boundaries + +Code/commits/PRs: write normal. "stop caveman" or "normal mode": revert. Level persist until changed or session end. \ No newline at end of file diff --git a/.claude/skills/add-admin-endpoint/SKILL.md b/.claude/skills/add-admin-endpoint/SKILL.md deleted file mode 100644 index 264a27aa..00000000 --- a/.claude/skills/add-admin-endpoint/SKILL.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -name: add-admin-endpoint -description: Add a new paginated admin API endpoint to AdminApiController. Use when the user wants to add a backend admin list, stats, or action endpoint — things like "add an endpoint for X", "create an admin API for Y", "I need a route that lists Z". ---- - -## Target file -`src/Admin/Controller/AdminApiController.php` - -All admin endpoints live here. The class already has `#[IsGranted('ROLE_ADMIN')]` and injects `EntityManagerInterface $em`. - -## Checklist - -1. **Stats endpoint** (optional but standard): a separate `#[Route('/api/v1/admin/{entity}/stats')]` method that returns counts via raw SQL (`$this->em->getConnection()->fetchOne()`). Return with `$this->success([...])`. - -2. **List endpoint**: use QueryBuilder with `->getArrayResult()` — never load full entities for list queries (entity getters may not exist for all fields). Pattern: - -```php -#[Route('/api/v1/admin/{entities}', methods: ['GET'])] -public function list{Entity}(Request $request): JsonResponse -{ - $page = max(1, (int) $request->query->get('page', 1)); - $limit = min(100, max(1, (int) $request->query->get('limit', 15))); - $search = trim((string) $request->query->get('search', '')); - - $qb = $this->em->createQueryBuilder() - ->select('e.id, e.uuid, e.someField, e.createdAt') - ->from(SomeEntity::class, 'e'); - - if ($search !== '') { - $qb->andWhere('e.name LIKE :s')->setParameter('s', "%$search%"); - } - - $total = (clone $qb)->select('COUNT(e.id)')->getQuery()->getSingleScalarResult(); - - $items = $qb - ->orderBy('e.id', 'DESC') - ->setFirstResult(($page - 1) * $limit) - ->setMaxResults($limit) - ->getQuery() - ->getArrayResult(); - - return $this->paginated($items, (int) $total, $page, $limit); -} -``` - -3. **Action endpoints** (toggle status, etc.) follow this shape: - -```php -#[Route('/api/v1/admin/{entities}/{uuid}/status', methods: ['POST'])] -public function toggle{Entity}Status(string $uuid): JsonResponse -{ - $entity = $this->em->getRepository(SomeEntity::class)->findOneBy(['uuid' => $uuid]); - if (!$entity) return $this->error('NOT_FOUND', 'Entity not found', 404); - - $entity->setIsActive(!$entity->getIsActive()); - $this->em->flush(); - - return $this->success(['is_active' => $entity->getIsActive()]); -} -``` - -## Critical rules - -- **Always use `getArrayResult()`** for list queries. Never call entity getters inside admin list methods. -- `createdAt` and `updatedAt` are Unix integer timestamps — do not format them in PHP, let the frontend handle it. -- For JOINs to categories (city, state, specialty), use LEFT JOIN in DQL and select the name field directly into the array result. -- Response shape for lists: `$this->paginated($items, $total, $page, $limit)` — frontend reads `data?.data` for items and `data?.meta?.totalRecords` for count. -- Add `use` imports for any new entity class at the top of the file. -- Run `/sync-db` only if a new entity or column was added as part of this change. diff --git a/.claude/skills/add-admin-page/SKILL.md b/.claude/skills/add-admin-page/SKILL.md deleted file mode 100644 index 50efea37..00000000 --- a/.claude/skills/add-admin-page/SKILL.md +++ /dev/null @@ -1,220 +0,0 @@ ---- -name: add-admin-page -description: Add a new admin React page to the frontend. Use when the user wants a new admin panel page — list views, detail pages, management UIs. Triggers on "add a page for X", "create an admin page", "I need a UI for managing Y", "build the frontend for Z". ---- - -## Files to create/modify - -| Action | Path | -|--------|------| -| Create | `assets/admin/pages/{Name}Page.tsx` | -| Edit | `assets/admin/types/index.ts` — add the TypeScript interface | -| Edit | `assets/admin/App.tsx` — add the route | - -## Step 1 — Add the TypeScript type - -Add to `assets/admin/types/index.ts`: - -```ts -export interface SomeName { - uuid: string; - // ... fields matching the backend array result -} -``` - -## Step 2 — Register the route - -In `assets/admin/App.tsx`, add inside the `` routes block: - -```tsx -import SomeNamePage from './pages/SomeNamePage'; -// ... -} /> -} /> {/* if detail page needed */} -``` - -## Step 3 — Create the page - -Standard list page pattern: - -```tsx -import React, { useState, useEffect } from 'react'; -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { useNavigate } from 'react-router-dom'; -import { MagnifyingGlassIcon, PlusIcon, EyeIcon, TrashIcon, ArrowPathIcon, CheckCircleIcon, XCircleIcon } from '@heroicons/react/24/outline'; -import { toast } from 'sonner'; -import { api } from '../lib/api'; -import type { ApiResponse, PaginatedResponse } from '../lib/api'; -import { formatDate } from '../lib/utils'; -import ConfirmDialog from '../components/ui/ConfirmDialog'; -import Pagination from '../components/ui/Pagination'; -import type { SomeName } from '../types'; - -export default function SomeNamePage() { - const navigate = useNavigate(); - const qc = useQueryClient(); - const [page, setPage] = useState(1); - const [limit] = useState(15); - const [searchInput, setSearchInput] = useState(''); - const [search, setSearch] = useState(''); - const [deleteTarget, setDeleteTarget] = useState(null); - - // Debounced search — always 350 ms - useEffect(() => { - const t = setTimeout(() => { setSearch(searchInput); setPage(1); }, 350); - return () => clearTimeout(t); - }, [searchInput]); - - // List query - const listQ = useQuery({ - queryKey: ['admin-some-names', page, limit, search], - queryFn: () => { - const p = new URLSearchParams({ page: String(page), limit: String(limit) }); - if (search) p.set('search', search); - return api.get>(`/api/v1/admin/some-names?${p}`); - }, - }); - - const items = listQ.data?.data ?? []; - const total = listQ.data?.meta?.totalRecords ?? 0; - - // Stats query (if stats endpoint exists) - const statsQ = useQuery({ - queryKey: ['admin-some-names-stats'], - queryFn: () => api.get>('/api/v1/admin/some-names/stats'), - staleTime: 30_000, - }); - // IMPORTANT: stats data may be double-nested depending on backend shape. - // Use this pattern to handle both cases: - const stats = (statsQ.data?.data as any)?.data ?? statsQ.data?.data; - - // Delete mutation - const deleteMut = useMutation({ - mutationFn: (uuid: string) => api.delete>(`/api/v1/some-names/${uuid}`), - onSuccess: () => { - toast.success('حذف شد'); - setDeleteTarget(null); - qc.invalidateQueries({ queryKey: ['admin-some-names'] }); - }, - onError: (e: Error) => toast.error(e.message), - }); - - // Toggle status mutation - const toggleMut = useMutation({ - mutationFn: (uuid: string) => api.post>(`/api/v1/admin/some-names/${uuid}/status`, {}), - onSuccess: () => { - toast.success('وضعیت تغییر کرد'); - qc.invalidateQueries({ queryKey: ['admin-some-names'] }); - }, - onError: (e: Error) => toast.error(e.message), - }); - - return ( -
- {/* Header */} -
-
-

عنوان صفحه

-
توضیح کوتاه
-
- -
- - {/* KPI cards — only if stats endpoint exists */} - {/*
...
*/} - - {/* Main card */} -
- {/* Toolbar */} -
-
-
- - setSearchInput(e.target.value)} placeholder="جستجو..." /> -
-
- -
-
- - {/* Table */} -
- - - - - - - - - - - {listQ.isLoading && Array.from({ length: 5 }).map((_, i) => ( - {Array.from({ length: 4 }).map((_, j) => ( - - ))} - ))} - {!listQ.isLoading && items.length === 0 && ( - - )} - {!listQ.isLoading && items.map((item) => ( - navigate(`/admin/some-names/${item.uuid}`)}> - - - - - - ))} - -
ناموضعیتتاریخ
موردی یافت نشد
{/* render fields */} - - - {(item as any).is_active ? 'فعال' : 'غیرفعال'} - - {formatDate((item as any).created_at)} e.stopPropagation()}> -
- - - -
-
-
- - -
- - deleteTarget && deleteMut.mutate(deleteTarget.uuid)} - onCancel={() => setDeleteTarget(null)} - /> -
- ); -} -``` - -## Critical rules - -- **Never use `data?.data?.data`** unless the endpoint is a `$this->success(['data' => ...])` double-nest. Standard `$this->success($array)` → extract with `data?.data`. `$this->paginated()` → items at `data?.data`, total at `data?.meta?.totalRecords`. -- Stats from `$this->success($stats)` may still be double-nested in older endpoints — use `(statsQ.data?.data as any)?.data ?? statsQ.data?.data` to handle both. -- Category API (`/api/v1/categorys/{bundle}`) is always triple-nested: extract with `data?.data?.data ?? []`. -- Search debounce is always 350ms via `setTimeout` in a `useEffect`. -- Query keys follow the format `['admin-entity-name', page, limit, search, ...filters]`. -- After creating the page, run `ddev exec yarn dev` to check for TypeScript errors. diff --git a/.claude/skills/sync-db/SKILL.md b/.claude/skills/sync-db/SKILL.md deleted file mode 100644 index 2c62431a..00000000 --- a/.claude/skills/sync-db/SKILL.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -name: sync-db -description: Run the standard Doctrine migration cycle after any entity change. Use whenever an entity is added or modified, a new column/relation is needed, or the user says "migrate", "sync the database", "generate migration", or asks to apply schema changes. -disable-model-invocation: true ---- - -Run these three commands in sequence and report the output of each step: - -```bash -ddev exec php bin/console doctrine:migrations:diff --no-interaction -ddev exec php bin/console doctrine:migrations:migrate --no-interaction -ddev exec php bin/console cache:clear -``` - -If `migrations:diff` reports "No changes detected", skip `migrations:migrate` and say so. If any step fails, stop and show the full error output. diff --git a/assets/admin/pages/MyPatientsPage.tsx b/assets/admin/pages/MyPatientsPage.tsx index 2b6d721e..edf54746 100644 --- a/assets/admin/pages/MyPatientsPage.tsx +++ b/assets/admin/pages/MyPatientsPage.tsx @@ -1313,6 +1313,25 @@ function MyPatientsPageInner() { ); } +function patientAvatar(name: string, size = 28) { + return ( +
+ {(name === "—" ? "؟" : name).charAt(0)} +
+ ); +} + +function fileNumber(record: PatientRecord): string { + return `P-${record.uuid.slice(0, 8).toUpperCase()}`; +} + function PatientCard({ record, onOpen, @@ -1322,122 +1341,63 @@ function PatientCard({ }) { const name = getPatientName(record); const phone = getPatientPhone(record); - const nationalCode = getPatientNationalCode(record); + + const infoRow = (label: string, value: React.ReactNode) => ( +
+ {label}: + {value} +
+ ); + return ( +
+
+ ··· +
+ {name} + {patientAvatar(name)} +
+
+ {infoRow("شماره پرونده", fileNumber(record))} + {infoRow("موبایل", phone)} +
+ برچسب‌ها: + +
+
+ ); +} + +function PatientRow({ + record, + onOpen, +}: { + record: PatientRecord; + onOpen: () => void; +}) { + const name = getPatientName(record); return (
-
-
- {(name === "—" ? "؟" : name).charAt(0)} -
-
-
- {name} -
-
- {formatDate(record.created_at)} -
-
-
- -
-
- - {phone} -
-
- - - {nationalCode ?? ( - - کد ملی ثبت نشده - - )} - -
-
- + {patientAvatar(name, 38)} + {name} + {fileNumber(record)} + {getPatientPhone(record)}
); diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 00000000..e15aeded --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,11 @@ +{ + "version": 1, + "skills": { + "caveman": { + "source": "juliusbrussee/caveman", + "sourceType": "github", + "skillPath": "skills/caveman/SKILL.md", + "computedHash": "1902fa0b569912d0c05736d8d98a72097d9b82719aac88c0c1d03bb546f9176d" + } + } +}