display document details and document meta data and added skeleton

This commit is contained in:
pramod mahajan 2025-09-02 16:39:06 +05:30
parent 06503ac4d3
commit acd642c935
6 changed files with 501 additions and 55 deletions

View File

@ -0,0 +1,88 @@
import React from "react";
import VersionListSkeleton from "./VersionListSkeleton";
const SkeletonLine = ({ height = 16, width = "100%", className = "" }) => (
<div
className={`skeleton mb-2 ${className}`}
style={{
height,
width,
borderRadius: 4,
}}
></div>
);
const DocumentDetailsSkeleton = () => {
return (
<div className="p-1">
<p className="fw-bold fs-6">Document Details</p>
{/* Row 1 */}
<div className="row mb-2">
<div className="col-12 col-md-6">
<div className="d-flex gap-2">
<SkeletonLine width="130px" />
<SkeletonLine width="60%" />
</div>
</div>
<div className="col-12 col-md-6">
<div className="d-flex gap-2">
<SkeletonLine width="130px" />
<SkeletonLine width="50%" />
</div>
</div>
</div>
{/* Row 2 */}
<div className="row mb-2">
<div className="col-12 col-md-6">
<div className="d-flex gap-2">
<SkeletonLine width="130px" />
<SkeletonLine width="40%" />
</div>
</div>
<div className="col-12 col-md-6">
<div className="d-flex gap-2">
<SkeletonLine width="130px" />
<SkeletonLine width="60%" />
</div>
</div>
</div>
{/* Row 3 */}
<div className="row mb-2">
<div className="col-12 col-md-6">
<div className="d-flex gap-2">
<SkeletonLine width="130px" />
<SkeletonLine width="40%" />
</div>
</div>
<div className="col-12 col-md-6">
<div className="d-flex gap-2">
<SkeletonLine width="130px" />
<SkeletonLine width="50%" />
</div>
</div>
</div>
{/* Row 6 - Description */}
<div className="row mb-2">
<div className="col-12">
<div className="d-flex">
<SkeletonLine width="100%" height={40} />
</div>
</div>
</div>
{/* Version list skeleton */}
<div className="row text-start py-2">
<VersionListSkeleton items={2} />
</div>
</div>
);
};
export default DocumentDetailsSkeleton;

View File

