update-full
This commit is contained in:
@@ -1,97 +1,94 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { LockKeyhole, ShieldPlus, User } from "lucide-react";
|
||||
import { ShieldPlus, User } from "lucide-react";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import { RootState, store } from "@/redux/store";
|
||||
|
||||
import {
|
||||
setDataLoginStorage,
|
||||
setStateLogin,
|
||||
setToken,
|
||||
} from "@/redux/reducer/auth";
|
||||
|
||||
import { setRememberPassword } from "@/redux/reducer/site";
|
||||
import { setInfoUser } from "@/redux/reducer/user";
|
||||
|
||||
import authServices from "@/services/authServices";
|
||||
import authServices, { LoginResponse } from "@/services/authServices";
|
||||
import { httpRequest } from "@/services";
|
||||
|
||||
import { PATH } from "@/constant/config";
|
||||
|
||||
import FormCustom from "@/components/utils/FormCustom";
|
||||
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
||||
import CustomLoading from "@/components/customs/custom-loading";
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
import { ContextFormCustom } from "@/components/utils/FormCustom/contexts";
|
||||
|
||||
export default function MainLogin() {
|
||||
const router = useRouter();
|
||||
|
||||
const { dataLoginStorage } = useSelector((state: RootState) => state.auth);
|
||||
|
||||
const { isRememberPassword } = useSelector((state: RootState) => state.site);
|
||||
|
||||
const [form, setForm] = useState({
|
||||
// Khởi tạo state form từ data lưu trữ
|
||||
const [form, setForm] = useState(() => ({
|
||||
username: isRememberPassword ? dataLoginStorage?.usernameStorage || "" : "",
|
||||
|
||||
password: isRememberPassword ? dataLoginStorage?.passwordStorage || "" : "",
|
||||
}));
|
||||
|
||||
deviceId: isRememberPassword ? dataLoginStorage?.deviceIdStorage || "" : "",
|
||||
});
|
||||
|
||||
const loginMutation = useMutation({
|
||||
const loginMutation = useMutation<LoginResponse>({
|
||||
mutationFn: async () => {
|
||||
return httpRequest({
|
||||
showMessageFailed: true,
|
||||
showMessageSuccess: true,
|
||||
msgSuccess: "Đăng nhập thành công!",
|
||||
return (await httpRequest({
|
||||
http: authServices.login({
|
||||
username: form.username,
|
||||
password: form.password,
|
||||
deviceId: form.username,
|
||||
deviceId: "Browser-Chrome-Windowns",
|
||||
}),
|
||||
});
|
||||
})) as LoginResponse;
|
||||
},
|
||||
|
||||
onSuccess(data: any) {
|
||||
if (!data) return;
|
||||
|
||||
store.dispatch(setStateLogin(true));
|
||||
|
||||
store.dispatch(setToken(data.token));
|
||||
|
||||
store.dispatch(
|
||||
setInfoUser({
|
||||
accessToken: data?.token || "",
|
||||
refreshToken: data?.refreshToken || "",
|
||||
accessExpiresAt: data?.accessExpiresAt || "",
|
||||
refreshExpiresAt: data?.refreshExpiresAt || "",
|
||||
avatar: data?.avatar || "",
|
||||
fullname: data?.fullname || "",
|
||||
}),
|
||||
);
|
||||
|
||||
if (isRememberPassword) {
|
||||
onSuccess(data) {
|
||||
if (data) {
|
||||
store.dispatch(setToken(data?.token));
|
||||
store.dispatch(
|
||||
setDataLoginStorage({
|
||||
usernameStorage: form.username,
|
||||
passwordStorage: form.password,
|
||||
deviceIdStorage: form.deviceId,
|
||||
setInfoUser({
|
||||
accessToken: data?.token || "",
|
||||
refreshToken: data?.refreshToken || "",
|
||||
userId: data?.userId || "",
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
store.dispatch(setDataLoginStorage(null));
|
||||
}
|
||||
store.dispatch(setStateLogin(true));
|
||||
|
||||
router.replace(PATH.HOME);
|
||||
if (isRememberPassword) {
|
||||
store.dispatch(
|
||||
setDataLoginStorage({
|
||||
usernameStorage: form.username,
|
||||
passwordStorage: form.password,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
store.dispatch(setDataLoginStorage(null));
|
||||
}
|
||||
|
||||
router.replace(PATH.HOME, undefined);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const handleLogin = () => {
|
||||
// Kiểm tra nhanh điều kiện dữ liệu trước khi trigger API
|
||||
if (!form.username || !form.password) return;
|
||||
|
||||
if (isRememberPassword) {
|
||||
store.dispatch(
|
||||
setDataLoginStorage({
|
||||
usernameStorage: form.username,
|
||||
passwordStorage: form.password,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
store.dispatch(setDataLoginStorage(null));
|
||||
}
|
||||
|
||||
loginMutation.mutate();
|
||||
};
|
||||
|
||||
@@ -101,24 +98,8 @@ export default function MainLogin() {
|
||||
|
||||
<FormCustom form={form} setForm={setForm} onSubmit={handleLogin}>
|
||||
{/* TITLE */}
|
||||
<h3
|
||||
className="
|
||||
text-[34px]
|
||||
font-semibold
|
||||
text-[#1A1B2D]
|
||||
"
|
||||
>
|
||||
Đăng nhập
|
||||
</h3>
|
||||
|
||||
<p
|
||||
className="
|
||||
mt-1
|
||||
text-[14px]
|
||||
font-medium
|
||||
text-[#6F767E]
|
||||
"
|
||||
>
|
||||
<h3 className="text-[34px] font-semibold text-[#1A1B2D]">Đăng nhập</h3>
|
||||
<p className="mt-1 text-[14px] font-medium text-[#6F767E]">
|
||||
Chào mừng bạn đến với hệ thống quản lý
|
||||
</p>
|
||||
|
||||
@@ -128,8 +109,7 @@ export default function MainLogin() {
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Tài khoản
|
||||
<span className="text-red-500"> *</span>
|
||||
Tài khoản<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Tài khoản"
|
||||
@@ -142,32 +122,12 @@ export default function MainLogin() {
|
||||
icon={<User size={22} />}
|
||||
/>
|
||||
|
||||
{/* <div className="mt-5">
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
deviceId
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="deviceId"
|
||||
type="text"
|
||||
name="deviceId"
|
||||
onClean
|
||||
isRequired
|
||||
isBlur
|
||||
showDone
|
||||
icon={<ShieldPlus size={22} />}
|
||||
/>
|
||||
</div> */}
|
||||
|
||||
{/* PASSWORD */}
|
||||
<div className="mt-5">
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Mật khẩu
|
||||
<span className="text-red-500"> *</span>
|
||||
Mật khẩu<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Mật khẩu"
|
||||
@@ -182,15 +142,7 @@ export default function MainLogin() {
|
||||
</div>
|
||||
|
||||
{/* REMEMBER PASSWORD */}
|
||||
<div
|
||||
className="
|
||||
mt-[14px]
|
||||
flex
|
||||
items-center
|
||||
gap-2
|
||||
cursor-pointer
|
||||
"
|
||||
>
|
||||
<div className="mt-[14px] flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
id="rememberPassword"
|
||||
type="checkbox"
|
||||
@@ -198,23 +150,11 @@ export default function MainLogin() {
|
||||
onChange={() =>
|
||||
store.dispatch(setRememberPassword(!isRememberPassword))
|
||||
}
|
||||
className="
|
||||
h-5
|
||||
w-5
|
||||
cursor-pointer
|
||||
accent-[#0011AB]
|
||||
"
|
||||
className="h-5 w-5 cursor-pointer accent-[#0011AB]"
|
||||
/>
|
||||
|
||||
<label
|
||||
htmlFor="rememberPassword"
|
||||
className="
|
||||
cursor-pointer
|
||||
select-none
|
||||
text-[14px]
|
||||
font-medium
|
||||
text-[#171717]
|
||||
"
|
||||
className="cursor-pointer select-none text-[14px] font-medium text-[#171717]"
|
||||
>
|
||||
Nhớ mật khẩu
|
||||
</label>
|
||||
@@ -223,64 +163,40 @@ export default function MainLogin() {
|
||||
{/* BUTTONS */}
|
||||
<div className="mt-6">
|
||||
<div className="flex gap-2">
|
||||
{/* LOGIN */}
|
||||
<CustomButton
|
||||
variant="midnightBlue"
|
||||
disabled={loginMutation.isPending}
|
||||
rounded="full"
|
||||
type="submit"
|
||||
className="font-bold text-2xl disabled:cursor-not-allowed
|
||||
disabled:opacity-50 h-[52px]"
|
||||
>
|
||||
Đăng nhập
|
||||
</CustomButton>
|
||||
{/* REGISTER */}
|
||||
{/* LOGIN BUTTON */}
|
||||
<ContextFormCustom.Consumer>
|
||||
{({ isDone }) => {
|
||||
// Tự kiểm tra xem form hiện tại đã có sẵn dữ liệu hợp lệ chưa
|
||||
const hasRememberedData = !!(form.username && form.password);
|
||||
|
||||
// Sáng nút nếu FormCustom báo Done HOẶC form đã có sẵn dữ liệu từ Redux
|
||||
const canSubmit = isDone || hasRememberedData;
|
||||
|
||||
return (
|
||||
<CustomButton
|
||||
variant="midnightBlue"
|
||||
disabled={!canSubmit}
|
||||
rounded="full"
|
||||
type="button" // Chuyển từ "submit" sang "button" để tránh bị FormCustom chặn
|
||||
onClick={handleLogin} // Gọi trực tiếp hàm submit khi click
|
||||
className="font-bold text-2xl disabled:cursor-not-allowed disabled:opacity-50 h-[52px]"
|
||||
>
|
||||
Đăng nhập
|
||||
</CustomButton>
|
||||
);
|
||||
}}
|
||||
</ContextFormCustom.Consumer>
|
||||
|
||||
{/* REGISTER BUTTON */}
|
||||
<CustomButton
|
||||
variant="default"
|
||||
rounded="full"
|
||||
className="font-bold text-2xl disabled:cursor-not-allowed
|
||||
disabled:opacity-50 h-[52px]"
|
||||
className="font-bold text-2xl disabled:cursor-not-allowed disabled:opacity-50 h-[52px]"
|
||||
onClick={() => router.push(PATH.REGISTER)}
|
||||
>
|
||||
Đăng ký
|
||||
</CustomButton>
|
||||
</div>
|
||||
|
||||
{/* LINE */}
|
||||
{/* <div
|
||||
className="
|
||||
my-5
|
||||
h-[1px]
|
||||
w-full
|
||||
bg-[#E5E5E5]
|
||||
"
|
||||
/> */}
|
||||
|
||||
{/* FORGOT PASSWORD */}
|
||||
{/* <button
|
||||
type="button"
|
||||
onClick={() => router.push(PATH.FORGOT_PASSWORD)}
|
||||
className="
|
||||
flex
|
||||
h-[52px]
|
||||
w-full
|
||||
items-center
|
||||
justify-center
|
||||
gap-2
|
||||
rounded-full
|
||||
border
|
||||
border-gray-300
|
||||
bg-white
|
||||
px-6
|
||||
font-bold
|
||||
text-[#171717]
|
||||
transition-all
|
||||
hover:bg-gray-50
|
||||
"
|
||||
>
|
||||
<LockKeyhole size={22} />
|
||||
Quên mật khẩu
|
||||
</button> */}
|
||||
</div>
|
||||
</div>
|
||||
</FormCustom>
|
||||
|
||||
@@ -120,15 +120,29 @@ const DetailConsultation = () => {
|
||||
type PrescriptionItem =
|
||||
ConsultationDetail["aggregatedPrescriptionItems"][number];
|
||||
|
||||
// const getFileInfo = (file: AttachmentFile | string, index: number) => {
|
||||
// if (typeof file === "string") {
|
||||
// return {
|
||||
// fileName: file.split("/").pop() || `File ${index + 1}`,
|
||||
// path: file,
|
||||
// };
|
||||
// }
|
||||
|
||||
// return file;
|
||||
// };
|
||||
|
||||
const getFileInfo = (file: AttachmentFile | string, index: number) => {
|
||||
if (typeof file === "string") {
|
||||
return {
|
||||
fileName: file.split("/").pop() || `File ${index + 1}`,
|
||||
path: file,
|
||||
path: `${process.env.NEXT_PUBLIC_IMAGE}${file}`,
|
||||
};
|
||||
}
|
||||
|
||||
return file;
|
||||
return {
|
||||
...file,
|
||||
path: `${process.env.NEXT_PUBLIC_IMAGE}${file.path}`,
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -282,7 +296,7 @@ const DetailConsultation = () => {
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{consultation.createdBy || "---"}
|
||||
{consultation.createdByName || "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import consultationServices, {
|
||||
} from "@/services/consultationServices";
|
||||
import uploadServices from "@/services/uploadServices";
|
||||
import UploadMultipleFile from "@/components/utils/UploadMultipleFile";
|
||||
import TextArea from "@/components/utils/FormCustom/components/TextArea";
|
||||
|
||||
/* =========================
|
||||
TYPES
|
||||
@@ -29,6 +30,7 @@ interface IUpdateConsul {
|
||||
treatmentPlan: string;
|
||||
doctorNotes: string;
|
||||
isGlassesDelivered: boolean;
|
||||
glassType: string;
|
||||
generalAttachmentUrls: string[];
|
||||
}
|
||||
|
||||
@@ -43,6 +45,7 @@ const defaultForm: IUpdateConsul = {
|
||||
treatmentPlan: "",
|
||||
doctorNotes: "",
|
||||
isGlassesDelivered: false,
|
||||
glassType: "",
|
||||
generalAttachmentUrls: [],
|
||||
};
|
||||
|
||||
@@ -101,9 +104,14 @@ const UpdateConsultation = () => {
|
||||
/* =========================
|
||||
MAP & SET FORM
|
||||
========================= */
|
||||
const consul = consulDetailQuery.data;
|
||||
useEffect(() => {
|
||||
const consul = consulDetailQuery.data;
|
||||
if (!consul) return;
|
||||
const eyeClinic = consul?.specialtyClinics?.find(
|
||||
(item) =>
|
||||
item.specialtyName?.toLowerCase().includes("mắt") &&
|
||||
item.glassRequired === true,
|
||||
);
|
||||
|
||||
setForm({
|
||||
chiefComplaint: consul.chiefComplaint || "",
|
||||
@@ -112,7 +120,8 @@ const UpdateConsultation = () => {
|
||||
diagnosis: consul.diagnosis || "",
|
||||
treatmentPlan: consul.treatmentPlan || "",
|
||||
doctorNotes: consul.doctorNotes || "",
|
||||
isGlassesDelivered: consul.isGlassesDelivered,
|
||||
isGlassesDelivered: !!eyeClinic,
|
||||
glassType: eyeClinic?.glassType || "",
|
||||
generalAttachmentUrls: consul.generalAttachmentUrls || [],
|
||||
});
|
||||
|
||||
@@ -212,6 +221,11 @@ const UpdateConsultation = () => {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const eyeClinic = consul?.specialtyClinics?.find(
|
||||
(item) =>
|
||||
item.specialtyName?.toLowerCase().includes("mắt") &&
|
||||
item.glassRequired === true,
|
||||
);
|
||||
|
||||
/* =========================
|
||||
MAIN UI
|
||||
@@ -324,6 +338,56 @@ const UpdateConsultation = () => {
|
||||
}
|
||||
/>
|
||||
</GridColumn>
|
||||
|
||||
{eyeClinic && (
|
||||
<div className="mt-2 rounded-xl border border-blue-100 bg-[#f8fafc] p-5 transition-all">
|
||||
<h3 className="mb-4 text-sm font-bold uppercase tracking-wider text-blue-600">
|
||||
Chỉ định thị lực (Khoa Mắt)
|
||||
</h3>
|
||||
|
||||
<div className="flex flex-col gap-5">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-700">
|
||||
Bệnh nhân có chỉ định đeo kính không?
|
||||
</label>
|
||||
<div className="mt-2.5 flex items-center gap-8">
|
||||
<label className="flex cursor-pointer items-center gap-2 text-sm font-medium text-gray-900">
|
||||
<input
|
||||
type="radio"
|
||||
name="glassRequired"
|
||||
className="h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
checked={form.isGlassesDelivered === true}
|
||||
onChange={() =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
isGlassesDelivered: true,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
Có chỉ định
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TextArea
|
||||
name="glassType"
|
||||
placeholder="Nhập thông số chi tiết loại kính được chỉ định"
|
||||
label="Thông số / Loại kính chỉ định"
|
||||
value={form.glassType}
|
||||
isRequired
|
||||
readOnly
|
||||
isBlur
|
||||
max={5000}
|
||||
onChangeValue={(v) =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
glassType: String(v),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-4">
|
||||
<label className="text-sm font-medium">Tài liệu đính kèm</label>
|
||||
|
||||
|
||||
+81
-64
@@ -14,6 +14,7 @@ import consultationServices, {
|
||||
} from "@/services/consultationServices";
|
||||
import uploadServices from "@/services/uploadServices";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import moment from "moment";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
@@ -62,6 +63,7 @@ const UpdateSpecialtyClinicId = () => {
|
||||
res || {
|
||||
id: "",
|
||||
patientId: "",
|
||||
patientName: "",
|
||||
sessionCode: "",
|
||||
visitDate: "",
|
||||
status: 1,
|
||||
@@ -71,50 +73,34 @@ const UpdateSpecialtyClinicId = () => {
|
||||
diagnosis: null,
|
||||
treatmentPlan: null,
|
||||
doctorNotes: null,
|
||||
isGlassesDelivered: false,
|
||||
createdBy: "",
|
||||
createdByName: "",
|
||||
isDeleted: false,
|
||||
specialtyClinics: [
|
||||
{
|
||||
id: "",
|
||||
specialtyId: "",
|
||||
specialtyName: "",
|
||||
specialtyNotes: null,
|
||||
diagnosis: null,
|
||||
procedureNotes: null,
|
||||
glassRequired: null,
|
||||
glassType: null,
|
||||
examinerName: "",
|
||||
specialtyStatus: 1,
|
||||
attachmentUrls: [],
|
||||
sharedItems: [],
|
||||
},
|
||||
],
|
||||
generalAttachmentUrls: [],
|
||||
specialtyClinics: [],
|
||||
aggregatedPrescriptionItems: [],
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// Khai báo dữ liệu lấy từ Query phục vụ hiển thị
|
||||
const generalData = consulDetailQuery.data;
|
||||
|
||||
// Tìm chuyên khoa hiện tại từ data trả về
|
||||
const selectedSpecialtyClinic =
|
||||
consulDetailQuery.data?.specialtyClinics?.find(
|
||||
(item) => item.id === specialtyClinicId,
|
||||
);
|
||||
const selectedSpecialtyClinic = generalData?.specialtyClinics?.find(
|
||||
(item) => item.id === specialtyClinicId,
|
||||
);
|
||||
|
||||
/**
|
||||
* ĐIỀU KIỆN KIỂM TRA KHOA MẮT
|
||||
* Bạn hãy thay đổi điều kiện này tùy thuộc vào database của bạn.
|
||||
* Ví dụ: selectedSpecialtyClinic?.specialtyId === "MẮT" hoặc kiểm tra theo tên.
|
||||
*/
|
||||
const isEyeClinic =
|
||||
selectedSpecialtyClinic?.specialtyName?.toLowerCase().includes("mắt") ||
|
||||
selectedSpecialtyClinic?.specialtyId?.toLowerCase().includes("mat");
|
||||
|
||||
useEffect(() => {
|
||||
const consul = consulDetailQuery.data;
|
||||
if (!consul) return;
|
||||
if (!generalData) return;
|
||||
|
||||
const specialtyClinic = consul.specialtyClinics?.find(
|
||||
const specialtyClinic = generalData.specialtyClinics?.find(
|
||||
(item) => item.id === specialtyClinicId,
|
||||
);
|
||||
|
||||
@@ -130,18 +116,17 @@ const UpdateSpecialtyClinicId = () => {
|
||||
fileUrls: specialtyClinic.attachmentUrls || [],
|
||||
});
|
||||
setImages(
|
||||
(consul.generalAttachmentUrls || []).map((url) => ({
|
||||
(specialtyClinic.attachmentUrls || []).map((url) => ({
|
||||
file: null,
|
||||
img: url,
|
||||
path: url,
|
||||
})),
|
||||
);
|
||||
}, [consulDetailQuery.data, specialtyClinicId]);
|
||||
}, [generalData, specialtyClinicId]);
|
||||
|
||||
/* =========================
|
||||
UPDATE MUTATION
|
||||
========================= */
|
||||
|
||||
const specialtyResultsMutation = useMutation({
|
||||
mutationFn: async (body: { paths: string[] }) => {
|
||||
if (!consultationId) {
|
||||
@@ -151,14 +136,13 @@ const UpdateSpecialtyClinicId = () => {
|
||||
return httpRequest({
|
||||
showMessageFailed: true,
|
||||
showMessageSuccess: true,
|
||||
msgSuccess: "Cập nhật phiên khám thành công!",
|
||||
msgSuccess: "Cập nhật kết quả khám chuyên khoa thành công!",
|
||||
http: consultationServices.specialtyResults(
|
||||
form.specialtyAssignmentId,
|
||||
{
|
||||
specialtyNotes: form.specialtyNotes,
|
||||
diagnosis: form.diagnosis,
|
||||
procedureNotes: form.procedureNotes,
|
||||
// Nếu không phải khoa mắt, gán mặc định false và chuỗi rỗng khi gửi API
|
||||
glassRequired: isEyeClinic ? form.glassRequired : false,
|
||||
glassType: isEyeClinic ? form.glassType : "",
|
||||
fileUrls: body.paths,
|
||||
@@ -177,7 +161,6 @@ const UpdateSpecialtyClinicId = () => {
|
||||
});
|
||||
|
||||
router.back();
|
||||
|
||||
router.push(PATH.CONSULTATION || "/consultation");
|
||||
},
|
||||
});
|
||||
@@ -195,12 +178,9 @@ const UpdateSpecialtyClinicId = () => {
|
||||
if (files.length > 0) {
|
||||
const uploadResult: any = await uploadServices.uploadImage({
|
||||
Files: files,
|
||||
SessionCode: consulDetailQuery.data?.sessionCode,
|
||||
SessionCode: generalData?.sessionCode,
|
||||
});
|
||||
|
||||
console.log("Cấu trúc log kiểm tra uploadResult:", uploadResult);
|
||||
|
||||
// CẬP NHẬT Ở ĐÂY: Lấy trực tiếp từ uploadResult hoặc bóc tách đúng tầng data
|
||||
const uploadedUrls =
|
||||
uploadResult?.fileUrls || uploadResult?.data?.fileUrls || [];
|
||||
|
||||
@@ -243,26 +223,64 @@ const UpdateSpecialtyClinicId = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CHUYÊN KHOA INFO */}
|
||||
<InputForm
|
||||
label="Chuyên khoa đảm nhận"
|
||||
name="specialtyAssignmentId"
|
||||
type="text"
|
||||
value={
|
||||
selectedSpecialtyClinic
|
||||
? `${selectedSpecialtyClinic.specialtyName}`
|
||||
: "Chưa có thông tin chuyên khoa"
|
||||
}
|
||||
placeholder="Thông tin chuyên khoa"
|
||||
isRequired
|
||||
readOnly
|
||||
/>
|
||||
|
||||
{/* KHỐI THÔNG TIN KHÁM CHUNG (Luôn cố định 3 cột đẹp mắt) */}
|
||||
<div className="mt-2">
|
||||
<h3 className="mb-4 text-sm font-semibold uppercase tracking-wider text-gray-400">
|
||||
Thông tin khám chung
|
||||
{/* THÔNG TIN HÀNH CHÍNH & PHIÊN KHÁM TỔNG QUÁT */}
|
||||
<div className="rounded-xl border border-gray-200 bg-[#f8fafc] p-5">
|
||||
<h3 className="mb-4 text-xs font-bold uppercase tracking-wider text-gray-500">
|
||||
Thông tin tổng quan phiên khám
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<div>
|
||||
<span className="text-xs font-medium text-gray-400 block uppercase">
|
||||
Tên bệnh nhân
|
||||
</span>
|
||||
<span className="text-base font-bold text-gray-700 mt-0.5 block">
|
||||
{generalData?.patientName || "Chưa có thông tin"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-xs font-medium text-gray-400 block uppercase">
|
||||
Ngày sinh
|
||||
</span>
|
||||
<span className="text-base font-medium text-gray-700 mt-0.5 block">
|
||||
{generalData?.patientDateOfBirth
|
||||
? moment(generalData?.patientDateOfBirth).format("DD/MM/YYYY")
|
||||
: "---"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-xs font-medium text-gray-400 block uppercase">
|
||||
Mã phiên khám tổng quát
|
||||
</span>
|
||||
<span className="text-base font-semibold text-blue-600 mt-0.5 block">
|
||||
#{generalData?.sessionCode || "Chưa có mã"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* KHỐI THÔNG TIN KHÁM CHUYÊN KHOA */}
|
||||
<div className="mt-2">
|
||||
<h3 className="mb-4 text-xs font-bold uppercase tracking-wider text-gray-500">
|
||||
Kết quả khám chuyên khoa đảm nhận
|
||||
</h3>
|
||||
|
||||
<div className="mb-5">
|
||||
<InputForm
|
||||
label="Chuyên khoa tiếp nhận"
|
||||
name="specialtyAssignmentId"
|
||||
type="text"
|
||||
value={
|
||||
selectedSpecialtyClinic
|
||||
? `${selectedSpecialtyClinic.specialtyName}`
|
||||
: "Chưa có thông tin chuyên khoa"
|
||||
}
|
||||
placeholder="Thông tin chuyên khoa"
|
||||
isRequired
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
|
||||
<GridColumn col={3}>
|
||||
<InputForm
|
||||
label="Ghi chú chuyên khoa"
|
||||
@@ -277,9 +295,9 @@ const UpdateSpecialtyClinicId = () => {
|
||||
/>
|
||||
<InputForm
|
||||
type="text"
|
||||
label="Chuẩn đoán chuyên khoa"
|
||||
label="Chẩn đoán chuyên khoa"
|
||||
name="diagnosis"
|
||||
placeholder="Nhập Chuẩn đoán"
|
||||
placeholder="Nhập chẩn đoán chuyên khoa"
|
||||
isRequired
|
||||
value={form.diagnosis}
|
||||
onChangeValue={(v) =>
|
||||
@@ -308,7 +326,6 @@ const UpdateSpecialtyClinicId = () => {
|
||||
</h3>
|
||||
|
||||
<div className="flex flex-col gap-5">
|
||||
{/* Radio Chọn Có/Không */}
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-700">
|
||||
Bệnh nhân có chỉ định đeo kính không?
|
||||
@@ -342,7 +359,6 @@ const UpdateSpecialtyClinicId = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TextArea Loại Kính (Trải rộng ra 100% dòng tạo cảm giác thoáng đãng) */}
|
||||
{form.glassRequired && (
|
||||
<div className="mt-2 animate-fadeIn ">
|
||||
<TextArea
|
||||
@@ -359,11 +375,12 @@ const UpdateSpecialtyClinicId = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
<label className="text-sm font-medium">Tài liệu đính kèm</label>
|
||||
|
||||
<label className="text-sm font-medium">
|
||||
Tài liệu đính kèm chuyên khoa
|
||||
</label>
|
||||
<UploadMultipleFile images={images} setImages={setImages} />
|
||||
|
||||
{uploading && (
|
||||
<p className="text-sm text-blue-500">Đang upload file...</p>
|
||||
)}
|
||||
|
||||
@@ -159,7 +159,8 @@ const PopupCreatePatient = ({ onClose }: PropsPopupCreatePatient) => {
|
||||
}
|
||||
placeholder="Mã định danh"
|
||||
name="identificationNumber"
|
||||
type="text"
|
||||
type="number"
|
||||
isNumber
|
||||
value={form.identificationNumber}
|
||||
isRequired
|
||||
/>
|
||||
|
||||
@@ -121,7 +121,7 @@ const DetailPrescription = () => {
|
||||
{data.prescriptionImg ? (
|
||||
<>
|
||||
<Image
|
||||
src={`${data.prescriptionImg}`}
|
||||
src={`${process.env.NEXT_PUBLIC_IMAGE}/${data.prescriptionImg}`}
|
||||
alt="prescription"
|
||||
width={140}
|
||||
height={140}
|
||||
|
||||
@@ -160,8 +160,8 @@ const MainPrescription = () => {
|
||||
render: (item: any) =>
|
||||
item.prescriptionImg ? (
|
||||
<Image
|
||||
// src={`${process.env.NEXT_PUBLIC_IMAGE}${item.prescriptionImg}`}
|
||||
src={item.prescriptionImg}
|
||||
src={`${process.env.NEXT_PUBLIC_IMAGE}${item.prescriptionImg}`}
|
||||
// src={item.prescriptionImg}
|
||||
alt="prescription"
|
||||
width={40}
|
||||
height={40}
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
"use client";
|
||||
|
||||
import { memo, useMemo } from "react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import icons from "@/constant/images/icons";
|
||||
import { PATH } from "@/constant/config";
|
||||
import { QUERY_KEY } from "@/constant/config/enum";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { httpRequest } from "@/services";
|
||||
|
||||
import Moment from "react-moment";
|
||||
import {
|
||||
Cake,
|
||||
Phone,
|
||||
MapPin,
|
||||
Mail,
|
||||
SquareUser,
|
||||
ShieldUser,
|
||||
} from "lucide-react";
|
||||
|
||||
import userServices from "@/services/userServices";
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
import profileServices, { ProfileResponse } from "@/services/profileServices";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "@/redux/store";
|
||||
|
||||
function MainPageProfile() {
|
||||
const router = useRouter();
|
||||
const { infoUser } = useSelector((state: RootState) => state.user);
|
||||
|
||||
const profileQuery = useQuery<any>({
|
||||
queryKey: [QUERY_KEY.table_list_profile],
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<any>({
|
||||
showMessageFailed: true,
|
||||
http: profileServices.getProfile(), // Đảm bảo endpoint trỏ về /api/v1/UserProfile/list
|
||||
});
|
||||
|
||||
// Trả về đúng object chứa items theo cấu trúc của API
|
||||
return (
|
||||
res || {
|
||||
items: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// Tìm kiếm thông tin profile của user hiện tại đang đăng nhập bằng useMemo
|
||||
// eslint-disable-next-line react-hooks/preserve-manual-memoization
|
||||
const currentUserProfile = useMemo(() => {
|
||||
const listItems = profileQuery.data?.items || [];
|
||||
if (!infoUser?.userId) return null;
|
||||
|
||||
// So khớp accountId của API trả về với userId trong Redux (infoUser)
|
||||
return (
|
||||
listItems.find((item: any) => item.accountId === infoUser.userId) || null
|
||||
);
|
||||
}, [profileQuery.data, infoUser?.userId]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col p-6 bg-white/80 rounded-2xl">
|
||||
{/* HEADER */}
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<h2 className="text-xl font-semibold">Thông tin tài khoản</h2>
|
||||
|
||||
<div className="flex gap-2.5">
|
||||
<CustomButton
|
||||
variant="midnightBlue"
|
||||
size="md"
|
||||
rounded="full"
|
||||
fullWidth={false}
|
||||
href={PATH.UPDATEPROFILE}
|
||||
>
|
||||
Chỉnh sửa
|
||||
</CustomButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AVATAR */}
|
||||
<div className="flex gap-4 mt-6 mb-4">
|
||||
<Image
|
||||
src={
|
||||
currentUserProfile?.avatarUrl
|
||||
? `${process.env.NEXT_PUBLIC_IMAGE}${currentUserProfile.avatarUrl}`
|
||||
: icons.avatar
|
||||
}
|
||||
alt="avatar"
|
||||
width={120}
|
||||
height={120}
|
||||
className="object-cover rounded-lg "
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* MAIN */}
|
||||
<div className="flex gap-4 mt-6 flex-col md:flex-row">
|
||||
{/* BASIC INFO */}
|
||||
<div className="flex flex-col border border-gray-200 bg-gray-50 rounded-xl w-full md:w-1/2 p-5 text-gray-600">
|
||||
<div className="text-xl font-semibold mb-2 text-gray-800">
|
||||
Thông tin cơ bản
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* NAME */}
|
||||
<div className="grid grid-cols-[180px_1fr] items-center font-semibold">
|
||||
<p className="flex gap-2">
|
||||
<SquareUser size={24} />
|
||||
Họ và tên
|
||||
</p>
|
||||
<span className="text-gray-900">
|
||||
{currentUserProfile?.fullName || "---"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* DOB */}
|
||||
<div className="grid grid-cols-[180px_1fr] items-center font-semibold">
|
||||
<p className="flex gap-2">
|
||||
<Cake size={24} />
|
||||
Ngày sinh
|
||||
</p>
|
||||
<span className="text-gray-900">
|
||||
{currentUserProfile?.birthDate ? (
|
||||
<Moment format="DD/MM/YYYY">
|
||||
{currentUserProfile.birthDate}
|
||||
</Moment>
|
||||
) : (
|
||||
"---"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CONTACT */}
|
||||
<div className="flex flex-col border border-gray-200 bg-gray-50 rounded-xl w-full md:w-1/2 p-5 text-gray-600">
|
||||
<div className="text-xl font-semibold mb-2 text-gray-800">
|
||||
Liên hệ
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* EMAIL */}
|
||||
<div className="grid grid-cols-[180px_1fr] items-center font-semibold">
|
||||
<p className="flex gap-2">
|
||||
<Mail size={24} />
|
||||
Email
|
||||
</p>
|
||||
<span className="text-gray-900">
|
||||
{currentUserProfile?.email || "---"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* PHONE */}
|
||||
<div className="grid grid-cols-[180px_1fr] items-center font-semibold">
|
||||
<p className="flex gap-2">
|
||||
<Phone size={24} />
|
||||
Số điện thoại
|
||||
</p>
|
||||
<span className="text-gray-900">
|
||||
{currentUserProfile?.phone || "---"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(MainPageProfile);
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./MainPageProfile";
|
||||
@@ -0,0 +1,300 @@
|
||||
"use client";
|
||||
|
||||
import { Fragment, memo, useEffect, useMemo, useState } from "react";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import Button from "@/components/customs/custom-button";
|
||||
import UploadAvatar from "@/components/utils/UploadAvatar";
|
||||
import GridColumn from "@/components/layouts/GridColumn";
|
||||
import Loading from "@/components/customs/custom-loading";
|
||||
|
||||
import { PATH } from "@/constant/config";
|
||||
import { QUERY_KEY } from "@/constant/config/enum";
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { httpRequest } from "@/services";
|
||||
import uploadServices from "@/services/uploadServices";
|
||||
import profileServices from "@/services/profileServices";
|
||||
|
||||
import { toastWarn } from "@/common/funcs/toast";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "@/redux/store";
|
||||
import icons from "@/constant/images/icons";
|
||||
|
||||
import { ContextFormCustom } from "@/components/utils/FormCustom/contexts";
|
||||
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
||||
import FormCustom from "@/components/utils/FormCustom/FormCustom";
|
||||
import moment from "moment";
|
||||
import { timeSubmit } from "@/common/funcs/optionConvert";
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
|
||||
function MainUpdateProfile() {
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { infoUser } = useSelector((state: RootState) => state.user);
|
||||
const [fileAvatar, setFileAvatar] = useState<File | null>(null);
|
||||
|
||||
// Khởi tạo form dựa hoàn toàn trên các trường Backend trả về
|
||||
const [form, setForm] = useState<{
|
||||
fullName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
birthDate: string;
|
||||
avatarUrl: string;
|
||||
}>({
|
||||
fullName: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
birthDate: "",
|
||||
avatarUrl: "",
|
||||
});
|
||||
|
||||
/* =========================================================
|
||||
1. GỌI API LIST PROFILE & LỌC USER ĐANG ĐĂNG NHẬP
|
||||
========================================================= */
|
||||
const profileQuery = useQuery<any>({
|
||||
queryKey: [QUERY_KEY.table_list_profile],
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<any>({
|
||||
showMessageFailed: true,
|
||||
http: profileServices.getProfile(),
|
||||
});
|
||||
return (
|
||||
res || {
|
||||
items: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/preserve-manual-memoization
|
||||
const currentUserProfile = useMemo(() => {
|
||||
const listItems = profileQuery.data?.items || [];
|
||||
if (!infoUser?.userId) return null;
|
||||
return (
|
||||
listItems.find((item: any) => item.accountId === infoUser.userId) || null
|
||||
);
|
||||
}, [profileQuery.data, infoUser?.userId]);
|
||||
|
||||
/* =========================================================
|
||||
2. ĐỔ DỮ LIỆU TỪ PROFILE VÀO FORM KHI TẢI TRANG
|
||||
========================================================= */
|
||||
useEffect(() => {
|
||||
if (currentUserProfile) {
|
||||
setForm({
|
||||
fullName: currentUserProfile.fullName || "",
|
||||
email: currentUserProfile.email || "",
|
||||
phone: currentUserProfile.phone || "",
|
||||
// Định dạng input date HTML yêu cầu format YYYY-MM-DD
|
||||
birthDate: currentUserProfile.birthDate
|
||||
? moment(currentUserProfile.birthDate).format("YYYY-MM-DD")
|
||||
: "",
|
||||
avatarUrl: currentUserProfile.avatarUrl || "",
|
||||
});
|
||||
}
|
||||
}, [currentUserProfile]);
|
||||
|
||||
/* =========================================================
|
||||
3. API MUTATION UPDATE PROFILE (Thay thế API userServices cũ)
|
||||
========================================================= */
|
||||
const funcUpdateProfile = useMutation({
|
||||
mutationFn: (body: { path: string }) =>
|
||||
httpRequest({
|
||||
showMessageFailed: true,
|
||||
showMessageSuccess: true,
|
||||
msgSuccess: "Chỉnh sửa thông tin thành công!",
|
||||
// Bạn có thể đổi sang endpoint PATCH/PUT tùy thuộc vào route Update của bạn
|
||||
http: profileServices.putProfile(currentUserProfile?.id, {
|
||||
fullName: form.fullName,
|
||||
email: form.email,
|
||||
phone: form.phone,
|
||||
birthDate: form.birthDate,
|
||||
avatarUrl: body.path || form.avatarUrl,
|
||||
}), // Lưu ý: Hãy cập nhật đúng service update của user profile tại đây
|
||||
}),
|
||||
onSuccess() {
|
||||
// Làm mới dữ liệu list profile trên toàn hệ thống (bao gồm cả Header và Profile Page)
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEY.table_list_profile],
|
||||
});
|
||||
router.push(PATH.PROFILE || "/profile");
|
||||
},
|
||||
onError() {
|
||||
toastWarn({ msg: "Cập nhật thông tin thất bại!" });
|
||||
},
|
||||
});
|
||||
|
||||
/* =========================================================
|
||||
4. XỬ LÝ SUBMIT (UPLOAD AVATAR TRƯỚC - UPDATE PROFILE SAU)
|
||||
========================================================= */
|
||||
const handleSubmit = async () => {
|
||||
const today = new Date(timeSubmit(new Date())!);
|
||||
const birthDay = new Date(form.birthDate);
|
||||
|
||||
if (today < birthDay) {
|
||||
return toastWarn({ msg: "Ngày sinh không hợp lệ!" });
|
||||
}
|
||||
try {
|
||||
// Nếu người dùng chọn file avatar mới
|
||||
if (fileAvatar) {
|
||||
const uploadResult: any = await uploadServices.uploadImage({
|
||||
Files: [fileAvatar],
|
||||
});
|
||||
|
||||
const uploadedUrl =
|
||||
uploadResult?.fileUrls?.[0] ||
|
||||
uploadResult?.data?.fileUrls?.[0] ||
|
||||
"";
|
||||
|
||||
return funcUpdateProfile.mutate({ path: uploadedUrl });
|
||||
}
|
||||
|
||||
// Nếu không thay đổi ảnh, dùng lại link avatar cũ
|
||||
return funcUpdateProfile.mutate({ path: form.avatarUrl });
|
||||
} catch (error) {
|
||||
console.error("Lỗi cập nhật ảnh đại diện:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Loading
|
||||
loading={funcUpdateProfile.isPending || profileQuery.isLoading}
|
||||
/>
|
||||
<FormCustom form={form} setForm={setForm} onSubmit={handleSubmit}>
|
||||
<div className="flex flex-col p-6 bg-white/80 rounded-2xl gap-6 shadow-sm">
|
||||
{/* HEADER */}
|
||||
<div className="flex items-center justify-between w-full border-b pb-4">
|
||||
<h2 className="text-xl font-semibold text-gray-800">
|
||||
Chỉnh sửa thông tin tài khoản
|
||||
</h2>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<CustomButton
|
||||
variant="grey"
|
||||
rounded="full"
|
||||
size="md"
|
||||
fullWidth={false}
|
||||
type="button"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
Hủy bỏ
|
||||
</CustomButton>
|
||||
<ContextFormCustom.Consumer>
|
||||
{({ isDone }) => {
|
||||
const hasRememberedData = !!(
|
||||
form.fullName &&
|
||||
form.email &&
|
||||
form.birthDate &&
|
||||
form.phone
|
||||
);
|
||||
const canSubmit = isDone || hasRememberedData;
|
||||
return (
|
||||
<CustomButton
|
||||
variant="midnightBlue"
|
||||
rounded="full"
|
||||
size="md"
|
||||
fullWidth={false}
|
||||
type="button"
|
||||
disabled={!canSubmit}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{funcUpdateProfile.isPending ? "Đang lưu..." : "Cập nhật"}
|
||||
</CustomButton>
|
||||
);
|
||||
}}
|
||||
</ContextFormCustom.Consumer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* MAIN FORM */}
|
||||
<div className="space-y-6">
|
||||
{/* KHỐI AVATAR */}
|
||||
<div className="flex justify-start">
|
||||
<UploadAvatar
|
||||
path={
|
||||
form?.avatarUrl
|
||||
? `${process.env.NEXT_PUBLIC_IMAGE}${form?.avatarUrl}`
|
||||
: icons.avatar
|
||||
}
|
||||
name="avatar"
|
||||
onSetFile={(file) => setFileAvatar(file)}
|
||||
resetPath={() => {
|
||||
setFileAvatar(null);
|
||||
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
avatarUrl: "",
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* KHỐI THÔNG TIN CÁ NHÂN CHUẨN BACKEND */}
|
||||
<div className="mt-4">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
{/* Cột trái */}
|
||||
<div className="space-y-4">
|
||||
<InputForm
|
||||
type="text"
|
||||
placeholder="Nhập họ và tên"
|
||||
name="fullName"
|
||||
value={form.fullName}
|
||||
label="Họ và tên"
|
||||
isRequired
|
||||
onChangeValue={(v) =>
|
||||
setForm((prev) => ({ ...prev, fullName: String(v) }))
|
||||
}
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
type="date"
|
||||
placeholder="Nhập ngày sinh"
|
||||
name="birthDate"
|
||||
label="Ngày sinh"
|
||||
value={form.birthDate}
|
||||
onChangeValue={(v) =>
|
||||
setForm((prev) => ({ ...prev, birthDate: String(v) }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Cột phải */}
|
||||
<div className="space-y-4">
|
||||
<InputForm
|
||||
placeholder="Nhập email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={form.email}
|
||||
label="Email"
|
||||
isRequired
|
||||
onChangeValue={(v) =>
|
||||
setForm((prev) => ({ ...prev, email: String(v) }))
|
||||
}
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
placeholder="Nhập số điện thoại"
|
||||
name="phone"
|
||||
type="number"
|
||||
isNumber
|
||||
value={form.phone}
|
||||
label="Số điện thoại"
|
||||
onChangeValue={(v) =>
|
||||
setForm((prev) => ({ ...prev, phone: String(v) }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormCustom>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(MainUpdateProfile);
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./MainUpdateProfile";
|
||||
@@ -136,7 +136,7 @@ const MainPageStatistical = () => {
|
||||
column={[
|
||||
{
|
||||
title: "MÃ CHUYÊN KHOA",
|
||||
render: (item) => <span>{item.specialtyId}</span>,
|
||||
render: (item) => <span>{item.specialtyCode}</span>,
|
||||
},
|
||||
{
|
||||
title: "TÊN CHUYÊN KHOA",
|
||||
|
||||
Reference in New Issue
Block a user