refactor: improve type safety by replacing any types with specific interfaces across API services and components.

This commit is contained in:
taDuc
2026-05-14 21:54:44 +07:00
parent dca3ca67ad
commit b220798978
20 changed files with 249 additions and 71 deletions
+10 -1
View File
@@ -73,7 +73,16 @@ const Calendar: React.FC = () => {
const handleEventClick = (clickInfo: EventClickArg) => {
const event = clickInfo.event;
setSelectedEvent(event as unknown as CalendarEvent);
setSelectedEvent({
id: event.id,
title: event.title,
start: event.startStr,
end: event.endStr,
extendedProps: {
calendar: event.extendedProps.calendar,
},
} as CalendarEvent);
setEventTitle(event.title);
setEventStartDate(event.start?.toISOString().split("T")[0] || "");
setEventEndDate(event.end?.toISOString().split("T")[0] || "");
+2 -1
View File
@@ -7,9 +7,10 @@ import "react-quill-new/dist/quill.snow.css";
const ReactQuillEditor = dynamic(
async () => {
const { default: RQ } = await import("react-quill-new");
return ({ forwardedRef, ...props }: any) => (
return ({ forwardedRef, ...props }: { forwardedRef: React.Ref<any>; [key: string]: any }) => (
<RQ ref={forwardedRef} {...props} />
);
},
{
ssr: false,
@@ -15,10 +15,13 @@ import { IsolatedContent } from "@/components/ui/IsolatedContent";
import { apiDeleteHistorianCV } from "@/service/historianService";
import { statusConfig } from "@/service/handler";
import { Application } from "@/interface/historian";
import { MediaItem } from "@/components/tables/MediaTable";
interface Props {
isOpen: boolean;
onClose: () => void;
application: any;
application: Application | null;
onRefresh: () => void;
}
@@ -42,7 +45,7 @@ export default function ApplicationDetailModal({
}
}, [isOpen, application]);
const isImageFile = (file: any) => {
const isImageFile = (file: MediaItem) => {
const isImageMime = file.mime_type?.startsWith("image/");
const isImageExt = /\.(jpg|jpeg|png|webp|gif)$/i.test(file.storage_key);
return isImageMime || isImageExt;
@@ -51,17 +54,17 @@ export default function ApplicationDetailModal({
const mediaList = application?.media || [];
const imageMediaOnly = mediaList.filter(isImageFile);
const imageSlides = imageMediaOnly.map((item: any) => ({
const imageSlides = imageMediaOnly.map((item: MediaItem) => ({
src: `${URL_MEDIA}${item.storage_key}`,
title: item.original_name,
description: `Dung lượng: ${(item.size / 1024).toFixed(2)} KB`,
}));
const handleMediaClick = (item: any) => {
const handleMediaClick = (item: MediaItem) => {
const fileUrl = `${URL_MEDIA}${item.storage_key}`;
if (isImageFile(item)) {
const photoIndex = imageMediaOnly.findIndex(
(img: any) => img.id === item.id,
(img: MediaItem) => img.id === item.id,
);
setIndex(photoIndex);
} else {
@@ -93,7 +96,9 @@ export default function ApplicationDetailModal({
};
const handleDeleteApplication = async () => {
if (!application) return;
await apiDeleteHistorianCV(application.id);
Swal.fire("Thành công!", "Hồ sơ đã được xóa.", "success");
onRefresh();
onClose();
@@ -186,7 +191,7 @@ export default function ApplicationDetailModal({
</h4>
{mediaList.length > 0 ? (
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4">
{mediaList.map((media: any, idx: number) => {
{mediaList.map((media: MediaItem, idx: number) => {
const isImg = isImageFile(media);
return (
<div
+2 -1
View File
@@ -23,7 +23,8 @@ export interface MediaItem {
original_name: string;
mime_type: string;
size: number;
file_metadata: any;
file_metadata: Record<string, unknown> | null;
created_at: string;
updated_at: string;
}
+16 -3
View File
@@ -2,6 +2,8 @@
import { Modal } from "../ui/modal";
import UserMetaCard from "@/components/user-profile/UserMetaCard";
import UserInfoCard from "@/components/user-profile/UserInfoCard";
import { UserMetaCardProps } from "@/interface/user";
import { fullDataUser } from "@/interface/admin";
import { useEffect, useState } from "react";
import { MediaDto } from "@/interface/media";
@@ -28,7 +30,17 @@ export default function UserDetailModal({
const [mediaData, setMediaData] = useState<MediaDto | null>(null);
const [loading, setLoading] = useState(true);
const formattedData = { data: user };
const formattedData: UserMetaCardProps = {
data: user
? {
id: user.id,
email: user.email,
profile: user.profile,
roles: user.roles as any, // UserRole and Role are slightly different, need a better fix or small cast
}
: undefined,
};
useEffect(() => {
if (user?.id && isOpen) {
@@ -68,8 +80,9 @@ export default function UserDetailModal({
</div>
<div className="space-y-6 custom-scrollbar max-h-[65vh] overflow-y-auto pr-2">
<UserMetaCard data={formattedData as any} />
<UserInfoCard data={formattedData as any} />
<UserMetaCard data={formattedData} />
<UserInfoCard data={formattedData} />
<div className="min-h-[150px] relative">
{loading ? (
+6 -2
View File
@@ -3,6 +3,8 @@
import React, { useState, useRef, useEffect } from "react";
import { ChatbotPayload } from "@/interface/chatbot";
import { apiChatbot } from "@/service/chatbotService";
import { AxiosError } from "axios";
type Message = {
id: string;
@@ -67,16 +69,18 @@ export default function ChatbotWidget({
};
setMessages((prev) => [...prev, botMessage]);
} catch (error: any) {
} catch (error) {
const axiosError = error as AxiosError<{ message: string }>;
const errorMessage: Message = {
id: (Date.now() + 1).toString(),
sender: "bot",
text:
error?.response?.data?.message ||
axiosError.response?.data?.message ||
"Có lỗi xảy ra khi kết nối. Vui lòng thử lại sau.",
};
setMessages((prev) => [...prev, errorMessage]);
} finally {
setIsLoading(false);
}
};
@@ -20,7 +20,10 @@ const formatFullDateTime = (dateString: string) => {
return `${time} ${day}`;
};
const processMedia = (mediaArray: any[]) => {
import { Application } from "@/interface/historian";
import { MediaItem } from "../tables/MediaTable";
const processMedia = (mediaArray: MediaItem[]) => {
if (!mediaArray || mediaArray.length === 0) return { type: "empty" };
const imageFiles = mediaArray.filter((file) => {
const isImageMime = file.mime_type?.startsWith("image/");
@@ -49,15 +52,16 @@ const processMedia = (mediaArray: any[]) => {
export default function ApplicationList({
applications,
}: {
applications: any[];
applications: Application[];
}) {
const router = useRouter();
const dispatch = useDispatch();
const handleViewDetail = (app: any) => {
const handleViewDetail = (app: Application) => {
dispatch(setSelectedApplication(app));
router.push(`/user/account/applications`);
};
const StatusIcons: Record<string, React.ReactNode> = {
APPROVED: (
+6 -3
View File
@@ -11,6 +11,8 @@ import { URL_MEDIA } from "../../../api";
import { deleteMedia } from "@/service/mediaService";
import { INITIAL_LIMIT } from "../../../constant";
import { MediaItem } from "../tables/MediaTable";
export default function MediaLibrary({
data,
@@ -32,7 +34,7 @@ export default function MediaLibrary({
setLocalMedia(data?.data || []);
}, [data]);
const isImageFile = (file: any) => {
const isImageFile = (file: MediaItem) => {
const isImageMime = file.mime_type?.startsWith("image/");
const isImageExt = /\.(jpg|jpeg|png|webp|gif)$/i.test(file.storage_key);
return isImageMime || isImageExt;
@@ -77,7 +79,7 @@ export default function MediaLibrary({
}
};
const handleItemClick = (item: any, idx: number, isImage: boolean) => {
const handleItemClick = (item: MediaItem, idx: number, isImage: boolean) => {
if (isSelectionMode) {
toggleItemSelection(item.id);
} else {
@@ -166,7 +168,7 @@ export default function MediaLibrary({
}
};
const renderItemCard = (item: any, isImage: boolean, idx: number) => {
const renderItemCard = (item: MediaItem, isImage: boolean, idx: number) => {
const isSelected = selectedIds.includes(item.id);
return (
@@ -254,6 +256,7 @@ export default function MediaLibrary({
);
};
return (
<div className="rounded-2xl border border-gray-200 bg-white p-6 dark:border-zinc-800 dark:bg-zinc-900/50">
<div className="mb-6 flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center">
+7 -3
View File
@@ -10,6 +10,8 @@ import { Profile, UserMetaCardProps } from "@/interface/user";
import { apiUpdateUser } from "@/service/userService";
import { toast } from "sonner";
import Link from "next/link";
import { AxiosError } from "axios";
export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
const router = useRouter();
@@ -62,8 +64,9 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
setTimeout(() => {
window.location.reload();
}, 1000);
} catch (error: any) {
const serverResponse = error.response?.data;
} catch (error) {
const axiosError = error as AxiosError<{ status: boolean; message: string }>;
const serverResponse = axiosError.response?.data;
if (serverResponse && serverResponse.status === false) {
const msg = serverResponse.message || "";
@@ -77,8 +80,9 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
toast.error("Không thể kết nối đến máy chủ hoặc lỗi hệ thống.");
}
console.error("Lỗi chi tiết:", error);
console.error("Lỗi chi tiết:", axiosError);
}
};
return (