feat: redesign StaffPage with KPI bar, improved table layout, and enhanced form fields
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user