Compare commits

...

24 Commits

Author SHA1 Message Date
034099c23a set up action properly 2025-07-24 14:20:38 +05:30
c14fdae4f7 correctly set isActive flag from document data 2025-07-24 14:19:54 +05:30
9a1fba347a added optinal chain for color , for prevent error 2025-07-24 12:51:30 +05:30
eaf49b513d Merge pull request 'Feature#796_EditExpense : Edit Expense Request' (#282) from Feature#796_EditExpense into Feature_Expense
Reviewed-on: #282
mwrged
2025-07-24 07:18:29 +00:00
719c916af3 Merge branch 'Feature#796_EditExpense' of https://git.marcoaiot.com/admin/marco.pms.web into Feature#796_EditExpense 2025-07-24 12:45:57 +05:30
23ee4a83d1 completed full updation for expnse feature 2025-07-24 12:45:21 +05:30
cf719205d9 progress in expense update 2025-07-24 12:45:21 +05:30
195a88c1e4 renamed status of expse list - name to displayName 2025-07-24 12:43:09 +05:30
f4f8a61445 added utils fun that has code convert into bootstrap color like priamery. 2025-07-24 12:43:08 +05:30
7b2a4ea834 initiated forn edit and created with one component 2025-07-24 12:41:56 +05:30
de689b0af8 completed full updation for expnse feature 2025-07-24 12:35:12 +05:30
289a732600 progress in expense update 2025-07-24 02:55:39 +05:30
5e93798d01 Merge branch 'Feature_Expense' of https://git.marcoaiot.com/admin/marco.pms.web into Feature#796_EditExpense 2025-07-23 18:03:52 +05:30
209e1341de Merge pull request 'Feature#791_ViewExpense : View Expense Detailas from calling expense details api' (#286) from Feature#791_ViewExpense into Feature_Expense
Reviewed-on: #286
merged
2025-07-23 12:11:18 +00:00
ab737a80d4 view expense with document (image), not yet PDF 2025-07-23 17:36:54 +05:30
f30430c254 updated global modal for size customize 2025-07-23 17:27:47 +05:30
f6e18e0482 Merge branch 'Feature_Expense' of https://git.marcoaiot.com/admin/marco.pms.web into Feature#796_EditExpense 2025-07-23 14:17:34 +05:30
fa9736b620 changed added expense button 2025-07-23 14:15:16 +05:30
a23ca8a412 Merge branch 'Feature_Expense' of https://git.marcoaiot.com/admin/marco.pms.web into Feature#796_EditExpense 2025-07-23 14:07:09 +05:30
00b3a3420e renamed status of expse list - name to displayName 2025-07-23 11:59:02 +05:30
6c276a3c8d added utils fun that has code convert into bootstrap color like priamery. 2025-07-23 11:44:59 +05:30
5209b78df7 color, displayName - of Expense status columns names added insinde hidden column 2025-07-23 10:48:18 +05:30
5f6a00f9f2 initiated forn edit and created with one component 2025-07-22 18:53:17 +05:30
9bbd2dd014 Merge pull request 'Feature#794_ActionExpense : Partially Implemented Expense Action' (#279) from Feature#794_ActionExpense into Feature_Expense
Reviewed-on: #279
Merged
2025-07-22 12:07:15 +00:00
14 changed files with 794 additions and 219 deletions

View File

@ -27,6 +27,7 @@
<link rel="stylesheet" href="/assets/vendor/css/theme-default.css" class="template-customizer-theme-css" /> <link rel="stylesheet" href="/assets/vendor/css/theme-default.css" class="template-customizer-theme-css" />
<link rel="stylesheet" href="/assets/css/core-extend.css" /> <link rel="stylesheet" href="/assets/css/core-extend.css" />
<link rel="stylesheet" href="/assets/css/default.css" /> <link rel="stylesheet" href="/assets/css/default.css" />
<link rel="stylesheet" href="/assets/css/skeleton.css" />
<link rel="stylesheet" href="/assets/vendor/libs/perfect-scrollbar/perfect-scrollbar.css" /> <link rel="stylesheet" href="/assets/vendor/libs/perfect-scrollbar/perfect-scrollbar.css" />

32
public/assets/css/skeleton.css vendored Normal file
View File

@ -0,0 +1,32 @@
/* skeleton.css */
.skeleton {
background-color: #e2e8f0; /* Tailwind's gray-300 */
border-radius: 0.25rem; /* Tailwind's rounded */
position: relative;
overflow: hidden;
}
.skeleton::after {
content: '';
display: block;
position: absolute;
top: 0; left: -150px;
height: 100%;
width: 150px;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.4),
transparent
);
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% {
left: -150px;
}
100% {
left: 100%;
}
}

View File

@ -5,9 +5,11 @@ import { useExpenseContext } from "../../pages/Expense/ExpensePage";
import { formatDate, formatUTCToLocalTime } from "../../utils/dateUtils"; import { formatDate, formatUTCToLocalTime } from "../../utils/dateUtils";
import Pagination from "../common/Pagination"; import Pagination from "../common/Pagination";
import { ITEMS_PER_PAGE } from "../../utils/constants"; import { ITEMS_PER_PAGE } from "../../utils/constants";
import { AppColorconfig, getColorNameFromHex } from "../../utils/appUtils";
import { ExpenseTableSkeleton } from "./ExpenseSkeleton";
const ExpenseList = () => { const ExpenseList = () => {
const { setViewExpense } = useExpenseContext(); const { setViewExpense,setManageExpenseModal } = useExpenseContext();
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const pageSize = 10; const pageSize = 10;
@ -21,9 +23,10 @@ const ExpenseList = () => {
}; };
const { data, isLoading, isError,isInitialLoading } = useExpenseList(2, currentPage, filter); const { data, isLoading, isError,isInitialLoading,error,isFetching } = useExpenseList(10, currentPage, filter);
if (isInitialLoading) return <div>Loading...</div>; if (isInitialLoading ) return <ExpenseTableSkeleton/>;
const items = data.data ?? []; if (isError) return <div>{error}</div>;
const items = data?.data ?? [];
const totalPages = data?.totalPages ?? 1; const totalPages = data?.totalPages ?? 1;
const hasMore = currentPage < totalPages; const hasMore = currentPage < totalPages;
@ -125,7 +128,7 @@ const ExpenseList = () => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{isLoading && ( {/* {isLoading && (
<tr> <tr>
<td colSpan={7} className="text-center py-3"> <td colSpan={7} className="text-center py-3">
Loading... Loading...
@ -139,7 +142,7 @@ const ExpenseList = () => {
No expenses found. No expenses found.
</td> </td>
</tr> </tr>
)} )} */}
{!isInitialLoading && {!isInitialLoading &&
items.map((expense) => ( items.map((expense) => (
@ -170,19 +173,13 @@ const ExpenseList = () => {
</span> </span>
</div> </div>
</td> </td>
<td className="d-none d-md-table-cell text-end"><i className='bx bx-rupee b-xs'></i>{expense.amount}</td> <td className="d-none d-md-table-cell text-end"><i className='bx bx-rupee b-xs'></i>{expense?.amount}</td>
<td> <td>
<span <span
style={{ className={`badge bg-label-${getColorNameFromHex(expense?.status?.color) || 'secondary'}`}
backgroundColor: expense.status?.color || "#e2e3e5",
color: "#ffff",
padding: "2px 8px",
borderRadius: "0.375rem",
fontSize: "0.75rem",
fontWeight: 500,
}}
> >
{expense.status?.name || "Unknown"}
{expense.status?.displayName || "Unknown"}
</span> </span>
</td> </td>
<td > <td >
@ -190,21 +187,19 @@ const ExpenseList = () => {
<span <span
className="cursor-pointer" className="cursor-pointer"
onClick={() => onClick={() =>
setViewExpense({ expenseId: expense, view: true }) setViewExpense({ expenseId:expense.id, view: true })
} }
> >
<i className="bx bx-show text-primary "></i> <i className="bx bx-show text-primary "></i>
</span> </span>
<span <span
className="cursor-pointer" className="cursor-pointer"
onClick={()=>setManageExpenseModal({IsOpen:true,expenseId:expense.id})}
> >
<i className='bx bx-edit bx-sm text-secondary'></i> <i className='bx bx-edit bx-sm text-secondary'></i>
</span> </span>
<span <span
className="cursor-pointer" className="cursor-pointer"
> >
<i className='bx bx-trash bx-sm text-danger' ></i> <i className='bx bx-trash bx-sm text-danger' ></i>
</span> </span>

View File

@ -35,14 +35,16 @@ export const ExpenseSchema = (expenseTypes) => {
.array( .array(
z.object({ z.object({
fileName: z.string().min(1, { message: "Filename is required" }), fileName: z.string().min(1, { message: "Filename is required" }),
base64Data: z.string().min(1, { message: "File data is required" }), base64Data: z.string().nullable(),
contentType: z.string().refine((val) => ALLOWED_TYPES.includes(val), { contentType: z.string().refine((val) => ALLOWED_TYPES.includes(val), {
message: "Only PDF, PNG, JPG, or JPEG files are allowed", message: "Only PDF, PNG, JPG, or JPEG files are allowed",
}), }),
documentId:z.string().optional(),
fileSize: z.number().max(MAX_FILE_SIZE, { fileSize: z.number().max(MAX_FILE_SIZE, {
message: "File size must be less than or equal to 5MB", message: "File size must be less than or equal to 5MB",
}), }),
description: z.string().optional(), description: z.string().optional(),
isActive:z.boolean().default(true)
}) })
) )
.nonempty({ message: "At least one file attachment is required" }), .nonempty({ message: "At least one file attachment is required" }),
@ -68,6 +70,21 @@ export const ExpenseSchema = (expenseTypes) => {
}); });
}; };
export const defaultExpense = {
projectId: "",
expensesTypeId: "",
paymentModeId: "",
paidById: "",
transactionDate: "",
transactionId: "",
description: "",
location: "",
supplerName: "",
amount: "",
noOfPersons: "",
billAttachments: [],
}
export const ActionSchema = z.object({ export const ActionSchema = z.object({
comment : z.string().min(1,{message:"Please leave comment"}), comment : z.string().min(1,{message:"Please leave comment"}),
selectedStatus: z.string().min(1, { message: "Please select a status" }), selectedStatus: z.string().min(1, { message: "Please select a status" }),

View File

@ -0,0 +1,218 @@
import React from "react";
const SkeletonLine = ({ height = 20, width = "100%", className = "" }) => (
<div
className={`skeleton mb-2 ${className}`}
style={{
height,
width,
}}
></div>
);
const ExpenseSkeleton = () => {
return (
<div className="container p-3">
<div className="d-flex justify-content-center">
<SkeletonLine height={20} width="200px" />
</div>
{[...Array(5)].map((_, idx) => (
<div className="row my-2" key={idx}>
<div className="col-md-6">
<SkeletonLine />
</div>
<div className="col-md-6">
<SkeletonLine />
</div>
</div>
))}
<div className="row my-2">
<div className="col-md-12">
<SkeletonLine height={60} />
</div>
</div>
<div className="row my-2">
<div className="col-md-12">
<SkeletonLine height={120} />
</div>
</div>
<div className="d-flex justify-content-center gap-2 mt-3">
<SkeletonLine height={35} width="100px" />
<SkeletonLine height={35} width="100px" />
</div>
</div>
);
};
export default ExpenseSkeleton;
export const ExpenseDetailsSkeleton = () => {
return (
<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" />
</div>
{[...Array(3)].map((_, i) => (
<div className="col-12 col-md-4 mb-3" key={`row-1-${i}`}>
<SkeletonLine height={14} className="mb-1" />
<SkeletonLine />
</div>
))}
{[...Array(6)].map((_, i) => (
<div className="col-12 col-md-4 mb-3" key={`row-2-${i}`}>
<SkeletonLine height={14} className="mb-1" />
<SkeletonLine />
</div>
))}
<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="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%" />
</div>
</div>
))}
</div>
<hr className="divider my-1" />
<div className="col-12 mb-3">
<SkeletonLine height={14} width="80px" className="mb-1" />
<SkeletonLine height={60} className="mb-2" />
<div className="d-flex gap-2 flex-wrap">
{[...Array(2)].map((_, i) => (
<SkeletonLine
key={i}
height={30}
width="100px"
className="rounded"
/>
))}
</div>
</div>
</div>
</div>
);
};
const SkeletonCell = ({ width = "100%", height = 20, className = "" }) => (
<div
className={`skeleton ${className}`}
style={{
width,
height,
borderRadius: 4,
}}
/>
);
export const ExpenseTableSkeleton = ({ rows = 5 }) => {
return (
<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 colSpan={2}>
<div className="text-start ms-6">Date Time</div>
</th>
<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>
<tbody>
{[...Array(rows)].map((_, idx) => (
<tr key={idx} className={idx % 2 === 0 ? "odd" : "even"}>
{/* Date Time colSpan=2 */}
<td colSpan={2} className="sorting_1">
<div className="d-flex justify-content-start align-items-center user-name ms-6">
<SkeletonCell width="120px" height={18} />
</div>
</td>
{/* 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 (icons) */}
<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>
))}
</tbody>
</table>
);
};

View File

@ -1,7 +1,7 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { ExpenseSchema } from "./ExpenseSchema"; import { defaultExpense, ExpenseSchema } from "./ExpenseSchema";
import { formatFileSize } from "../../utils/appUtils"; import { formatFileSize } from "../../utils/appUtils";
import { useProjectName } from "../../hooks/useProjects"; import { useProjectName } from "../../hooks/useProjects";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
@ -16,9 +16,19 @@ import {
useEmployeesByProject, useEmployeesByProject,
} from "../../hooks/useEmployees"; } from "../../hooks/useEmployees";
import Avatar from "../common/Avatar"; import Avatar from "../common/Avatar";
import { useCreateExpnse } from "../../hooks/useExpense"; import {
useCreateExpnse,
useExpense,
useUpdateExpense,
} from "../../hooks/useExpense";
import ExpenseSkeleton from "./ExpenseSkeleton";
const CreateExpense = ({closeModal}) => { const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
const {
data,
isLoading,
error: ExpenseErrorLoad,
} = useExpense(expenseToEdit);
const [ExpenseType, setExpenseType] = useState(); const [ExpenseType, setExpenseType] = useState();
const dispatch = useDispatch(); const dispatch = useDispatch();
const { const {
@ -36,20 +46,7 @@ const CreateExpense = ({closeModal}) => {
formState: { errors }, formState: { errors },
} = useForm({ } = useForm({
resolver: zodResolver(schema), resolver: zodResolver(schema),
defaultValues: { defaultValues: defaultExpense,
projectId: "",
expensesTypeId: "",
paymentModeId: "",
paidById: "",
transactionDate: "",
transactionId: "",
description: "",
location: "",
supplerName: "",
amount: "",
noOfPersons: "",
billAttachments: [],
},
}); });
const selectedproject = watch("projectId"); const selectedproject = watch("projectId");
@ -90,6 +87,7 @@ const CreateExpense = ({closeModal}) => {
contentType: file.type, contentType: file.type,
fileSize: file.size, fileSize: file.size,
description: "", description: "",
isActive:true
}; };
}) })
); );
@ -115,35 +113,95 @@ const CreateExpense = ({closeModal}) => {
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
const reader = new FileReader(); const reader = new FileReader();
reader.readAsDataURL(file); reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result.split(",")[1]); // base64 only, no prefix reader.onload = () => resolve(reader.result.split(",")[1]);
reader.onerror = (error) => reject(error); reader.onerror = (error) => reject(error);
}); });
const removeFile = (index) => { const removeFile = (index) => {
const newFiles = files.filter((_, i) => i !== index); if (expenseToEdit) {
setValue("billAttachments", newFiles, { shouldValidate: true }); const newFiles = files.map((file, i) => {
if (file.documentId !== index) return file;
return {
...file,
isActive: false,
};
});
setValue("billAttachments", newFiles, { shouldValidate: true });
} else {
const newFiles = files.filter((_, i) => i !== index);
setValue("billAttachments", newFiles, { shouldValidate: true });
}
}; };
const {mutate:CreateExpense,isPending} = useCreateExpnse(()=>{ useEffect(() => {
if (expenseToEdit && data) {
reset({
projectId: data.project.id || "",
expensesTypeId: data.expensesType.id || "",
paymentModeId: data.paymentMode.id || "",
paidById: data.paidBy.id || "",
transactionDate: data.transactionDate?.slice(0, 10) || "",
transactionId: data.transactionId || "",
description: data.description || "",
location: data.location || "",
supplerName: data.supplerName || "",
amount: data.amount || "",
noOfPersons: data.noOfPersons || "",
billAttachments: data.documents
? data.documents.map((doc) => ({
fileName: doc.fileName,
base64Data: null,
contentType: doc.contentType,
documentId: doc.documentId,
fileSize: 0,
description: "",
preSignedUrl: doc.preSignedUrl,
isActive: doc.isActive ?? true,
}))
: [],
});
}
}, [data, reset, employees]);
const { mutate: ExpenseUpdate, isPending } = useUpdateExpense(() =>
handleClose() handleClose()
}) );
const { mutate: CreateExpense, isPending: createPending } = useCreateExpnse(
() => {
handleClose();
}
);
const onSubmit = (payload) => { const onSubmit = (payload) => {
console.log("Form Data:", payload); if (expenseToEdit) {
const editPayload = { ...payload, id: data.id };
CreateExpense(payload) ExpenseUpdate({ id: data.id, payload: editPayload });
} else {
CreateExpense(payload);
}
}; };
const ExpenseTypeId = watch("expensesTypeId"); const ExpenseTypeId = watch("expensesTypeId");
useEffect(() => { useEffect(() => {
setExpenseType(ExpenseTypes?.find((type) => type.id === ExpenseTypeId)); setExpenseType(ExpenseTypes?.find((type) => type.id === ExpenseTypeId));
return () => reset(defaultExpense);
}, [ExpenseTypeId]); }, [ExpenseTypeId]);
const handleClose =()=>{ const handleClose = () => {
reset() reset();
closeModal() closeModal();
} };
if (
EmpLoading ||
StatusLoadding ||
projectLoading ||
ExpenseLoading ||
isLoading
)
return <ExpenseSkeleton />;
return ( return (
<div className="container p-3"> <div className="container p-3">
<h5 className="m-0">Create New Expense</h5> <h5 className="m-0">
{expenseToEdit ? "Update Expense " : "Create New Expense"}
</h5>
<form id="expenseForm" onSubmit={handleSubmit(onSubmit)}> <form id="expenseForm" onSubmit={handleSubmit(onSubmit)}>
<div className="row my-2"> <div className="row my-2">
<div className="col-md-6"> <div className="col-md-6">
@ -206,7 +264,7 @@ const CreateExpense = ({closeModal}) => {
Payment Mode Payment Mode
</label> </label>
<select <select
class="form-select form-select-sm" className="form-select form-select-sm"
id="paymentModeId" id="paymentModeId"
{...register("paymentModeId")} {...register("paymentModeId")}
> >
@ -235,7 +293,7 @@ const CreateExpense = ({closeModal}) => {
Paid By Paid By
</label> </label>
<select <select
class="form-select form-select-sm" className="form-select form-select-sm"
id="paymentModeId" id="paymentModeId"
{...register("paidById")} {...register("paidById")}
disabled={!selectedproject} disabled={!selectedproject}
@ -248,16 +306,13 @@ const CreateExpense = ({closeModal}) => {
) : ( ) : (
employees?.map((emp) => ( employees?.map((emp) => (
<option key={emp.id} value={emp.id}> <option key={emp.id} value={emp.id}>
{`${emp.firstName} ${emp.lastName} `} {`${emp.firstName} ${emp.lastName} `}
</option> </option>
)) ))
)} )}
</select> </select>
{errors.paidById && ( {errors.paidById && (
<small className="danger-text"> <small className="danger-text">{errors.paidById.message}</small>
{errors.paidById.message}
</small>
)} )}
</div> </div>
</div> </div>
@ -281,7 +336,7 @@ const CreateExpense = ({closeModal}) => {
)} )}
</div> </div>
<div class="col-md-6"> <div className="col-md-6">
<label for="amount" className="form-label "> <label for="amount" className="form-label ">
Amount Amount
</label> </label>
@ -318,7 +373,7 @@ const CreateExpense = ({closeModal}) => {
)} )}
</div> </div>
<div class="col-md-6"> <div className="col-md-6">
<label for="location" className="form-label "> <label for="location" className="form-label ">
Location Location
</label> </label>
@ -333,7 +388,7 @@ const CreateExpense = ({closeModal}) => {
)} )}
</div> </div>
</div> </div>
<div class="row my-2"> <div className="row my-2">
<div className="col-md-6"> <div className="col-md-6">
<label for="statusId" className="form-label "> <label for="statusId" className="form-label ">
TransactionId TransactionId
@ -341,7 +396,7 @@ const CreateExpense = ({closeModal}) => {
<input <input
type="text" type="text"
id="transactionId" id="transactionId"
class="form-control form-control-sm" className="form-control form-control-sm"
min="1" min="1"
{...register("transactionId")} {...register("transactionId")}
/> />
@ -374,7 +429,7 @@ const CreateExpense = ({closeModal}) => {
</div> </div>
<div className="row my-2"> <div className="row my-2">
<div class="col-md-12" className="form-label "> <div className="col-md-12" className="form-label ">
<label for="description">Description</label> <label for="description">Description</label>
<textarea <textarea
id="description" id="description"
@ -392,9 +447,7 @@ const CreateExpense = ({closeModal}) => {
<div className="row my-2"> <div className="row my-2">
<div className="col-md-12"> <div className="col-md-12">
<label className="form-label "> <label className="form-label ">Upload Bill </label>
Upload Bill{" "}
</label>
<div <div
className="border border-secondary border-dashed rounded p-4 text-center bg-textMuted position-relative" className="border border-secondary border-dashed rounded p-4 text-center bg-textMuted position-relative"
@ -425,35 +478,52 @@ const CreateExpense = ({closeModal}) => {
{errors.billAttachments.message} {errors.billAttachments.message}
</small> </small>
)} )}
{files.length > 0 && ( {files.length > 0 && (
<div className="d-block"> <div className="d-block">
{files.map((file, idx) => ( {files
<a .filter((file) => {
key={idx} if (expenseToEdit) {
class="d-flex justify-content-between text-start p-1" return file.isActive;
> }
<div> return true;
<span className="mb-0 text-secondary small d-block"> })
{file.fileName} .map((file, idx) => (
</span> <a
<span className="text-body-secondary small d-block"> key={idx}
{formatFileSize(file.fileSize)} className="d-flex justify-content-between text-start p-1"
</span> href={file.preSignedUrl || "#"}
</div> target="_blank"
<i rel="noopener noreferrer"
className="bx bx-trash bx-sm cursor-pointer text-danger" >
onClick={() => removeFile(idx)} <div>
></i> <span className="mb-0 text-secondary small d-block">
</a> {file.fileName}
))} </span>
<span className="text-body-secondary small d-block">
{file.fileSize ? formatFileSize(file.fileSize) : ""}
</span>
</div>
<i
className="bx bx-trash bx-sm cursor-pointer text-danger"
onClick={(e) => {
e.preventDefault();
removeFile(expenseToEdit ? file.documentId : idx);
}}
></i>
</a>
))}
</div> </div>
)} )}
{Array.isArray(errors.billAttachments) && {Array.isArray(errors.billAttachments) &&
errors.billAttachments.map((fileError, index) => ( errors.billAttachments.map((fileError, index) => (
<div key={index} className="danger-text small mt-1"> <div key={index} className="danger-text small mt-1">
{fileError?.fileSize?.message || {
fileError?.contentType?.message} (fileError?.fileSize?.message ||
fileError?.contentType?.message ||
fileError?.base64Data?.message,
fileError?.documentId.message)
}
</div> </div>
))} ))}
</div> </div>
@ -461,10 +531,19 @@ const CreateExpense = ({closeModal}) => {
<div className="d-flex justify-content-center gap-2"> <div className="d-flex justify-content-center gap-2">
{" "} {" "}
<button type="submit" className="btn btn-primary btn-sm mt-3" disabled={isPending}> <button
{isPending ? "Please Wait...":"Submit"} type="submit"
className="btn btn-primary btn-sm mt-3"
disabled={isPending}
>
{isPending ? "Please Wait..." : expenseToEdit ? "Update" : "Submit"}
</button> </button>
<button type="reset" onClick={handleClose} className="btn btn-secondary btn-sm mt-3"> <button
type="reset"
disabled={isPending || createPending}
onClick={handleClose}
className="btn btn-secondary btn-sm mt-3"
>
Cancel Cancel
</button> </button>
</div> </div>
@ -473,5 +552,4 @@ const CreateExpense = ({closeModal}) => {
); );
}; };
export default CreateExpense; export default ManageExpense;

