feat(caveman): add new caveman communication mode with intensity levels
This commit is contained in:
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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 `<AdminLayout>` routes block:
|
||||
|
||||
```tsx
|
||||
import SomeNamePage from './pages/SomeNamePage';
|
||||
// ...
|
||||
<Route path="some-names" element={<SomeNamePage />} />
|
||||
<Route path="some-names/:uuid" element={<SomeNameDetailPage />} /> {/* 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<SomeName | null>(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<PaginatedResponse<SomeName>>(`/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<ApiResponse<{ total: number; active: number }>>('/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<ApiResponse<null>>(`/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<ApiResponse<{ is_active: boolean }>>(`/api/v1/admin/some-names/${uuid}/status`, {}),
|
||||
onSuccess: () => {
|
||||
toast.success('وضعیت تغییر کرد');
|
||||
qc.invalidateQueries({ queryKey: ['admin-some-names'] });
|
||||
},
|
||||
onError: (e: Error) => toast.error(e.message),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="fade-in">
|
||||
{/* Header */}
|
||||
<div className="card-title-row" style={{ marginBottom: 'var(--gap)' }}>
|
||||
<div>
|
||||
<h1 className="section-title">عنوان صفحه</h1>
|
||||
<div className="muted" style={{ fontSize: 13, marginTop: 3 }}>توضیح کوتاه</div>
|
||||
</div>
|
||||
<button className="btn primary sm" onClick={() => navigate('/admin/some-names/new')}>
|
||||
<PlusIcon style={{ width: 15, height: 15 }} /> افزودن
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* KPI cards — only if stats endpoint exists */}
|
||||
{/* <div className="stat-grid"> ... </div> */}
|
||||
|
||||
{/* Main card */}
|
||||
<div className="card">
|
||||
{/* Toolbar */}
|
||||
<div className="card-pad" style={{ paddingBottom: 0 }}>
|
||||
<div className="toolbar">
|
||||
<div className="field" style={{ minWidth: 240 }}>
|
||||
<MagnifyingGlassIcon style={{ width: 17, height: 17 }} />
|
||||
<input value={searchInput} onChange={(e) => setSearchInput(e.target.value)} placeholder="جستجو..." />
|
||||
</div>
|
||||
<div className="spacer" />
|
||||
<button className="btn ghost sm" onClick={() => listQ.refetch()} disabled={listQ.isFetching}>
|
||||
<ArrowPathIcon style={{ width: 15, height: 15 }} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="table-wrap">
|
||||
<table className="t">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>نام</th>
|
||||
<th>وضعیت</th>
|
||||
<th>تاریخ</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{listQ.isLoading && Array.from({ length: 5 }).map((_, i) => (
|
||||
<tr key={i}>{Array.from({ length: 4 }).map((_, j) => (
|
||||
<td key={j}><div className="skeleton" style={{ height: 14, borderRadius: 6, width: '60%' }} /></td>
|
||||
))}</tr>
|
||||
))}
|
||||
{!listQ.isLoading && items.length === 0 && (
|
||||
<tr><td colSpan={4}><div className="empty">موردی یافت نشد</div></td></tr>
|
||||
)}
|
||||
{!listQ.isLoading && items.map((item) => (
|
||||
<tr key={item.uuid} style={{ cursor: 'pointer' }} onClick={() => navigate(`/admin/some-names/${item.uuid}`)}>
|
||||
<td>{/* render fields */}</td>
|
||||
<td>
|
||||
<span className={`badge ${(item as any).is_active ? 'green' : 'gray'}`}>
|
||||
<span className="bdot" />
|
||||
{(item as any).is_active ? 'فعال' : 'غیرفعال'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="muted">{formatDate((item as any).created_at)}</td>
|
||||
<td onClick={(e) => e.stopPropagation()}>
|
||||
<div className="row-actions">
|
||||
<button className="mini-btn" onClick={() => navigate(`/admin/some-names/${item.uuid}`)}>
|
||||
<EyeIcon style={{ width: 16, height: 16 }} />
|
||||
</button>
|
||||
<button className="mini-btn" onClick={() => toggleMut.mutate(item.uuid)} disabled={toggleMut.isPending}>
|
||||
{(item as any).is_active
|
||||
? <XCircleIcon style={{ width: 16, height: 16 }} />
|
||||
: <CheckCircleIcon style={{ width: 16, height: 16 }} />}
|
||||
</button>
|
||||
<button className="mini-btn danger" onClick={() => setDeleteTarget(item)}>
|
||||
<TrashIcon style={{ width: 16, height: 16 }} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<Pagination page={page} total={total} limit={limit} onPageChange={setPage} />
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
open={!!deleteTarget}
|
||||
title="حذف"
|
||||
message={`آیا از حذف این مورد اطمینان دارید؟`}
|
||||
confirmLabel="حذف"
|
||||
danger
|
||||
loading={deleteMut.isPending}
|
||||
onConfirm={() => deleteTarget && deleteMut.mutate(deleteTarget.uuid)}
|
||||
onCancel={() => setDeleteTarget(null)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 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.
|
||||
@@ -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.
|
||||
@@ -1313,6 +1313,25 @@ function MyPatientsPageInner() {
|
||||
);
|
||||
}
|
||||
|
||||
function patientAvatar(name: string, size = 28) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: size, height: size, borderRadius: "50%", flexShrink: 0,
|
||||
background: "linear-gradient(145deg, #f8945a, #f0682a)",
|
||||
color: "#fff", display: "flex", alignItems: "center",
|
||||
justifyContent: "center", fontWeight: 700, fontSize: size * 0.42,
|
||||
}}
|
||||
>
|
||||
{(name === "—" ? "؟" : name).charAt(0)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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) => (
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", fontSize: 13 }}>
|
||||
<span style={{ color: "var(--text-3)" }}>{label}:</span>
|
||||
<span dir="ltr" style={{ color: "var(--text-2)" }}>{value}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="card"
|
||||
style={{ padding: 16, display: "flex", flexDirection: "column", gap: 12, cursor: "pointer" }}
|
||||
onClick={onOpen}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
||||
<span className="mini-btn" style={{ pointerEvents: "none" }}>···</span>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<b style={{ fontSize: 14 }}>{name}</b>
|
||||
{patientAvatar(name)}
|
||||
</div>
|
||||
</div>
|
||||
{infoRow("شماره پرونده", fileNumber(record))}
|
||||
{infoRow("موبایل", phone)}
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", fontSize: 13 }}>
|
||||
<span style={{ color: "var(--text-3)" }}>برچسبها:</span>
|
||||
<span style={{ color: "var(--text-3)" }}>—</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PatientRow({
|
||||
record,
|
||||
onOpen,
|
||||
}: {
|
||||
record: PatientRecord;
|
||||
onOpen: () => void;
|
||||
}) {
|
||||
const name = getPatientName(record);
|
||||
return (
|
||||
<div
|
||||
className="card"
|
||||
style={{
|
||||
padding: 16,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 14,
|
||||
padding: "12px 16px", display: "flex", alignItems: "center", gap: 14,
|
||||
cursor: "pointer",
|
||||
transition: "border-color .15s, box-shadow .15s",
|
||||
}}
|
||||
onClick={onOpen}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
||||
<div
|
||||
className="avatar"
|
||||
style={{
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: "50%",
|
||||
background:
|
||||
"linear-gradient(145deg, #f8945a, #f0682a)",
|
||||
color: "#fff",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontWeight: 700,
|
||||
fontSize: 17,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{(name === "—" ? "؟" : name).charAt(0)}
|
||||
</div>
|
||||
<div style={{ minWidth: 0, flex: 1 }}>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: 15,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: "var(--text-3)",
|
||||
marginTop: 2,
|
||||
}}
|
||||
>
|
||||
{formatDate(record.created_at)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 7,
|
||||
fontSize: 13,
|
||||
color: "var(--text-2)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ display: "flex", alignItems: "center", gap: 7 }}
|
||||
>
|
||||
<PhoneIcon
|
||||
style={{
|
||||
width: 14,
|
||||
color: "var(--text-3)",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
<span dir="ltr">{phone}</span>
|
||||
</div>
|
||||
<div
|
||||
style={{ display: "flex", alignItems: "center", gap: 7 }}
|
||||
>
|
||||
<IdentificationIcon
|
||||
style={{
|
||||
width: 14,
|
||||
color: "var(--text-3)",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
<span dir="ltr">
|
||||
{nationalCode ?? (
|
||||
<span style={{ color: "var(--text-3)" }}>
|
||||
کد ملی ثبت نشده
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{patientAvatar(name, 38)}
|
||||
<b style={{ fontSize: 14, minWidth: 140 }}>{name}</b>
|
||||
<span style={{ fontSize: 13, color: "var(--text-3)" }} dir="ltr">{fileNumber(record)}</span>
|
||||
<span style={{ fontSize: 13, color: "var(--text-2)", flex: 1 }} dir="ltr">{getPatientPhone(record)}</span>
|
||||
<button
|
||||
className="btn primary sm"
|
||||
style={{
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 5,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onOpen();
|
||||
}}
|
||||
onClick={(e) => { e.stopPropagation(); onOpen(); }}
|
||||
>
|
||||
<FolderOpenIcon style={{ width: 14 }} />
|
||||
مشاهده پرونده
|
||||
<FolderOpenIcon style={{ width: 14 }} /> مشاهده
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": 1,
|
||||
"skills": {
|
||||
"caveman": {
|
||||
"source": "juliusbrussee/caveman",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/caveman/SKILL.md",
|
||||
"computedHash": "1902fa0b569912d0c05736d8d98a72097d9b82719aac88c0c1d03bb546f9176d"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user