Files
clinicpro/assets/admin/pages/RecordNumberSettingsPage.tsx
T
hamed 85a27812c7 feat(patient): implement record number pattern management
- Add RecordNumberSettingsController for managing patient record number patterns.
- Create RecordNumberPattern entity to represent the pattern configuration.
- Implement RecordNumberPatternRepository for database interactions.
- Develop RecordNumberGenerator service for generating and validating record numbers.
- Add tests for record number generation, backfilling, and API interactions.
- Ensure proper access control for viewing and updating patterns based on user roles.
2026-08-04 12:30:17 +03:30

165 lines
7.7 KiB
TypeScript

import { useEffect, useState } from 'react';
import SettingsLayout from '../components/layout/SettingsLayout';
import SearchableSelect from '../components/ui/SearchableSelect';
import { useRecordNumberSettings } from '../hooks/useRecordNumberSettings';
import type { RecordNumberResetPolicy } from '../hooks/useRecordNumberSettings';
const RESET_OPTIONS: { value: RecordNumberResetPolicy; label: string }[] = [
{ value: 'none', label: 'هرگز — شمارنده پیوسته جلو می‌رود' },
{ value: 'yearly', label: 'هر سال شمسی از ۱ شروع شود' },
{ value: 'monthly', label: 'هر ماه شمسی از ۱ شروع شود' },
];
const TOKENS: [string, string][] = [
['{SEQ}', 'شمارنده — {SEQ:4} یعنی چهار رقمی با صفرِ ابتدایی'],
['{YY}', 'دو رقم آخر سال شمسی'],
['{YYYY}', 'سال شمسی کامل'],
['{MM}', 'ماه شمسی دو رقمی'],
];
/**
* شمارهٔ پرونده — الگوی شماره‌گذاری خودکار پرونده‌های همین مجموعه.
*
* پیش‌نمایش سمت کلاینت ساخته نمی‌شود: همان `next_preview` سرور نشان داده می‌شود تا
* دو پیاده‌سازیِ رندر (اینجا و در `RecordNumberGenerator`) از هم واگرا نشوند.
*/
export default function RecordNumberSettingsPage() {
const { settings, loading, save } = useRecordNumberSettings();
const [enabled, setEnabled] = useState(false);
const [pattern, setPattern] = useState('');
const [resetPolicy, setResetPolicy] = useState<RecordNumberResetPolicy>('none');
useEffect(() => {
if (!settings) return;
setEnabled(settings.enabled);
setPattern(settings.pattern);
setResetPolicy(settings.reset_policy);
}, [settings]);
const readOnly = !settings?.can_edit;
const dirty = !!settings && (
enabled !== settings.enabled || pattern !== settings.pattern || resetPolicy !== settings.reset_policy
);
return (
<SettingsLayout active="record-number">
<div
style={{ background: 'var(--surface)', minWidth: 0 }}
className="px-4 py-4 md:px-6 md:py-6 rounded-[var(--r-lg)]"
>
<h1 className="section-title" style={{ marginBottom: 6 }}>شماره پرونده</h1>
<p style={{ fontSize: 12.5, color: 'var(--text-3)', marginBottom: 18, lineHeight: 2 }}>
با روشن‌کردن این گزینه، شمارهٔ هر پروندهٔ تازه چه از فرم تشکیل پرونده و چه از
نوبتی که قطعی می‌شود بر اساس همین الگو ساخته می‌شود.
</p>
{loading ? (
<div className="space-y-2">{Array.from({ length: 3 }).map((_, i) => <div key={i} className="h-12 rounded-xl skeleton" />)}</div>
) : !settings ? (
<div className="card" style={{ padding: 24, textAlign: 'center', color: 'var(--text-3)', fontSize: 13.5 }}>
دسترسی به تنظیمات شمارهٔ پرونده ندارید.
</div>
) : (
<>
{readOnly && (
<div style={{
marginBottom: 16, padding: '10px 14px', borderRadius: 'var(--r-sm)',
background: 'var(--warning-bg)', color: 'var(--text-2)', fontSize: 12.5,
}}>
تغییر الگو فقط با حساب صاحب مجموعه ممکن است؛ اینجا فقط مقدار فعلی را می‌بینید.
</div>
)}
<label style={{ display: 'inline-flex', alignItems: 'center', gap: 10, fontSize: 14, cursor: readOnly ? 'not-allowed' : 'pointer' }}>
<span style={{
position: 'relative', width: 42, height: 22, borderRadius: 999, flexShrink: 0,
background: enabled ? 'var(--primary)' : 'var(--border-2)', transition: 'background .2s',
opacity: readOnly ? 0.6 : 1,
}}>
<input
type="checkbox"
role="switch"
aria-label="شماره‌گذاری خودکار پرونده"
checked={enabled}
disabled={readOnly}
onChange={(e) => setEnabled(e.target.checked)}
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', margin: 0, opacity: 0, cursor: 'inherit' }}
/>
<span style={{
position: 'absolute', top: 2, insetInlineStart: enabled ? 22 : 2, width: 18, height: 18,
borderRadius: 999, background: 'var(--surface)', transition: 'inset-inline-start .2s',
boxShadow: '0 1px 2px rgba(0,0,0,.2)',
}} />
</span>
شماره‌گذاری خودکار پرونده
</label>
<div style={{ maxWidth: 420, marginTop: 18 }}>
<label className="field-label" htmlFor="record-number-pattern">الگوی شماره</label>
<div className="field" style={{ marginTop: 6 }}>
<input
id="record-number-pattern"
value={pattern}
dir="ltr"
readOnly={readOnly}
onChange={(e) => setPattern(e.target.value)}
placeholder="MD-{YY}-{SEQ:4}"
/>
</div>
<ul style={{ margin: '10px 0 0', padding: 0, listStyle: 'none', display: 'flex', flexDirection: 'column', gap: 4 }}>
{TOKENS.map(([token, hint]) => (
<li key={token} style={{ fontSize: 12, color: 'var(--text-3)' }}>
<code dir="ltr" style={{ color: 'var(--primary)' }}>{token}</code> {hint}
</li>
))}
</ul>
</div>
<div style={{ maxWidth: 420, marginTop: 18 }}>
<label className="field-label" id="reset-policy-label">ریست شمارنده</label>
<div style={{ marginTop: 6 }}>
<SearchableSelect
options={RESET_OPTIONS}
value={resetPolicy}
onChange={(v) => setResetPolicy((v ?? 'none') as RecordNumberResetPolicy)}
ariaLabelledBy="reset-policy-label"
isDisabled={readOnly}
height={44}
/>
</div>
</div>
<div style={{
marginTop: 18, padding: '12px 14px', borderRadius: 'var(--r-sm)',
background: 'var(--surface-2)', border: '1px solid var(--border)', maxWidth: 420,
}}>
<span style={{ fontSize: 12.5, color: 'var(--text-2)' }}>شمارهٔ بعدی: </span>
<b dir="ltr" style={{ fontSize: 14, color: 'var(--text)' }}>{settings.next_preview}</b>
<div style={{ fontSize: 11.5, color: 'var(--text-3)', marginTop: 6 }}>
{dirty
? 'پیش‌نمایش پس از ذخیره به‌روز می‌شود.'
: `شمارنده تا اینجا ${settings.counter} پرونده را شماره‌گذاری کرده است.`}
</div>
</div>
{!readOnly && (
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 22 }}>
<button
type="button"
className="btn primary"
disabled={!dirty || save.isPending}
onClick={() => save.mutate({ enabled, pattern, reset_policy: resetPolicy })}
>
{save.isPending ? 'در حال ذخیره…' : 'ذخیره تنظیمات'}
</button>
</div>
)}
</>
)}
</div>
</SettingsLayout>
);
}