View File

@ -0,0 +1,28 @@
import { useState } from 'react';
const PreviewDocument = ({ imageUrl }) => {
const [loading, setLoading] = useState(true);
return (
<div className="d-flex justify-content-center align-items-center" style={{ minHeight: "50vh" }}>
{loading && (
<div className="text-secondary text-center mb-2">
Loading...
</div>
)}
<img
src={imageUrl}
alt="Full View"
className="img-fluid"
style={{
maxHeight: "100vh",
objectFit: "contain",
display: loading ? "none" : "block",
}}
onLoad={() => setLoading(false)}
/>
</div>
);
};
export default PreviewDocument;

View File

@ -1,15 +1,22 @@
import React from "react"; import React, { useState } from "react";
import { useActionOnExpense } from "../../hooks/useExpense"; import { useActionOnExpense, useExpense } from "../../hooks/useExpense";
import { formatUTCToLocalTime } from "../../utils/dateUtils"; import { formatUTCToLocalTime } from "../../utils/dateUtils";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { ActionSchema } from "./ExpenseSchema"; import { ActionSchema } from "./ExpenseSchema";
import { useExpenseContext } from "../../pages/Expense/ExpensePage";
import { getColorNameFromHex } from "../../utils/appUtils";
import { ExpenseDetailsSkeleton } from "./ExpenseSkeleton";
const ViewExpense = ({ ExpenseId }) => { const ViewExpense = ({ ExpenseId }) => {
const { data, isLoading, isError, error } = useExpense(ExpenseId);
const [imageLoaded, setImageLoaded] = useState({});
const { setDocumentView } = useExpenseContext();
const { const {
register, register,
handleSubmit, handleSubmit,
setValue, setValue,
reset,
formState: { errors }, formState: { errors },
} = useForm({ } = useForm({
resolver: zodResolver(ActionSchema), resolver: zodResolver(ActionSchema),
@ -19,11 +26,11 @@ const ViewExpense = ({ ExpenseId }) => {
}, },
}); });
const { mutate: MakeAction } = useActionOnExpense(); const { mutate: MakeAction } = useActionOnExpense(()=>reset());
const onSubmit = (formData) => { const onSubmit = (formData) => {
const Payload = { const Payload = {
expenseId: ExpenseId?.id, expenseId: ExpenseId,
statusId: formData.selectedStatus, statusId: formData.selectedStatus,
comment: formData.comment, comment: formData.comment,
}; };
@ -31,6 +38,11 @@ const ViewExpense = ({ ExpenseId }) => {
MakeAction(Payload); MakeAction(Payload);
}; };
if (isLoading) return <ExpenseDetailsSkeleton/>
const handleImageLoad = (id) => {
setImageLoaded((prev) => ({ ...prev, [id]: true }));
};
return ( return (
<form className="container px-3" onSubmit={handleSubmit(onSubmit)}> <form className="container px-3" onSubmit={handleSubmit(onSubmit)}>
<div className="row mb-3"> <div className="row mb-3">
@ -40,100 +52,168 @@ const ViewExpense = ({ ExpenseId }) => {
</div> </div>
{/* Expense Info Rows */} {/* Expense Info Rows */}
<div className="col-12 col-md-4 mb-3"> <div className="col-12 col-md-4 mb-3">
<div className="d-flex"> <div className="d-flex">
<label className="form-label me-2 mb-0 fw-semibold">Transaction Date :</label> <label className="form-label me-2 mb-0 fw-semibold">
<div className="text-muted">{formatUTCToLocalTime(ExpenseId.transactionDate)}</div> Transaction Date :
</label>
<div className="text-muted">
{formatUTCToLocalTime(data.transactionDate)}
</div>
</div> </div>
</div> </div>
<div className="col-12 col-md-4 mb-3"> <div className="col-12 col-md-4 mb-3">
<div className="d-flex"> <div className="d-flex">
<label className="form-label me-2 mb-0 fw-semibold">Expense Type :</label> <label className="form-label me-2 mb-0 fw-semibold">
<div className="text-muted">{ExpenseId.expensesType.name}</div> Expense Type :
</label>
<div className="text-muted">{data.expensesType.name}</div>
</div> </div>
</div> </div>
<div className="col-12 col-md-4 mb-3"> <div className="col-12 col-md-4 mb-3">
<div className="d-flex"> <div className="d-flex">
<label className="form-label me-2 mb-0 fw-semibold">Supplier :</label> <label className="form-label me-2 mb-0 fw-semibold">
<div className="text-muted">{ExpenseId.supplerName}</div> Supplier :
</label>
<div className="text-muted">{data.supplerName}</div>
</div> </div>
</div> </div>
<div className="col-12 col-md-4 mb-3"> <div className="col-12 col-md-4 mb-3">
<div className="d-flex"> <div className="d-flex">
<label className="form-label me-2 mb-0 fw-semibold">Amount :</label> <label className="form-label me-2 mb-0 fw-semibold">Amount :</label>
<div className="text-muted"> {ExpenseId.amount}</div> <div className="text-muted"> {data.amount}</div>
</div> </div>
</div> </div>
<div className="col-12 col-md-4 mb-3"> <div className="col-12 col-md-4 mb-3">
<div className="d-flex"> <div className="d-flex">
<label className="form-label me-2 mb-0 fw-semibold">Payment Mode :</label> <label className="form-label me-2 mb-0 fw-semibold">
<div className="text-muted">{ExpenseId.paymentMode.name}</div> Payment Mode :
</label>
<div className="text-muted">{data.paymentMode.name}</div>
</div> </div>
</div> </div>
<div className="col-12 col-md-4 mb-3"> <div className="col-12 col-md-4 mb-3">
<div className="d-flex"> <div className="d-flex">
<label className="form-label me-2 mb-0 fw-semibold">Paid By :</label> <label className="form-label me-2 mb-0 fw-semibold">
Paid By :
</label>
<div className="text-muted"> <div className="text-muted">
{ExpenseId.paidBy.firstName} {ExpenseId.paidBy.lastName} {data.paidBy.firstName} {data.paidBy.lastName}
</div> </div>
</div> </div>
</div> </div>
<div className="col-12 col-md-4 mb-3"> <div className="col-12 text-start col-md-4 mb-3">
<div className="d-flex align-items-center">
<label className="form-label me-2 mb-0 fw-semibold">Status :</label> <label className="form-label me-2 mb-0 fw-semibold">Status :</label>
<span className="badge" style={{ backgroundColor: ExpenseId.status.color }}>
{ExpenseId.status.displayName} <span className={`badge bg-label-${getColorNameFromHex(data?.status?.color) || 'secondary'}`}>
{data?.status?.displayName}
</span> </span>
</div> </div>
</div>
<div className="col-12 col-md-4 mb-3"> <div className="col-12 text-start d-flex col-md-4 mb-3">
<label className="form-label me-2 mb-0 fw-semibold">
Pre-Approved :
</label>
<div className="text-muted">{data.preApproved ? "Yes" : "No"}</div>
</div>
<div className="col-12 col-md-4 mb-3">
<div className="d-flex"> <div className="d-flex">
<label className="form-label me-2 mb-0 fw-semibold">Pre-Approved :</label> <label className="form-label me-2 mb-0 fw-semibold">
<div className="text-muted">{ExpenseId.preApproved ? "Yes" : "No"}</div> Project :
</label>
<div className="text-muted text-start">{data?.project?.name}</div>
</div> </div>
</div> </div>
<div className="col-12 col-md-4 mb-3"> <div className="col-12 col-md-4 mb-3">
<div className="d-flex"> <div className="d-flex">
<label className="form-label me-2 mb-0 fw-semibold">Project :</label> <label className="form-label me-2 mb-0 fw-semibold">
<div className="text-muted text-start">{ExpenseId.project.name}</div> Created By :
</div> </label>
</div>
<div className="col-12 col-md-4 mb-3">
<div className="d-flex">
<label className="form-label me-2 mb-0 fw-semibold">Created By :</label>
<div className="text-muted"> <div className="text-muted">
{ExpenseId.createdBy.firstName} {ExpenseId.createdBy.lastName} {data?.createdBy?.firstName} {data?.createdBy?.lastName}
</div> </div>
</div> </div>
</div> </div>
<div className="col-12 col-md-4 mb-3"> <div className="col-12 col-md-4 mb-3">
<div className="d-flex"> <div className="d-flex">
<label className="form-label me-2 mb-0 fw-semibold">Created At :</label> <label className="form-label me-2 mb-0 fw-semibold">
<div className="text-muted">{formatUTCToLocalTime(ExpenseId.createdAt, true)}</div> Created At :
</label>
<div className="text-muted">
{formatUTCToLocalTime(data?.createdAt, true)}
</div>
</div> </div>
</div> </div>
</div> </div>
<div className="text-start"> <div className="text-start">
<label className="form-label me-2 mb-0 fw-semibold">Description:</label> <label className="form-label me-2 mb-0 fw-semibold">Description:</label>
<div className="text-muted"> <div className="text-muted">{data?.description}</div>
Local travel reimbursement for delivery of materials to client site via City Taxi Service </div>
</div> <div className="col-12 my-2 text-start ">
<label className="form-label me-2 mb-0 fw-semibold">
Attachement :
</label>
{data?.documents && data?.documents?.map((doc) => (
<div
className="list-group-item list-group-item-action d-flex align-items-center"
key={doc.id}
>
<div
className="rounded me-1 d-flex align-items-center justify-content-center cursor-pointer"
style={{ height: "50px", width: "80px", position: "relative" }}
>
{doc.contentType === "application/pdf" ? (
<div><i className="bx bxs-file-pdf" style={{fontSize:"45px"}}></i></div>
) : (
<>
{!imageLoaded[doc.id] && (
<div className="position-absolute text-secondary">
Loading...
</div>
)}
<img
src={doc.thumbPreSignedUrl}
alt={doc.fileName}
className="img-fluid rounded"
style={{
maxHeight: "100%",
maxWidth: "100%",
objectFit: "cover",
opacity: imageLoaded[doc.id] ? 1 : 0,
transition: "opacity 0.3s ease-in-out",
}}
onLoad={() => handleImageLoad(doc.id)}
onClick={() =>
setDocumentView({ IsOpen: true, Image: doc.preSignedUrl })
}
/>
</>
)}
</div>
<div className="w-100">
<small className="mb-0 small">{doc.fileName}</small>
<div className="d">
<i className="bx bx-cloud-download cursor-pointer"></i>
</div>
</div>
</div>
))}
</div> </div>
<hr className="divider my-1" /> <hr className="divider my-1" />
{Array.isArray(ExpenseId.nextStatus) && ExpenseId.nextStatus.length > 0 && ( {Array.isArray(data.nextStatus) && data.nextStatus.length > 0 && (
<div className="col-12 mb-3 text-start"> <div className="col-12 mb-3 text-start">
<label className="form-label me-2 mb-0 fw-semibold">Comment:</label> <label className="form-label me-2 mb-0 fw-semibold">Comment:</label>
<textarea <textarea
@ -147,8 +227,8 @@ const ViewExpense = ({ ExpenseId }) => {
<input type="hidden" {...register("selectedStatus")} /> <input type="hidden" {...register("selectedStatus")} />
<div className="d-flex flex-wrap gap-2 my-2"> <div className="text-center flex-wrap gap-2 my-2">
{ExpenseId.nextStatus.map((status, index) => ( {data.nextStatus.map((status, index) => (
<button <button
key={index} key={index}
type="button" type="button"
@ -156,14 +236,10 @@ const ViewExpense = ({ ExpenseId }) => {
setValue("selectedStatus", status.id); setValue("selectedStatus", status.id);
handleSubmit(onSubmit)(); handleSubmit(onSubmit)();
}} }}
className="badge cursor-pointer border-0" className="btn btn-primary btn-sm cursor-pointer mx-2 border-0"
style={{
backgroundColor: status.color || "#6c757d",
color: "#fff",
fontSize: "0.85rem",
}}
> >
{status.displayName || status.name} {status.displayName || status.name}
</button> </button>
))} ))}
</div> </div>

View File

@ -68,6 +68,21 @@ useEffect(() => {
backgroundColor: 'rgba(0,0,0,0.3)', backgroundColor: 'rgba(0,0,0,0.3)',
}; };
const getPositionClass = (type) => {
switch (type) {
case 'top':
return 'mt-3';
case 'bottom':
return 'mt-auto mb-0 d-flex flex-column';
case 'left':
return 'position-absolute top-50 start-0 translate-middle-y me-auto';
case 'right':
return 'position-absolute top-50 end-0 translate-middle-y ms-auto';
case 'center':
default:
return 'modal-dialog-centered';
}
};
return ( return (
<div <div
@ -80,7 +95,7 @@ useEffect(() => {
{...dataAttributesProps} {...dataAttributesProps}
style={backdropStyle} style={backdropStyle}
> >
<div className={`modal-dialog ${dialogClass} ${modalSizeClass } mx-sm-auto mx-1`} role={role} > <div className={`modal-dialog ${modalSizeClass} ${getPositionClass(modalType)} ${dialogClass} mx-sm-auto mx-1`} role={role}>
<div className="modal-content"> <div className="modal-content">
<div className="modal-header p-0"> <div className="modal-header p-0">
{/* Close button inside the modal header */} {/* Close button inside the modal header */}

View File

@ -6,7 +6,7 @@ import { queryClient } from "../layouts/AuthLayout";
// -------------------Query------------------------------------------------------ // -------------------Query------------------------------------------------------
export const useExpenseList = (pageSize, pageNumber, filter) => { export const useExpenseList = (pageSize, pageNumber, filter) => {
return useQuery({ return useQuery({
queryKey: ["expenses", pageNumber, pageSize, filter], queryKey: ["Expenses", pageNumber, pageSize, filter],
queryFn: async () => queryFn: async () =>
await ExpenseRepository.GetExpenseList(pageSize, pageNumber, filter).then( await ExpenseRepository.GetExpenseList(pageSize, pageNumber, filter).then(
(res) => res.data (res) => res.data
@ -18,7 +18,11 @@ export const useExpenseList = (pageSize, pageNumber, filter) => {
export const useExpense = (ExpenseId) => { export const useExpense = (ExpenseId) => {
return useQuery({ return useQuery({
queryKey: ["Expense", ExpenseId], queryKey: ["Expense", ExpenseId],
queryFn: async () => await ExpenseRepository.GetExpenseDetails(ExpenseId), queryFn: async () =>
await ExpenseRepository.GetExpenseDetails(ExpenseId).then(
(res) => res.data
),
enabled: !!ExpenseId,
}); });
}; };
@ -31,8 +35,8 @@ export const useCreateExpnse = (onSuccessCallBack) => {
await ExpenseRepository.CreateExpense(payload); await ExpenseRepository.CreateExpense(payload);
}, },
onSuccess: (_, variables) => { onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ["Expenses"] });
showToast("Expense Created Successfully", "success"); showToast("Expense Created Successfully", "success");
queryClient.invalidateQueries({ queryKey: ["expenses"] });
if (onSuccessCallBack) onSuccessCallBack(); if (onSuccessCallBack) onSuccessCallBack();
}, },
onError: (error) => { onError: (error) => {
@ -44,6 +48,68 @@ export const useCreateExpnse = (onSuccessCallBack) => {
}); });
}; };
export const useUpdateExpense = (onSuccessCallBack) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, payload }) => {
const response = await ExpenseRepository.UpdateExpense(id, payload);
return response.data;
},
onSuccess: (updatedExpense, variables) => {
// queryClient.setQueriesData(
// {queryKey:['expenses'],exact:true},
// (oldData) => {
// if (!oldData || !oldData.data) return oldData;
// const updatedList = oldData.data.map((expense) => {
// if (expense.id !== variables.id) return expense;
// return {
// ...expense,
// project:
// expense.project.id !== updatedExpense.project.id
// ? updatedExpense.project
// : expense.project,
// expensesType:
// expense.expensesType.id !== updatedExpense.expensesType.id
// ? updatedExpense.expensesType
// : expense.expensesType,
// paymentMode:
// expense.paymentMode.id !== updatedExpense.paymentMode.id
// ? updatedExpense.paymentMode
// : expense.paymentMode,
// paidBy:
// expense.paidBy.id !== updatedExpense.paidBy.id
// ? updatedExpense.paidBy
// : expense.paidBy,
// createdBy:
// expense.createdBy.id !== updatedExpense.createdBy.id
// ? updatedExpense.createdBy
// : expense.createdBy,
// createdAt: updatedExpense.createdAt,
// status: updatedExpense.status,
// nextStatus: updatedExpense.nextStatus,
// preApproved: updatedExpense.preApproved,
// transactionDate: updatedExpense.transactionDate,
// amount: updatedExpense.amount,
// };
// });
// return {
// ...oldData,
// data: updatedList,
// };
// }
// );
queryClient.removeQueries({ queryKey: ["Expense", variables.id] });
queryClient.invalidateQueries({ queryKey: ["Expenses"] });
showToast("Expense updated Successfully", "success");
if (onSuccessCallBack) onSuccessCallBack();
},
});
};
export const useActionOnExpense = (onSuccessCallBack) => { export const useActionOnExpense = (onSuccessCallBack) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@ -53,11 +119,11 @@ export const useActionOnExpense = (onSuccessCallBack) => {
return response.data; return response.data;
}, },
onSuccess: (updatedExpense, variables) => { onSuccess: (updatedExpense, variables) => {
showToast("Expense updated successfully", "success"); showToast("Request processed successfully.", "success");
queryClient.setQueriesData( queryClient.setQueriesData(
{ {
queryKey: ["expenses"], queryKey: ["Expenses"],
exact: false, exact: false,
}, },
(oldData) => { (oldData) => {
@ -76,6 +142,16 @@ export const useActionOnExpense = (onSuccessCallBack) => {
}; };
} }
); );
queryClient.setQueriesData(
{ queryKey: ["Expense", updatedExpense.id] },
(oldData) => {
return {
...oldData,
nextStatus: updatedExpense.nextStatus,
status: updatedExpense.status,
};
}
);
if (onSuccessCallBack) onSuccessCallBack(); if (onSuccessCallBack) onSuccessCallBack();
}, },

View File

@ -1,23 +1,33 @@
import React, { createContext, useContext, useState } from "react"; import React, { createContext, useContext, useState } from "react";
import ExpenseList from "../../components/Expenses/ExpenseList"; import ExpenseList from "../../components/Expenses/ExpenseList";
import CreateExpense from "../../components/Expenses/CreateExpense";
import ViewExpense from "../../components/Expenses/ViewExpense"; import ViewExpense from "../../components/Expenses/ViewExpense";
import Breadcrumb from "../../components/common/Breadcrumb"; import Breadcrumb from "../../components/common/Breadcrumb";
import GlobalModel from "../../components/common/GlobalModel"; import GlobalModel from "../../components/common/GlobalModel";
import PreviewDocument from "../../components/Expenses/PreviewDocument";
import ManageExpense from "../../components/Expenses/ManageExpense";
export const ExpenseContext = createContext(); export const ExpenseContext = createContext();
export const useExpenseContext = () => useContext(ExpenseContext); export const useExpenseContext = () => useContext(ExpenseContext);
const ExpensePage = () => { const ExpensePage = () => {
const [isNewExpense, setNewExpense] = useState(false); const [ManageExpenseModal, setManageExpenseModal] = useState({
IsOpen: null,
expenseId: null,
});
const [viewExpense, setViewExpense] = useState({ const [viewExpense, setViewExpense] = useState({
expenseId: null, expenseId: null,
view: false, view: false,
}); });
const [ViewDocument, setDocumentView] = useState({
IsOpen: false,
Image: null,
});
const contextValue = { const contextValue = {
setViewExpense, setViewExpense,
setManageExpenseModal,
setDocumentView,
}; };
return ( return (
@ -46,11 +56,20 @@ const ExpensePage = () => {
<div className="col-7 col-sm-8 text-end gap-2"> <div className="col-7 col-sm-8 text-end gap-2">
<button <button
type="button" type="button"
className="btn btn-sm btn-primary me-2" data-bs-toggle="tooltip"
onClick={() => setNewExpense(true)} data-bs-offset="0,8"
data-bs-placement="top"
data-bs-custom-class="tooltip"
title="Add New Expense"
className={`p-1 me-2 bg-primary rounded-circle `}
onClick={() =>
setManageExpenseModal({
IsOpen: true,
expenseId: null,
})
}
> >
<span className="icon-base bx bx-plus-circle me-1"></span> <i className="bx bx-plus fs-4 text-white"></i>
Add New
</button> </button>
</div> </div>
</div> </div>
@ -58,31 +77,53 @@ const ExpensePage = () => {
</div> </div>
<ExpenseList /> <ExpenseList />
{ManageExpenseModal.IsOpen && (
<GlobalModel
isOpen={ManageExpenseModal.IsOpen}
size="lg"
closeModal={() =>
setManageExpenseModal({
IsOpen: null,
expenseId: null,
})
}
>
<ManageExpense
key={ManageExpenseModal.expenseId ?? "new"}
expenseToEdit={ManageExpenseModal.expenseId}
closeModal={() =>
setManageExpenseModal({ IsOpen: null, expenseId: null })
}
/>
</GlobalModel>
)}
{isNewExpense && ( {viewExpense.view && (
<GlobalModel <GlobalModel
isOpen={isNewExpense} isOpen={viewExpense.view}
size="lg" size="lg"
closeModal={() => setNewExpense(false)} modalType="top"
> closeModal={() =>
<CreateExpense closeModal={() => setNewExpense(false)} /> setViewExpense({
</GlobalModel> expenseId: null,
)} view: false,
})
}
>
<ViewExpense ExpenseId={viewExpense.expenseId} />
</GlobalModel>
)}
{ViewDocument.IsOpen && (
{viewExpense.view && (<GlobalModel <GlobalModel
isOpen={viewExpense.view} size="lg"
size="lg" key={ViewDocument.IsOpen ?? "new"}
closeModal={() => isOpen={ViewDocument.IsOpen}
setViewExpense({ closeModal={() => setDocumentView({ IsOpen: false, Image: null })}
expenseId: null, >
view: false, <PreviewDocument imageUrl={ViewDocument.Image} />
}) </GlobalModel>
} )}
>
<ViewExpense ExpenseId={viewExpense.expenseId}/>
</GlobalModel>) }
</div> </div>
</ExpenseContext.Provider> </ExpenseContext.Provider>
); );

View File

@ -17,7 +17,9 @@ const MasterTable = ({ data, columns, loading, handleModalData }) => {
"checkLists", "checkLists",
"isSystem", "isSystem",
"isActive", "isActive",
"noOfPersonsRequired" "noOfPersonsRequired",
"color",
"displayName"
]; ];
const safeData = Array.isArray(data) ? data : []; const safeData = Array.isArray(data) ? data : [];

View File

@ -12,10 +12,11 @@ const ExpenseRepository = {
GetExpenseDetails:(id)=>api.get(`/api/Expense/details/${id}`), GetExpenseDetails:(id)=>api.get(`/api/Expense/details/${id}`),
CreateExpense:(data)=>api.post("/api/Expense/create",data), CreateExpense:(data)=>api.post("/api/Expense/create",data),
UpdateExpense:(id)=>api.put(`/api/Expense/edit/${id}`), UpdateExpense:(id,data)=>api.put(`/api/Expense/edit/${id}`,data),
DeleteExpense:(id)=>api.delete(`/api/Expense/edit/${id}`), DeleteExpense:(id)=>api.delete(`/api/Expense/edit/${id}`),
ActionOnExpense:(data)=>api.post('/api/expense/action',data) ActionOnExpense:(data)=>api.post('/api/expense/action',data),
} }

View File

@ -3,39 +3,34 @@ export const formatFileSize=(bytes)=> {
else if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + " KB"; else if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + " KB";
else return (bytes / (1024 * 1024)).toFixed(2) + " MB"; else return (bytes / (1024 * 1024)).toFixed(2) + " MB";
} }
export const getExpenseIcon = (type) => { export const AppColorconfig = {
switch (type.toLowerCase()) { colors: {
case 'vendor/supplier payments': primary: '#696cff',
return 'bx-briefcase'; // Business-related secondary: '#8592a3',
case 'transport': success: '#71dd37',
return 'bx-car'; // Vehicle or logistics info: '#03c3ec',
case 'compliance & safety': warning: '#ffab00',
return 'bx-shield-quarter'; // Security/safety danger: '#ff3e1d',
case 'mobilization': dark: '#233446',
return 'bx-building-house'; // Setup / site infra black: '#000',
case 'procurement': white: '#fff',
return 'bx-package'; // Box/package/supplies cardColor: '#fff',
case 'maintenance & utilities': bodyBg: '#f5f5f9',
return 'bx-wrench'; // Repair/maintenance bodyColor: '#697a8d',
case 'travelling': headingColor: '#566a7f',
return 'bx-plane'; // Personnel delivery textMuted: '#a1acb8',
case 'employee welfare': borderColor: '#eceef1'
return 'bx-user-heart'; // Welfare / people
default:
return 'bx-folder'; // Fallback icon
} }
}; };
export const getPaymentModeIcon = (mode) => { export const getColorNameFromHex = (hex) => {
switch (mode.toLowerCase()) { const normalizedHex = hex?.replace(/'/g, '').toLowerCase();
case 'cash': const colors = AppColorconfig.colors;
return 'bx-money'; // Cash/coins
case 'upi': for (const [name, value] of Object.entries(colors)) {
return 'bx-mobile-alt'; // Mobile payment if (value.toLowerCase() === normalizedHex) {
case 'cheque': return name;
return 'bx-receipt'; // Paper receipt }
case 'netbanking':
return 'bx-globe'; // Online/internet
default:
return 'bx-credit-card'; // Generic fallback
} }
return null; //
}; };