feat: add national code to patient records and user entity, update related functionality
This commit is contained in:
@@ -2,6 +2,7 @@ import {
|
||||
CheckCircleIcon,
|
||||
ChevronRightIcon,
|
||||
FolderOpenIcon,
|
||||
IdentificationIcon,
|
||||
MagnifyingGlassIcon,
|
||||
PencilIcon,
|
||||
PhoneIcon,
|
||||
@@ -15,7 +16,6 @@ import React, { useCallback, useRef, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import DataTable, { type Column } from "../components/ui/DataTable";
|
||||
import FeatureGate from "../components/ui/FeatureGate";
|
||||
import Modal from "../components/ui/Modal";
|
||||
import PageHeader from "../components/ui/PageHeader";
|
||||
@@ -65,13 +65,15 @@ const EMPTY_RECORDS: PatientRecord[] = [];
|
||||
const EMPTY_SESSIONS: PatientSession[] = [];
|
||||
|
||||
function getPatientName(record?: PatientRecord | null) {
|
||||
const user = record?.user;
|
||||
return user?.fullName || user?.name || record?.user_name || "—";
|
||||
return record?.user_name || "—";
|
||||
}
|
||||
|
||||
function getPatientPhone(record?: PatientRecord | null) {
|
||||
const user = record?.user;
|
||||
return user?.phone || record?.user_mobile || "—";
|
||||
return record?.user_mobile || "—";
|
||||
}
|
||||
|
||||
function getPatientNationalCode(record?: PatientRecord | null) {
|
||||
return record?.user_national_code || null;
|
||||
}
|
||||
|
||||
function calcFinalPrice(
|
||||
@@ -121,7 +123,9 @@ function MyPatientsPageInner() {
|
||||
uuid: string;
|
||||
name: string | null;
|
||||
mobile: string;
|
||||
national_code?: string | null;
|
||||
} | null>(null);
|
||||
const [recordNationalCode, setRecordNationalCode] = useState("");
|
||||
const [searchError, setSearchError] = useState("");
|
||||
const mobileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
@@ -296,72 +300,6 @@ function MyPatientsPageInner() {
|
||||
setPage(1);
|
||||
}, []);
|
||||
|
||||
const recordColumns: Column<PatientRecord>[] = [
|
||||
{
|
||||
key: "user",
|
||||
header: "بیمار",
|
||||
render: (r) => (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||
<div
|
||||
className="avatar sm"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(145deg, oklch(0.62 0.15 256), oklch(0.48 0.16 256))",
|
||||
flexShrink: 0,
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{(r.user?.fullName ?? "?").charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontWeight: 600, fontSize: 14 }}>
|
||||
{r.user?.fullName ?? "—"}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: "var(--text-3)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
marginTop: 2,
|
||||
}}
|
||||
>
|
||||
<PhoneIcon style={{ width: 11 }} />
|
||||
<span dir="ltr">{r.user?.phone ?? "—"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "created_at",
|
||||
header: "تاریخ ثبت",
|
||||
render: (r) => (
|
||||
<span style={{ fontSize: 13, color: "var(--text-3)" }}>
|
||||
{formatDate(r.created_at)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "uuid",
|
||||
header: "",
|
||||
render: (r) => (
|
||||
<button
|
||||
className="btn primary sm"
|
||||
style={{ display: "flex", alignItems: "center", gap: 5 }}
|
||||
onClick={() => {
|
||||
setSelectedRecord(r);
|
||||
setSessionPage(1);
|
||||
}}
|
||||
>
|
||||
<FolderOpenIcon style={{ width: 14 }} />
|
||||
مشاهده پرونده
|
||||
</button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
if (!selectedRecord) {
|
||||
return (
|
||||
<>
|
||||
@@ -408,65 +346,77 @@ function MyPatientsPageInner() {
|
||||
}}
|
||||
value={search}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
placeholder="جستجو بر اساس نام یا تلفن..."
|
||||
placeholder="جستجو بر اساس نام، شماره موبایل یا کد ملی..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
{records.length === 0 && !isLoading ? (
|
||||
{records.length === 0 && !isLoading ? (
|
||||
<div
|
||||
className="card"
|
||||
style={{
|
||||
textAlign: "center",
|
||||
padding: "60px 24px",
|
||||
color: "var(--text-3)",
|
||||
}}
|
||||
>
|
||||
<UsersIcon
|
||||
style={{
|
||||
width: 48,
|
||||
margin: "0 auto 16px",
|
||||
display: "block",
|
||||
opacity: 0.4,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
textAlign: "center",
|
||||
padding: "60px 24px",
|
||||
color: "var(--text-3)",
|
||||
fontWeight: 600,
|
||||
fontSize: 15,
|
||||
marginBottom: 8,
|
||||
color: "var(--text-2)",
|
||||
}}
|
||||
>
|
||||
<UsersIcon
|
||||
style={{
|
||||
width: 48,
|
||||
margin: "0 auto 16px",
|
||||
display: "block",
|
||||
opacity: 0.4,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: 15,
|
||||
marginBottom: 8,
|
||||
color: "var(--text-2)",
|
||||
}}
|
||||
>
|
||||
{search
|
||||
? "بیماری یافت نشد"
|
||||
: "هنوز بیماری ثبت نشده است"}
|
||||
</div>
|
||||
<div style={{ fontSize: 13 }}>
|
||||
{search
|
||||
? "عبارت جستجو را تغییر دهید"
|
||||
: "پس از ثبت نوبت، پرونده بیمار به طور خودکار ایجاد میشود"}
|
||||
</div>
|
||||
{search
|
||||
? "بیماری یافت نشد"
|
||||
: "هنوز بیماری ثبت نشده است"}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<DataTable
|
||||
columns={recordColumns}
|
||||
data={records}
|
||||
loading={isLoading}
|
||||
emptyMessage="بیماری ثبت نشده است"
|
||||
/>
|
||||
<div style={{ padding: "0 16px 12px" }}>
|
||||
<Pagination
|
||||
page={page}
|
||||
total={totalRec}
|
||||
limit={limit}
|
||||
onPageChange={setPage}
|
||||
<div style={{ fontSize: 13 }}>
|
||||
{search
|
||||
? "عبارت جستجو را تغییر دهید"
|
||||
: "پس از ثبت نوبت، پرونده بیمار به طور خودکار ایجاد میشود"}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns:
|
||||
"repeat(auto-fill, minmax(280px, 1fr))",
|
||||
gap: 14,
|
||||
}}
|
||||
>
|
||||
{records.map((r) => (
|
||||
<PatientCard
|
||||
key={r.uuid}
|
||||
record={r}
|
||||
onOpen={() => {
|
||||
setSelectedRecord(r);
|
||||
setSessionPage(1);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Pagination
|
||||
page={page}
|
||||
total={totalRec}
|
||||
limit={limit}
|
||||
onPageChange={setPage}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Modal ایجاد پرونده دستی */}
|
||||
<Modal
|
||||
@@ -1115,6 +1065,136 @@ function MyPatientsPageInner() {
|
||||
);
|
||||
}
|
||||
|
||||
function PatientCard({
|
||||
record,
|
||||
onOpen,
|
||||
}: {
|
||||
record: PatientRecord;
|
||||
onOpen: () => void;
|
||||
}) {
|
||||
const name = getPatientName(record);
|
||||
const phone = getPatientPhone(record);
|
||||
const nationalCode = getPatientNationalCode(record);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="card"
|
||||
style={{
|
||||
padding: 16,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
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, oklch(0.62 0.15 256), oklch(0.48 0.16 256))",
|
||||
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>
|
||||
|
||||
<button
|
||||
className="btn primary sm"
|
||||
style={{
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 5,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onOpen();
|
||||
}}
|
||||
>
|
||||
<FolderOpenIcon style={{ width: 14 }} />
|
||||
مشاهده پرونده
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SessionRow({
|
||||
session,
|
||||
onEdit,
|
||||
|
||||
@@ -426,15 +426,10 @@ export interface PatientRecord {
|
||||
uuid: string;
|
||||
entity_type: string;
|
||||
entity_id: number;
|
||||
user?: {
|
||||
uuid?: string;
|
||||
fullName?: string;
|
||||
name?: string;
|
||||
phone?: string;
|
||||
} | null;
|
||||
user_uuid?: string;
|
||||
user_name?: string | null;
|
||||
user_mobile?: string | null;
|
||||
user_national_code?: string | null;
|
||||
created_at: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20260622160119 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE users ADD national_code VARCHAR(10) DEFAULT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE users DROP national_code');
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
#[ORM\Column(name: 'real_name', type: 'string', length: 100, nullable: true)]
|
||||
private ?string $realName = null;
|
||||
|
||||
#[ORM\Column(name: 'national_code', type: 'string', length: 10, nullable: true)]
|
||||
private ?string $nationalCode = null;
|
||||
|
||||
#[ORM\Column(type: 'json')]
|
||||
private array $roles = ['ROLE_USER'];
|
||||
|
||||
@@ -57,6 +60,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
public function getMobileNumber(): string { return $this->mobileNumber; }
|
||||
public function getEmail(): ?string { return $this->email; }
|
||||
public function getRealName(): ?string { return $this->realName; }
|
||||
public function getNationalCode(): ?string { return $this->nationalCode; }
|
||||
public function getStatus(): int { return $this->status; }
|
||||
public function getCreatedAt(): int { return $this->createdAt; }
|
||||
|
||||
@@ -77,6 +81,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
|
||||
public function setEmail(?string $email): self { $this->email = $email; return $this; }
|
||||
public function setRealName(?string $name): self { $this->realName = $name; $this->updatedAt = time(); return $this; }
|
||||
public function setNationalCode(?string $code): self { $this->nationalCode = $code !== null && $code !== '' ? $code : null; $this->updatedAt = time(); return $this; }
|
||||
public function setPasswordHash(?string $hash): self { $this->passwordHash = $hash; $this->updatedAt = time(); return $this; }
|
||||
public function setRoles(array $roles): self { $this->roles = $roles; $this->updatedAt = time(); return $this; }
|
||||
public function setStatus(int $status): self { $this->status = $status; $this->updatedAt = time(); return $this; }
|
||||
|
||||
@@ -59,6 +59,7 @@ class PatientController extends BaseController
|
||||
'uuid' => $patient->getUuid(),
|
||||
'name' => $patient->getRealName(),
|
||||
'mobile' => $patient->getMobileNumber(),
|
||||
'national_code' => $patient->getNationalCode(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -101,6 +102,15 @@ class PatientController extends BaseController
|
||||
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'کاربر یافت نشد', 404);
|
||||
}
|
||||
|
||||
$nationalCode = trim((string) ($data['national_code'] ?? ''));
|
||||
if ($nationalCode !== '' && $patient->getNationalCode() === null) {
|
||||
if (!preg_match('/^\d{10}$/', $nationalCode)) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'کد ملی باید ۱۰ رقم باشد', 422);
|
||||
}
|
||||
$patient->setNationalCode($nationalCode);
|
||||
$this->userRepo->save($patient);
|
||||
}
|
||||
|
||||
$existing = $this->recordRepo->findByEntityAndUser($entityType, $entityId, $patient);
|
||||
if ($existing !== null) {
|
||||
return $this->success($existing->toArray());
|
||||
|
||||
@@ -72,8 +72,9 @@ class PatientRecord
|
||||
'entity_type' => $this->entityType,
|
||||
'entity_id' => $this->entityId,
|
||||
'user_uuid' => $this->user->getUuid(),
|
||||
'user_name' => $this->user->getRealName(),
|
||||
'user_mobile' => $this->user->getMobileNumber(),
|
||||
'user_name' => $this->user->getRealName(),
|
||||
'user_mobile' => $this->user->getMobileNumber(),
|
||||
'user_national_code' => $this->user->getNationalCode(),
|
||||
'created_by_type' => $this->createdByType,
|
||||
'created_at' => $this->createdAt,
|
||||
];
|
||||
|
||||
@@ -41,7 +41,7 @@ class PatientRecordRepository extends ServiceEntityRepository
|
||||
->setMaxResults($limit);
|
||||
|
||||
if ($search !== null && $search !== '') {
|
||||
$qb->andWhere('u.realName LIKE :search OR u.mobileNumber LIKE :search')
|
||||
$qb->andWhere('u.realName LIKE :search OR u.mobileNumber LIKE :search OR u.nationalCode LIKE :search')
|
||||
->setParameter('search', '%' . $search . '%');
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ class PatientRecordRepository extends ServiceEntityRepository
|
||||
->setParameter('id', $entityId);
|
||||
|
||||
if ($search !== null && $search !== '') {
|
||||
$qb->andWhere('u.realName LIKE :search OR u.mobileNumber LIKE :search')
|
||||
$qb->andWhere('u.realName LIKE :search OR u.mobileNumber LIKE :search OR u.nationalCode LIKE :search')
|
||||
->setParameter('search', '%' . $search . '%');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user