This commit is contained in:
2026-04-17 18:01:19 +07:00
parent 5b6b59dc01
commit 9d35fd3653
6 changed files with 467 additions and 258 deletions

View File

@@ -32,14 +32,23 @@ type PendingFile = {
export default function RoleUpgrade() { export default function RoleUpgrade() {
const [content, setContent] = useState<string>(""); const [content, setContent] = useState<string>("");
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [verifyType, setVerifyType] = useState<string>("OTHER"); const [verifyType, setVerifyType] = useState<string>("OTHER");
const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([]); const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([]);
const [lightboxIndex, setLightboxIndex] = useState(-1); const [lightboxIndex, setLightboxIndex] = useState(-1);
const [isPreparingFiles, setIsPreparingFiles] = useState(false); const [isPreparingFiles, setIsPreparingFiles] = useState(false);
const [showPreview, setShowPreview] = useState(false);
const cleanHTMLContent = (rawHtml: string) => {
if (!rawHtml) return "";
const doc = new DOMParser().parseFromString(rawHtml, "text/html");
let decoded = doc.documentElement.textContent || rawHtml;
decoded = decoded.replace(/<pre[^>]*>/g, "").replace(/<\/pre>/g, "");
return decoded;
};
const handleContentChange = (value: string) => { const handleContentChange = (value: string) => {
setContent(value); setContent(value);
}; };
@@ -51,12 +60,10 @@ export default function RoleUpgrade() {
if (!files || files.length === 0) return; if (!files || files.length === 0) return;
setIsPreparingFiles(true); setIsPreparingFiles(true);
try { try {
const newFilesPromises = Array.from(files).map(async (file) => { const newFilesPromises = Array.from(files).map(async (file) => {
const isImage = file.type.startsWith("image/"); const isImage = file.type.startsWith("image/");
const extension = file.name.split(".").pop()?.toLowerCase() || ""; const extension = file.name.split(".").pop()?.toLowerCase() || "";
const presigned = await getPresignedUrl(file); const presigned = await getPresignedUrl(file);
return { return {
@@ -74,8 +81,8 @@ export default function RoleUpgrade() {
const newPendingFiles = await Promise.all(newFilesPromises); const newPendingFiles = await Promise.all(newFilesPromises);
setPendingFiles((prev) => [...prev, ...newPendingFiles]); setPendingFiles((prev) => [...prev, ...newPendingFiles]);
} catch (error) { } catch (error) {
console.error("Lỗi khi chuẩn bị file đính kèm:", error); console.error("Lỗi khi chuẩn bị file:", error);
alert("Lỗi khi chuẩn bị kết nối upload. Vui lòng thử lại!"); toast.error("Lỗi khi chuẩn bị kết nối upload.");
} finally { } finally {
setIsPreparingFiles(false); setIsPreparingFiles(false);
if (event.target) event.target.value = ""; if (event.target) event.target.value = "";
@@ -89,6 +96,11 @@ export default function RoleUpgrade() {
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!content.trim()) {
toast.warning("Vui lòng nhập nội dung hồ sơ!");
return;
}
try { try {
setIsSubmitting(true); setIsSubmitting(true);
@@ -99,29 +111,28 @@ export default function RoleUpgrade() {
}); });
const uploadedMediaIds = await Promise.all(uploadPromises); const uploadedMediaIds = await Promise.all(uploadPromises);
const cleanPayloadContent = cleanHTMLContent(content);
const payload = { const payload = {
content: content, content: cleanPayloadContent,
media_ids: uploadedMediaIds, media_ids: uploadedMediaIds,
verify_type: verifyType, verify_type: verifyType,
}; };
console.log("Payload chuẩn bị gửi (JSON):", payload); await createHistorianCV(payload);
const cvResponse = await createHistorianCV(payload);
console.log("Response từ API:", cvResponse);
Swal.fire({ Swal.fire({
title: "Gửi yêu cầu thành công!", title: "Gửi yêu cầu thành công!",
text: "Yêu cầu nâng cấp đã được gi đi. Chúng tôi sẽ xem xét và phản hồi sớm nhất có thể. Vui lòng kiểm tra Email khi có thông báo mới.", text: "Hồ sơ của bạn đã được ghi nhận và đang chờ duyệt.",
icon: "success", icon: "success",
confirmButtonText: "OK",
}); });
setContent(""); setContent("");
setPendingFiles([]); setPendingFiles([]);
setVerifyType("ID_CARD"); setVerifyType("OTHER");
} catch (error) { } catch (error) {
console.error("Lỗi:", error); console.error("Lỗi submit:", error);
toast.error("Có lỗi xảy ra khi gửi yêu cầu. Vui lòng kiểm tra lại!"); toast.error("Có lỗi xảy ra khi gửi yêu cầu.");
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
@@ -155,10 +166,53 @@ export default function RoleUpgrade() {
}; };
return ( return (
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto pb-20">
<PageBreadcrumb pageTitle="Role Upgrade" /> <PageBreadcrumb pageTitle="Nâng cấp tài khoản Nhà sử học" />
<div className="flex items-center justify-between bg-white dark:bg-gray-900 p-2 rounded-xl border border-gray-200 dark:border-gray-800 mb-6">
<div className="flex gap-2">
<button
type="button"
onClick={() => setShowPreview(false)}
className={`px-4 py-2 text-sm font-bold rounded-lg transition-all ${!showPreview ? "bg-blue-600 text-white" : "text-gray-500 hover:bg-gray-100"}`}
>
Soạn thảo
</button>
<button
type="button"
onClick={() => setShowPreview(true)}
className={`px-4 py-2 text-sm font-bold rounded-lg transition-all ${showPreview ? "bg-blue-600 text-white" : "text-gray-500 hover:bg-gray-100"}`}
>
Xem trước giao diện
</button>
</div>
<span className="text-[10px] font-black uppercase text-gray-400 mr-2 tracking-widest">
{showPreview ? "Preview Mode" : "Edit Mode"}
</span>
</div>
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<RichTextEditor value={content} onChange={handleContentChange} /> <div className="relative min-h-[400px]">
{!showPreview ? (
<RichTextEditor value={content} onChange={handleContentChange} />
) : (
<div className="bg-white border border-zinc-200 rounded-xl shadow-inner overflow-hidden min-h-[400px]">
{content ? (
<div className="preview-wrapper bg-white text-black">
<div
dangerouslySetInnerHTML={{
__html: cleanHTMLContent(content),
}}
/>
</div>
) : (
<div className="flex flex-col items-center justify-center h-[400px] text-zinc-400 italic bg-gray-50 dark:bg-gray-900">
<p>Chưa nội dung đ xem trước</p>
</div>
)}
</div>
)}
</div>
<div className="p-5 bg-white border border-gray-200 rounded-2xl dark:border-gray-800 dark:bg-gray-900"> <div className="p-5 bg-white border border-gray-200 rounded-2xl dark:border-gray-800 dark:bg-gray-900">
<label className="block mb-3 text-sm font-semibold text-gray-700 dark:text-white"> <label className="block mb-3 text-sm font-semibold text-gray-700 dark:text-white">
@@ -167,43 +221,20 @@ export default function RoleUpgrade() {
<select <select
value={verifyType} value={verifyType}
onChange={(e) => setVerifyType(e.target.value)} onChange={(e) => setVerifyType(e.target.value)}
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-200 dark:bg-gray-800 dark:border-gray-700 transition-all cursor-pointer text-gray-700 dark:text-gray-200" className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-500 dark:bg-gray-800 dark:border-gray-700"
> >
<option value="OTHER">Tài liệu khác (Other)</option> <option value="OTHER">Tài liệu khác (Other)</option>
<option value="ID_CARD">Thẻ nhận dạng nhà nghiên cứu (ID Card)</option> <option value="ID_CARD">Thẻ nhận dạng (ID Card)</option>
<option value="EDUCATION">Bằng cấp giáo dục (Education)</option> <option value="EDUCATION">Bằng cấp giáo dục (Education)</option>
<option value="EXPERT">Chứng nhận chuyên gia (Expert)</option> <option value="EXPERT">Chứng nhận chuyên gia (Expert)</option>
</select> </select>
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
* Vui lòng chọn loại tài liệu tương ng với các tệp bạn đính kèm bên dưới.
</p>
<p className="mt-2 text-xs text-red-400 dark:text-gray-400">
* Nếu bạn đính kèm nhiều loại tài liệu, vui lòng chọn mục " Tài liệu khác (Other) ".
</p>
</div> </div>
{/* Upload Files */}
<div className="p-5 bg-white border border-gray-200 rounded-2xl dark:border-gray-800 dark:bg-gray-900"> <div className="p-5 bg-white border border-gray-200 rounded-2xl dark:border-gray-800 dark:bg-gray-900">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-800 dark:text-white"> <h3 className="text-lg font-semibold">Tài liệu đính kèm</h3>
Tài liệu đính kèm <label className="cursor-pointer px-4 py-2 rounded-lg border border-dashed border-blue-500 text-blue-500 hover:bg-blue-50 transition">
</h3>
<label
className={`flex items-center gap-2 cursor-pointer px-4 py-2 rounded-lg border border-dashed border-blue-500 text-blue-500 hover:bg-blue-50 transition ${isSubmitting ? "opacity-50 pointer-events-none" : ""}`}
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 4v16m8-8H4"
/>
</svg>
<span>Đính kèm tệp</span>
<input <input
type="file" type="file"
multiple multiple
@@ -211,50 +242,41 @@ export default function RoleUpgrade() {
accept="image/*,.pdf,.doc,.docx" accept="image/*,.pdf,.doc,.docx"
onChange={handleFileChange} onChange={handleFileChange}
/> />
<span>+ Thêm tệp</span>
</label> </label>
</div> </div>
{pendingFiles.length > 0 && ( {pendingFiles.length > 0 && (
<div className="grid grid-cols-2 gap-4 pt-4 mt-4 border-t border-gray-100 sm:grid-cols-4 md:grid-cols-5 dark:border-gray-800"> <div className="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-5 gap-4">
{pendingFiles.map((item) => { {pendingFiles.map((item) => {
const docStyle = getDocumentStyle(item.extension); const docStyle = getDocumentStyle(item.extension);
return ( return (
<div <div
key={item.id} key={item.id}
onClick={() => handleItemClick(item)} className="relative group aspect-square border rounded-xl overflow-hidden bg-gray-50"
className="relative overflow-hidden transition-colors border border-gray-200 cursor-pointer group aspect-square rounded-xl dark:border-gray-700 bg-gray-50 dark:bg-gray-800 hover:border-blue-400"
> >
{item.type === "image" ? ( {item.type === "image" ? (
<Image <Image
src={item.previewUrl} src={item.previewUrl}
alt={item.name} alt="preview"
fill fill
className="object-cover transition-transform group-hover:scale-110" className="object-cover"
/> />
) : ( ) : (
<div <div
className={`absolute inset-0 flex flex-col items-center justify-center p-3 ${docStyle.bg} dark:bg-gray-800`} className={`flex flex-col items-center justify-center h-full ${docStyle.bg} p-2`}
> >
<svg <span className={`text-xs font-bold ${docStyle.color}`}>
className={`w-10 h-10 ${docStyle.color} mb-2`} {item.extension.toUpperCase()}
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zm-1 1.5L18.5 9H13V3.5zM6 20V4h5v7h7v9H6z" />
</svg>
<span className="text-xs font-medium text-center text-gray-700 break-all line-clamp-2 dark:text-gray-300">
{item.name}
</span> </span>
<span className="text-[10px] text-gray-500 mt-1 uppercase font-bold"> <span className="text-[10px] truncate w-full text-center mt-1">
{item.extension} {item.name}
</span> </span>
</div> </div>
)} )}
<button <button
onClick={(e) => removePendingFile(item.id, e)} onClick={(e) => removePendingFile(item.id, e)}
className="absolute z-10 p-1 text-white transition-opacity bg-red-500 rounded-full opacity-0 top-1 right-1 group-hover:opacity-100 hover:bg-red-600" className="absolute top-1 right-1 bg-red-500 text-white rounded-full p-1 opacity-0 group-hover:opacity-100 transition"
> >
<svg <svg
className="w-3 h-3" className="w-3 h-3"
@@ -262,32 +284,9 @@ export default function RoleUpgrade() {
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<path <path d="M6 18L18 6M6 6l12 12" />
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg> </svg>
</button> </button>
{item.type === "image" && (
<div className="absolute inset-0 flex items-center justify-center transition-opacity opacity-0 pointer-events-none bg-black/40 group-hover:opacity-100">
<svg
className="w-6 h-6 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v3m0 0v3m0-3h3m-3 0H7"
/>
</svg>
</div>
)}
</div> </div>
); );
})} })}
@@ -298,26 +297,19 @@ export default function RoleUpgrade() {
<button <button
onClick={handleSubmit} onClick={handleSubmit}
disabled={isSubmitting || isPreparingFiles} disabled={isSubmitting || isPreparingFiles}
className={`w-full py-4 text-white font-bold rounded-2xl shadow-lg transition-all transform flex justify-center items-center gap-2 className={`w-full py-4 text-white font-bold rounded-2xl shadow-lg transition-all ${isSubmitting || isPreparingFiles ? "bg-gray-400" : "bg-blue-600 hover:bg-blue-700"}`}
${isSubmitting || isPreparingFiles ? "bg-gray-400 cursor-not-allowed" : "bg-blue-600 hover:bg-blue-700 active:scale-[0.98]"}`}
> >
{isSubmitting ? "Đang tải dữ liệu lên..." : "Gửi yêu cầu nâng cấp"} {isSubmitting ? "Đang xử lý..." : "Gửi yêu cầu nâng cấp"}
</button> </button>
</div> </div>
<Lightbox <Lightbox
index={lightboxIndex} index={lightboxIndex}
open={lightboxIndex >= 0} open={lightboxIndex >= 0}
close={() => setLightboxIndex(-1)} close={() => setLightboxIndex(-1)}
slides={slides} slides={slides}
plugins={[Zoom, Captions]} plugins={[Zoom, Captions]}
zoom={{ maxZoomPixelRatio: 10 }}
styles={{
root: {
zIndex: 999999,
"--yarl__color_backdrop": "rgba(0, 0, 0, 0.9)",
},
}}
/> />
</div> </div>
); );
} }

View File

@@ -1,38 +1,212 @@
"use client"; "use client";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { RootState } from "@/store/store"; import { RootState } from "@/store/store";
import { SafeHTMLRenderer } from "@/components/ui/parse/SafeHTMLRenderer"; import { SafeHTMLRenderer } from "@/components/ui/parse/SafeHTMLRenderer";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { apiDeleteHistorianCV } from "@/service/historianService";
import Swal from "sweetalert2";
// Import statusConfig để đồng bộ logic
import { statusConfig } from "@/service/handler";
import Lightbox from "yet-another-react-lightbox";
import Zoom from "yet-another-react-lightbox/plugins/zoom";
import Captions from "yet-another-react-lightbox/plugins/captions";
import "yet-another-react-lightbox/styles.css";
import "yet-another-react-lightbox/plugins/captions.css";
import { URL_MEDIA } from "../../../../../../api";
export default function ApplicationDetailPage() { export default function ApplicationDetailPage() {
const application = useSelector((state: RootState) => state.user.selectedApplication); const application = useSelector(
(state: RootState) => state.user.selectedApplication,
);
const router = useRouter();
const [isDeleting, setIsDeleting] = useState(false);
const [index, setIndex] = useState(-1);
if (!application) { if (!application) {
return <div className="p-10 text-center">Đang tải hoặc không dữ liệu...</div>; return (
<div className="p-10 text-center text-zinc-500 font-medium">
Đang tải hoặc không dữ liệu...
</div>
);
} }
const config = statusConfig[application.status] || statusConfig.PENDING;
const isImageFile = (file: any) => {
const isImageMime = file.mime_type?.startsWith("image/");
const isImageExt = /\.(jpg|jpeg|png|webp|gif)$/i.test(file.storage_key);
return isImageMime || isImageExt;
};
const imageMediaOnly = (application.media || []).filter(isImageFile);
const imageSlides = imageMediaOnly.map((item: any) => ({
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 fileUrl = `${URL_MEDIA}${item.storage_key}`;
if (isImageFile(item)) {
const photoIndex = imageMediaOnly.findIndex(
(img: any) => img.id === item.id,
);
setIndex(photoIndex);
} else {
const googleDocsUrl = `https://docs.google.com/viewer?url=${encodeURIComponent(fileUrl)}&embedded=true`;
window.open(googleDocsUrl, "_blank");
}
};
const handleDelete = async () => {
const isDarkMode = document.documentElement.classList.contains("dark");
const result = await Swal.fire({
title: "Xác nhận xóa?",
text: "Bạn sẽ không thể khôi phục lại đơn đăng ký này!",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#ef4444",
cancelButtonColor: "#71717a",
confirmButtonText: "Xóa ngay",
cancelButtonText: "Hủy",
background: isDarkMode ? "#18181b" : "#fff",
color: isDarkMode ? "#fff" : "#000",
});
if (result.isConfirmed) {
try {
setIsDeleting(true);
await apiDeleteHistorianCV(application.id);
await Swal.fire({
title: "Đã xóa!",
icon: "success",
timer: 1500,
showConfirmButton: false,
background: isDarkMode ? "#18181b" : "#fff",
color: isDarkMode ? "#fff" : "#000",
});
router.push("/profile");
} catch (error) {
Swal.fire({ title: "Lỗi!", icon: "error" });
} finally {
setIsDeleting(false);
}
}
};
console.log("Application Detail:", application); // Debug log để kiểm tra dữ liệu ứng dụng
return ( return (
<div className="p-6 bg-white dark:bg-gray-900 rounded-xl shadow"> <div className="max-w-5xl mx-auto p-6 bg-white dark:bg-zinc-900 rounded-2xl shadow-sm border dark:border-zinc-800">
<h2 className="text-2xl font-bold mb-6 border-b pb-2">Chi tiết Application</h2> <div className="flex justify-between items-center mb-8 border-b dark:border-zinc-800 pb-6">
<div className="space-y-6">
<div> <div>
<label className="text-sm font-semibold text-gray-400 uppercase">Loại yêu cầu:</label> <h2 className="text-xl font-semibold dark:text-zinc-50 tracking-tight">
<p className="text-lg font-medium">{application.verify_type}</p> Chi tiết đơn đăng
</h2>
</div> </div>
<div> <div
<label className="text-sm font-semibold text-gray-400 uppercase mb-3 block"> className={`flex items-center gap-1.5 px-2 py-1 rounded-md backdrop-blur-md border ${config.container}`}
Nội dung hiển thị (CV): >
</label> <span className={`text-[12px] font-bold uppercase tracking-wider`}>
{application.status}
{/* SỬ DỤNG Ở ĐÂY */} </span>
<div className="border border-gray-100 rounded-lg p-2 bg-gray-50">
<SafeHTMLRenderer html={application.content} />
</div>
</div> </div>
{/* Các phần khác như Media... */}
</div> </div>
<div className="space-y-10">
<section>
<label className="text-[11px] font-black text-zinc-400 uppercase tracking-widest mb-4 block">
Nội dung hiển thị (CV)
</label>
<div className="rounded-2xl border-2 border-zinc-50 dark:border-zinc-800 p-6 bg-zinc-50/50 dark:bg-zinc-950/30">
<SafeHTMLRenderer html={application.content} />
</div>
</section>
{application.media && application.media.length > 0 && (
<section>
<label className="text-[11px] font-black text-zinc-400 uppercase tracking-widest mb-4 block">
Tài liệu đính kèm ({application.media.length})
</label>
<div className="flex flex-row gap-4 overflow-x-auto pb-4 scrollbar-hide">
{application.media.map((item: any) => {
const isImg = isImageFile(item);
return (
<div
key={item.id}
onClick={() => handleMediaClick(item)}
className="group relative min-w-[160px] h-[160px] overflow-hidden rounded-2xl border-2 border-zinc-100 dark:border-zinc-800 cursor-pointer bg-zinc-100 dark:bg-zinc-900 flex flex-col items-center justify-center transition-all hover:border-blue-500"
>
{isImg ? (
<>
<img
src={`${URL_MEDIA}${item.storage_key}`}
alt={item.original_name}
className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-110"
/>
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-end p-3">
<p className="text-[10px] text-white font-bold truncate">
Xem nh
</p>
</div>
</>
) : (
<div className="flex flex-col items-center p-4">
<div className="w-12 h-12 mb-3 flex items-center justify-center bg-white dark:bg-zinc-800 rounded-xl shadow-inner text-2xl">
📄
</div>
<p className="text-[10px] font-black text-zinc-700 dark:text-zinc-200 uppercase tracking-tighter">
.{item.storage_key.split(".").pop()}
</p>
<p className="text-[9px] text-zinc-400 truncate w-28 mt-1 font-medium italic">
{item.original_name}
</p>
<span className="mt-3 text-[9px] bg-blue-600 text-white px-3 py-1 rounded-full font-black uppercase opacity-0 group-hover:opacity-100 transition-opacity">
Preview
</span>
</div>
)}
</div>
);
})}
</div>
</section>
)}
{application.status !== "APPROVED" && (
<div className="pt-8 border-t border-zinc-200 dark:border-zinc-800">
<div className="flex justify-end w-full">
<button
onClick={handleDelete}
disabled={isDeleting}
className="group flex items-center gap-2 px-8 py-3 bg-red-100 hover:bg-red-500 text-red-600 hover:text-white dark:bg-red-950/20 dark:hover:bg-red-600 dark:text-red-400 dark:hover:text-white text-sm font-black rounded-xl transition-all duration-300 disabled:opacity-50"
>
{isDeleting ? "ĐANG XỬ LÝ..." : "XÓA"}
</button>
</div>
</div>
)}
</div>
<Lightbox
index={index}
open={index >= 0}
close={() => setIndex(-1)}
slides={imageSlides}
plugins={[Zoom, Captions]}
styles={{
root: {
zIndex: 99999,
"--yarl__color_backdrop": "rgba(0, 0, 0, 0.95)",
},
}}
/>
</div> </div>
); );
} }

View File

@@ -3,11 +3,15 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import Image from "next/image"; import Image from "next/image";
import Swal from "sweetalert2"; import Swal from "sweetalert2";
import { ApplicationDto } from "@/interface/historian";
import { apiUpdateApplicationStatus } from "@/service/adminService"; import { apiUpdateApplicationStatus } from "@/service/adminService";
import Link from "next/link";
import { URL_MEDIA } from "../../../api"; import { URL_MEDIA } from "../../../api";
import Lightbox from "yet-another-react-lightbox";
import Zoom from "yet-another-react-lightbox/plugins/zoom";
import Captions from "yet-another-react-lightbox/plugins/captions";
import "yet-another-react-lightbox/styles.css";
import "yet-another-react-lightbox/plugins/captions.css";
interface Props { interface Props {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
@@ -24,6 +28,9 @@ export default function ApplicationDetailModal({
const [reviewNote, setReviewNote] = useState(""); const [reviewNote, setReviewNote] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
const [index, setIndex] = useState(-1);
useEffect(() => { useEffect(() => {
if (isOpen && application) { if (isOpen && application) {
setReviewNote(application.review_note || ""); setReviewNote(application.review_note || "");
@@ -32,21 +39,44 @@ export default function ApplicationDetailModal({
} }
}, [isOpen, application]); }, [isOpen, application]);
const isImageFile = (file: any) => {
const isImageMime = file.mime_type?.startsWith("image/");
const isImageExt = /\.(jpg|jpeg|png|webp|gif)$/i.test(file.storage_key);
return isImageMime || isImageExt;
};
const mediaList = application?.media || [];
const imageMediaOnly = mediaList.filter(isImageFile);
const imageSlides = imageMediaOnly.map((item: any) => ({
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 fileUrl = `${URL_MEDIA}${item.storage_key}`;
if (isImageFile(item)) {
const photoIndex = imageMediaOnly.findIndex((img: any) => img.id === item.id);
setIndex(photoIndex);
} else {
const googleDocsUrl = `https://docs.google.com/viewer?url=${encodeURIComponent(fileUrl)}&embedded=true`;
window.open(googleDocsUrl, "_blank");
}
};
const handleUpdateStatus = async (status: "APPROVED" | "REJECTED") => { const handleUpdateStatus = async (status: "APPROVED" | "REJECTED") => {
if (!application) return; if (!application) return;
if (!reviewNote.trim()) { if (!reviewNote.trim()) {
textareaRef.current?.focus(); textareaRef.current?.focus();
return; return;
} }
try { try {
setIsSubmitting(true); setIsSubmitting(true);
await apiUpdateApplicationStatus(application.id, { await apiUpdateApplicationStatus(application.id, {
status, status,
review_note: reviewNote, review_note: reviewNote,
}); });
Swal.fire("Thành công!", "Trạng thái hồ sơ đã được cập nhật.", "success"); Swal.fire("Thành công!", "Trạng thái hồ sơ đã được cập nhật.", "success");
onRefresh(); onRefresh();
onClose(); onClose();
@@ -60,65 +90,33 @@ export default function ApplicationDetailModal({
if (!isOpen || !application) return null; if (!isOpen || !application) return null;
const userData = application.user || {}; const userData = application.user || {};
const mediaList = application.media || [];
return ( return (
<div className="fixed inset-0 z-999 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"> <div className="fixed inset-0 z-999 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
<div className="w-full max-w-4xl max-h-[90vh] bg-white rounded-2xl dark:bg-gray-900 flex flex-col overflow-hidden text-gray-800 dark:text-gray-200"> <div className="w-full max-w-4xl max-h-[90vh] bg-white rounded-2xl dark:bg-gray-900 flex flex-col overflow-hidden text-gray-800 dark:text-gray-200">
{/* HEADER */}
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-800"> <div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-800">
<h3 className="text-xl font-semibold">Chi tiết yêu cầu nâng cấp</h3> <h3 className="text-xl font-semibold">Chi tiết yêu cầu nâng cấp</h3>
<button <button onClick={onClose} className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full">
onClick={onClose} <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full transition-colors" <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg> </svg>
</button> </button>
</div> </div>
{/* BODY */}
<div className="flex-1 p-6 overflow-y-auto custom-scrollbar"> <div className="flex-1 p-6 overflow-y-auto custom-scrollbar">
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
{/* THÔNG TIN NGƯỜI DÙNG */}
<div className="p-5 border border-gray-200 rounded-xl dark:border-gray-800 bg-gray-50/50 dark:bg-white/[0.02]"> <div className="p-5 border border-gray-200 rounded-xl dark:border-gray-800 bg-gray-50/50 dark:bg-white/[0.02]">
<h4 className="mb-4 text-xs font-bold text-gray-500 uppercase tracking-wider"> <h4 className="mb-4 text-xs font-bold text-gray-500 uppercase tracking-wider">Thông tin ng viên</h4>
Thông tin ng viên
</h4>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="relative w-16 h-16 overflow-hidden border border-gray-200 rounded-full shrink-0 dark:border-gray-700"> <div className="relative w-16 h-16 overflow-hidden border border-gray-200 rounded-full shrink-0">
<Image <Image fill src={userData.avatar_url || "/images/no-images.jpg"} alt="avatar" className="object-cover" />
fill
src={userData.avatar_url || "/images/no-images.jpg"}
alt="avatar"
className="object-cover"
/>
</div> </div>
<div> <div>
<h4 className="text-lg font-semibold"> <h4 className="text-lg font-semibold">{userData.display_name || "N/A"}</h4>
{userData.display_name || "N/A"} <p className="text-sm text-gray-500">{userData.email || "Không có email"}</p>
</h4>
<p className="text-sm text-gray-500">
{userData.email || "Không có email"}
</p>
<div className="mt-1 flex gap-2"> <div className="mt-1 flex gap-2">
<span className="px-2 py-0.5 text-[10px] font-bold uppercase text-blue-600 bg-blue-100 rounded-md dark:bg-blue-500/20 dark:text-blue-400"> <span className="px-2 py-0.5 text-[10px] font-bold uppercase text-blue-600 bg-blue-100 rounded-md">{application.verify_type}</span>
{application.verify_type} <span className={`px-2 py-0.5 text-[10px] font-semibold uppercase rounded-md ${application.status === "PENDING" ? "bg-yellow-100 text-yellow-600" : "bg-red-100 text-red-500"}`}>
</span>
<span
className={`px-2 py-0.5 text-[10px] font-semibold uppercase rounded-md ${application.status === "PENDING" ? "bg-yellow-100 text-yellow-600" : "bg-red-100 text-red-500"}`}
>
{application.status} {application.status}
</span> </span>
</div> </div>
@@ -127,80 +125,53 @@ export default function ApplicationDetailModal({
</div> </div>
<div> <div>
<h4 className="mb-3 text-xs font-bold text-gray-500 uppercase tracking-wider"> <h4 className="mb-3 text-xs font-bold text-gray-500 uppercase tracking-wider">Nội dung ng tuyển</h4>
Nội dung ng tuyển
</h4>
<div <div
className="p-4 prose bg-white border border-gray-200 min-h-[100px] rounded-xl dark:border-gray-800 dark:bg-gray-900 dark:text-gray-200 max-w-none" className="p-4 prose bg-white border border-gray-200 min-h-[100px] rounded-xl dark:border-gray-800 dark:bg-gray-900 dark:text-gray-200 max-w-none"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{ __html: application.content || "<i>Không có nội dung.</i>" }}
__html: application.content || "<i>Không có nội dung.</i>",
}}
/> />
</div> </div>
<div> <div>
<h4 className="mb-3 text-xs font-bold text-gray-500 uppercase tracking-wider"> <h4 className="mb-3 text-xs font-bold text-gray-500 uppercase tracking-wider">Tệp đính kèm ({mediaList.length})</h4>
Tệp đính kèm ({mediaList.length})
</h4>
{mediaList.length > 0 ? ( {mediaList.length > 0 ? (
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4"> <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: any, idx: number) => {
const isImage = media.mime_type?.startsWith("image/"); const isImg = isImageFile(media);
return ( return (
<div <div
key={idx} key={idx}
className="relative overflow-hidden border border-gray-200 group aspect-square rounded-xl dark:border-gray-700 bg-gray-50 dark:bg-gray-800" onClick={() => handleMediaClick(media)}
className="relative overflow-hidden border border-gray-200 group aspect-square rounded-xl dark:border-gray-700 bg-gray-50 dark:bg-gray-800 cursor-pointer"
> >
{isImage ? ( {isImg ? (
<Image <Image src={`${URL_MEDIA}${media.storage_key}`} alt="media" fill className="object-cover transition-transform duration-300 group-hover:scale-110" />
src={`${URL_MEDIA}${media.storage_key}`}
alt="media"
fill
className="object-cover transition-transform duration-300 group-hover:scale-110"
/>
) : ( ) : (
<div className="flex flex-col items-center justify-center w-full h-full p-3 text-center"> <div className="flex flex-col items-center justify-center w-full h-full p-3 text-center">
<svg <div className="w-10 h-10 mb-2 flex items-center justify-center bg-white dark:bg-zinc-800 rounded-lg shadow-sm text-xl">📄</div>
className="w-10 h-10 mb-2 text-blue-500" <span className="text-[10px] font-medium text-gray-600 line-clamp-2">{media.original_name}</span>
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zm-1 1.5L18.5 9H13V3.5zM6 20V4h5v7h7v9H6z" />
</svg>
<span className="text-[10px] font-medium text-gray-600 line-clamp-2">
{media.original_name}
</span>
</div> </div>
)} )}
<Link <div className="absolute inset-0 flex items-center justify-center transition-opacity bg-black/40 opacity-0 group-hover:opacity-100 z-10">
className="absolute inset-0 flex items-center justify-center transition-opacity bg-black/40 opacity-0 group-hover:opacity-100 z-10"
href={`${URL_MEDIA}${media.storage_key}`}
target="_blank"
>
<span className="text-white text-xs font-bold px-3 py-1 bg-white/20 backdrop-blur-md rounded-full border border-white/30"> <span className="text-white text-xs font-bold px-3 py-1 bg-white/20 backdrop-blur-md rounded-full border border-white/30">
Xem file {isImg ? "Xem ảnh" : "Xem file"}
</span> </span>
</Link> </div>
</div> </div>
); );
})} })}
</div> </div>
) : ( ) : (
<p className="text-sm italic text-gray-400"> <p className="text-sm italic text-gray-400">Không tệp đính kèm.</p>
Không tệp đính kèm.
</p>
)} )}
</div> </div>
{/* GHI CHÚ */}
<div> <div>
<h4 className="mb-3 text-xs font-bold text-gray-500 uppercase tracking-wider"> <h4 className="mb-3 text-xs font-bold text-gray-500 uppercase tracking-wider">Ghi chú duyệt hồ </h4>
Ghi chú duyệt hồ
</h4>
<textarea <textarea
ref={textareaRef} ref={textareaRef}
className="w-full p-4 text-sm bg-white border border-gray-200 rounded-xl dark:border-gray-800 dark:bg-gray-900 focus:ring-2 focus:ring-blue-500 outline-none resize-none h-[100px]" className="w-full p-4 text-sm bg-white border border-gray-200 rounded-xl dark:border-gray-800 dark:bg-gray-900 focus:ring-2 focus:ring-blue-500 outline-none h-[100px]"
placeholder="Nhập lý do (bắt buộc) hoặc ghi chú thêm..." placeholder="Nhập lý do (bắt buộc)..."
value={reviewNote} value={reviewNote}
onChange={(e) => setReviewNote(e.target.value)} onChange={(e) => setReviewNote(e.target.value)}
disabled={application.status !== "PENDING"} disabled={application.status !== "PENDING"}
@@ -209,34 +180,25 @@ export default function ApplicationDetailModal({
</div> </div>
</div> </div>
{/* FOOTER */}
<div className="flex items-center justify-end gap-3 px-6 py-4 bg-gray-50 border-t border-gray-200 dark:bg-gray-900/50 dark:border-gray-800"> <div className="flex items-center justify-end gap-3 px-6 py-4 bg-gray-50 border-t border-gray-200 dark:bg-gray-900/50 dark:border-gray-800">
<button <button onClick={onClose} className="px-5 py-2 text-sm font-medium border border-gray-300 rounded-xl hover:bg-gray-100 transition-colors">Đóng</button>
onClick={onClose}
className="px-5 py-2 text-sm font-medium border border-gray-300 rounded-xl hover:bg-gray-100 transition-colors"
>
Đóng
</button>
{application.status === "PENDING" && ( {application.status === "PENDING" && (
<> <>
<button <button onClick={() => handleUpdateStatus("REJECTED")} disabled={isSubmitting} className="px-5 py-2 text-sm font-medium text-white bg-red-500 rounded-xl hover:bg-red-600">Từ chối</button>
onClick={() => handleUpdateStatus("REJECTED")} <button onClick={() => handleUpdateStatus("APPROVED")} disabled={isSubmitting} className="px-5 py-2 text-sm font-medium text-white bg-green-500 rounded-xl hover:bg-green-600">Phê duyệt</button>
disabled={isSubmitting}
className="px-5 py-2 text-sm font-medium text-white bg-red-500 rounded-xl hover:bg-red-600 disabled:opacity-50 transition-all"
>
Từ chối
</button>
<button
onClick={() => handleUpdateStatus("APPROVED")}
disabled={isSubmitting}
className="px-5 py-2 text-sm font-medium text-white bg-green-500 rounded-xl hover:bg-green-600 disabled:opacity-50 transition-all"
>
Phê duyệt
</button>
</> </>
)} )}
</div> </div>
</div> </div>
<Lightbox
index={index}
open={index >= 0}
close={() => setIndex(-1)}
slides={imageSlides}
plugins={[Zoom, Captions]}
styles={{ root: { zIndex: 999999 } }}
/>
</div> </div>
); );
} }

View File

@@ -4,11 +4,19 @@ import { setSelectedApplication } from "@/store/features/userSlice";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { URL_MEDIA } from "../../../api"; import { URL_MEDIA } from "../../../api";
import { statusConfig } from "@/service/handler";
const formatFullDateTime = (dateString: string) => { const formatFullDateTime = (dateString: string) => {
const date = new Date(dateString); const date = new Date(dateString);
const time = date.toLocaleTimeString("vi-VN", { hour: "2-digit", minute: "2-digit" }); const time = date.toLocaleTimeString("vi-VN", {
const day = date.toLocaleDateString("vi-VN", { day: "2-digit", month: "2-digit", year: "numeric" }); hour: "2-digit",
minute: "2-digit",
});
const day = date.toLocaleDateString("vi-VN", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
return `${time} ${day}`; return `${time} ${day}`;
}; };
@@ -20,18 +28,29 @@ const processMedia = (mediaArray: any[]) => {
return isImageMime || isImageExt; return isImageMime || isImageExt;
}); });
const docFiles = mediaArray.filter((file) => { const docFiles = mediaArray.filter((file) => {
const isImage = file.mime_type?.startsWith("image/") || /\.(jpg|jpeg|png|webp|gif)$/i.test(file.storage_key); const isImage =
file.mime_type?.startsWith("image/") ||
/\.(jpg|jpeg|png|webp|gif)$/i.test(file.storage_key);
return !isImage; return !isImage;
}); });
if (imageFiles.length > 0) return { type: "image", src: `${URL_MEDIA}${imageFiles[0].storage_key}` }; if (imageFiles.length > 0)
return { type: "image", src: `${URL_MEDIA}${imageFiles[0].storage_key}` };
if (docFiles.length > 0) { if (docFiles.length > 0) {
const extensions = docFiles.map((file) => file.mime_type ? file.mime_type.split("/")[1] : file.storage_key.split(".").pop() || "file"); const extensions = docFiles.map((file) =>
file.mime_type
? file.mime_type.split("/")[1]
: file.storage_key.split(".").pop() || "file",
);
return { type: "documents", extensions }; return { type: "documents", extensions };
} }
return { type: "empty" }; return { type: "empty" };
}; };
export default function ApplicationSquareCardList({ applications }: { applications: any[] }) { export default function ApplicationSquareCardList({
applications,
}: {
applications: any[];
}) {
const router = useRouter(); const router = useRouter();
const dispatch = useDispatch(); const dispatch = useDispatch();
@@ -45,16 +64,21 @@ export default function ApplicationSquareCardList({ applications }: { applicatio
<h4 className="text-lg font-bold text-zinc-800 dark:text-white/90 mb-5 tracking-tight"> <h4 className="text-lg font-bold text-zinc-800 dark:text-white/90 mb-5 tracking-tight">
Applications CV Applications CV
</h4> </h4>
<div className="flex flex-wrap gap-4"> <div className="flex flex-wrap gap-4">
{applications?.map((app) => { {applications?.map((app) => {
const mediaState = processMedia(app.media); const mediaState = processMedia(app.media);
// --- LOGIC STATUS NẰM TRONG VÒNG LẶP ---
const config = statusConfig[app.status] || statusConfig.PENDING;
return ( return (
<div <div
key={app.id} key={app.id}
onClick={() => handleViewDetail(app)} onClick={() => handleViewDetail(app)}
className="group relative h-40 aspect-square border dark:border-zinc-800 rounded-xl cursor-pointer overflow-hidden transition-all duration-300 hover:ring-2 hover:ring-blue-500/50" className="group relative h-60 aspect-square border dark:border-zinc-800 rounded-xl cursor-pointer overflow-hidden transition-all duration-300 hover:ring-2 hover:ring-blue-500/50"
> >
{/* BACKGROUND */}
<div className="absolute inset-0 z-0"> <div className="absolute inset-0 z-0">
{mediaState.type === "image" ? ( {mediaState.type === "image" ? (
<img <img
@@ -67,7 +91,10 @@ export default function ApplicationSquareCardList({ applications }: { applicatio
{mediaState.type === "documents" ? ( {mediaState.type === "documents" ? (
<div className="flex flex-wrap gap-1 justify-center"> <div className="flex flex-wrap gap-1 justify-center">
{mediaState.extensions?.slice(0, 3).map((ext, i) => ( {mediaState.extensions?.slice(0, 3).map((ext, i) => (
<span key={i} className="text-[9px] font-black px-1.5 py-0.5 bg-white dark:bg-zinc-800 rounded border dark:border-zinc-700 uppercase"> <span
key={i}
className="text-[9px] font-black px-1.5 py-0.5 bg-white dark:bg-zinc-800 rounded border dark:border-zinc-700 uppercase"
>
.{ext} .{ext}
</span> </span>
))} ))}
@@ -79,15 +106,24 @@ export default function ApplicationSquareCardList({ applications }: { applicatio
)} )}
</div> </div>
{/* OVERLAY */}
<div className="absolute inset-0 z-10 bg-gradient-to-t from-black/90 via-black/20 to-transparent transition-opacity group-hover:opacity-90" /> <div className="absolute inset-0 z-10 bg-gradient-to-t from-black/90 via-black/20 to-transparent transition-opacity group-hover:opacity-90" />
{/* TOP INFO: STATUS & FILE COUNT */}
<div className="absolute top-2 left-2 right-2 z-20 flex justify-between items-start"> <div className="absolute top-2 left-2 right-2 z-20 flex justify-between items-start">
<div className={`flex items-center gap-1.5 rounded-md backdrop-blur-md border border-white/10 ${ <div
app.status === "PENDING" ? "bg-amber-500/20 text-amber-400" : "bg-emerald-500/20 text-emerald-400" className={`flex items-center p-1 rounded-full border ${config.container}`}
}`}> >
<span className={`w-2 h-2 rounded-full animate-pulse ${app.status === "PENDING" ? "bg-amber-500" : "bg-emerald-500"}`} /> <span
className={`w-2 h-2 rounded-full ${config.dot} ${
app.status === "PENDING" ? "animate-pulse" : ""
}`}
/>
{/* <span className="text-[9px] font-bold uppercase tracking-wider">
{app.status}
</span> */}
</div> </div>
{app.media?.length > 0 && ( {app.media?.length > 0 && (
<span className="text-[9px] font-bold text-white/60 bg-black/40 px-1.5 py-0.5 rounded-md backdrop-blur-sm"> <span className="text-[9px] font-bold text-white/60 bg-black/40 px-1.5 py-0.5 rounded-md backdrop-blur-sm">
{app.media.length} FILE {app.media.length} FILE
@@ -95,13 +131,14 @@ export default function ApplicationSquareCardList({ applications }: { applicatio
)} )}
</div> </div>
{/* BOTTOM INFO */}
<div className="absolute bottom-2 left-2 right-2 z-20 text-white"> <div className="absolute bottom-2 left-2 right-2 z-20 text-white">
<div className="mb-1"> <div className="mb-1">
<p className="text-[10px] font-black tracking-tighter truncate"> <p className="text-[10px] font-bold uppercase opacity-80 truncate">
{app.verify_type || "VERIFY"} {app.verify_type || "VERIFY"}
</p> </p>
{app?.reviewer?.display_name && ( {app?.reviewer?.display_name && (
<p className="text-[9px] font-medium text-white/50 truncate"> <p className="text-[9px] font-medium text-blue-400 truncate">
By: {app.reviewer.display_name} By: {app.reviewer.display_name}
</p> </p>
)} )}
@@ -112,8 +149,18 @@ export default function ApplicationSquareCardList({ applications }: { applicatio
{formatFullDateTime(app.created_at)} {formatFullDateTime(app.created_at)}
</p> </p>
<div className="transform translate-x-2 opacity-0 group-hover:translate-x-0 group-hover:opacity-100 transition-all duration-300"> <div className="transform translate-x-2 opacity-0 group-hover:translate-x-0 group-hover:opacity-100 transition-all duration-300">
<svg className="w-4 h-4 text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}> <svg
<path strokeLinecap="round" strokeLinejoin="round" d="M13 7l5 5m0 0l-5 5m5-5H6" /> className="w-4 h-4 text-blue-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={3}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M13 7l5 5m0 0l-5 5m5-5H6"
/>
</svg> </svg>
</div> </div>
</div> </div>
@@ -124,4 +171,4 @@ export default function ApplicationSquareCardList({ applications }: { applicatio
</div> </div>
</div> </div>
); );
} }

29
src/service/handler.ts Normal file
View File

@@ -0,0 +1,29 @@
export const statusConfig: Record<string, { container: string; dot: string }> = {
PENDING: {
container: `
bg-amber-50
border-amber-200
text-amber-700
shadow-sm
`,
dot: "bg-amber-500",
},
APPROVED: {
container: `
bg-emerald-50
border-emerald-200
text-emerald-700
shadow-sm
`,
dot: "bg-emerald-500",
},
REJECTED: {
container: `
bg-red-50
border-red-200
text-red-700
shadow-sm
`,
dot: "bg-red-500",
},
};

View File

@@ -10,3 +10,8 @@ export const apiGetUserApplications = async (payload :any) => {
const response = await api.get(API.Historian.APPLICATION, { params: payload }); const response = await api.get(API.Historian.APPLICATION, { params: payload });
return response?.data; return response?.data;
}; };
export const apiDeleteHistorianCV = async (id: number | string) => {
const response = await api.delete(API.Historian.DELETE_CV(id));
return response?.data;
};