feat: redesign StaffPage with KPI bar, improved table layout, and enhanced form fields

This commit is contained in:
hamed
2026-06-14 23:36:37 +03:30
parent 5be63a23b6
commit 55f646e2d4
6 changed files with 1118 additions and 258 deletions
+86 -47
View File
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { PlusIcon, PencilIcon, TrashIcon } from '@heroicons/react/24/outline';
import { PlusIcon, PencilIcon, TrashIcon, IdentificationIcon, PhoneIcon } from '@heroicons/react/24/outline';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
@@ -10,6 +10,7 @@ import type { ApiResponse } from '../lib/api';
import type { Secretary, SecretaryPermissions } from '../types';
import { formatDate, maskMobile } from '../lib/utils';
import { ActiveBadge } from '../components/ui/StatusBadge';
import DataTable, { type Column } from '../components/ui/DataTable';
import Modal from '../components/ui/Modal';
import ConfirmDialog from '../components/ui/ConfirmDialog';
import PageHeader from '../components/ui/PageHeader';
@@ -83,27 +84,31 @@ function PermissionsMatrix({
return (
<div style={{ overflowX: 'auto' }}>
<table className="t">
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
<thead>
<tr>
<th>بخش</th>
<tr style={{ background: 'oklch(0.97 0.01 256)' }}>
<th style={{ textAlign: 'right', padding: '10px 12px', fontWeight: 600, color: 'var(--text-2)', borderBottom: '2px solid var(--border)' }}>بخش</th>
{ACTION_HEADERS.map((h) => (
<th key={h} style={{ textAlign: 'center', fontSize: 12 }}>{h}</th>
<th key={h} style={{ textAlign: 'center', padding: '10px 8px', fontSize: 12, fontWeight: 600, color: 'var(--text-2)', borderBottom: '2px solid var(--border)' }}>{h}</th>
))}
</tr>
</thead>
<tbody>
{(Object.keys(PERMISSION_LABELS) as PermSection[]).map((section) => {
{(Object.keys(PERMISSION_LABELS) as PermSection[]).map((section, idx) => {
const config = PERMISSION_LABELS[section];
const sectionPrm = permissions[section] as Record<string, boolean>;
return (
<tr key={section}>
<td><b>{config.label}</b></td>
<tr key={section} style={{ background: idx % 2 === 0 ? 'transparent' : 'oklch(0.985 0.005 256)' }}>
<td style={{ padding: '10px 12px', borderBottom: '1px solid var(--border)', fontWeight: 600 }}>
{config.label}
</td>
{ALL_ACTIONS.map((action) => {
const ac = config.actions.find((a) => a.key === action);
if (!ac) return <td key={action} style={{ textAlign: 'center', color: 'var(--border)' }}></td>;
if (!ac) return (
<td key={action} style={{ textAlign: 'center', borderBottom: '1px solid var(--border)', color: 'var(--border)' }}></td>
);
return (
<td key={action} style={{ textAlign: 'center' }}>
<td key={action} style={{ textAlign: 'center', borderBottom: '1px solid var(--border)' }}>
<input
type="checkbox"
checked={sectionPrm[action] ?? false}
@@ -188,6 +193,63 @@ export default function MySecretariesPage() {
setEditPerms(s.permissions ?? DEFAULT_PERMISSIONS);
};
const columns: Column<Secretary>[] = [
{
key: 'user_name',
header: 'منشی',
render: (s) => (
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div
className="avatar sm"
style={{
background: 'linear-gradient(145deg, oklch(0.62 0.15 295), oklch(0.48 0.16 295))',
flexShrink: 0, fontWeight: 700,
}}
>
{(s.user_name ?? '?').charAt(0)}
</div>
<div style={{ fontWeight: 600, fontSize: 14 }}>{s.user_name}</div>
</div>
),
},
{
key: 'mobile_number',
header: 'موبایل',
render: (s) => (
<div style={{ display: 'flex', alignItems: 'center', gap: 5, fontSize: 13, color: 'var(--text-2)' }}>
<PhoneIcon style={{ width: 13 }} />
<span dir="ltr">{maskMobile(s.mobile_number)}</span>
</div>
),
},
{
key: 'is_active',
header: 'وضعیت',
render: (s) => <ActiveBadge active={s.is_active} />,
},
{
key: 'created_at',
header: 'تاریخ ثبت',
render: (s) => (
<span style={{ fontSize: 13, color: 'var(--text-3)' }}>{formatDate(Number(s.created_at))}</span>
),
},
{
key: 'uuid',
header: 'عملیات',
render: (s) => (
<div style={{ display: 'flex', gap: 6 }}>
<button className="btn sm" onClick={() => openEdit(s)} title="ویرایش دسترسی‌ها">
<PencilIcon style={{ width: 14 }} />
</button>
<button className="btn sm" onClick={() => setDeleteTarget(s)} title="غیرفعال‌سازی">
<TrashIcon style={{ width: 14 }} />
</button>
</div>
),
},
];
return (
<>
<PageHeader
@@ -205,45 +267,22 @@ export default function MySecretariesPage() {
<div className="card card-pad" style={{ textAlign: 'center', color: 'var(--text-3)' }}>
پروفایل پزشک یافت نشد
</div>
) : isLoading ? (
<div className="card card-pad" style={{ color: 'var(--text-3)' }}>در حال بارگذاری...</div>
) : secretaries.length === 0 ? (
<div className="card card-pad" style={{ textAlign: 'center', padding: '48px 0', color: 'var(--text-3)' }}>
هنوز منشیای ثبت نشده است
</div>
) : (
<div className="card">
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13.5 }}>
<thead>
<tr style={{ borderBottom: '1px solid var(--border)' }}>
<th style={{ textAlign: 'right', padding: '10px 16px', color: 'var(--text-3)', fontWeight: 500 }}>نام</th>
<th style={{ textAlign: 'right', padding: '10px 16px', color: 'var(--text-3)', fontWeight: 500 }}>موبایل</th>
<th style={{ textAlign: 'right', padding: '10px 16px', color: 'var(--text-3)', fontWeight: 500 }}>وضعیت</th>
<th style={{ textAlign: 'right', padding: '10px 16px', color: 'var(--text-3)', fontWeight: 500 }}>تاریخ ثبت</th>
<th style={{ textAlign: 'right', padding: '10px 16px', color: 'var(--text-3)', fontWeight: 500 }}>عملیات</th>
</tr>
</thead>
<tbody>
{secretaries.map((s, i) => (
<tr key={s.uuid} style={{ borderBottom: i < secretaries.length - 1 ? '1px solid var(--border)' : 'none' }}>
<td style={{ padding: '10px 16px' }}><b>{s.user_name}</b></td>
<td style={{ padding: '10px 16px', direction: 'ltr', textAlign: 'left' }}>{maskMobile(s.mobile_number)}</td>
<td style={{ padding: '10px 16px' }}><ActiveBadge active={s.is_active} /></td>
<td style={{ padding: '10px 16px', color: 'var(--text-3)' }}>{formatDate(Number(s.created_at))}</td>
<td style={{ padding: '10px 16px' }}>
<div style={{ display: 'flex', gap: 6 }}>
<button className="btn sm" onClick={() => openEdit(s)} title="ویرایش دسترسی‌ها">
<PencilIcon style={{ width: 14 }} />
</button>
<button className="btn sm" onClick={() => setDeleteTarget(s)} title="غیرفعال‌سازی">
<TrashIcon style={{ width: 14 }} />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
{secretaries.length === 0 && !isLoading ? (
<div style={{ textAlign: 'center', padding: '60px 24px' }}>
<IdentificationIcon style={{ width: 48, color: 'var(--text-3)', margin: '0 auto 16px', display: 'block', opacity: 0.4 }} />
<div style={{ fontWeight: 600, fontSize: 15, marginBottom: 8, color: 'var(--text-2)' }}>هنوز منشیای اضافه نشده</div>
<div style={{ color: 'var(--text-3)', fontSize: 13.5, marginBottom: 20 }}>
منشی میتواند نوبتها و اطلاعات کلینیک را مدیریت کند
</div>
<button className="btn primary sm" onClick={() => setCreateOpen(true)}>
<PlusIcon style={{ width: 16 }} /> افزودن اولین منشی
</button>
</div>
) : (
<DataTable columns={columns} data={secretaries} loading={isLoading} emptyMessage="منشی‌ای ثبت نشده است" />
)}
</div>
)}