@ -13,6 +13,7 @@ import {
} from "./DocumentSchema";
import { zodResolver } from "@hookform/resolvers/zod";
import ManageDocument from "./ManageDocument";
import ViewDocument from "./ViewDocument";
// Context
export const DocumentContext = createContext();
@ -24,6 +25,24 @@ export const useDocumentContext = () => {
return context;
};
export const getDocuementsStatus = (status) => {
switch (status) {
case true:
return (
<span className="badge rounded-pill bg-label-success">Verified</span>
);
case false:
return (
<span className="badge rounded-pill bg-label-danger">Rejected</span>
);
case null:
default:
return (
<span className="badge rounded-pill bg-label-warning"> Pending</span>
);
}
};
const Documents = ({ Document_Entity, Entity }) => {
const [searchText, setSearchText] = useState("");
const [filters, setFilter] = useState();
@ -35,6 +54,10 @@ const Documents = ({ Document_Entity, Entity }) => {
document: null,
isOpen: false,
});
const [viewDoc, setViewDoc] = useState({
document: null,
isOpen: false,
});
const { setOffcanvasContent, setShowTrigger } = useFab();
@ -67,6 +90,8 @@ const Documents = ({ Document_Entity, Entity }) => {
const contextValues = {
ManageDoc,
setManageDoc,
viewDoc,
setViewDoc
}
useEffect(()=>{
@ -93,7 +118,7 @@ const Documents = ({ Document_Entity, Entity }) => {
{/* Actions */}
<div className="col-6 col-md-6 col-lg-9 text-end">
<span
{/* <span
className="text-tiny text-muted p-1 border-0 bg-none lead mx-3 cursor-pointer"
disabled={isRefetching}
onClick={() => {
@ -108,7 +133,7 @@ const Documents = ({ Document_Entity, Entity }) => {
isRefetching ? "bx-spin" : ""
}`}
></i>
</span>
</span> */}
<button
type="button"
@ -155,6 +180,15 @@ const Documents = ({ Document_Entity, Entity }) => {
/>
</GlobalModel>
)}
{viewDoc.isOpen && (
<GlobalModel size="lg" isOpen={viewDoc.isOpen} closeModal={()=>setViewDoc({
document:null,
isOpen:false
})}>
<ViewDocument />
</GlobalModel>
)}
</div>
</DocumentContext.Provider>

View File

@ -0,0 +1,47 @@
import React from "react";
const SkeletonLine = ({ height = 16, width = "100%", className = "" }) => (
<div
className={`skeleton mb-1 ${className}`}
style={{
height,
width,
borderRadius: 4,
}}
></div>
);
const VersionListSkeleton = ({ items = 5 }) => {
return (
<div className="list-group mx-0">
{[...Array(items)].map((_, idx) => (
<div
key={idx}
className="list-group-item py-2 border border-bottom border-top-0 border-start-0 border-end-0"
>
{/* Top row: document name + version/status */}
<div className="d-flex w-100 justify-content-between">
<SkeletonLine width="40%" height={16} />
<div className="d-flex gap-2">
<SkeletonLine width="60px" height={14} />
<SkeletonLine width="80px" height={14} />
</div>
</div>
{/* Upload by row */}
<div className="d-flex align-items-center gap-2 mt-2">
<SkeletonLine width="24px" height="24px" className="rounded-circle" />
<SkeletonLine width="120px" height={14} />
</div>
{/* Updated at row */}
<div className="d-flex gap-2 mt-2">
<SkeletonLine width="150px" height={14} />
</div>
</div>
))}
</div>
);
};
export default VersionListSkeleton;

View File

@ -0,0 +1,223 @@
import React, { useState } from "react";
import {
useDocumentDetails,
useDocumentVersionList,
} from "../../hooks/useDocument";
import { getDocuementsStatus, useDocumentContext } from "./Documents";
import { formatUTCToLocalTime } from "../../utils/dateUtils";
import Avatar from "../common/Avatar";
import { ITEMS_PER_PAGE } from "../../utils/constants";
import Pagination from "../common/Pagination";
import VersionListSkeleton from "./VersionListSkeleton";
import DocumentDetailsSkeleton from "./DocumentDetailsSkeleton ";
const ViewDocument = () => {
const { viewDoc, setViewDoc } = useDocumentContext();
const [currentPage, setCurrentPage] = useState(1);
const { data, isLoading, isError, error } = useDocumentDetails(
viewDoc?.document
);
const {
data: versionList,
isError: isVersionError,
isLoading: versionLoding,
error: versionError,
} = useDocumentVersionList(
data?.parentAttachmentId,
ITEMS_PER_PAGE,
currentPage
);
const paginate = (page) => {
if (page >= 1 && page <= (versionList?.totalPages ?? 1)) {
setCurrentPage(page);
}
};
if (isLoading) return <DocumentDetailsSkeleton/>;
if (isError) return <div>{error.message}</div>;
return (
<div className="p-1">
<p className="fw-bold fs-6">Document Details</p>
{/* Row 1 */}
{/* Row 1 */}
<div className="row mb-2">
<div className="col-12 col-md-6">
<div className="d-flex text-start">
<span className="fw-semibold me-2" style={{ minWidth: "130px" }}>
Document Name:
</span>
<span className="text-muted">{data.name || "-"}</span>
</div>
</div>
<div className="col-12 col-md-6">
<div className="d-flex text-start">
<span className="fw-semibold me-2" style={{ minWidth: "130px" }}>
Document ID:
</span>
<span className="text-muted">{data.documentId || "-"}</span>
</div>
</div>
</div>
{/* Row 2 */}
<div className="row mb-2">
<div className="col-12 col-md-6 text-start">
<div className="d-flex">
<span className="fw-semibold me-2" style={{ minWidth: "130px" }}>
Version:
</span>
<span className="text-muted">{data.version || "-"}</span>
</div>
</div>
<div className="col-12 col-md-6 text-start">
<div className="d-flex">
<span className="fw-semibold me-2" style={{ minWidth: "130px" }}>
Uploaded At:
</span>
<span className="text-muted">
{formatUTCToLocalTime(data.uploadedAt)}
</span>
</div>
</div>
</div>
{/* Row 3 */}
<div className="row mb-2 text-start">
<div className="col-12 col-md-6">
<div className="d-flex">
<span className="fw-semibold me-2" style={{ minWidth: "130px" }}>
Uploaded By:
</span>
<span className="text-muted">
{data.uploadedBy?.firstName || "-"}
</span>
</div>
</div>
<div className="col-12 col-md-6">
<div className="d-flex">
<span className="fw-semibold me-2" style={{ minWidth: "130px" }}>
Updated At:
</span>
<span className="text-muted">
{formatUTCToLocalTime(data.updatedAt) || "-"}
</span>
</div>
</div>
</div>
{/* Row 4 */}
<div className="row mb-2 text-start">
<div className="col-12 col-md-6">
<div className="d-flex">
<span className="fw-semibold me-2" style={{ minWidth: "130px" }}>
Category:
</span>
<span className="text-muted">
{data.documentType?.documentCategory?.name || "-"}
</span>
</div>
</div>
<div className="col-12 col-md-6">
<div className="d-flex">
<span className="fw-semibold me-2" style={{ minWidth: "130px" }}>
Type:
</span>
<span className="text-muted">{data.documentType?.name || "-"}</span>
</div>
</div>
</div>
{/* Row 5 - Tags full width */}
<div className="row mb-2 text-start">
<div className="col-12">
<div className="d-flex">
<span className="fw-semibold me-2" style={{ minWidth: "130px" }}>
Tags:
</span>
<div className="d-flex flex-wrap gap-2">
{data.tags?.length > 0 ? (
data.tags.map((t, i) => (
<span
key={i}
className="badge rounded-pill bg-label-secondary"
>
{t.name}
</span>
))
) : (
<span className="text-muted">-</span>
)}
</div>
</div>
</div>
</div>
{/* Row 6 - Description full width */}
<div className="row mb-2 text-start">
<div className="col-12">
<div className="d-flex">
<span className="fw-semibold me-2" style={{ minWidth: "130px" }}>
Description:
</span>
<span className="text-muted">{data.description || "-"}</span>
</div>
</div>
</div>
<div className="row text-start py-2">
<p className="m-0 fw-semibold : ">Documents</p>
{versionLoding && <VersionListSkeleton items={2}/>}
{!versionLoding &&(<div className="list-group mx-0">
{
versionList?.data.map((document) => (
<a className="list-group-item list-group-item-action py-1 border border-bottom border-top-0 border-start-0 border-end-0">
<div className="d-flex w-100 justify-content-between m-0">
<p className="m-0">
{document.name}{" "}
<em className="text-secondary ms-3">
{formatUTCToLocalTime(document?.uploadedAt)}
</em>
</p>
<div className="d-flex align-items-center gap-1">
<small>Version {document.version}</small>
<small>{getDocuementsStatus(document.isVerified)}</small>
</div>
</div>
<div className="d-flex align-items-center text-secondary">
Upload By
<Avatar
size="xs"
classAvatar="m-0"
firstName={document.uploadedBy?.firstName}
lastName={document.uploadedBy?.lastName}
/>
<span className="text-truncate m-0 ">
{`${document.uploadedBy?.firstName ?? ""} ${
document.uploadedBy?.lastName ?? ""
}`.trim() || "N/A"}
</span>
</div>
<div className="d-flex gap-2">
{document?.updatedAt && (
<span className="small text-secondary">
Updated At : {formatUTCToLocalTime(document.updatedAt)}
</span>
)}
</div>
</a>
))}
</div>)}
{!versionLoding && versionList?.data?.length > 0 && (
<Pagination
currentPage={currentPage}
totalPages={versionList?.totalPages}
onPageChange={paginate}
/>
)}
</div>
</div>
);
};
export default ViewDocument;

View File

@ -1,4 +1,4 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import showToast from "../services/toastService";
import { DocumentRepository } from "../repositories/DocumentRepository";
@ -6,7 +6,12 @@ import { DocumentRepository } from "../repositories/DocumentRepository";
const cleanFilter = (filter) => {
const cleaned = { ...filter };
["uploadedByIds", "documentCategoryIds", "documentTypeIds", "documentTagIds"].forEach((key) => {
[
"uploadedByIds",
"documentCategoryIds",
"documentTypeIds",
"documentTagIds",
].forEach((key) => {
if (Array.isArray(cleaned[key]) && cleaned[key].length === 0) {
delete cleaned[key];
}
@ -20,73 +25,112 @@ const cleanFilter = (filter) => {
return cleaned;
};
export const useDocumentListByEntityId=(entityTypeId,entityId,pageSize, pageNumber, filter,searchString="")=>{
export const useDocumentListByEntityId = (
entityTypeId,
entityId,
pageSize,
pageNumber,
filter,
searchString = ""
) => {
return useQuery({
queryKey:["DocumentList",entityTypeId,entityId,pageSize, pageNumber, filter,searchString],
queryFn:async()=>{
queryKey: [
"DocumentList",
entityTypeId,
entityId,
pageSize,
pageNumber,
filter,
searchString,
],
queryFn: async () => {
const cleanedFilter = cleanFilter(filter);
const resp = await DocumentRepository.getDocumentList(entityTypeId,entityId,pageSize, pageNumber,cleanedFilter,searchString);
return resp.data;
const resp = await DocumentRepository.getDocumentList(
entityTypeId,
entityId,
pageSize,
pageNumber,
cleanedFilter,
searchString
);
return resp.data
},
enabled:!!entityTypeId && !! entityId
})
}
enabled: !!entityTypeId && !!entityId,
});
};
export const useDocumentFilterEntities =(entityTypeId)=>{
export const useDocumentFilterEntities = (entityTypeId) => {
return useQuery({
queryKey:["DFilter",entityTypeId],
queryFn:async()=> await DocumentRepository.getFilterEntities(entityTypeId)
})
}
queryKey: ["DFilter", entityTypeId],
queryFn: async () =>
await DocumentRepository.getFilterEntities(entityTypeId),
});
};
export const useDocumentDetails =(documentId)=>{
export const useDocumentDetails = (documentId) => {
return useQuery({
queryKey:["Document",documentId],
queryFn:async()=> {
queryKey: ["Document", documentId],
queryFn: async () => {
const resp = await DocumentRepository.getDocumentById(documentId);
return resp.data;
},
enabled:!!documentId
})
enabled: !!documentId,
});
};
}
export const useDocumentVersionList = (parentAttachmentId,pageSize,pageNumber) => {
return useQuery({
queryKey: ["DocumentVersionList", parentAttachmentId,pageSize,pageNumber],
queryFn: async () => {
const resp = await DocumentRepository.getDocumentVersionList(parentAttachmentId,pageSize,pageNumber);
return resp.data
},
enabled: !!parentAttachmentId,
});
};
//----------------------- MUTATION -------------------------
export const useUploadDocument =(onSuccessCallBack)=>{
const queryClient = useQueryClient()
return useMutation(({
mutationFn:async(DocumentPayload)=>DocumentRepository.uploadDocument(DocumentPayload),
onSuccess:(data,variables)=>{
queryClient.invalidateQueries({queryKey:["DocumentList"]});
if(onSuccessCallBack) onSuccessCallBack()
},
onError: (error) => {
console.log(error)
showToast(
error.response.data.message || "Something went wrong please try again !",
export const useUploadDocument = (onSuccessCallBack) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (DocumentPayload) =>
DocumentRepository.uploadDocument(DocumentPayload),
onSuccess: (data, variables) => {
queryClient.invalidateQueries({ queryKey: ["DocumentList"] });
if (onSuccessCallBack) onSuccessCallBack();
},
onError: (error) => {
console.log(error);
showToast(
error.response.data.message ||
"Something went wrong please try again !",
"error"
);
},
}))
}
export const useUpdateDocument =(onSuccessCallBack)=>{
const queryClient = useQueryClient()
return useMutation(({
mutationFn:async({documentId,DocumentPayload})=>DocumentRepository.UpdateDocument(documentId,DocumentPayload),
onSuccess:(data,variables)=>{
const {documentId} = variables;
queryClient.invalidateQueries({queryKey:["DocumentList"]});
queryClient.invalidateQueries({queryKey:["Document",documentId]})
if(onSuccessCallBack) onSuccessCallBack()
},
onError: (error) => {
console.log(error)
showToast(
error.response.data.message || "Something went wrong please try again !",
});
};
export const useUpdateDocument = (onSuccessCallBack) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ documentId, DocumentPayload }) =>
DocumentRepository.UpdateDocument(documentId, DocumentPayload),
onSuccess: (data, variables) => {
const { documentId } = variables;
queryClient.invalidateQueries({ queryKey: ["DocumentList"] });
queryClient.invalidateQueries({ queryKey: ["Document", documentId] });
if (onSuccessCallBack) onSuccessCallBack();
},
onError: (error) => {
console.log(error);
showToast(
error.response.data.message ||
"Something went wrong please try again !",
"error"
);
},
}))
}
});
};

View File

@ -10,5 +10,15 @@ export const DocumentRepository = {
getFilterEntities:(entityTypeId)=>api.get(`/api/Document/get/filter/${entityTypeId}`),
UpdateDocument:(documentId,data)=>api.put(`/api/Document/edit/${documentId}`,data)
UpdateDocument:(documentId,data)=>api.put(`/api/Document/edit/${documentId}`,data),
getDocumentVersionList:(parentAttachmentId,pageSize,pageNumber)=>api.get(`/api/Document/list/versions/${parentAttachmentId}/?pageSize=${pageSize}&pageNumber=${pageNumber}`),
getDocumentVersion:(id)=>api.get(`/api/Document/get/version/${id}`),
verifyDocument:(id)=>api.post(`/api/Document/verify/${id}`),
deleteDocument:(id)=>api.delete(`/api/Document/delete/${id}`)
}