attachement display horizontically

This commit is contained in:
pramod mahajan 2025-08-06 10:22:47 +05:30
parent a87d3fb20a
commit 3d7a8a1c94
10 changed files with 287 additions and 171 deletions

View File

@ -28,6 +28,7 @@
<link rel="stylesheet" href="/assets/css/core-extend.css" />
<link rel="stylesheet" href="/assets/css/default.css" />
<link rel="stylesheet" href="/assets/css/skeleton.css" />
<link rel="stylesheet" href="/assets/css/hover-utility.css" />
<link rel="stylesheet" href="/assets/vendor/libs/perfect-scrollbar/perfect-scrollbar.css" />

View File

@ -0,0 +1,86 @@
/* Hover background color */
.hover-bg-light:hover {
background-color: #f8f9fa !important;
}
.hover-bg-primary:hover {
background-color: var(--bs-primary) !important;
color: #fff !important;
}
.hover-bg-danger:hover {
background-color: #dc3545 !important;
color: #fff !important;
}
.hover-bg-success:hover {
background-color: var(--bg-success) !important;
color: #fff !important;
}
.hover-bg-warning:hover {
background-color: #ffc107 !important;
color: #212529 !important;
}
/* Hover text color */
.hover-text-primary:hover {
color: #0d6efd !important;
}
.hover-text-danger:hover {
color: #dc3545 !important;
}
.hover-text-success:hover {
color: #198754 !important;
}
.hover-text-muted:hover {
color: #6c757d !important;
}
/* Hover shadow */
.hover-shadow-sm:hover {
box-shadow: 0 .125rem .25rem rgba(0, 0, 0, 0.075);
}
.hover-shadow:hover {
box-shadow: 0 .5rem 1rem rgba(0, 0, 0, 0.15);
}
.hover-shadow-lg:hover {
box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175);
}
/* Hover scale */
.hover-scale:hover {
transform: scale(1.05);
}
.hover-scale-sm:hover {
transform: scale(1.03);
}
.hover-scale-lg:hover {
transform: scale(1.1);
}
/* Add smooth transition to hover effects */
.hover-transition {
transition: all 0.2s ease-in-out;
}
/* Hover border color */
.hover-border-primary:hover {
border-color: #0d6efd !important;
}
.hover-border-danger:hover {
border-color: #dc3545 !important;
}
.thick-divider {
height: 3px;
font-size: 10px;
}

View File

@ -120,7 +120,7 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
firstName={e.createdBy?.firstName}
lastName={e.createdBy?.lastName}
/>
<span>
<span className="text-truncate">
{`${e.createdBy?.firstName ?? ""} ${
e.createdBy?.lastName ?? ""
}`.trim() || "N/A"}

View File

