From 43c3f76f5d4fc1ed70a6061a048d846c5e2d7f85 Mon Sep 17 00:00:00 2001 From: pramod mahajan Date: Sat, 19 Jul 2025 20:19:56 +0530 Subject: [PATCH 1/3] created list view interface --- src/components/Expenses/CreateExpense.jsx | 128 ++++++++++++++++++++++ src/components/Expenses/ExpenseList.jsx | 98 +++++++++++++++++ src/components/Expenses/ExpenseSchema.js | 66 +++++++++++ src/data/menuData.json | 6 + src/hooks/useExpense.js | 30 +++++ src/pages/Expense/ExpensePage.jsx | 48 ++++++++ src/repositories/ExpsenseRepository.jsx | 13 +++ src/router/AppRoutes.jsx | 2 + 8 files changed, 391 insertions(+) create mode 100644 src/components/Expenses/CreateExpense.jsx create mode 100644 src/components/Expenses/ExpenseList.jsx create mode 100644 src/components/Expenses/ExpenseSchema.js create mode 100644 src/hooks/useExpense.js create mode 100644 src/pages/Expense/ExpensePage.jsx create mode 100644 src/repositories/ExpsenseRepository.jsx diff --git a/src/components/Expenses/CreateExpense.jsx b/src/components/Expenses/CreateExpense.jsx new file mode 100644 index 00000000..927269a9 --- /dev/null +++ b/src/components/Expenses/CreateExpense.jsx @@ -0,0 +1,128 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import React from "react"; +import { useForm } from "react-hook-form"; +import { ExpenseSchema } from "./ExpenseSchema"; + +const CreateExpense = () => { + const {} = useForm({ + resolver: zodResolver(ExpenseSchema), + defaultValues: { + projectId: "", + expensesTypeId: "", + paymentModeId: "", + paidById: "", + transactionDate: "", + transactionId: "", + description: "", + location: "", + supplerName: "", + amount: "", + noOfPersons: "", + statusId: "", + billAttachments: [], + }, + }); + return ( +
+

Create New Expense

+
+
+
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+
+
+ + ); +}; + +export default CreateExpense; diff --git a/src/components/Expenses/ExpenseList.jsx b/src/components/Expenses/ExpenseList.jsx new file mode 100644 index 00000000..8fc54a7e --- /dev/null +++ b/src/components/Expenses/ExpenseList.jsx @@ -0,0 +1,98 @@ +import React from 'react' + +const ExpenseList = () => { + return ( +
+
+
+ + + + + + + + + + + + + + + + + +
+
Date
+
+
Expense Type
+
+
Payment mode
+
+
Paid By
+
+ Status + + Amount +
+
+
+
+ ) +} + +export default ExpenseList \ No newline at end of file diff --git a/src/components/Expenses/ExpenseSchema.js b/src/components/Expenses/ExpenseSchema.js new file mode 100644 index 00000000..1b5f4bb1 --- /dev/null +++ b/src/components/Expenses/ExpenseSchema.js @@ -0,0 +1,66 @@ +import { z } from 'zod'; + +const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB +const ALLOWED_TYPES = [ + 'application/pdf', + 'image/png', + 'image/jpg', + 'image/jpeg', +]; + +export const ExpenseSchema = z.object({ + projectId: z.string().min(1, { message: "Project is required" }), + expensesTypeId: z.string().min(1, { message: "Expense type is required" }), + paymentModeId: z.string().min(1, { message: "Payment mode is required" }), + paidById: z.string().min(1, { message: "Employee name is required" }), + transactionDate: z.string().min(1, { message: "Date is required" }), + transactionId: z.string().optional(), // if optional, else use .min(1) + description: z.string().min(1, { message: "Description is required" }), + location: z.string().min(1, { message: "Location is required" }), + supplerName: z.string().min(1, { message: "Supplier name is required" }), + amount: z.number().min(1, { message: "Amount must be at least 1" }).max(10000, { message: "Amount must not exceed 10,000" }), + noOfPersons: z.number().min(1, { message: "1 Employee at least required" }), + statusId: z.string().min(1, { message: "Please select status" }), + + billAttachments: z + .array( + z.object({ + fileName: z.string().min(1, { message: "Filename is required" }), + base64Data: z.string().min(1, { message: "File data is required" }), + contentType: z.string().refine((val) => ALLOWED_TYPES.includes(val), { + message: "Only PDF, PNG, JPG, or JPEG files are allowed", + }), + fileSize: z.number().max(MAX_FILE_SIZE, { + message: "File size must be less than or equal to 5MB", + }), + description: z.string().optional(), + }) + ) + .nonempty({ message: "At least one file attachment is required" }), +}); + + +let payload= +{ +"projectId": "2618f2ef-2823-11f0-9d9e-bc241163f504", +"expensesTypeId": "dd120bc4-ab0a-45ba-8450-5cd45ff221ca", +"paymentModeId": "ed667353-8eea-4fd1-8750-719405932480", +"paidById": "08dda7d8-014e-443f-858d-a55f4b484bc4", +"transactionDate": "2025-07-12T09:56:54.122Z", +"transactionId": "string", +"description": "string", +"location": "string", +"supplerName": "string", +"amount": 390, +"noOfPersons": 0, +"statusId": "297e0d8f-f668-41b5-bfea-e03b354251c8", +"billAttachments": [ +{ +"fileName": "string", +"base64Data": "string", +"contentType": "string", +"fileSize": 0, +"description": "string" +} +] +} \ No newline at end of file diff --git a/src/data/menuData.json b/src/data/menuData.json index bbd37e2c..d4b2104d 100644 --- a/src/data/menuData.json +++ b/src/data/menuData.json @@ -66,6 +66,12 @@ "available": true, "link": "/gallary" }, + { + "text": "Expense", + "icon": "bx bx-receipt", + "available": true, + "link": "/expenses" + }, { "text": "Administration", "icon": "bx bx-box", diff --git a/src/hooks/useExpense.js b/src/hooks/useExpense.js new file mode 100644 index 00000000..a2f72cc6 --- /dev/null +++ b/src/hooks/useExpense.js @@ -0,0 +1,30 @@ +import { useMutation } from "@tanstack/react-query" +import ExpenseRepository from "../repositories/ExpsenseRepository" +import showToast from "../services/toastService" + + + +// -------------------Query------------------------------------------------------ +export const useExpenseList = ()=>{ + +} + + + + + +// ---------------------------Mutation--------------------------------------------- + +export const useCreateExpnse =()=>{ + return useMutation({ + mutationFn:(payload)=>{ + await ExpenseRepository.CreateExpense(payload) + }, + onSuccess:(_,variables)=>{ + showToast("Expense Created Successfully","success") + }, + onError:(error)=>{ + showToast(error.message || "Something went wrong please try again !","success") + } + }) +} \ No newline at end of file diff --git a/src/pages/Expense/ExpensePage.jsx b/src/pages/Expense/ExpensePage.jsx new file mode 100644 index 00000000..136b2d2f --- /dev/null +++ b/src/pages/Expense/ExpensePage.jsx @@ -0,0 +1,48 @@ +import React, { useState } from "react"; +import ExpenseList from "../../components/Expenses/ExpenseList"; +import Breadcrumb from "../../components/common/Breadcrumb"; +import GlobalModel from "../../components/common/GlobalModel"; +import CreateExpense from "../../components/Expenses/createExpense"; + +const ExpensePage = () => { + const[IsNewExpen,setNewExpense] = useState(false) + + return ( +
+ +
+
+
+
+ +
+
+ +
+
+
+
+ + + setNewExpense(false)}> + + +
+ ); +}; + +export default ExpensePage; diff --git a/src/repositories/ExpsenseRepository.jsx b/src/repositories/ExpsenseRepository.jsx new file mode 100644 index 00000000..03199212 --- /dev/null +++ b/src/repositories/ExpsenseRepository.jsx @@ -0,0 +1,13 @@ +import { api } from "../utils/axiosClient"; + + +const ExpenseRepository = { + GetExpenseList:()=>api.get("/api/expanse/list"), + GetExpenseDetails:(id)=>api.get(`/api/Expanse/details/${id}`), + CreateExpense:(data)=>api.post("/api/Expanse/create",data), + UpdateExpense:(id)=>api.put(`/api/Expanse/edit/${id}`), + DeleteExpense:(id)=>api.delete(`/api/Expanse/edit/${id}`) + +} + +export default ExpenseRepository; \ No newline at end of file diff --git a/src/router/AppRoutes.jsx b/src/router/AppRoutes.jsx index 5ce5b6d4..d89191d8 100644 --- a/src/router/AppRoutes.jsx +++ b/src/router/AppRoutes.jsx @@ -38,6 +38,7 @@ import LegalInfoCard from "../pages/TermsAndConditions/LegalInfoCard"; import ProtectedRoute from "./ProtectedRoute"; import Directory from "../pages/Directory/Directory"; import LoginWithOtp from "../pages/authentication/LoginWithOtp"; +import ExpensePage from "../pages/Expense/ExpensePage"; const router = createBrowserRouter( [ @@ -76,6 +77,7 @@ const router = createBrowserRouter( { path: "/activities/task", element: }, { path: "/activities/reports", element: }, { path: "/gallary", element: }, + { path: "/expenses", element: }, { path: "/masters", element: }, { path: "/help/support", element: }, { path: "/help/docs", element: }, -- 2.43.0 From bf4a5cb48269c459e4e3d243504fa28b776cb9a3 Mon Sep 17 00:00:00 2001 From: pramod mahajan Date: Sun, 20 Jul 2025 13:50:00 +0530 Subject: [PATCH 2/3] making a expense page layout --- src/components/Expenses/CreateExpense.jsx | 372 ++++++++++++++++------ src/pages/Expense/ExpensePage.jsx | 6 +- src/utils/appUtils.js | 5 + 3 files changed, 285 insertions(+), 98 deletions(-) create mode 100644 src/utils/appUtils.js diff --git a/src/components/Expenses/CreateExpense.jsx b/src/components/Expenses/CreateExpense.jsx index 927269a9..2cd2d7c1 100644 --- a/src/components/Expenses/CreateExpense.jsx +++ b/src/components/Expenses/CreateExpense.jsx @@ -1,10 +1,17 @@ import { zodResolver } from "@hookform/resolvers/zod"; -import React from "react"; +import React, { useState } from "react"; import { useForm } from "react-hook-form"; import { ExpenseSchema } from "./ExpenseSchema"; +import { formatFileSize } from "../../utils/appUtils"; const CreateExpense = () => { - const {} = useForm({ + const { + register, + handleSubmit, + watch, + setValue, + formState: { errors }, + } = useForm({ resolver: zodResolver(ExpenseSchema), defaultValues: { projectId: "", @@ -22,106 +29,279 @@ const CreateExpense = () => { billAttachments: [], }, }); + + const files = watch("billAttachments"); +const onFileChange = async (e) => { + const newFiles = Array.from(e.target.files); + if (newFiles.length === 0) return; + + const existingFiles = watch("billAttachments") || []; + + const parsedFiles = await Promise.all( + newFiles.map(async (file) => { + const base64Data = await toBase64(file); + return { + fileName: file.name, + base64Data, + contentType: file.type, + fileSize: file.size, + description: "", + }; + }) + ); + + // Avoid duplicates based on file name + size + const combinedFiles = [ + ...existingFiles, + ...parsedFiles.filter( + (newFile) => + !existingFiles.some( + (f) => f.fileName === newFile.fileName && f.fileSize === newFile.fileSize + ) + ), + ]; + + setValue("billAttachments", combinedFiles, { shouldDirty: true, shouldValidate: true }); +}; + + + const toBase64 = (file) => + new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => resolve(reader.result.split(",")[1]); // base64 only, no prefix + reader.onerror = (error) => reject(error); + }); +const removeFile = (index) => { + const newFiles = files.filter((_, i) => i !== index); + setValue("billAttachments", newFiles, { shouldValidate: true }); +}; + const onSubmit = (data) => { + console.log("Form Data:", data); + }; return (
-

Create New Expense

-
-
-
- - -
+

Create New Expense

+ +
+
+ + +
-
- - -
+
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
document.getElementById("billAttachments").click()} + > + + + Click to select or click here to browse + + + { + onFileChange(e); + e.target.value = ""; // ← this line resets the file input + }} + /> +
+ + {files.length > 0 && ( + + )} +{Array.isArray(errors.billAttachments) && + errors.billAttachments.map((fileError, index) => ( +
+ {fileError?.fileSize?.message || fileError?.contentType?.message}
+ ))} +
+
-
-
- - -
- -
- - -
+
+ {" "} + + +
+
- -
-
- - -
- -
- - -
-
- -
-
- - -
- -
- - -
-
- -
-
- - -
- -
- - -
-
- -
-
- - -
-
- -
-
- - -
-
- -
-
- -
- ); }; diff --git a/src/pages/Expense/ExpensePage.jsx b/src/pages/Expense/ExpensePage.jsx index 136b2d2f..ca511bbd 100644 --- a/src/pages/Expense/ExpensePage.jsx +++ b/src/pages/Expense/ExpensePage.jsx @@ -27,8 +27,10 @@ const ExpensePage = () => { aria-controls="DataTables_Table_0" /> +
-
+
+ @@ -38,7 +40,7 @@ const ExpensePage = () => {
- setNewExpense(false)}> + setNewExpense(false)}>
diff --git a/src/utils/appUtils.js b/src/utils/appUtils.js new file mode 100644 index 00000000..eac45ec7 --- /dev/null +++ b/src/utils/appUtils.js @@ -0,0 +1,5 @@ +export const formatFileSize=(bytes)=> { + if (bytes < 1024) return bytes + " B"; + else if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + " KB"; + else return (bytes / (1024 * 1024)).toFixed(2) + " MB"; +} \ No newline at end of file -- 2.43.0 From 5a158ee8492846ab4c893f415c3ac1d856565395 Mon Sep 17 00:00:00 2001 From: pramod mahajan Date: Mon, 21 Jul 2025 19:45:54 +0530 Subject: [PATCH 3/3] created and display expenses list --- src/components/Expenses/CreateExpense.jsx | 424 +++++++++++++++------- src/components/Expenses/ExpenseList.jsx | 334 ++++++++++++----- src/components/Expenses/ExpenseSchema.js | 119 +++--- src/components/Expenses/ViewExpense.jsx | 14 + src/hooks/masterHook/useMaster.js | 70 ++++ src/hooks/useExpense.js | 48 ++- src/pages/Expense/ExpensePage.jsx | 111 ++++-- src/repositories/ExpsenseRepository.jsx | 13 +- src/utils/dateUtils.jsx | 2 +- 9 files changed, 812 insertions(+), 323 deletions(-) create mode 100644 src/components/Expenses/ViewExpense.jsx diff --git a/src/components/Expenses/CreateExpense.jsx b/src/components/Expenses/CreateExpense.jsx index 2cd2d7c1..0ed1b119 100644 --- a/src/components/Expenses/CreateExpense.jsx +++ b/src/components/Expenses/CreateExpense.jsx @@ -1,18 +1,41 @@ import { zodResolver } from "@hookform/resolvers/zod"; -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { ExpenseSchema } from "./ExpenseSchema"; import { formatFileSize } from "../../utils/appUtils"; +import { useProjectName } from "../../hooks/useProjects"; +import { useDispatch, useSelector } from "react-redux"; +import { changeMaster } from "../../slices/localVariablesSlice"; +import useMaster, { + useExpenseStatus, + useExpenseType, + usePaymentMode, +} from "../../hooks/masterHook/useMaster"; +import { + useEmployeesAllOrByProjectId, + useEmployeesByProject, +} from "../../hooks/useEmployees"; +import Avatar from "../common/Avatar"; +import { useCreateExpnse } from "../../hooks/useExpense"; -const CreateExpense = () => { +const CreateExpense = ({closeModal}) => { + const [ExpenseType, setExpenseType] = useState(); + const dispatch = useDispatch(); + const { + ExpenseTypes, + loading: ExpenseLoading, + error: ExpenseError, + } = useExpenseType(); + const schema = ExpenseSchema(ExpenseTypes); const { register, handleSubmit, watch, setValue, + reset, formState: { errors }, } = useForm({ - resolver: zodResolver(ExpenseSchema), + resolver: zodResolver(schema), defaultValues: { projectId: "", expensesTypeId: "", @@ -25,45 +48,68 @@ const CreateExpense = () => { supplerName: "", amount: "", noOfPersons: "", - statusId: "", billAttachments: [], }, }); - const files = watch("billAttachments"); -const onFileChange = async (e) => { - const newFiles = Array.from(e.target.files); - if (newFiles.length === 0) return; - - const existingFiles = watch("billAttachments") || []; - - const parsedFiles = await Promise.all( - newFiles.map(async (file) => { - const base64Data = await toBase64(file); - return { - fileName: file.name, - base64Data, - contentType: file.type, - fileSize: file.size, - description: "", - }; - }) + const selectedproject = watch("projectId"); + const selectedProject = useSelector( + (store) => store.localVariables.projectId ); + const { projectNames, loading: projectLoading, error } = useProjectName(); - // Avoid duplicates based on file name + size - const combinedFiles = [ - ...existingFiles, - ...parsedFiles.filter( - (newFile) => - !existingFiles.some( - (f) => f.fileName === newFile.fileName && f.fileSize === newFile.fileSize - ) - ), - ]; + const { + PaymentModes, + loading: PaymentModeLoading, + error: PaymentModeError, + } = usePaymentMode(); + const { + ExpenseStatus, + loading: StatusLoadding, + error: stausError, + } = useExpenseStatus(); + const { + employees, + loading: EmpLoading, + error: EmpError, + } = useEmployeesByProject(selectedproject); - setValue("billAttachments", combinedFiles, { shouldDirty: true, shouldValidate: true }); -}; + const files = watch("billAttachments"); + const onFileChange = async (e) => { + const newFiles = Array.from(e.target.files); + if (newFiles.length === 0) return; + const existingFiles = watch("billAttachments") || []; + + const parsedFiles = await Promise.all( + newFiles.map(async (file) => { + const base64Data = await toBase64(file); + return { + fileName: file.name, + base64Data, + contentType: file.type, + fileSize: file.size, + description: "", + }; + }) + ); + + const combinedFiles = [ + ...existingFiles, + ...parsedFiles.filter( + (newFile) => + !existingFiles.some( + (f) => + f.fileName === newFile.fileName && f.fileSize === newFile.fileSize + ) + ), + ]; + + setValue("billAttachments", combinedFiles, { + shouldDirty: true, + shouldValidate: true, + }); + }; const toBase64 = (file) => new Promise((resolve, reject) => { @@ -72,86 +118,162 @@ const onFileChange = async (e) => { reader.onload = () => resolve(reader.result.split(",")[1]); // base64 only, no prefix reader.onerror = (error) => reject(error); }); -const removeFile = (index) => { - const newFiles = files.filter((_, i) => i !== index); - setValue("billAttachments", newFiles, { shouldValidate: true }); -}; - const onSubmit = (data) => { - console.log("Form Data:", data); + const removeFile = (index) => { + const newFiles = files.filter((_, i) => i !== index); + setValue("billAttachments", newFiles, { shouldValidate: true }); }; + + const {mutate:CreateExpense,isPending} = useCreateExpnse(()=>{ + handleClose() + }) + const onSubmit = (payload) => { + console.log("Form Data:", payload); + + CreateExpense(payload) + }; + const ExpenseTypeId = watch("expensesTypeId"); + + useEffect(() => { + setExpenseType(ExpenseTypes?.find((type) => type.id === ExpenseTypeId)); + }, [ExpenseTypeId]); + + const handleClose =()=>{ + reset() + closeModal() + } return ( -
-

Create New Expense

+
+
Create New Expense
-
-
-