style(patients): match tauri /files toolbar pixel-for-pixel
Rebuild the پروندهها header to mirror tauri files/head + files/inputs: - title on its own row (font-bold, #525252, 16/18/20px). - search field: 48px tall, #FAFAFA bg, #EFEFEF border, rounded-[6px], with a #5559CE 40px search-icon box (SearchHeaderP). - view toggle with the exact tauri SVGs (PatientsGridView table / Patients CategoryView card), active bg #f4f5fd, #E0E0E0 border. - filter button as a 62x48 #5559ce-bordered box (TurnsFilter icon). - تشکیل پرونده as a 48h/137w #5559ce button (AddTurn icon + label). Ported icons verbatim into components/icons/FilesToolbarIcons.tsx. Frontend only. Tests + tsc + build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Toolbar icons ported verbatim from clinic-pro-tauri `src/assets/icon` so the
|
||||
* پروندهها header matches the tauri /files page pixel-for-pixel.
|
||||
*/
|
||||
|
||||
export function SearchHeaderP({ color = '#7E7E7E' }: { color?: string }) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M11.5 21C16.7467 21 21 16.7467 21 11.5C21 6.25329 16.7467 2 11.5 2C6.25329 2 2 6.25329 2 11.5C2 16.7467 6.25329 21 11.5 21Z" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M22 22L20 20" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function TurnsFilter({ color = '#616161' }: { color?: string }) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 26 26" fill="none">
|
||||
<path d="M11.191 17.9756H4.36523" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M14.2363 7.47539H21.0621" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M9.45413 7.41677C9.45413 6.01315 8.30779 4.875 6.89406 4.875C5.48033 4.875 4.33398 6.01315 4.33398 7.41677C4.33398 8.8204 5.48033 9.95855 6.89406 9.95855C8.30779 9.95855 9.45413 8.8204 9.45413 7.41677Z" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M21.667 17.9331C21.667 16.5295 20.5216 15.3914 19.1078 15.3914C17.6932 15.3914 16.5469 16.5295 16.5469 17.9331C16.5469 19.3368 17.6932 20.4749 19.1078 20.4749C20.5216 20.4749 21.667 19.3368 21.667 17.9331Z" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** Table-view toggle icon (tauri `PatientsGridView`). */
|
||||
export function PatientsGridView({ width = 24, height = 24, color = '#5559CE' }: { width?: number; height?: number; color?: string }) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={width} height={height} viewBox="0 0 24 24" fill="none">
|
||||
<path d="M9 22H15C20 22 22 20 22 15V9C22 4 20 2 15 2H9C4 2 2 4 2 9V15C2 20 4 22 9 22Z" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M2.0293 8.5H21.9993" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M2.0293 15.5H21.9993" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M8.50977 21.9898V2.00977" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M15.5098 21.9898V2.00977" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** Card-view toggle icon (tauri `PatientsCategoryView`). */
|
||||
export function PatientsCategoryView({ width = 24, height = 24, color = '#616161' }: { width?: number; height?: number; color?: string }) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={width} height={height} viewBox="0 0 24 24" fill="none">
|
||||
<path d="M5 10H7C9 10 10 9 10 7V5C10 3 9 2 7 2H5C3 2 2 3 2 5V7C2 9 3 10 5 10Z" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M17 10H19C21 10 22 9 22 7V5C22 3 21 2 19 2H17C15 2 14 3 14 5V7C14 9 15 10 17 10Z" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M17 22H19C21 22 22 21 22 19V17C22 15 21 14 19 14H17C15 14 14 15 14 17V19C14 21 15 22 17 22Z" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M5 22H7C9 22 10 21 10 19V17C10 15 9 14 7 14H5C3 14 2 15 2 17V19C2 21 3 22 5 22Z" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function AddTurn({ color = '#E1E1E1' }: { color?: string }) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M5 10H15" stroke={color} strokeWidth="1.5" strokeLinecap="round" />
|
||||
<path d="M10 15V5" stroke={color} strokeWidth="1.5" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -2,8 +2,7 @@ import { useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
PlusIcon, PencilIcon, EyeIcon, MagnifyingGlassIcon, AdjustmentsHorizontalIcon,
|
||||
Squares2X2Icon, TableCellsIcon, ExclamationCircleIcon, IdentificationIcon,
|
||||
PencilIcon, EyeIcon, ExclamationCircleIcon, IdentificationIcon,
|
||||
UserIcon, EllipsisHorizontalIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { api } from '../lib/api';
|
||||
@@ -13,6 +12,9 @@ import { formatNumber, toDate } from '../lib/utils';
|
||||
import Pagination from '../components/ui/Pagination';
|
||||
import PatientTagsCell from '../components/PatientTagsCell';
|
||||
import PatientsFilterModal, { type PatientFilters } from '../components/PatientsFilterModal';
|
||||
import {
|
||||
SearchHeaderP, TurnsFilter, PatientsGridView, PatientsCategoryView, AddTurn,
|
||||
} from '../components/icons/FilesToolbarIcons';
|
||||
|
||||
// tauri /files paginates card view by 16 and table view by 12.
|
||||
const CARD_PAGE_SIZE = 16;
|
||||
@@ -37,7 +39,6 @@ function countFilters(f: PatientFilters): number {
|
||||
/**
|
||||
* A single patient card — mirrors tauri `files/list/CardView` pixel-for-pixel
|
||||
* (avatar + name + ⋮ menu header, file-number/mobile rows, tags footer).
|
||||
* The whole card navigates to the detail; the ⋮ menu offers view/edit.
|
||||
*/
|
||||
function PatientCard({ r, onView, onEdit }: { r: PatientRecord; onView: () => void; onEdit: () => void }) {
|
||||
const [menu, setMenu] = useState(false);
|
||||
@@ -46,7 +47,6 @@ function PatientCard({ r, onView, onEdit }: { r: PatientRecord; onView: () => vo
|
||||
className="bg-white dark:bg-[#222433] rounded-[6px] p-[14px] border border-[#EDEDED] dark:border-[#35343D] shadow-sm transition-all duration-300 hover:shadow-md hover:-translate-y-1 cursor-pointer"
|
||||
onClick={(e) => { if ((e.target as HTMLElement).closest('.more-square-icon')) return; onView(); }}
|
||||
>
|
||||
{/* نام بیمار */}
|
||||
<div className="flex items-center justify-between gap-[6px] mb-[12px] pb-[8px] border-b border-[#E0E0E0] dark:border-[#404040]">
|
||||
<div className="flex items-center gap-[6px] flex-1 min-w-0">
|
||||
<span style={{ width: 28, height: 28, borderRadius: '50%', background: 'var(--primary-soft)', display: 'grid', placeItems: 'center', flexShrink: 0 }}>
|
||||
@@ -74,19 +74,16 @@ function PatientCard({ r, onView, onEdit }: { r: PatientRecord; onView: () => vo
|
||||
</div>
|
||||
|
||||
<div className="mb-[12px] my-4" style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{/* شماره پرونده */}
|
||||
<div className="flex items-start justify-between text-[15px]">
|
||||
<span className="text-[#616161] dark:text-[#A1A1A1] font-medium min-w-[65px]">شماره پرونده:</span>
|
||||
<span className="text-[#525252] dark:text-[#D7D8ED] truncate">{r.record_number || '—'}</span>
|
||||
</div>
|
||||
{/* شماره موبایل */}
|
||||
<div className="flex items-start justify-between text-[15px]">
|
||||
<span className="text-[#616161] dark:text-[#A1A1A1] font-medium min-w-[65px]">موبایل:</span>
|
||||
<span className="text-[#525252] dark:text-[#D7D8ED] truncate" dir="ltr">{r.user_mobile || '—'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* فوتر: برچسبها */}
|
||||
<div
|
||||
className="flex items-center pt-[8px] justify-end gap-[8px] border-t border-[#E0E0E0] dark:border-[#404040]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
@@ -134,38 +131,67 @@ export default function PatientsListPage() {
|
||||
const editHref = (r: PatientRecord) => `/admin/patients/${r.uuid}/edit`;
|
||||
const viewHref = (r: PatientRecord) => `/admin/patients/${r.uuid}`;
|
||||
|
||||
// نمایش جدولی/کارتی — دو دکمه با آیکون SVG سفارشی (عین tauri ViewModeToggle).
|
||||
const viewToggle = (
|
||||
<div className="flex border border-[#E0E0E0] dark:border-[#404040] rounded-[4px] overflow-hidden shrink-0">
|
||||
<button type="button" aria-label="نمایش جدولی" onClick={() => switchView('table')} style={{ padding: 8, border: 'none', cursor: 'pointer', display: 'grid', placeItems: 'center', background: view === 'table' ? '#f4f5fd' : 'transparent' }}>
|
||||
<PatientsGridView color={view === 'table' ? '#5559ce' : '#616161'} />
|
||||
</button>
|
||||
<button type="button" aria-label="نمایش کارتی" onClick={() => switchView('card')} style={{ padding: 8, border: 'none', cursor: 'pointer', display: 'grid', placeItems: 'center', background: view === 'card' ? '#f4f5fd' : 'transparent' }}>
|
||||
<PatientsCategoryView color={view === 'card' ? '#5559ce' : '#616161'} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="fade-in" style={{ maxWidth: 1160, margin: '0 auto' }}>
|
||||
{/* Header */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap', marginBottom: 20 }}>
|
||||
<h1 className="section-title">پروندهها</h1>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<MagnifyingGlassIcon style={{ width: 16, position: 'absolute', insetInlineStart: 10, top: '50%', transform: 'translateY(-50%)', color: 'var(--text-3)', pointerEvents: 'none' }} />
|
||||
<input
|
||||
className="cp-input" placeholder="کد ملی، شماره پرونده یا نام مراجعه کننده را وارد کنید..."
|
||||
value={search} onChange={(e) => { setSearch(e.target.value); setPage(1); }}
|
||||
style={{ width: 320, maxWidth: '60vw', height: 40, paddingInlineStart: 34 }}
|
||||
/>
|
||||
<div className="fade-in">
|
||||
{/* Head — تیتر تنها (tauri files/head) */}
|
||||
<p className="text-[#525252] dark:text-[#D7D8ED] text-[16px] md:text-[18px] lg:text-[20px] font-bold">پروندهها</p>
|
||||
|
||||
{/* Inputs — ردیف کنترلها (tauri files/inputs) */}
|
||||
<div className="my-[16px] md:my-[20px] lg:my-[24px]">
|
||||
<div className="flex flex-col lg:flex-row items-stretch lg:items-center gap-[16px] lg:gap-[20px]">
|
||||
{/* جستجو + سوییچ نما */}
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 flex-1 w-full">
|
||||
<div className="w-full md:w-[442px]">
|
||||
<div className="flex items-center rounded-[6px] border border-[#EFEFEF] dark:border-[#35343D] bg-[#FAFAFA] dark:bg-[#222433]" style={{ height: 48, paddingInline: 4 }}>
|
||||
<input
|
||||
dir="rtl"
|
||||
value={search}
|
||||
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
|
||||
placeholder="کد ملی، شماره پرونده یا نام مراجعه کننده را وارد کنید..."
|
||||
className="text-[#525252] dark:text-[#D7D8ED]"
|
||||
style={{ flex: 1, minWidth: 0, background: 'transparent', border: 'none', outline: 'none', fontSize: 13, textAlign: 'left' }}
|
||||
/>
|
||||
<span style={{ width: 40, height: 40, marginInlineEnd: -4, flexShrink: 0, borderRadius: 6, background: '#5559CE', display: 'grid', placeItems: 'center' }}>
|
||||
<SearchHeaderP color="white" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{viewToggle}
|
||||
</div>
|
||||
{/* view toggle */}
|
||||
<div style={{ display: 'flex', border: '1px solid var(--border)', borderRadius: 'var(--r-sm)', overflow: 'hidden' }}>
|
||||
<button aria-label="نمایش کارتی" onClick={() => switchView('card')} style={{ padding: '8px 10px', border: 'none', cursor: 'pointer', background: view === 'card' ? 'var(--primary-soft)' : 'var(--surface)', color: view === 'card' ? 'var(--primary)' : 'var(--text-3)' }}>
|
||||
<Squares2X2Icon style={{ width: 18 }} />
|
||||
|
||||
{/* فیلتر + تشکیل پرونده */}
|
||||
<div className="flex items-center w-full lg:w-auto justify-between lg:justify-end" style={{ gap: 12 }}>
|
||||
<button
|
||||
type="button" aria-label="فیلترها" onClick={() => setFilterOpen(true)}
|
||||
className="flex items-center justify-center rounded-[4px] cursor-pointer"
|
||||
style={{ width: 62, height: 48, border: '1px solid #5559ce', background: 'transparent', position: 'relative' }}
|
||||
>
|
||||
<TurnsFilter color="#5559ce" />
|
||||
{activeFilters > 0 && (
|
||||
<span style={{ position: 'absolute', top: -6, insetInlineEnd: -6, minWidth: 16, height: 16, padding: '0 4px', borderRadius: 999, background: '#5559ce', color: '#fff', fontSize: 10, display: 'grid', placeItems: 'center' }}>{formatNumber(activeFilters)}</span>
|
||||
)}
|
||||
</button>
|
||||
<button aria-label="نمایش جدولی" onClick={() => switchView('table')} style={{ padding: '8px 10px', border: 'none', cursor: 'pointer', background: view === 'table' ? 'var(--primary-soft)' : 'var(--surface)', color: view === 'table' ? 'var(--primary)' : 'var(--text-3)' }}>
|
||||
<TableCellsIcon style={{ width: 18 }} />
|
||||
<button
|
||||
type="button" onClick={() => navigate('/admin/patients/new')}
|
||||
className="flex items-center justify-center gap-2 rounded-[4px] cursor-pointer"
|
||||
style={{ height: 48, minWidth: 137, background: '#5559ce', border: 'none', padding: '0 16px' }}
|
||||
>
|
||||
<AddTurn color="#fff" />
|
||||
<span style={{ color: '#fff', fontSize: 14 }}>تشکیل پرونده</span>
|
||||
</button>
|
||||
</div>
|
||||
<button className="btn" aria-label="فیلترها" onClick={() => setFilterOpen(true)} style={{ height: 40, position: 'relative', width: 44, padding: 0, display: 'grid', placeItems: 'center', color: activeFilters ? 'var(--primary)' : undefined }}>
|
||||
<AdjustmentsHorizontalIcon style={{ width: 18 }} />
|
||||
{activeFilters > 0 && (
|
||||
<span style={{ position: 'absolute', top: -6, insetInlineEnd: -6, minWidth: 16, height: 16, padding: '0 4px', borderRadius: 999, background: 'var(--primary)', color: '#fff', fontSize: 10, display: 'grid', placeItems: 'center' }}>{formatNumber(activeFilters)}</span>
|
||||
)}
|
||||
</button>
|
||||
<button className="btn primary" style={{ height: 40 }} onClick={() => navigate('/admin/patients/new')}>
|
||||
<PlusIcon style={{ width: 16 }} /> تشکیل پرونده
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user