feat:update web

This commit is contained in:
TuanVT
2026-05-24 23:33:43 +07:00
parent b6ccde2c74
commit b0baa509d9
58 changed files with 4920 additions and 292 deletions
@@ -0,0 +1,189 @@
"use client";
import React from "react";
import { useParams, useRouter } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import patientServices, { PatientDetail } from "@/services/patientServices";
import { httpRequest } from "@/services";
import { QUERY_KEY } from "@/constant/config/enum";
import { ArrowLeft } from "lucide-react";
const DetailPatient = () => {
const params = useParams();
const router = useRouter();
const patientId = params?.id as string;
const patientDetailQuery = useQuery<PatientDetail>({
queryKey: [QUERY_KEY.chi_tiet_benh_nhan, patientId],
enabled: !!patientId,
queryFn: async () => {
const res = await httpRequest<PatientDetail>({
showMessageFailed: true,
http: patientServices.detailPatient(patientId),
});
return (
res || {
id: "",
patientCode: "",
identificationNumber: "",
fullName: "",
dateOfBirth: "",
gender: "",
phone: "",
email: "",
address: "",
emergencyContactName: null,
emergencyContactPhone: null,
allergyNotes: null,
chronicDiseaseNotes: null,
insuranceNumber: null,
notes: null,
isActive: false,
}
);
},
});
const patient = patientDetailQuery.data;
if (patientDetailQuery.isLoading) {
return <div>Đang tải dữ liệu...</div>;
}
return (
<div className="flex flex-col gap-4">
<div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
<div className="flex flex-col gap-5">
<h2 className="text-[28px] flex items-center gap-2 font-semibold text-[#111827]">
<ArrowLeft
onClick={() => router.back()}
className="cursor-pointer"
/>{" "}
{patient?.fullName}
</h2>
<div className="grid grid-cols-2 gap-5">
<div className="space-y-1">
<p className="text-[14px] font-medium text-[#697586]">
bệnh nhân
</p>
<p className="text-[15px] font-medium text-[#202939]">
{patient?.patientCode}
</p>
</div>
<div className="space-y-1">
<p className="text-[14px] font-medium text-[#697586]">Họ tên</p>
<p className="text-[15px] font-medium text-[#202939]">
{patient?.fullName}
</p>
</div>
<div className="space-y-1">
<p className="text-[14px] font-medium text-[#697586]">
đnh danh
</p>
<p className="text-[15px] font-medium text-[#202939]">
{patient?.identificationNumber}
</p>
</div>
<div className="space-y-1">
<p className="text-[14px] font-medium text-[#697586]">
Ngày sinh
</p>
<p className="text-[15px] font-medium text-[#202939]">
{patient?.dateOfBirth}
</p>
</div>
<div className="space-y-1">
<p className="text-[14px] font-medium text-[#697586]">
Giới tính
</p>
<p className="text-[15px] font-medium text-[#202939]">
{patient?.gender}
</p>
</div>
<div className="space-y-1">
<p className="text-[14px] font-medium text-[#697586]">
Số điện thoại
</p>
<p className="text-[15px] font-medium text-[#202939]">
{patient?.phone}
</p>
</div>
<div className="space-y-1">
<p className="text-[14px] font-medium text-[#697586]">Email</p>
<p className="text-[15px] font-medium text-[#202939]">
{patient?.email}
</p>
</div>
<div className="space-y-1">
<p className="text-[14px] font-medium text-[#697586]">Đa chỉ</p>
<p className="text-[15px] font-medium text-[#202939]">
{patient?.address || "-"}
</p>
</div>
<div className="space-y-1">
<p className="text-[14px] font-medium text-[#697586]">
Người liên hệ khẩn cấp
</p>
<p className="text-[15px] font-medium text-[#202939]">
{patient?.emergencyContactName || "-"}
</p>
</div>
<div className="space-y-1">
<p className="text-[14px] font-medium text-[#697586]">
SĐT khẩn cấp
</p>
<p className="text-[15px] font-medium text-[#202939]">
{patient?.emergencyContactPhone || "-"}
</p>
</div>
<div className="space-y-1">
<p className="text-[14px] font-medium text-[#697586]">Dị ng</p>
<p className="text-[15px] font-medium text-[#202939]">
{patient?.allergyNotes || "-"}
</p>
</div>
<div className="space-y-1">
<p className="text-[14px] font-medium text-[#697586]">Bệnh nền</p>
<p className="text-[15px] font-medium text-[#202939]">
{patient?.chronicDiseaseNotes || "-"}
</p>
</div>
</div>
</div>
</div>
</div>
);
};
export default DetailPatient;
@@ -0,0 +1 @@
export { default } from "./DetailPatient";
@@ -0,0 +1,279 @@
"use client";
import CustomButton from "@/components/customs/custom-button";
import CustomDialog from "@/components/customs/custom-dialog";
import CustomPopup from "@/components/customs/custom-popup";
import DataWrapper from "@/components/customs/DataWrapper";
import Search from "@/components/customs/custom-search";
import Table from "@/components/customs/custom-table";
import { QUERY_KEY } from "@/constant/config/enum";
import { httpRequest } from "@/services";
import patientServices, {
PatientItem,
PatientResponse,
} from "@/services/patientServices";
import { useQuery } from "@tanstack/react-query";
import { Pencil, Trash2, Users } from "lucide-react";
import Link from "next/link";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import React, { useMemo, useState } from "react";
import PopupCreatePatient from "../PopupCreatePatient";
import PopupUpdatePatient from "../PopupUpdatePatient";
const MainPagePatient = () => {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
const [deleteItemId, setDeleteItemId] = useState<string>("");
const _page = searchParams.get("_page");
const _pageSize = searchParams.get("_pageSize");
const _keyword = searchParams.get("_keyword");
/**
* PARAMS
*/
const createParam = searchParams.get("_create");
const updateParam = searchParams.get("_update");
/**
* OPEN POPUP
*/
const openCreatePopup = createParam !== null;
const openUpdatePopup = !!updateParam;
/**
* QUERY LIST PATIENT
*/
const patientQuery = useQuery<PatientResponse>({
queryKey: [QUERY_KEY.table_list_patient, _page, _pageSize, _keyword],
queryFn: async () => {
const res = await httpRequest<PatientResponse>({
showMessageFailed: true,
http: patientServices.getPatient({
Page: Number(_page || 1),
PageSize: Number(_pageSize || 10),
Search: _keyword || "",
SortBy: "id",
Desc: true,
}),
});
return (
res || {
items: [],
page: 1,
pageSize: 10,
total: 0,
totalPages: 0,
}
);
},
});
/**
* UPDATE QUERY PARAMS
*/
const updateQuery = (key: string, value?: string | number) => {
const params = new URLSearchParams(searchParams.toString());
if (!value || value === "") {
params.delete(key);
} else {
params.set(key, String(value));
}
const queryString = params.toString();
router.push(queryString ? `${pathname}?${queryString}` : pathname);
};
/**
* OPEN CREATE
*/
const handleOpenCreate = () => {
const params = new URLSearchParams(searchParams.toString());
params.set("_create", "");
router.push(`${pathname}?${params.toString()}`);
};
/**
* OPEN UPDATE
*/
const handleOpenUpdate = (id: string) => {
const params = new URLSearchParams(searchParams.toString());
params.delete("_create");
params.set("_update", id);
router.push(`${pathname}?${params.toString()}`);
};
/**
* CLOSE ALL POPUP
*/
const handleClosePopup = () => {
const params = new URLSearchParams(searchParams.toString());
params.delete("_create");
params.delete("_update");
const queryString = params.toString();
router.push(queryString ? `${pathname}?${queryString}` : pathname);
setOpenDeleteDialog(false);
setDeleteItemId("");
};
/**
* DELETE
*/
const handleOpenDelete = (id: string) => {
setDeleteItemId(id);
setOpenDeleteDialog(true);
};
/**
* DATA
*/
const patientData = useMemo(() => {
return patientQuery.data?.items || [];
}, [patientQuery.data]);
return (
<div className="flex flex-col gap-4 rounded-lg bg-white p-6 shadow">
{/* HEADER */}
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-black">Bệnh nhân</h2>
<CustomButton
variant="midnightBlue"
fullWidth={false}
icon={<Users />}
onClick={handleOpenCreate}
>
Tạo mới
</CustomButton>
</div>
{/* SEARCH */}
<div className="flex items-center justify-between gap-4">
<div className="w-full md:min-w-[400px]">
<Search
keyword={_keyword ?? ""}
setKeyword={(value) => updateQuery("_keyword", value)}
placeholder="Tìm kiếm theo tên người dùng, ID"
/>
</div>
</div>
{/* TABLE */}
<DataWrapper
data={patientData}
loading={patientQuery.isLoading}
title="No New Requests"
note="Please check back later or view your schedule"
>
<Table
rowKey={(item) => item.id}
data={patientData}
column={[
{
fixedLeft: true,
title: "TÊN BỆNH NHÂN",
render: (item: PatientItem) => (
<Link
href={`/patient/${item.id}`}
className="text-blue-500 hover:underline"
>
{item.fullName}
</Link>
),
},
{
title: "MÃ ĐỊNH DANH",
render: (item: PatientItem) => (
<span>{item.identificationNumber}</span>
),
},
{
title: "SỐ ĐIỆN THOẠI",
render: (item: PatientItem) => <span>{item.phone}</span>,
},
{
title: "EMAIL",
render: (item: PatientItem) => <span>{item.email}</span>,
},
{
title: "GIỚI TÍNH",
render: (item: PatientItem) => <span>{item.gender}</span>,
},
{
fixedRight: true,
title: "ACTION",
render: (item: PatientItem) => (
<div className="flex items-center gap-2">
{/* UPDATE */}
<CustomButton
size="sm"
icon={<Pencil className="text-blue-400" />}
onClick={() => handleOpenUpdate(item.id)}
/>
{/* DELETE */}
<CustomButton
size="sm"
icon={<Trash2 className="text-red-400" />}
onClick={() => handleOpenDelete(item.id)}
/>
</div>
),
},
]}
/>
</DataWrapper>
{/* DELETE DIALOG */}
<CustomDialog
open={openDeleteDialog}
title="Xóa bệnh nhân"
note="Bạn có chắc chắn muốn xóa bệnh nhân này?"
type="error"
onClose={handleClosePopup}
onSubmit={() => {}}
titleCancel="Hủy"
titleSubmit="Xác nhận"
/>
{/* CREATE POPUP */}
<CustomPopup open={openCreatePopup} onClose={handleClosePopup}>
<PopupCreatePatient onClose={handleClosePopup} />
</CustomPopup>
{/* UPDATE POPUP */}
<CustomPopup open={openUpdatePopup} onClose={handleClosePopup}>
<PopupUpdatePatient onClose={handleClosePopup} id={updateParam || ""} />
</CustomPopup>
</div>
);
};
export default MainPagePatient;
@@ -0,0 +1 @@
export { default } from "./MainPagePatient";
@@ -0,0 +1,259 @@
"use client";
import CustomButton from "@/components/customs/custom-button";
import FormCustom from "@/components/utils/FormCustom";
import InputForm from "@/components/utils/FormCustom/components/InputForm";
import TextArea from "@/components/utils/FormCustom/components/TextArea";
import { ContextFormCustom } from "@/components/utils/FormCustom/contexts";
import { QUERY_KEY, TYPE_GENDER } from "@/constant/config/enum";
import { X } from "lucide-react";
import React, { useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { httpRequest } from "@/services";
import patientServices from "@/services/patientServices";
import CustomLoading from "@/components/customs/custom-loading";
export interface ICreatePatient {
identificationNumber: string;
fullName: string;
dateOfBirth: string;
gender: string;
phone: string;
email: string;
address: string;
}
export interface PropsPopupCreatePatient {
onClose: () => void;
}
/* QUERY KEY */
// export const QUERY_KEY = {
// table_list_patient: "table_list_patient",
// };
const PopupCreatePatient = ({ onClose }: PropsPopupCreatePatient) => {
const queryClient = useQueryClient();
const [form, setForm] = useState<ICreatePatient>({
identificationNumber: "",
fullName: "",
dateOfBirth: "",
gender: "",
phone: "",
email: "",
address: "",
});
const createPatientMutation = useMutation({
mutationFn: async () => {
return await httpRequest({
showMessageFailed: true,
showMessageSuccess: true,
msgSuccess: "Tạo bệnh nhân thành công!",
http: patientServices.createPatient({
identificationNumber: form.identificationNumber,
fullName: form.fullName,
dateOfBirth: form.dateOfBirth,
gender: form.gender,
phone: form.phone,
email: form.email,
address: form.address,
}),
});
},
onSuccess() {
/* REFRESH LIST */
queryClient.invalidateQueries({
queryKey: [QUERY_KEY.table_list_patient],
});
onClose();
},
});
const handleSubmit = () => {
createPatientMutation.mutate();
};
return (
<FormCustom form={form} setForm={setForm} onSubmit={handleSubmit}>
<div
className="
relative
h-[680px]
w-[640px]
max-w-[95vw]
rounded-3xl
bg-white
shadow-xl
flex
flex-col
overflow-hidden
"
>
{/* CLOSE */}
<button
type="button"
onClick={onClose}
className="
absolute
right-5
top-5
z-10
text-gray-400
transition
hover:text-gray-600
"
>
<X size={22} />
</button>
{/* HEADER */}
<div
className="
shrink-0
border-b
px-7
py-5
bg-white
"
>
<h2 className="text-[30px] font-semibold text-[#111827]">
Thêm bệnh nhân
</h2>
</div>
{/* BODY */}
<div
className="
flex-1
overflow-y-auto
px-7
py-5
"
>
<div className="space-y-5">
<InputForm
label={
<span>
đnh danh
<span className="text-red-500"> *</span>
</span>
}
placeholder="Mã định danh"
name="identificationNumber"
type="text"
value={form.identificationNumber}
isRequired
/>
<InputForm
label={
<span>
Tên bệnh nhân
<span className="text-red-500"> *</span>
</span>
}
placeholder="Tên bệnh nhân"
name="fullName"
type="text"
value={form.fullName}
isRequired
/>
<InputForm
label={
<span>
Ngày sinh
<span className="text-red-500"> *</span>
</span>
}
placeholder="Ngày sinh"
name="dateOfBirth"
type="Date"
value={form.dateOfBirth}
isRequired
/>
<InputForm
label={
<span>
Số điện thoại
<span className="text-red-500"> *</span>
</span>
}
placeholder="Số điện thoại"
name="phone"
type="text"
value={form.phone}
isRequired
/>
<InputForm
label={
<span>
Email
<span className="text-red-500"> *</span>
</span>
}
placeholder="Email"
name="email"
type="text"
value={form.email}
isRequired
/>
<TextArea
name="address"
placeholder="Địa chỉ"
label="Địa chỉ"
isBlur
max={5000}
/>
</div>
</div>
{/* FOOTER */}
<div
className="
shrink-0
border-t
bg-white
px-7
py-5
flex
items-center
justify-end
gap-3
"
>
<CustomButton variant="grey" rounded="md" onClick={onClose}>
Hủy bỏ
</CustomButton>
<ContextFormCustom.Consumer>
{({ isDone }) => (
<CustomButton
disabled={!isDone || createPatientMutation.isPending}
variant="midnightBlue"
rounded="md"
type="submit"
>
{createPatientMutation.isPending ? "Đang tạo..." : "Tạo"}
</CustomButton>
)}
</ContextFormCustom.Consumer>
</div>
</div>
</FormCustom>
);
};
export default PopupCreatePatient;
@@ -0,0 +1 @@
export { default } from "./PopupCreatePatient";
@@ -0,0 +1,419 @@
"use client";
import React, { useEffect, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { X } from "lucide-react";
import CustomButton from "@/components/customs/custom-button";
import CustomLoading from "@/components/customs/custom-loading";
import FormCustom from "@/components/utils/FormCustom";
import InputForm from "@/components/utils/FormCustom/components/InputForm";
import TextArea from "@/components/utils/FormCustom/components/TextArea";
import { ContextFormCustom } from "@/components/utils/FormCustom/contexts";
import { QUERY_KEY, TYPE_GENDER } from "@/constant/config/enum";
import { httpRequest } from "@/services";
import patientServices, { PatientDetail } from "@/services/patientServices";
interface PopupUpdatePatientProps {
onClose: () => void;
id: string;
}
interface IUpdatePatient {
fullName: string;
gender: string;
phone: string;
email: string;
address: string;
emergencyContactName: string;
emergencyContactPhone: string;
allergyNotes: string;
chronicDiseaseNotes: string;
insuranceNumber: string;
notes: string;
}
const defaultForm: IUpdatePatient = {
fullName: "",
gender: "",
phone: "",
email: "",
address: "",
emergencyContactName: "",
emergencyContactPhone: "",
allergyNotes: "",
chronicDiseaseNotes: "",
insuranceNumber: "",
notes: "",
};
const PopupUpdatePatient = ({ onClose, id }: PopupUpdatePatientProps) => {
const queryClient = useQueryClient();
const [form, setForm] = useState<IUpdatePatient>(defaultForm);
const patientDetailQuery = useQuery<PatientDetail>({
queryKey: [QUERY_KEY.chi_tiet_benh_nhan, id],
enabled: !!id,
queryFn: async () => {
const res = await httpRequest<PatientDetail>({
showMessageFailed: true,
http: patientServices.detailPatient(id),
});
return (
res || {
id: "",
patientCode: "",
identificationNumber: "",
fullName: "",
dateOfBirth: "",
gender: "",
phone: "",
email: "",
address: "",
emergencyContactName: null,
emergencyContactPhone: null,
allergyNotes: null,
chronicDiseaseNotes: null,
insuranceNumber: null,
notes: null,
isActive: false,
}
);
},
});
useEffect(() => {
const patient = patientDetailQuery.data;
if (!patient) return;
setForm((prev) => {
/**
* tránh render loop
*/
if (
prev.fullName === patient.fullName &&
prev.phone === patient.phone &&
prev.email === patient.email
) {
return prev;
}
return {
fullName: patient.fullName || "",
gender: patient.gender || TYPE_GENDER.MALE,
phone: patient.phone || "",
email: patient.email || "",
address: patient.address || "",
emergencyContactName: patient.emergencyContactName || "",
emergencyContactPhone: patient.emergencyContactPhone || "",
allergyNotes: patient.allergyNotes || "",
chronicDiseaseNotes: patient.chronicDiseaseNotes || "",
insuranceNumber: patient.insuranceNumber || "",
notes: patient.notes || "",
};
});
}, [patientDetailQuery.data]);
const updatePatientMutation = useMutation({
mutationFn: async () => {
return httpRequest({
showMessageFailed: true,
showMessageSuccess: true,
msgSuccess: "Cập nhật bệnh nhân thành công!",
http: patientServices.putPatient(id, {
fullName: form.fullName,
gender: form.gender,
phone: form.phone,
email: form.email,
address: form.address,
emergencyContactName: form.emergencyContactName,
emergencyContactPhone: form.emergencyContactPhone,
allergyNotes: form.allergyNotes,
chronicDiseaseNotes: form.chronicDiseaseNotes,
insuranceNumber: form.insuranceNumber,
notes: form.notes,
}),
});
},
onSuccess(data) {
if (!data) return;
queryClient.invalidateQueries({
queryKey: [QUERY_KEY.table_list_patient],
});
queryClient.invalidateQueries({
queryKey: [QUERY_KEY.chi_tiet_benh_nhan, id],
});
onClose();
},
});
const handleSubmit = () => {
updatePatientMutation.mutate();
};
if (patientDetailQuery.isLoading) {
return (
<div className="flex h-[400px] items-center justify-center">
<CustomLoading />
</div>
);
}
return (
<FormCustom form={form} setForm={setForm} onSubmit={handleSubmit}>
<div
className="
relative
h-[680px]
w-[640px]
max-w-[95vw]
rounded-3xl
bg-white
shadow-xl
flex
flex-col
overflow-hidden
"
>
{/* CLOSE */}
<button
type="button"
onClick={onClose}
className="
absolute
right-5
top-5
z-10
text-gray-400
transition
hover:text-gray-600
"
>
<X size={22} />
</button>
{/* HEADER */}
<div
className="
shrink-0
border-b
px-7
py-5
bg-white
"
>
<h2 className="text-[30px] font-semibold text-[#111827]">
Cập nhật thông tin bệnh nhân
</h2>
</div>
{/* BODY */}
<div
className="
flex-1
overflow-y-auto
px-7
py-5
"
>
<div className="space-y-5">
<InputForm
label={
<span>
Tên bệnh nhân
<span className="text-red-500"> *</span>
</span>
}
placeholder="Tên bệnh nhân"
name="fullName"
type="text"
value={form.fullName}
isRequired
/>
{/* GENDER */}
<div>
<label className="text-base font-medium">Giới tính</label>
<div className="mt-2 flex items-center gap-6">
{/* NAM */}
<div className="flex items-center gap-2">
<input
type="radio"
id="male"
checked={form.gender === TYPE_GENDER.MALE}
onChange={() =>
setForm((prev) => ({
...prev,
gender: TYPE_GENDER.MALE,
}))
}
/>
<label htmlFor="male">Nam</label>
</div>
{/* NỮ */}
<div className="flex items-center gap-2">
<input
type="radio"
id="female"
checked={form.gender === TYPE_GENDER.FEMALE}
onChange={() =>
setForm((prev) => ({
...prev,
gender: TYPE_GENDER.FEMALE,
}))
}
/>
<label htmlFor="female">Nữ</label>
</div>
{/* KHÁC */}
<div className="flex items-center gap-2">
<input
type="radio"
id="other"
checked={form.gender === TYPE_GENDER.OTHER}
onChange={() =>
setForm((prev) => ({
...prev,
gender: TYPE_GENDER.OTHER,
}))
}
/>
<label htmlFor="other">Khác</label>
</div>
</div>
</div>
<InputForm
label={
<span>
Số điện thoại
<span className="text-red-500"> *</span>
</span>
}
placeholder="Số điện thoại"
name="phone"
type="text"
value={form.phone}
isRequired
/>
<InputForm
label={
<span>
Email
<span className="text-red-500"> *</span>
</span>
}
placeholder="Email"
name="email"
type="text"
value={form.email}
isRequired
/>
<TextArea
name="address"
placeholder="Địa chỉ"
label="Địa chỉ"
value={form.address}
isBlur
max={5000}
/>
<InputForm
label="Người liên hệ khẩn cấp"
placeholder="Người liên hệ khẩn cấp"
name="emergencyContactName"
type="text"
value={form.emergencyContactName}
/>
<InputForm
label="SĐT khẩn cấp"
placeholder="SĐT khẩn cấp"
name="emergencyContactPhone"
type="text"
value={form.emergencyContactPhone}
/>
<TextArea
name="allergyNotes"
placeholder="Dị ứng"
label="Dị ứng"
value={form.allergyNotes}
isBlur
max={5000}
/>
<TextArea
name="chronicDiseaseNotes"
placeholder="Bệnh nền"
label="Bệnh nền"
value={form.chronicDiseaseNotes}
isBlur
max={5000}
/>
<InputForm
label="Số bảo hiểm"
placeholder="Số bảo hiểm"
name="insuranceNumber"
type="text"
value={form.insuranceNumber}
/>
<TextArea
name="notes"
placeholder="Ghi chú"
label="Ghi chú"
value={form.notes}
isBlur
max={5000}
/>
</div>
</div>
{/* FOOTER */}
<div
className="
shrink-0
border-t
bg-white
px-7
py-5
flex
items-center
justify-end
gap-3
"
>
<CustomButton variant="grey" rounded="md" onClick={onClose}>
Hủy bỏ
</CustomButton>
<ContextFormCustom.Consumer>
{({ isDone }) => (
<CustomButton
disabled={!isDone || updatePatientMutation.isPending}
variant="midnightBlue"
rounded="md"
type="submit"
>
{updatePatientMutation.isPending
? "Đang cập nhật..."
: "Cập nhật"}
</CustomButton>
)}
</ContextFormCustom.Consumer>
</div>
</div>
</FormCustom>
);
};
export default PopupUpdatePatient;
@@ -0,0 +1 @@
export { default } from "./PopupUpdatePatient";