@ -1,6 +1,5 @@
import React from "react";
const SkeletonLine = ({ height = 20, width = "100%", className = "" }) => (
<div
className={`skeleton mb-2 ${className}`}
@ -11,13 +10,12 @@ const SkeletonLine = ({ height = 20, width = "100%", className = "" }) => (
></div>
);
const ExpenseSkeleton = () => {
return (
<div className="container p-3">
<div className="d-flex justify-content-center">
<SkeletonLine height={20} width="200px" />
</div>
<div className="d-flex justify-content-center">
<SkeletonLine height={20} width="200px" />
</div>
{[...Array(5)].map((_, idx) => (
<div className="row my-2" key={idx}>
@ -52,12 +50,9 @@ const ExpenseSkeleton = () => {
export default ExpenseSkeleton;
export const ExpenseDetailsSkeleton = () => {
return (
<div className="container px-3">
<div className="container px-3">
<div className="row mb-3">
<div className="d-flex justify-content-center mb-3">
<SkeletonLine height={20} width="180px" className="mb-2" />
@ -65,7 +60,7 @@ export const ExpenseDetailsSkeleton = () => {
{[...Array(3)].map((_, i) => (
<div className="col-12 col-md-4 mb-3" key={`row-1-${i}`}>
<SkeletonLine height={14} className="mb-1" />
<SkeletonLine height={14} className="mb-1" />
<SkeletonLine />
</div>
))}
@ -77,30 +72,38 @@ export const ExpenseDetailsSkeleton = () => {
</div>
))}
<div className="col-12 text-start">
<SkeletonLine height={14} width="150px" className="mb-1" />
<div className="col-12 my-2">
<SkeletonLine height={14} width="100px" className="mb-2" />
{[...Array(2)].map((_, i) => (
<div
className="list-group-item d-flex align-items-center mb-2"
key={i}
>
<div className="d-flex flex-wrap gap-2">
{[...Array(2)].map((_, i) => (
<div
className="rounded me-2"
style={{
height: "50px",
width: "80px",
backgroundColor: "#dcdcdc",
borderRadius: "4px",
}}
/>
<div className="w-100">
<SkeletonLine height={14} width="60%" className="mb-1" />
<SkeletonLine height={14} width="20%" />
key={i}
className="border rounded p-2 d-flex flex-column align-items-center"
style={{ width: "80px" }}
>
{/* Icon placeholder */}
<div
style={{
height: "30px",
width: "30px",
borderRadius: "4px",
marginBottom: "6px",
}}
className="skeleton"
/>
{/* Filename placeholder */}
<div
style={{
height: "10px",
width: "100%",
}}
className="skeleton"
/>
</div>
</div>
))}
))}
</div>
</div>
<hr className="divider my-1" />
@ -123,7 +126,12 @@ export const ExpenseDetailsSkeleton = () => {
</div>
);
};
const SkeletonCell = ({ width = "100%", height = 20, className = "", style = {} }) => (
const SkeletonCell = ({
width = "100%",
height = 20,
className = "",
style = {},
}) => (
<div
className={`skeleton ${className}`}
style={{
@ -137,93 +145,108 @@ const SkeletonCell = ({ width = "100%", height = 20, className = "", style = {}
export const ExpenseTableSkeleton = ({ groups = 3, rowsPerGroup = 3 }) => {
return (
<div className="card px-2">
<div className="card px-2">
<table
className="card-body table border-top dataTable no-footer dtr-column text-nowrap"
aria-describedby="DataTables_Table_0_info"
id="horizontal-example"
>
<thead>
<tr>
<th className="d-none d-sm-table-cell">
<div className="text-start ms-5">Expense Type</div>
</th>
<th className="d-none d-sm-table-cell">
<div className="text-start ms-5">Payment Mode</div>
</th>
<th className="d-none d-sm-table-cell">Paid By</th>
<th className="d-none d-md-table-cell">Amount</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
className="card-body table border-top dataTable no-footer dtr-column text-nowrap"
aria-describedby="DataTables_Table_0_info"
id="horizontal-example"
>
<thead>
<tr>
<th className="d-none d-sm-table-cell">
<div className="text-start ms-5">Expense Type</div>
</th>
<th className="d-none d-sm-table-cell">
<div className="text-start ms-5">Payment Mode</div>
</th>
<th className="d-none d-sm-table-cell">Submitted By</th>
<th className="d-none d-sm-table-cell">Submitted</th>
<th className="d-none d-md-table-cell">Amount</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{[...Array(groups)].map((_, groupIdx) => (
<React.Fragment key={`group-${groupIdx}`}>
{/* Fake Date Group Header Row */}
<tr className="bg-light">
<td colSpan={8}>
<SkeletonCell width="150px" height={20} />
</td>
</tr>
{/* Rows under this group */}
{[...Array(rowsPerGroup)].map((__, rowIdx) => (
<tr key={`row-${groupIdx}-${rowIdx}`} className={rowIdx % 2 === 0 ? "odd" : "even"}>
{/* Expense Type */}
<td className="text-start d-none d-sm-table-cell ms-5">
<SkeletonCell width="90px" height={16} />
</td>
{/* Payment Mode */}
<td className="text-start d-none d-sm-table-cell ms-5">
<SkeletonCell width="90px" height={16} />
</td>
{/* Paid By (Avatar + name) */}
<td className="text-start d-none d-sm-table-cell ms-5">
<div className="d-flex align-items-center gap-2">
<SkeletonCell width="30px" height={30} className="rounded-circle" />
<SkeletonCell width="80px" height={16} />
</div>
</td>
{/* Amount */}
<td className="d-none d-md-table-cell text-end">
<SkeletonCell width="60px" height={16} />
</td>
{/* Status */}
<td>
<SkeletonCell width="80px" height={22} className="rounded" />
</td>
{/* Action */}
<td>
<div className="d-flex justify-content-center align-items-center gap-2">
{[...Array(3)].map((__, i) => (
<SkeletonCell
key={i}
width={20}
height={20}
className="rounded"
style={{ display: "inline-block" }}
/>
))}
</div>
<tbody>
{[...Array(groups)].map((_, groupIdx) => (
<React.Fragment key={`group-${groupIdx}`}>
{/* Fake Date Group Header Row */}
<tr className="bg-light">
<td colSpan={8}>
<SkeletonCell width="150px" height={20} />
</td>
</tr>
))}
</React.Fragment>
))}
</tbody>
</table>
</div>
{/* Rows under this group */}
{[...Array(rowsPerGroup)].map((__, rowIdx) => (
<tr
key={`row-${groupIdx}-${rowIdx}`}
className={rowIdx % 2 === 0 ? "odd" : "even"}
>
{/* Expense Type */}
<td className="text-start d-none d-sm-table-cell ms-5">
<SkeletonCell width="90px" height={16} />
</td>
{/* Payment Mode */}
<td className="text-start d-none d-sm-table-cell ms-5">
<SkeletonCell width="90px" height={16} />
</td>
{/* Submitted By (Avatar + name) */}
<td className="text-start d-none d-sm-table-cell ms-5">
<div className="d-flex align-items-center gap-2">
<SkeletonCell
width="30px"
height={30}
className="rounded-circle"
/>
<SkeletonCell width="80px" height={16} />
</div>
</td>
{/* Submitted */}
<td className="d-none d-md-table-cell text-end">
<SkeletonCell width="70px" height={16} />
</td>
{/* Amount */}
<td className="d-none d-md-table-cell text-end">
<SkeletonCell width="60px" height={16} />
</td>
{/* Status */}
<td>
<SkeletonCell
width="80px"
height={22}
className="rounded"
/>
</td>
{/* Action */}
<td>
<div className="d-flex justify-content-center align-items-center gap-2">
{[...Array(3)].map((__, i) => (
<SkeletonCell
key={i}
width={20}
height={20}
className="rounded"
style={{ display: "inline-block" }}
/>
))}
</div>
</td>
</tr>
))}
</React.Fragment>
))}
</tbody>
</table>
</div>
);
};
export const ExpenseFilterSkeleton = () => {
return (
<div className="p-3 text-start">

View File

@ -5,7 +5,10 @@ import { formatUTCToLocalTime } from "../../utils/dateUtils";
const ExpenseStatusLogs = ({ data }) => {
const [visibleCount, setVisibleCount] = useState(4);
const logsToShow = data?.expenseLogs?.slice(0, visibleCount) || [];
const logsToShow = [...(data?.expenseLogs || [])]
.sort((a, b) => new Date(b.updateAt) - new Date(a.updateAt))
.slice(0, visibleCount);
const handleShowMore = () => {
setVisibleCount((prev) => prev + 4);

View File

@ -9,7 +9,7 @@ import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { defaultActionValues, ExpenseActionScheam } from "./ExpenseSchema";
import { useExpenseContext } from "../../pages/Expense/ExpensePage";
import { getColorNameFromHex } from "../../utils/appUtils";
import { getColorNameFromHex, getIconByFileType } from "../../utils/appUtils";
import { ExpenseDetailsSkeleton } from "./ExpenseSkeleton";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import {
@ -271,37 +271,24 @@ const ViewExpense = ({ ExpenseId }) => {
</div>
</div>
</div>
)}
)}
</div>
<div className="col-12 text-start">
<label className="form-label me-2 mb-0 fw-semibold">Attachment :</label>
{data?.documents?.map((doc) => {
const getIconByType = (type) => {
if (!type) return "bx bx-file";
<div className="col-12 text-start">
<label className="form-label me-2 mb-2 fw-semibold">Attachment :</label>
if (type.includes("pdf")) return "bxs-file-pdf";
if (type.includes("word")) return "bxs-file-doc";
if (type.includes("excel") || type.includes("spreadsheet"))
return "bxs-file-xls";
if (type.includes("image")) return "bxs-file-image";
if (type.includes("zip") || type.includes("rar"))
return "bxs-file-archive";
<div className="d-flex flex-wrap gap-2">
{data?.documents?.map((doc) => {
const isImage = doc.contentType?.includes("image");
return "bx bx-file";
};
const isImage = doc.contentType?.includes("image");
return (
<div
className="list-group-item list-group-item-action d-flex align-items-center"
key={doc.documentId}
>
return (
<div
className="rounded me-1 d-flex align-items-center justify-content-center cursor-pointer"
style={{ height: "50px", width: "60px", position: "relative" }}
key={doc.documentId}
className="border rounded hover-scale p-2 d-flex flex-column align-items-center"
style={{
width: "80px",
cursor: isImage ? "pointer" : "default",
}}
onClick={() => {
if (isImage) {
setDocumentView({
@ -312,31 +299,22 @@ const ViewExpense = ({ ExpenseId }) => {
}}
>
<i
className={`bx ${getIconByType(
doc.contentType
)} text-primary`}
style={{ fontSize: "35px" }}
className={`bx ${getIconByFileType(doc.contentType)}`}
style={{ fontSize: "30px" }}
></i>
<small
className="text-center text-tiny text-truncate w-100"
title={doc.fileName}
>
{doc.fileName}
</small>
</div>
<div className="w-100">
<small className="mb-0 small">{doc.fileName}</small>
<div className="d">
<a
href={doc.preSignedUrl}
target="_blank"
rel="noopener noreferrer"
className="bx bx-cloud-download cursor-pointer"
/>
</div>
</div>
</div>
);
})}
);
})}
</div>
</div>
{data.expensesReimburse && (
{data.expensesReimburse && (
<div className="row text-start">
<div className="col-md-6 mb-sm-0 mb-2">
<label className="form-label me-2 mb-0 fw-semibold">
@ -371,7 +349,7 @@ const ViewExpense = ({ ExpenseId }) => {
)}
</div>
)}
<hr className="divider my-1" />
<hr className="divider my-1 py-3 divider-primary" />
{Array.isArray(data?.nextStatus) && data.nextStatus.length > 0 && (
<>
@ -457,11 +435,8 @@ const ViewExpense = ({ ExpenseId }) => {
</>
)}
<ExpenseStatusLogs data={data}/>
<ExpenseStatusLogs data={data} />
</form>
);
};

View File

@ -1,5 +1,14 @@
// it important ------
export const mastersList = [ {id: 1, name: "Application Role"}, {id: 2, name: "Job Role"}, {id: 3, name: "Activity"},{id: 4, name:"Work Category"},{id:5,name:"Contact Category"},{id:6,name:"Contact Tag"}]
export const mastersList = [
{ id: 1, name: "Application Role" },
{ id: 2, name: "Job Role" },
{ id: 3, name: "Activity" },
{ id: 4, name: "Work Category" },
{ id: 5, name: "Contact Category" },
{ id: 6, name: "Contact Tag" },
{ id: 7, name: "Expense Type" },
{ id: 8, name: "Payment Mode" },
];
// -------------------
export const dailyTask = [

View File

@ -180,7 +180,7 @@ const ExpensePage = () => {
{ViewDocument.IsOpen && (
<GlobalModel
isOpen
size="lg"
size="md"
key={ViewDocument.Image ?? "doc"}
closeModal={() => setDocumentView({ IsOpen: false, Image: null })}
>

View File

@ -9,13 +9,18 @@ const MasterTable = ({ data, columns, loading, handleModalData }) => {
const selectedMaster = useSelector(
(store) => store.localVariables.selectedMaster
);
const hiddenColumns = [
const hiddenColumns = [
"id",
"featurePermission",
"tenant",
"tenantId",
"checkLists",
"isSystem",
"isActive",
"noOfPersonsRequired",
"color",
"displayName",
"permissionIds"
];
const safeData = Array.isArray(data) ? data : [];

View File

@ -46,4 +46,18 @@ export const useDebounce = (value, delay = 500) => {
}, [value, delay]);
return debouncedValue;
};
};
export const getIconByFileType = (type = "") => {
const lower = type.toLowerCase();
if (lower === "application/pdf") return "bxs-file-pdf";
if (lower.includes("word")) return "bxs-file-doc";
if (lower.includes("excel") || lower.includes("spreadsheet"))
return "bxs-file-xls";
if (lower === "image/png") return "bxs-file-png";
if (lower === "image/jpeg" || lower === "image/jpg") return "bxs-file-jpg";
if (lower.includes("zip") || lower.includes("rar")) return "bxs-file-archive";
return "bx bx-file";
};