diff --git a/index.html b/index.html index 748027e9..a1531220 100644 --- a/index.html +++ b/index.html @@ -27,6 +27,7 @@ + diff --git a/public/assets/css/skeleton.css b/public/assets/css/skeleton.css new file mode 100644 index 00000000..2ee909d2 --- /dev/null +++ b/public/assets/css/skeleton.css @@ -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%; + } +} \ No newline at end of file diff --git a/src/components/Expenses/ExpenseList.jsx b/src/components/Expenses/ExpenseList.jsx index 7a93d2d0..1538bb84 100644 --- a/src/components/Expenses/ExpenseList.jsx +++ b/src/components/Expenses/ExpenseList.jsx @@ -6,9 +6,10 @@ 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,setExpenseModal } = useExpenseContext(); + const { setViewExpense,setManageExpenseModal } = useExpenseContext(); const [currentPage, setCurrentPage] = useState(1); const pageSize = 10; @@ -22,8 +23,9 @@ const ExpenseList = () => { }; - const { data, isLoading, isError,isInitialLoading } = useExpenseList(10, currentPage, filter); - if (isInitialLoading) return
Loading...
; + const { data, isLoading, isError,isInitialLoading,error } = useExpenseList(10, currentPage, filter); + if (isInitialLoading) return ; + if (isError) return
{error}
; const items = data.data ?? []; const totalPages = data?.totalPages ?? 1; const hasMore = currentPage < totalPages; @@ -192,7 +194,7 @@ const ExpenseList = () => { setExpenseModal({isOpen:true,ExpEdit:expense})} + onClick={()=>setManageExpenseModal({IsOpen:true,expenseId:expense.id})} > diff --git a/src/components/Expenses/ExpenseSchema.js b/src/components/Expenses/ExpenseSchema.js index a2e11ed7..7c4993c5 100644 --- a/src/components/Expenses/ExpenseSchema.js +++ b/src/components/Expenses/ExpenseSchema.js @@ -35,7 +35,7 @@ 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", }), diff --git a/src/components/Expenses/ExpenseSkeleton.jsx b/src/components/Expenses/ExpenseSkeleton.jsx new file mode 100644 index 00000000..904c2e9d --- /dev/null +++ b/src/components/Expenses/ExpenseSkeleton.jsx @@ -0,0 +1,218 @@ +import React from "react"; + + +const SkeletonLine = ({ height = 20, width = "100%", className = "" }) => ( +
+); + + +const ExpenseSkeleton = () => { + return ( +
+
+ +
+ + {[...Array(5)].map((_, idx) => ( +
+
+ +
+
+ +
+
+ ))} + +
+
+ +
+
+ +
+
+ +
+
+ +
+ + +
+
+ ); +}; + +export default ExpenseSkeleton; + + + + +export const ExpenseDetailsSkeleton = () => { + return ( +
+
+
+ +
+ + {[...Array(3)].map((_, i) => ( +
+ + +
+ ))} + + {[...Array(6)].map((_, i) => ( +
+ + +
+ ))} + + + +
+ + {[...Array(2)].map((_, i) => ( +
+
+
+ + +
+
+ ))} +
+ +
+ +
+ + +
+ {[...Array(2)].map((_, i) => ( + + ))} +
+
+
+
+ ); +}; +const SkeletonCell = ({ width = "100%", height = 20, className = "" }) => ( +
+); +export const ExpenseTableSkeleton = ({ rows = 5 }) => { + return ( + + + + + + + + + + + + + + + {[...Array(rows)].map((_, idx) => ( + + {/* Date Time colSpan=2 */} + + + {/* Expense Type */} + + + {/* Payment Mode */} + + + {/* Paid By (Avatar + name) */} + + + {/* Amount */} + + + {/* Status */} + + + {/* Action (icons) */} + + + ))} + +
+
Date Time
+
+
Expense Type
+
+
Payment Mode
+
Paid ByAmountStatusAction
+
+ +
+
+ + + + +
+ + +
+
+ + + + +
+ {[...Array(3)].map((__, i) => ( + + ))} +
+
+ ); +}; \ No newline at end of file diff --git a/src/components/Expenses/ManageExpense.jsx b/src/components/Expenses/ManageExpense.jsx index 44283453..f2b18713 100644 --- a/src/components/Expenses/ManageExpense.jsx +++ b/src/components/Expenses/ManageExpense.jsx @@ -16,9 +16,19 @@ import { useEmployeesByProject, } from "../../hooks/useEmployees"; import Avatar from "../common/Avatar"; -import { useCreateExpnse } from "../../hooks/useExpense"; +import { + useCreateExpnse, + useExpense, + useUpdateExepse, +} from "../../hooks/useExpense"; +import ExpenseSkeleton from "./ExpenseSkeleton"; -const CreateExpense = ({closeModal,expenseToEdit,}) => { +const ManageExpense = ({ closeModal, expenseToEdit = null }) => { + const { + data, + isLoading, + error: ExpenseErrorLoad, + } = useExpense(expenseToEdit); const [ExpenseType, setExpenseType] = useState(); const dispatch = useDispatch(); const { @@ -38,27 +48,6 @@ const CreateExpense = ({closeModal,expenseToEdit,}) => { resolver: zodResolver(schema), defaultValues: defaultExpense, }); - console.log(expenseToEdit) - - useEffect(() => { - if (expenseToEdit) { - reset({ - projectId: expenseToEdit.project.id || "", - expensesTypeId: expenseToEdit.expensesType.id - || "", - paymentModeId: expenseToEdit.paymentMode.id || "", - paidById: expenseToEdit.paidBy.id || "", - transactionDate: expenseToEdit.transactionDate?.slice(0, 10) || "", - transactionId: expenseToEdit.transactionId || "", - description: expenseToEdit.description || "", - location: expenseToEdit.location || "", - supplerName: expenseToEdit.supplerName || "", - amount: expenseToEdit.amount || "", - noOfPersons: expenseToEdit.noOfPersons || "", - billAttachments: expenseToEdit.billAttachments || [], - }); - } -}, [expenseToEdit, reset]); const selectedproject = watch("projectId"); @@ -83,6 +72,7 @@ const CreateExpense = ({closeModal,expenseToEdit,}) => { error: EmpError, } = useEmployeesByProject(selectedproject); + const files = watch("billAttachments"); const onFileChange = async (e) => { const newFiles = Array.from(e.target.files); @@ -132,27 +122,66 @@ const CreateExpense = ({closeModal,expenseToEdit,}) => { 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, + fileSize: 0, + description: "", + preSignedUrl: doc.preSignedUrl, + })) + : [], + }); + } + }, [data, reset, employees]); + const { mutate: UpdateExpense, isPending } = useUpdateExepse(() => 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 }; + UpdateExpense({ 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 + + return (
-
Create New Expense
+
{expenseToEdit ? "Update Expense ": "Create New Expense"}
@@ -257,16 +286,13 @@ const CreateExpense = ({closeModal,expenseToEdit,}) => { ) : ( employees?.map((emp) => ( )) )} - {errors.paidById && ( - - {errors.paidById.message} - + {errors.paidById && ( + {errors.paidById.message} )}
@@ -401,9 +427,7 @@ const CreateExpense = ({closeModal,expenseToEdit,}) => {
- + )} + {Array.isArray(errors.billAttachments) && errors.billAttachments.map((fileError, index) => (
{fileError?.fileSize?.message || - fileError?.contentType?.message} + fileError?.contentType?.message || + fileError?.base64Data?.message}
))}
@@ -470,10 +502,18 @@ const CreateExpense = ({closeModal,expenseToEdit,}) => {
{" "} - -
@@ -482,5 +522,4 @@ const CreateExpense = ({closeModal,expenseToEdit,}) => { ); }; -export default CreateExpense; - +export default ManageExpense; diff --git a/src/components/Expenses/ViewExpense.jsx b/src/components/Expenses/ViewExpense.jsx index a6fd2f45..79088512 100644 --- a/src/components/Expenses/ViewExpense.jsx +++ b/src/components/Expenses/ViewExpense.jsx @@ -5,12 +5,13 @@ 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, @@ -36,16 +37,7 @@ const ViewExpense = ({ ExpenseId }) => { MakeAction(Payload); }; - if (isLoading) { - return ( -
-
Loading...
-
- ); - } + if (isLoading) return const handleImageLoad = (id) => { setImageLoaded((prev) => ({ ...prev, [id]: true })); }; @@ -59,7 +51,7 @@ const ViewExpense = ({ ExpenseId }) => {
{/* Expense Info Rows */} -
+
-
+
-
+
-
+
₹ {data.amount}
-
+
-
+
-
-
-
+
- - {ExpenseId.status.displayName} + + + {data?.status?.displayName}
-
-
-
+
{data.preApproved ? "Yes" : "No"}
-
-
+
-
{data.project.name}
+
{data?.project?.name}
-
+
- {data.createdBy.firstName} {data.createdBy.lastName} + {data?.createdBy?.firstName} {data?.createdBy?.lastName}
-
+
- {formatUTCToLocalTime(data.createdAt, true)} + {formatUTCToLocalTime(data?.createdAt, true)}
-
+
-
{data.description}
+
{data?.description}
))}
diff --git a/src/hooks/useExpense.js b/src/hooks/useExpense.js index 2e9a5e56..b2e0d679 100644 --- a/src/hooks/useExpense.js +++ b/src/hooks/useExpense.js @@ -54,7 +54,7 @@ export const useUpdateExepse =()=>{ const queryClient = useQueryClient(); return useMutation({ - mutationFn:async (id,payload)=>{ + mutationFn:async ({id,payload})=>{ const response = await ExpenseRepository.UpdateExpense(id,payload) }, onSuccess:(updatedExpense,variables)=>{ diff --git a/src/pages/Expense/ExpensePage.jsx b/src/pages/Expense/ExpensePage.jsx index 14749708..c4bd8526 100644 --- a/src/pages/Expense/ExpensePage.jsx +++ b/src/pages/Expense/ExpensePage.jsx @@ -1,27 +1,33 @@ import React, { createContext, useContext, useState } from "react"; import ExpenseList from "../../components/Expenses/ExpenseList"; -import CreateExpense from "../../components/Expenses/ManageExpense"; 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 [expenseModal, setExpenseModal] = useState({ - isOpen: false, - ExpEdit: 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, - setExpenseModal, + setManageExpenseModal, + setDocumentView, }; return ( @@ -56,7 +62,12 @@ const ExpensePage = () => { data-bs-custom-class="tooltip" title="Add New Expense" className={`p-1 me-2 bg-primary rounded-circle `} - onClick={() => setNewExpense(true)} + onClick={() => + setManageExpenseModal({ + IsOpen: true, + expenseId: null, + }) + } > @@ -66,25 +77,22 @@ const ExpensePage = () => {
- - {expenseModal.isOpen && ( + {ManageExpenseModal.IsOpen && ( - setExpenseModal({ - isOpen: false, - ExpEdit:null + setManageExpenseModal({ + IsOpen: null, + expenseId: null, }) } > - - setExpenseModal({ - isOpen: false, - ExpEdit:null - }) + setManageExpenseModal({ IsOpen: null, expenseId: null }) } /> @@ -94,6 +102,7 @@ const ExpensePage = () => { setViewExpense({ expenseId: null, @@ -104,6 +113,17 @@ const ExpensePage = () => { )} + + {ViewDocument.IsOpen && ( + setDocumentView({ IsOpen: false, Image: null })} + > + + + )}
);