Compare commits
24 Commits
280715d62f
...
034099c23a
| Author | SHA1 | Date | |
|---|---|---|---|
| 034099c23a | |||
| c14fdae4f7 | |||
| 9a1fba347a | |||
| eaf49b513d | |||
| 719c916af3 | |||
| 23ee4a83d1 | |||
| cf719205d9 | |||
| 195a88c1e4 | |||
| f4f8a61445 | |||
| 7b2a4ea834 | |||
| de689b0af8 | |||
| 289a732600 | |||
| 5e93798d01 | |||
| 209e1341de | |||
| ab737a80d4 | |||
| f30430c254 | |||
| f6e18e0482 | |||
| fa9736b620 | |||
| a23ca8a412 | |||
| 00b3a3420e | |||
| 6c276a3c8d | |||
| 5209b78df7 | |||
| 5f6a00f9f2 | |||
| 9bbd2dd014 |
@ -27,6 +27,7 @@
|
||||
<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/default.css" />
|
||||
<link rel="stylesheet" href="/assets/css/skeleton.css" />
|
||||
|
||||
<link rel="stylesheet" href="/assets/vendor/libs/perfect-scrollbar/perfect-scrollbar.css" />
|
||||
|
||||
|
||||
32
public/assets/css/skeleton.css
vendored
Normal file
32
public/assets/css/skeleton.css
vendored
Normal 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%;
|
||||
}
|
||||
}
|
||||
@ -5,9 +5,11 @@ import { useExpenseContext } from "../../pages/Expense/ExpensePage";
|
||||
import { formatDate, formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||
import Pagination from "../common/Pagination";
|
||||
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
||||
import { AppColorconfig, getColorNameFromHex } from "../../utils/appUtils";
|
||||
import { ExpenseTableSkeleton } from "./ExpenseSkeleton";
|
||||
|
||||
const ExpenseList = () => {
|
||||
const { setViewExpense } = useExpenseContext();
|
||||
const { setViewExpense,setManageExpenseModal } = useExpenseContext();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const pageSize = 10;
|
||||
|
||||
@ -21,9 +23,10 @@ const ExpenseList = () => {
|
||||
};
|
||||
|
||||
|
||||
const { data, isLoading, isError,isInitialLoading } = useExpenseList(2, currentPage, filter);
|
||||
if (isInitialLoading) return <div>Loading...</div>;
|
||||
const items = data.data ?? [];
|
||||
const { data, isLoading, isError,isInitialLoading,error,isFetching } = useExpenseList(10, currentPage, filter);
|
||||
if (isInitialLoading ) return <ExpenseTableSkeleton/>;
|
||||
if (isError) return <div>{error}</div>;
|
||||
const items = data?.data ?? [];
|
||||
const totalPages = data?.totalPages ?? 1;
|
||||
const hasMore = currentPage < totalPages;
|
||||
|
||||
@ -125,7 +128,7 @@ const ExpenseList = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{isLoading && (
|
||||
{/* {isLoading && (
|
||||
<tr>
|
||||
<td colSpan={7} className="text-center py-3">
|
||||
Loading...
|
||||
@ -139,7 +142,7 @@ const ExpenseList = () => {
|
||||
No expenses found.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
)} */}
|
||||
|
||||
{!isInitialLoading &&
|
||||
items.map((expense) => (
|
||||
@ -170,19 +173,13 @@ const ExpenseList = () => {
|
||||
</span>
|
||||
</div>
|
||||
</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>
|
||||
<span
|
||||
style={{
|
||||
backgroundColor: expense.status?.color || "#e2e3e5",
|
||||
color: "#ffff",
|
||||
padding: "2px 8px",
|
||||
borderRadius: "0.375rem",
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
className={`badge bg-label-${getColorNameFromHex(expense?.status?.color) || 'secondary'}`}
|
||||
>
|
||||
{expense.status?.name || "Unknown"}
|
||||
|
||||
{expense.status?.displayName || "Unknown"}
|
||||
</span>
|
||||
</td>
|
||||
<td >
|
||||
@ -190,21 +187,19 @@ const ExpenseList = () => {
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={() =>
|
||||
setViewExpense({ expenseId: expense, view: true })
|
||||
setViewExpense({ expenseId:expense.id, view: true })
|
||||
}
|
||||
>
|
||||
<i className="bx bx-show text-primary "></i>
|
||||
</span>
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
|
||||
onClick={()=>setManageExpenseModal({IsOpen:true,expenseId:expense.id})}
|
||||
>
|
||||
<i className='bx bx-edit bx-sm text-secondary'></i>
|
||||
</span>
|
||||
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
|
||||
>
|
||||
<i className='bx bx-trash bx-sm text-danger' ></i>
|
||||
</span>
|
||||
|
||||
@ -35,14 +35,16 @@ export const ExpenseSchema = (expenseTypes) => {
|
||||
.array(
|
||||
z.object({
|
||||
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), {
|
||||
message: "Only PDF, PNG, JPG, or JPEG files are allowed",
|
||||
}),
|
||||
documentId:z.string().optional(),
|
||||
fileSize: z.number().max(MAX_FILE_SIZE, {
|
||||
message: "File size must be less than or equal to 5MB",
|
||||
}),
|
||||
description: z.string().optional(),
|
||||
isActive:z.boolean().default(true)
|
||||
})
|
||||
)
|
||||
.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({
|
||||
comment : z.string().min(1,{message:"Please leave comment"}),
|
||||
selectedStatus: z.string().min(1, { message: "Please select a status" }),
|
||||
|
||||
218
src/components/Expenses/ExpenseSkeleton.jsx
Normal file
218
src/components/Expenses/ExpenseSkeleton.jsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@ -1,7 +1,7 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { ExpenseSchema } from "./ExpenseSchema";
|
||||
import { defaultExpense, ExpenseSchema } from "./ExpenseSchema";
|
||||
import { formatFileSize } from "../../utils/appUtils";
|
||||
import { useProjectName } from "../../hooks/useProjects";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
@ -16,9 +16,19 @@ import {
|
||||
useEmployeesByProject,
|
||||
} from "../../hooks/useEmployees";
|
||||
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 dispatch = useDispatch();
|
||||
const {
|
||||
@ -36,20 +46,7 @@ const CreateExpense = ({closeModal}) => {
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
projectId: "",
|
||||
expensesTypeId: "",
|
||||
paymentModeId: "",
|
||||
paidById: "",
|
||||
transactionDate: "",
|
||||
transactionId: "",
|
||||
description: "",
|
||||
location: "",
|
||||
supplerName: "",
|
||||
amount: "",
|
||||
noOfPersons: "",
|
||||
billAttachments: [],
|
||||
},
|
||||
defaultValues: defaultExpense,
|
||||
});
|
||||
|
||||
const selectedproject = watch("projectId");
|
||||
@ -90,6 +87,7 @@ const CreateExpense = ({closeModal}) => {
|
||||
contentType: file.type,
|
||||
fileSize: file.size,
|
||||
description: "",
|
||||
isActive:true
|
||||
};
|
||||
})
|
||||
);
|
||||
@ -115,35 +113,95 @@ const CreateExpense = ({closeModal}) => {
|
||||
new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
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);
|
||||
});
|
||||
const removeFile = (index) => {
|
||||
if (expenseToEdit) {
|
||||
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()
|
||||
})
|
||||
);
|
||||
const { mutate: CreateExpense, isPending: createPending } = useCreateExpnse(
|
||||
() => {
|
||||
handleClose();
|
||||
}
|
||||
);
|
||||
const onSubmit = (payload) => {
|
||||
console.log("Form Data:", payload);
|
||||
|
||||
CreateExpense(payload)
|
||||
if (expenseToEdit) {
|
||||
const editPayload = { ...payload, id: data.id };
|
||||
ExpenseUpdate({ id: data.id, payload: editPayload });
|
||||
} else {
|
||||
CreateExpense(payload);
|
||||
}
|
||||
};
|
||||
const ExpenseTypeId = watch("expensesTypeId");
|
||||
|
||||
useEffect(() => {
|
||||
setExpenseType(ExpenseTypes?.find((type) => type.id === ExpenseTypeId));
|
||||
return () => reset(defaultExpense);
|
||||
}, [ExpenseTypeId]);
|
||||
|
||||
const handleClose =()=>{
|
||||
reset()
|
||||
closeModal()
|
||||
}
|
||||
const handleClose = () => {
|
||||
reset();
|
||||
closeModal();
|
||||
};
|
||||
if (
|
||||
EmpLoading ||
|
||||
StatusLoadding ||
|
||||
projectLoading ||
|
||||
ExpenseLoading ||
|
||||
isLoading
|
||||
)
|
||||
return <ExpenseSkeleton />;
|
||||
|
||||
return (
|
||||
<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)}>
|
||||
<div className="row my-2">
|
||||
<div className="col-md-6">
|
||||
@ -206,7 +264,7 @@ const CreateExpense = ({closeModal}) => {
|
||||
Payment Mode
|
||||
</label>
|
||||
<select
|
||||
class="form-select form-select-sm"
|
||||
className="form-select form-select-sm"
|
||||
id="paymentModeId"
|
||||
{...register("paymentModeId")}
|
||||
>
|
||||
@ -235,7 +293,7 @@ const CreateExpense = ({closeModal}) => {
|
||||
Paid By
|
||||
</label>
|
||||
<select
|
||||
class="form-select form-select-sm"
|
||||
className="form-select form-select-sm"
|
||||
id="paymentModeId"
|
||||
{...register("paidById")}
|
||||
disabled={!selectedproject}
|
||||
@ -248,16 +306,13 @@ const CreateExpense = ({closeModal}) => {
|
||||
) : (
|
||||
employees?.map((emp) => (
|
||||
<option key={emp.id} value={emp.id}>
|
||||
|
||||
{`${emp.firstName} ${emp.lastName} `}
|
||||
</option>
|
||||
))
|
||||
)}
|
||||
</select>
|
||||
{errors.paidById && (
|
||||
<small className="danger-text">
|
||||
{errors.paidById.message}
|
||||
</small>
|
||||
<small className="danger-text">{errors.paidById.message}</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -281,7 +336,7 @@ const CreateExpense = ({closeModal}) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div className="col-md-6">
|
||||
<label for="amount" className="form-label ">
|
||||
Amount
|
||||
</label>
|
||||
@ -318,7 +373,7 @@ const CreateExpense = ({closeModal}) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div className="col-md-6">
|
||||
<label for="location" className="form-label ">
|
||||
Location
|
||||
</label>
|
||||
@ -333,7 +388,7 @@ const CreateExpense = ({closeModal}) => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row my-2">
|
||||
<div className="row my-2">
|
||||
<div className="col-md-6">
|
||||
<label for="statusId" className="form-label ">
|
||||
TransactionId
|
||||
@ -341,7 +396,7 @@ const CreateExpense = ({closeModal}) => {
|
||||
<input
|
||||
type="text"
|
||||
id="transactionId"
|
||||
class="form-control form-control-sm"
|
||||
className="form-control form-control-sm"
|
||||
min="1"
|
||||
{...register("transactionId")}
|
||||
/>
|
||||
@ -374,7 +429,7 @@ const CreateExpense = ({closeModal}) => {
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<textarea
|
||||
id="description"
|
||||
@ -392,9 +447,7 @@ const CreateExpense = ({closeModal}) => {
|
||||
|
||||
<div className="row my-2">
|
||||
<div className="col-md-12">
|
||||
<label className="form-label ">
|
||||
Upload Bill{" "}
|
||||
</label>
|
||||
<label className="form-label ">Upload Bill </label>
|
||||
|
||||
<div
|
||||
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}
|
||||
</small>
|
||||
)}
|
||||
|
||||
{files.length > 0 && (
|
||||
<div className="d-block">
|
||||
{files.map((file, idx) => (
|
||||
{files
|
||||
.filter((file) => {
|
||||
if (expenseToEdit) {
|
||||
return file.isActive;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((file, idx) => (
|
||||
<a
|
||||
key={idx}
|
||||
class="d-flex justify-content-between text-start p-1"
|
||||
className="d-flex justify-content-between text-start p-1"
|
||||
href={file.preSignedUrl || "#"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div>
|
||||
<span className="mb-0 text-secondary small d-block">
|
||||
{file.fileName}
|
||||
</span>
|
||||
<span className="text-body-secondary small d-block">
|
||||
{formatFileSize(file.fileSize)}
|
||||
{file.fileSize ? formatFileSize(file.fileSize) : ""}
|
||||
</span>
|
||||
</div>
|
||||
<i
|
||||
className="bx bx-trash bx-sm cursor-pointer text-danger"
|
||||
onClick={() => removeFile(idx)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
removeFile(expenseToEdit ? file.documentId : idx);
|
||||
}}
|
||||
></i>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{Array.isArray(errors.billAttachments) &&
|
||||
errors.billAttachments.map((fileError, index) => (
|
||||
<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>
|
||||
@ -461,10 +531,19 @@ const CreateExpense = ({closeModal}) => {
|
||||
|
||||
<div className="d-flex justify-content-center gap-2">
|
||||
{" "}
|
||||
<button type="submit" className="btn btn-primary btn-sm mt-3" disabled={isPending}>
|
||||
{isPending ? "Please Wait...":"Submit"}
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary btn-sm mt-3"
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending ? "Please Wait..." : expenseToEdit ? "Update" : "Submit"}
|
||||
</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
|
||||
</button>
|
||||
</div>
|
||||
@ -473,5 +552,4 @@ const CreateExpense = ({closeModal}) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateExpense;
|
||||
|
||||
export default ManageExpense;
|
||||
28
src/components/Expenses/PreviewDocument.jsx
Normal file
28
src/components/Expenses/PreviewDocument.jsx
Normal 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;
|
||||
@ -1,15 +1,22 @@
|
||||
import React from "react";
|
||||
import { useActionOnExpense } from "../../hooks/useExpense";
|
||||
import React, { useState } from "react";
|
||||
import { useActionOnExpense, useExpense } from "../../hooks/useExpense";
|
||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { ActionSchema } from "./ExpenseSchema";
|
||||
import { useExpenseContext } from "../../pages/Expense/ExpensePage";
|
||||
import { getColorNameFromHex } from "../../utils/appUtils";
|
||||
import { ExpenseDetailsSkeleton } from "./ExpenseSkeleton";
|
||||
|
||||
const ViewExpense = ({ ExpenseId }) => {
|
||||
const { data, isLoading, isError, error } = useExpense(ExpenseId);
|
||||
const [imageLoaded, setImageLoaded] = useState({});
|
||||
const { setDocumentView } = useExpenseContext();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
reset,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: zodResolver(ActionSchema),
|
||||
@ -19,11 +26,11 @@ const ViewExpense = ({ ExpenseId }) => {
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: MakeAction } = useActionOnExpense();
|
||||
const { mutate: MakeAction } = useActionOnExpense(()=>reset());
|
||||
|
||||
const onSubmit = (formData) => {
|
||||
const Payload = {
|
||||
expenseId: ExpenseId?.id,
|
||||
expenseId: ExpenseId,
|
||||
statusId: formData.selectedStatus,
|
||||
comment: formData.comment,
|
||||
};
|
||||
@ -31,6 +38,11 @@ const ViewExpense = ({ ExpenseId }) => {
|
||||
MakeAction(Payload);
|
||||
};
|
||||
|
||||
if (isLoading) return <ExpenseDetailsSkeleton/>
|
||||
const handleImageLoad = (id) => {
|
||||
setImageLoaded((prev) => ({ ...prev, [id]: true }));
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="container px-3" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="row mb-3">
|
||||
@ -42,98 +54,166 @@ const ViewExpense = ({ ExpenseId }) => {
|
||||
{/* Expense Info Rows */}
|
||||
<div className="col-12 col-md-4 mb-3">
|
||||
<div className="d-flex">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">Transaction Date :</label>
|
||||
<div className="text-muted">{formatUTCToLocalTime(ExpenseId.transactionDate)}</div>
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
Transaction Date :
|
||||
</label>
|
||||
<div className="text-muted">
|
||||
{formatUTCToLocalTime(data.transactionDate)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-4 mb-3">
|
||||
<div className="d-flex">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">Expense Type :</label>
|
||||
<div className="text-muted">{ExpenseId.expensesType.name}</div>
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
Expense Type :
|
||||
</label>
|
||||
<div className="text-muted">{data.expensesType.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-4 mb-3">
|
||||
<div className="d-flex">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">Supplier :</label>
|
||||
<div className="text-muted">{ExpenseId.supplerName}</div>
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
Supplier :
|
||||
</label>
|
||||
<div className="text-muted">{data.supplerName}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-4 mb-3">
|
||||
<div className="d-flex">
|
||||
<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 className="col-12 col-md-4 mb-3">
|
||||
<div className="d-flex">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">Payment Mode :</label>
|
||||
<div className="text-muted">{ExpenseId.paymentMode.name}</div>
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
Payment Mode :
|
||||
</label>
|
||||
<div className="text-muted">{data.paymentMode.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-4 mb-3">
|
||||
<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">
|
||||
{ExpenseId.paidBy.firstName} {ExpenseId.paidBy.lastName}
|
||||
{data.paidBy.firstName} {data.paidBy.lastName}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-4 mb-3">
|
||||
<div className="d-flex align-items-center">
|
||||
<div className="col-12 text-start col-md-4 mb-3">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">Pre-Approved :</label>
|
||||
<div className="text-muted">{ExpenseId.preApproved ? "Yes" : "No"}</div>
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
Project :
|
||||
</label>
|
||||
<div className="text-muted text-start">{data?.project?.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-4 mb-3">
|
||||
<div className="d-flex">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">Project :</label>
|
||||
<div className="text-muted text-start">{ExpenseId.project.name}</div>
|
||||
</div>
|
||||
</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>
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
Created By :
|
||||
</label>
|
||||
<div className="text-muted">
|
||||
{ExpenseId.createdBy.firstName} {ExpenseId.createdBy.lastName}
|
||||
{data?.createdBy?.firstName} {data?.createdBy?.lastName}
|
||||
</div>
|
||||
</div>
|
||||
</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 At :</label>
|
||||
<div className="text-muted">{formatUTCToLocalTime(ExpenseId.createdAt, true)}</div>
|
||||
<label className="form-label me-2 mb-0 fw-semibold">
|
||||
Created At :
|
||||
</label>
|
||||
<div className="text-muted">
|
||||
{formatUTCToLocalTime(data?.createdAt, true)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-start">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">Description:</label>
|
||||
<div className="text-muted">
|
||||
Local travel reimbursement for delivery of materials to client site via City Taxi Service
|
||||
<div className="text-muted">{data?.description}</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>
|
||||
|
||||
<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">
|
||||
<label className="form-label me-2 mb-0 fw-semibold">Comment:</label>
|
||||
<textarea
|
||||
@ -147,8 +227,8 @@ const ViewExpense = ({ ExpenseId }) => {
|
||||
|
||||
<input type="hidden" {...register("selectedStatus")} />
|
||||
|
||||
<div className="d-flex flex-wrap gap-2 my-2">
|
||||
{ExpenseId.nextStatus.map((status, index) => (
|
||||
<div className="text-center flex-wrap gap-2 my-2">
|
||||
{data.nextStatus.map((status, index) => (
|
||||
<button
|
||||
key={index}
|
||||
type="button"
|
||||
@ -156,12 +236,8 @@ const ViewExpense = ({ ExpenseId }) => {
|
||||
setValue("selectedStatus", status.id);
|
||||
handleSubmit(onSubmit)();
|
||||
}}
|
||||
className="badge cursor-pointer border-0"
|
||||
style={{
|
||||
backgroundColor: status.color || "#6c757d",
|
||||
color: "#fff",
|
||||
fontSize: "0.85rem",
|
||||
}}
|
||||
className="btn btn-primary btn-sm cursor-pointer mx-2 border-0"
|
||||
|
||||
>
|
||||
{status.displayName || status.name}
|
||||
</button>
|
||||
|
||||
@ -68,6 +68,21 @@ useEffect(() => {
|
||||
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 (
|
||||
<div
|
||||
@ -80,7 +95,7 @@ useEffect(() => {
|
||||
{...dataAttributesProps}
|
||||
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-header p-0">
|
||||
{/* Close button inside the modal header */}
|
||||
|
||||
@ -6,7 +6,7 @@ import { queryClient } from "../layouts/AuthLayout";
|
||||
// -------------------Query------------------------------------------------------
|
||||
export const useExpenseList = (pageSize, pageNumber, filter) => {
|
||||
return useQuery({
|
||||
queryKey: ["expenses", pageNumber, pageSize, filter],
|
||||
queryKey: ["Expenses", pageNumber, pageSize, filter],
|
||||
queryFn: async () =>
|
||||
await ExpenseRepository.GetExpenseList(pageSize, pageNumber, filter).then(
|
||||
(res) => res.data
|
||||
@ -18,7 +18,11 @@ export const useExpenseList = (pageSize, pageNumber, filter) => {
|
||||
export const useExpense = (ExpenseId) => {
|
||||
return useQuery({
|
||||
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);
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["Expenses"] });
|
||||
showToast("Expense Created Successfully", "success");
|
||||
queryClient.invalidateQueries({ queryKey: ["expenses"] });
|
||||
if (onSuccessCallBack) onSuccessCallBack();
|
||||
},
|
||||
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) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@ -53,11 +119,11 @@ export const useActionOnExpense = (onSuccessCallBack) => {
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: (updatedExpense, variables) => {
|
||||
showToast("Expense updated successfully", "success");
|
||||
showToast("Request processed successfully.", "success");
|
||||
|
||||
queryClient.setQueriesData(
|
||||
{
|
||||
queryKey: ["expenses"],
|
||||
queryKey: ["Expenses"],
|
||||
exact: false,
|
||||
},
|
||||
(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();
|
||||
},
|
||||
|
||||
@ -1,23 +1,33 @@
|
||||
import React, { createContext, useContext, useState } from "react";
|
||||
|
||||
import ExpenseList from "../../components/Expenses/ExpenseList";
|
||||
import CreateExpense from "../../components/Expenses/CreateExpense";
|
||||
import ViewExpense from "../../components/Expenses/ViewExpense";
|
||||
import Breadcrumb from "../../components/common/Breadcrumb";
|
||||
import GlobalModel from "../../components/common/GlobalModel";
|
||||
import PreviewDocument from "../../components/Expenses/PreviewDocument";
|
||||
import ManageExpense from "../../components/Expenses/ManageExpense";
|
||||
|
||||
export const ExpenseContext = createContext();
|
||||
export const useExpenseContext = () => useContext(ExpenseContext);
|
||||
|
||||
const ExpensePage = () => {
|
||||
const [isNewExpense, setNewExpense] = useState(false);
|
||||
const [ManageExpenseModal, setManageExpenseModal] = useState({
|
||||
IsOpen: null,
|
||||
expenseId: null,
|
||||
});
|
||||
const [viewExpense, setViewExpense] = useState({
|
||||
expenseId: null,
|
||||
view: false,
|
||||
});
|
||||
const [ViewDocument, setDocumentView] = useState({
|
||||
IsOpen: false,
|
||||
Image: null,
|
||||
});
|
||||
|
||||
const contextValue = {
|
||||
setViewExpense,
|
||||
setManageExpenseModal,
|
||||
setDocumentView,
|
||||
};
|
||||
|
||||
return (
|
||||
@ -46,11 +56,20 @@ const ExpensePage = () => {
|
||||
<div className="col-7 col-sm-8 text-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-primary me-2"
|
||||
onClick={() => setNewExpense(true)}
|
||||
data-bs-toggle="tooltip"
|
||||
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>
|
||||
Add New
|
||||
<i className="bx bx-plus fs-4 text-white"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -58,22 +77,32 @@ const ExpensePage = () => {
|
||||
</div>
|
||||
|
||||
<ExpenseList />
|
||||
|
||||
{isNewExpense && (
|
||||
{ManageExpenseModal.IsOpen && (
|
||||
<GlobalModel
|
||||
isOpen={isNewExpense}
|
||||
isOpen={ManageExpenseModal.IsOpen}
|
||||
size="lg"
|
||||
closeModal={() => setNewExpense(false)}
|
||||
closeModal={() =>
|
||||
setManageExpenseModal({
|
||||
IsOpen: null,
|
||||
expenseId: null,
|
||||
})
|
||||
}
|
||||
>
|
||||
<CreateExpense closeModal={() => setNewExpense(false)} />
|
||||
<ManageExpense
|
||||
key={ManageExpenseModal.expenseId ?? "new"}
|
||||
expenseToEdit={ManageExpenseModal.expenseId}
|
||||
closeModal={() =>
|
||||
setManageExpenseModal({ IsOpen: null, expenseId: null })
|
||||
}
|
||||
/>
|
||||
</GlobalModel>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
{viewExpense.view && (<GlobalModel
|
||||
{viewExpense.view && (
|
||||
<GlobalModel
|
||||
isOpen={viewExpense.view}
|
||||
size="lg"
|
||||
modalType="top"
|
||||
closeModal={() =>
|
||||
setViewExpense({
|
||||
expenseId: null,
|
||||
@ -81,8 +110,20 @@ const ExpensePage = () => {
|
||||
})
|
||||
}
|
||||
>
|
||||
<ViewExpense ExpenseId={viewExpense.expenseId}/>
|
||||
</GlobalModel>) }
|
||||
<ViewExpense ExpenseId={viewExpense.expenseId} />
|
||||
</GlobalModel>
|
||||
)}
|
||||
|
||||
{ViewDocument.IsOpen && (
|
||||
<GlobalModel
|
||||
size="lg"
|
||||
key={ViewDocument.IsOpen ?? "new"}
|
||||
isOpen={ViewDocument.IsOpen}
|
||||
closeModal={() => setDocumentView({ IsOpen: false, Image: null })}
|
||||
>
|
||||
<PreviewDocument imageUrl={ViewDocument.Image} />
|
||||
</GlobalModel>
|
||||
)}
|
||||
</div>
|
||||
</ExpenseContext.Provider>
|
||||
);
|
||||
|
||||
@ -17,7 +17,9 @@ const MasterTable = ({ data, columns, loading, handleModalData }) => {
|
||||
"checkLists",
|
||||
"isSystem",
|
||||
"isActive",
|
||||
"noOfPersonsRequired"
|
||||
"noOfPersonsRequired",
|
||||
"color",
|
||||
"displayName"
|
||||
];
|
||||
|
||||
const safeData = Array.isArray(data) ? data : [];
|
||||
|
||||
@ -12,10 +12,11 @@ const ExpenseRepository = {
|
||||
|
||||
GetExpenseDetails:(id)=>api.get(`/api/Expense/details/${id}`),
|
||||
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}`),
|
||||
|
||||
ActionOnExpense:(data)=>api.post('/api/expense/action',data)
|
||||
ActionOnExpense:(data)=>api.post('/api/expense/action',data),
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -3,39 +3,34 @@ export const formatFileSize=(bytes)=> {
|
||||
else if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + " KB";
|
||||
else return (bytes / (1024 * 1024)).toFixed(2) + " MB";
|
||||
}
|
||||
export const getExpenseIcon = (type) => {
|
||||
switch (type.toLowerCase()) {
|
||||
case 'vendor/supplier payments':
|
||||
return 'bx-briefcase'; // Business-related
|
||||
case 'transport':
|
||||
return 'bx-car'; // Vehicle or logistics
|
||||
case 'compliance & safety':
|
||||
return 'bx-shield-quarter'; // Security/safety
|
||||
case 'mobilization':
|
||||
return 'bx-building-house'; // Setup / site infra
|
||||
case 'procurement':
|
||||
return 'bx-package'; // Box/package/supplies
|
||||
case 'maintenance & utilities':
|
||||
return 'bx-wrench'; // Repair/maintenance
|
||||
case 'travelling':
|
||||
return 'bx-plane'; // Personnel delivery
|
||||
case 'employee welfare':
|
||||
return 'bx-user-heart'; // Welfare / people
|
||||
default:
|
||||
return 'bx-folder'; // Fallback icon
|
||||
export const AppColorconfig = {
|
||||
colors: {
|
||||
primary: '#696cff',
|
||||
secondary: '#8592a3',
|
||||
success: '#71dd37',
|
||||
info: '#03c3ec',
|
||||
warning: '#ffab00',
|
||||
danger: '#ff3e1d',
|
||||
dark: '#233446',
|
||||
black: '#000',
|
||||
white: '#fff',
|
||||
cardColor: '#fff',
|
||||
bodyBg: '#f5f5f9',
|
||||
bodyColor: '#697a8d',
|
||||
headingColor: '#566a7f',
|
||||
textMuted: '#a1acb8',
|
||||
borderColor: '#eceef1'
|
||||
}
|
||||
};
|
||||
export const getPaymentModeIcon = (mode) => {
|
||||
switch (mode.toLowerCase()) {
|
||||
case 'cash':
|
||||
return 'bx-money'; // Cash/coins
|
||||
case 'upi':
|
||||
return 'bx-mobile-alt'; // Mobile payment
|
||||
case 'cheque':
|
||||
return 'bx-receipt'; // Paper receipt
|
||||
case 'netbanking':
|
||||
return 'bx-globe'; // Online/internet
|
||||
default:
|
||||
return 'bx-credit-card'; // Generic fallback
|
||||
export const getColorNameFromHex = (hex) => {
|
||||
const normalizedHex = hex?.replace(/'/g, '').toLowerCase();
|
||||
const colors = AppColorconfig.colors;
|
||||
|
||||
for (const [name, value] of Object.entries(colors)) {
|
||||
if (value.toLowerCase() === normalizedHex) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
return null; //
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user