mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 16:28:12 +00:00
fix ui
This commit is contained in:
@@ -1,378 +1,378 @@
|
||||
import { useQuery, useQueries } from "@tanstack/react-query";
|
||||
import { getMyUploadedFiles } from "@/external-portal/services/portalOutgoingService";
|
||||
import { getDocumentRequirementsById } from "@/shared/services/organizationsService";
|
||||
import { getUserById } from "@/super-admin/services/api/userService";
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardContent,
|
||||
} from "@/shared/common/ui/card";
|
||||
import { Button } from "@/shared/common/ui/button";
|
||||
import { ScrollArea } from "@/shared/common/ui/scroll-area";
|
||||
import { Skeleton } from "@/shared/common/ui/skeleton";
|
||||
import { Alert, AlertDescription } from "@/shared/common/ui/alert";
|
||||
import { FileText, RefreshCw, User, Mail, Phone, Building } from "lucide-react";
|
||||
import { useLocalizedName } from "@/shared/common/localizedName";
|
||||
import {
|
||||
DocumentRequirementResponseDto,
|
||||
DocumentResponseDto,
|
||||
} from "@/shared/dto/External-Portal/External-PortalDto";
|
||||
import ActionModal from "./ResponseActions";
|
||||
import { toast } from "sonner";
|
||||
import { useGiveResponse } from "@/shared/hooks/useOrganizationReport";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/shared/common/ui/tooltip";
|
||||
|
||||
type ViewDocumentProps = {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
interface UploadsResponse {
|
||||
items: DocumentResponseDto[];
|
||||
counts: number;
|
||||
}
|
||||
|
||||
interface UserInfo {
|
||||
id: string;
|
||||
name: {
|
||||
am: string;
|
||||
en: string;
|
||||
};
|
||||
email: string;
|
||||
phoneNumber: string;
|
||||
username: string;
|
||||
userType: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
const ViewDocument: React.FC<ViewDocumentProps> = ({ userId }) => {
|
||||
const localizedName = useLocalizedName();
|
||||
const { t } = useTranslation();
|
||||
const [status, setStatus] = useState<"APPROVED" | "REJECTED" | "PENDING">(
|
||||
"APPROVED"
|
||||
);
|
||||
|
||||
// Utility function to truncate email if too long
|
||||
const truncateEmail = (email: string, maxLength: number = 25) => {
|
||||
if (email.length <= maxLength) return email;
|
||||
const [localPart, domain] = email.split("@");
|
||||
if (localPart.length > maxLength - 10) {
|
||||
return `${localPart.substring(0, maxLength - 10)}...@${domain}`;
|
||||
}
|
||||
return email;
|
||||
};
|
||||
|
||||
const { mutate, isLoading: isGiveResponseLoading } = useGiveResponse();
|
||||
|
||||
// 1️⃣ Fetch user information
|
||||
const {
|
||||
data: userInfo,
|
||||
isLoading: isUserLoading,
|
||||
isError: isUserError,
|
||||
} = useQuery<UserInfo, Error>({
|
||||
queryKey: ["user-info", userId],
|
||||
queryFn: async () => {
|
||||
const res = await getUserById(userId);
|
||||
return res.data;
|
||||
},
|
||||
});
|
||||
|
||||
// 2️⃣ Fetch uploaded documents
|
||||
const {
|
||||
data: uploadedDocuments,
|
||||
isLoading,
|
||||
isError,
|
||||
refetch,
|
||||
isRefetching,
|
||||
} = useQuery<UploadsResponse, Error>({
|
||||
queryKey: ["external-letters", userId],
|
||||
queryFn: async () => {
|
||||
const res = await getMyUploadedFiles(userId);
|
||||
return {
|
||||
items: res.data.items,
|
||||
counts: res.data.counts,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// 3️⃣ Fetch requirements for each uploaded document
|
||||
const documentRequirementsQueries = useQueries({
|
||||
queries:
|
||||
uploadedDocuments?.items.map((doc) => ({
|
||||
queryKey: ["document-requirements", doc.documentId],
|
||||
queryFn: async (): Promise<DocumentRequirementResponseDto> => {
|
||||
const res = await getDocumentRequirementsById(doc.documentId);
|
||||
return res.data;
|
||||
},
|
||||
enabled: !!uploadedDocuments && uploadedDocuments.items.length > 0,
|
||||
})) ?? [],
|
||||
});
|
||||
|
||||
// 4️⃣ Action submit handler
|
||||
const handleActionSubmit = async (
|
||||
documentId: string,
|
||||
actionStatus:
|
||||
| "pending"
|
||||
| "submitted"
|
||||
| "approved"
|
||||
| "adjusted"
|
||||
| "rejected",
|
||||
remark?: string
|
||||
) => {
|
||||
try {
|
||||
await mutate({ id: documentId, data: { status: actionStatus, remark } });
|
||||
toast.success(
|
||||
t("viewDocument.actions.success", { action: actionStatus })
|
||||
);
|
||||
} catch (err: any) {
|
||||
toast.error(
|
||||
t("viewDocument.actions.error", {
|
||||
error: err.message || t("viewDocument.loading.failedToLoad"),
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-4xl mx-auto mt-8 shadow-lg border-gray-200 dark:border-gray-700">
|
||||
<CardHeader className="border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-2xl font-bold flex items-center gap-2">
|
||||
<FileText className="w-6 h-6 text-primary-600" />
|
||||
{t("viewDocument.title")}
|
||||
</CardTitle>
|
||||
<Button
|
||||
onClick={() => refetch()}
|
||||
variant="default"
|
||||
className="bg-primary-600 hover:bg-primary-700 text-white"
|
||||
disabled={isRefetching}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`mr-2 h-4 w-4 ${isRefetching ? "animate-spin" : ""}`}
|
||||
/>
|
||||
{t("viewDocument.refreshButton")}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-6">
|
||||
{/* User Information Section */}
|
||||
{isUserLoading ? (
|
||||
<div className="mb-6 space-y-4">
|
||||
<Skeleton className="h-6 w-48" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Skeleton className="h-16 w-full rounded-lg" />
|
||||
<Skeleton className="h-16 w-full rounded-lg" />
|
||||
<Skeleton className="h-16 w-full rounded-lg" />
|
||||
</div>
|
||||
</div>
|
||||
) : isUserError ? (
|
||||
<Alert variant="destructive" className="mb-6">
|
||||
<AlertDescription>
|
||||
{t("viewDocument.failedToLoadUserInfo")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : userInfo ? (
|
||||
<div className="mb-6 p-4 bg-gray-50 dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center gap-2">
|
||||
<User className="w-5 h-5 text-primary-600" />
|
||||
{t("viewDocument.userInformation")}
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="flex items-center gap-3 p-3 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<Building className="w-5 h-5 text-blue-600" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
{t("viewDocument.organizationName")}
|
||||
</p>
|
||||
<p className="text-base font-semibold text-gray-900 dark:text-gray-100 truncate">
|
||||
{localizedName(userInfo.name)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<Mail className="w-5 h-5 text-primary-600" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
{t("viewDocument.email")}
|
||||
</p>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<p className="text-base font-semibold text-gray-900 dark:text-gray-100 truncate cursor-help">
|
||||
{truncateEmail(userInfo.email)}
|
||||
</p>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="max-w-xs break-all">{userInfo.email}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<Phone className="w-5 h-5 text-purple-600" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
{t("viewDocument.phoneNumber")}
|
||||
</p>
|
||||
<p className="text-base font-semibold text-gray-900 dark:text-gray-100 truncate">
|
||||
{userInfo.phoneNumber}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Documents Section */}
|
||||
{isLoading ? (
|
||||
<div className="space-y-4">
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<Skeleton key={i} className="h-24 w-full rounded-lg" />
|
||||
))}
|
||||
</div>
|
||||
) : isError ? (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>
|
||||
{t("viewDocument.loading.failedToLoad")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
<p className="mb-6 text-lg font-medium text-gray-700 dark:text-gray-300">
|
||||
{t("viewDocument.totalDocuments")}{" "}
|
||||
<span className="font-semibold text-primary-600">
|
||||
{uploadedDocuments?.counts ?? 0}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<ScrollArea className="h-[500px] border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||
<ul className="space-y-4 p-2">
|
||||
{uploadedDocuments?.items.map((doc, index) => {
|
||||
const eachDocument = documentRequirementsQueries[index]?.data;
|
||||
const isEachLoading =
|
||||
documentRequirementsQueries[index]?.isLoading;
|
||||
const isEachError =
|
||||
documentRequirementsQueries[index]?.isError;
|
||||
const requirements = eachDocument ? [eachDocument] : [];
|
||||
|
||||
// Translate status
|
||||
const getTranslatedStatus = (status: string) => {
|
||||
switch (status) {
|
||||
case "approved":
|
||||
return t("viewDocument.document.status.approved");
|
||||
case "rejected":
|
||||
return t("viewDocument.document.status.rejected");
|
||||
case "pending":
|
||||
default:
|
||||
return t("viewDocument.document.status.pending");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<li
|
||||
key={doc.id}
|
||||
className="flex flex-col p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50/50 dark:hover:bg-gray-800/60 transition-colors shadow-sm"
|
||||
>
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<div className="space-y-1">
|
||||
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{doc.fileInfo.originalname}
|
||||
</p>
|
||||
<span
|
||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
doc.status === "approved"
|
||||
? "bg-primary-100 text-primary-800"
|
||||
: doc.status === "rejected"
|
||||
? "bg-red-100 text-red-800"
|
||||
: "bg-yellow-100 text-yellow-800"
|
||||
}`}
|
||||
>
|
||||
{getTranslatedStatus(doc.status)}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-primary-600 hover:bg-primary-700 text-white"
|
||||
asChild
|
||||
>
|
||||
<a
|
||||
href={doc.presigned}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t("viewDocument.document.viewDocument")}
|
||||
</a>
|
||||
</Button>
|
||||
{doc.status !== "approved" && (
|
||||
<ActionModal
|
||||
documentId={doc.id}
|
||||
onActionSubmit={handleActionSubmit}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Document Requirements Section */}
|
||||
<div className="mt-2 border-t border-gray-200 dark:border-gray-700 pt-3">
|
||||
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
{t("viewDocument.document.requirements")}
|
||||
</h4>
|
||||
{isEachLoading ? (
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
</div>
|
||||
) : isEachError ? (
|
||||
<p className="text-sm text-red-500 dark:text-red-400">
|
||||
{t("viewDocument.loading.failedToLoadRequirements")}
|
||||
</p>
|
||||
) : requirements.length ? (
|
||||
<ul className="space-y-2 text-sm">
|
||||
{requirements.map((req) => (
|
||||
<li key={req.id} className="flex items-start">
|
||||
<span
|
||||
className={`inline-block w-2 h-2 rounded-full mt-1.5 mr-2 ${
|
||||
req.isOptional
|
||||
? "bg-blue-400"
|
||||
: "bg-primary-400"
|
||||
}`}
|
||||
/>
|
||||
<div>
|
||||
<span className="font-medium text-gray-800 dark:text-gray-200">
|
||||
{localizedName(req.title)}
|
||||
</span>
|
||||
<span className="text-gray-500 dark:text-gray-400 ml-1">
|
||||
{req.isOptional
|
||||
? t("viewDocument.document.optional")
|
||||
: t("viewDocument.document.required")}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 italic">
|
||||
{t("viewDocument.document.noRequirements")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</ScrollArea>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewDocument;
|
||||
import { useQuery, useQueries } from "@tanstack/react-query";
|
||||
import { getMyUploadedFiles } from "@/external-portal/services/portalOutgoingService";
|
||||
import { getDocumentRequirementsById } from "@/shared/services/organizationsService";
|
||||
import { getUserById } from "@/super-admin/services/api/userService";
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardContent,
|
||||
} from "@/shared/common/ui/card";
|
||||
import { Button } from "@/shared/common/ui/button";
|
||||
import { ScrollArea } from "@/shared/common/ui/scroll-area";
|
||||
import { Skeleton } from "@/shared/common/ui/skeleton";
|
||||
import { Alert, AlertDescription } from "@/shared/common/ui/alert";
|
||||
import { FileText, RefreshCw, User, Mail, Phone, Building } from "lucide-react";
|
||||
import { useLocalizedName } from "@/shared/common/localizedName";
|
||||
import {
|
||||
DocumentRequirementResponseDto,
|
||||
DocumentResponseDto,
|
||||
} from "@/shared/dto/External-Portal/External-PortalDto";
|
||||
import ActionModal from "./ResponseActions";
|
||||
import { toast } from "sonner";
|
||||
import { useGiveResponse } from "@/shared/hooks/useOrganizationReport";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/shared/common/ui/tooltip";
|
||||
|
||||
type ViewDocumentProps = {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
interface UploadsResponse {
|
||||
items: DocumentResponseDto[];
|
||||
counts: number;
|
||||
}
|
||||
|
||||
interface UserInfo {
|
||||
id: string;
|
||||
name: {
|
||||
am: string;
|
||||
en: string;
|
||||
};
|
||||
email: string;
|
||||
phoneNumber: string;
|
||||
username: string;
|
||||
userType: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
const ViewDocument: React.FC<ViewDocumentProps> = ({ userId }) => {
|
||||
const localizedName = useLocalizedName();
|
||||
const { t } = useTranslation();
|
||||
const [status, setStatus] = useState<"APPROVED" | "REJECTED" | "PENDING">(
|
||||
"APPROVED"
|
||||
);
|
||||
|
||||
// Utility function to truncate email if too long
|
||||
const truncateEmail = (email: string, maxLength: number = 25) => {
|
||||
if (email.length <= maxLength) return email;
|
||||
const [localPart, domain] = email.split("@");
|
||||
if (localPart.length > maxLength - 10) {
|
||||
return `${localPart.substring(0, maxLength - 10)}...@${domain}`;
|
||||
}
|
||||
return email;
|
||||
};
|
||||
|
||||
const { mutate, isLoading: isGiveResponseLoading } = useGiveResponse();
|
||||
|
||||
// 1️⃣ Fetch user information
|
||||
const {
|
||||
data: userInfo,
|
||||
isLoading: isUserLoading,
|
||||
isError: isUserError,
|
||||
} = useQuery<UserInfo, Error>({
|
||||
queryKey: ["user-info", userId],
|
||||
queryFn: async () => {
|
||||
const res = await getUserById(userId);
|
||||
return res.data;
|
||||
},
|
||||
});
|
||||
|
||||
// 2️⃣ Fetch uploaded documents
|
||||
const {
|
||||
data: uploadedDocuments,
|
||||
isLoading,
|
||||
isError,
|
||||
refetch,
|
||||
isRefetching,
|
||||
} = useQuery<UploadsResponse, Error>({
|
||||
queryKey: ["external-letters", userId],
|
||||
queryFn: async () => {
|
||||
const res = await getMyUploadedFiles(userId);
|
||||
return {
|
||||
items: res.data.items,
|
||||
counts: res.data.counts,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// 3️⃣ Fetch requirements for each uploaded document
|
||||
const documentRequirementsQueries = useQueries({
|
||||
queries:
|
||||
uploadedDocuments?.items.map((doc) => ({
|
||||
queryKey: ["document-requirements", doc.documentId],
|
||||
queryFn: async (): Promise<DocumentRequirementResponseDto> => {
|
||||
const res = await getDocumentRequirementsById(doc.documentId);
|
||||
return res.data;
|
||||
},
|
||||
enabled: !!uploadedDocuments && uploadedDocuments.items.length > 0,
|
||||
})) ?? [],
|
||||
});
|
||||
|
||||
// 4️⃣ Action submit handler
|
||||
const handleActionSubmit = async (
|
||||
documentId: string,
|
||||
actionStatus:
|
||||
| "pending"
|
||||
| "submitted"
|
||||
| "approved"
|
||||
| "adjusted"
|
||||
| "rejected",
|
||||
remark?: string
|
||||
) => {
|
||||
try {
|
||||
await mutate({ id: documentId, data: { status: actionStatus, remark } });
|
||||
toast.success(
|
||||
t("viewDocument.actions.success", { action: actionStatus })
|
||||
);
|
||||
} catch (err: any) {
|
||||
toast.error(
|
||||
t("viewDocument.actions.error", {
|
||||
error: err.message || t("viewDocument.loading.failedToLoad"),
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-4xl mx-auto mt-8 shadow-lg border-gray-200 dark:border-gray-700">
|
||||
<CardHeader className="border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-2xl font-bold flex items-center gap-2">
|
||||
<FileText className="w-6 h-6 text-primary-600" />
|
||||
{t("viewDocument.title")}
|
||||
</CardTitle>
|
||||
<Button
|
||||
onClick={() => refetch()}
|
||||
variant="default"
|
||||
className="bg-primary-600 hover:bg-primary-700 text-white"
|
||||
disabled={isRefetching}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`mr-2 h-4 w-4 ${isRefetching ? "animate-spin" : ""}`}
|
||||
/>
|
||||
{t("viewDocument.refreshButton")}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-6">
|
||||
{/* User Information Section */}
|
||||
{isUserLoading ? (
|
||||
<div className="mb-6 space-y-4">
|
||||
<Skeleton className="h-6 w-48" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Skeleton className="h-16 w-full rounded-lg" />
|
||||
<Skeleton className="h-16 w-full rounded-lg" />
|
||||
<Skeleton className="h-16 w-full rounded-lg" />
|
||||
</div>
|
||||
</div>
|
||||
) : isUserError ? (
|
||||
<Alert variant="destructive" className="mb-6">
|
||||
<AlertDescription>
|
||||
{t("viewDocument.failedToLoadUserInfo")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : userInfo ? (
|
||||
<div className="mb-6 p-4 bg-gray-50 dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center gap-2">
|
||||
<User className="w-5 h-5 text-primary-600" />
|
||||
{t("viewDocument.userInformation")}
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="flex items-center gap-3 p-3 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<Building className="w-5 h-5 text-blue-600" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
{t("viewDocument.organizationName")}
|
||||
</p>
|
||||
<p className="text-base font-semibold text-gray-900 dark:text-gray-100 truncate">
|
||||
{localizedName(userInfo.name)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<Mail className="w-5 h-5 text-primary-600" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
{t("viewDocument.email")}
|
||||
</p>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<p className="text-base font-semibold text-gray-900 dark:text-gray-100 truncate cursor-help">
|
||||
{truncateEmail(userInfo.email)}
|
||||
</p>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="max-w-xs break-all">{userInfo.email}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<Phone className="w-5 h-5 text-purple-600" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
{t("viewDocument.phoneNumber")}
|
||||
</p>
|
||||
<p className="text-base font-semibold text-gray-900 dark:text-gray-100 truncate">
|
||||
{userInfo.phoneNumber}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Documents Section */}
|
||||
{isLoading ? (
|
||||
<div className="space-y-4">
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<Skeleton key={i} className="h-24 w-full rounded-lg" />
|
||||
))}
|
||||
</div>
|
||||
) : isError ? (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>
|
||||
{t("viewDocument.loading.failedToLoad")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
<p className="mb-6 text-lg font-medium text-gray-700 dark:text-gray-300">
|
||||
{t("viewDocument.totalDocuments")}{" "}
|
||||
<span className="font-semibold text-primary-600">
|
||||
{uploadedDocuments?.counts ?? 0}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<ScrollArea className="h-[500px] border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||
<ul className="space-y-4 p-2">
|
||||
{uploadedDocuments?.items.map((doc, index) => {
|
||||
const eachDocument = documentRequirementsQueries[index]?.data;
|
||||
const isEachLoading =
|
||||
documentRequirementsQueries[index]?.isLoading;
|
||||
const isEachError =
|
||||
documentRequirementsQueries[index]?.isError;
|
||||
const requirements = eachDocument ? [eachDocument] : [];
|
||||
|
||||
// Translate status
|
||||
const getTranslatedStatus = (status: string) => {
|
||||
switch (status) {
|
||||
case "approved":
|
||||
return t("viewDocument.document.status.approved");
|
||||
case "rejected":
|
||||
return t("viewDocument.document.status.rejected");
|
||||
case "pending":
|
||||
default:
|
||||
return t("viewDocument.document.status.pending");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<li
|
||||
key={doc.id}
|
||||
className="flex flex-col p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50/50 dark:hover:bg-gray-800/60 transition-colors shadow-sm"
|
||||
>
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<div className="space-y-1">
|
||||
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{doc.fileInfo.originalname}
|
||||
</p>
|
||||
<span
|
||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
doc.status === "approved"
|
||||
? "bg-primary-100 text-primary-800"
|
||||
: doc.status === "rejected"
|
||||
? "bg-red-100 text-red-800"
|
||||
: "bg-yellow-100 text-yellow-800"
|
||||
}`}
|
||||
>
|
||||
{getTranslatedStatus(doc.status)}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-primary-600 hover:bg-primary-700 text-white"
|
||||
asChild
|
||||
>
|
||||
<a
|
||||
href={doc.presigned}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t("viewDocument.document.viewDocument")}
|
||||
</a>
|
||||
</Button>
|
||||
{doc.status !== "approved" && (
|
||||
<ActionModal
|
||||
documentId={doc.id}
|
||||
onActionSubmit={handleActionSubmit}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Document Requirements Section */}
|
||||
<div className="mt-2 border-t border-gray-200 dark:border-gray-700 pt-3">
|
||||
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
{t("viewDocument.document.requirements")}
|
||||
</h4>
|
||||
{isEachLoading ? (
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
</div>
|
||||
) : isEachError ? (
|
||||
<p className="text-sm text-red-500 dark:text-red-400">
|
||||
{t("viewDocument.loading.failedToLoadRequirements")}
|
||||
</p>
|
||||
) : requirements.length ? (
|
||||
<ul className="space-y-2 text-sm">
|
||||
{requirements.map((req) => (
|
||||
<li key={req.id} className="flex items-start">
|
||||
<span
|
||||
className={`inline-block w-2 h-2 rounded-full mt-1.5 mr-2 ${
|
||||
req.isOptional
|
||||
? "bg-blue-400"
|
||||
: "bg-primary-400"
|
||||
}`}
|
||||
/>
|
||||
<div>
|
||||
<span className="font-medium text-gray-800 dark:text-gray-200">
|
||||
{localizedName(req.title)}
|
||||
</span>
|
||||
<span className="text-gray-500 dark:text-gray-400 ml-1">
|
||||
{req.isOptional
|
||||
? t("viewDocument.document.optional")
|
||||
: t("viewDocument.document.required")}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 italic">
|
||||
{t("viewDocument.document.noRequirements")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</ScrollArea>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewDocument;
|
||||
|
||||
Reference in New Issue
Block a user