fixed file uploading and payee

This commit is contained in:
pramod.mahajan 2025-11-04 17:15:42 +05:30
parent 611116b70c
commit 0dc68eb20d
11 changed files with 604 additions and 584 deletions

View File

@ -307,7 +307,7 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
}) })
} }
></i> ></i>
{canDetetExpense(expense) && {
canEditExpense(expense) && ( canEditExpense(expense) && (
<div className="dropdown z-2"> <div className="dropdown z-2">
<button <button

View File

@ -13,7 +13,7 @@ export const ExpenseSchema = (expenseTypes) => {
return z return z
.object({ .object({
projectId: z.string().min(1, { message: "Project is required" }), projectId: z.string().min(1, { message: "Project is required" }),
expensesCategoryId: z expenseCategoryId: z
.string() .string()
.min(1, { message: "Expense type is required" }), .min(1, { message: "Expense type is required" }),
paymentModeId: z.string().min(1, { message: "Payment mode is required" }), paymentModeId: z.string().min(1, { message: "Payment mode is required" }),

View File

@ -12,7 +12,7 @@ const Filelist = ({ files, removeFile, expenseToEdit }) => {
return true; return true;
}) })
.map((file, idx) => ( .map((file, idx) => (
<div className="col-12 col-sm-6 col-md-4 col-lg-8 bg-white shadow-sm rounded p-2 m-2"> <div className="col-12 col-sm-6 col-md-4 col-lg-8 bg-white shadow-sm rounded p-2 m-2" key={idx}>
<div className="row align-items-center"> <div className="row align-items-center">
{/* File icon and info */} {/* File icon and info */}
<div className="col-10 d-flex align-items-center gap-2"> <div className="col-10 d-flex align-items-center gap-2">

View File

@ -31,6 +31,7 @@ import Label from "../common/Label";
import EmployeeSearchInput from "../common/EmployeeSearchInput"; import EmployeeSearchInput from "../common/EmployeeSearchInput";
import Filelist from "./Filelist"; import Filelist from "./Filelist";
const ManageExpense = ({ closeModal, expenseToEdit = null }) => { const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
const { const {
data, data,
@ -148,7 +149,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
if (expenseToEdit && data) { if (expenseToEdit && data) {
reset({ reset({
projectId: data.project.id || "", projectId: data.project.id || "",
expensesCategoryId: data.expensesType.id || "", expenseCategoryId: data.expensesCategory?.id || "",
paymentModeId: data.paymentMode.id || "", paymentModeId: data.paymentMode.id || "",
paidById: data.paidBy.id || "", paidById: data.paidBy.id || "",
transactionDate: data.transactionDate?.slice(0, 10) || "", transactionDate: data.transactionDate?.slice(0, 10) || "",
@ -245,7 +246,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
<select <select
className="form-select form-select-sm" className="form-select form-select-sm"
id="expensesCategoryId" id="expensesCategoryId"
{...register("expensesCategoryId")} {...register("expenseCategoryId")}
> >
<option value="" disabled> <option value="" disabled>
Select Type Select Type
@ -480,7 +481,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
</div> </div>
</div> </div>
<div className="row my-2 text-start"> <div className="row my-2 text-start">
<div className="col-md-12"> <div className="col-md-12">
<Label className="form-label" required> <Label className="form-label" required>
Upload Bill{" "} Upload Bill{" "}

File diff suppressed because it is too large Load Diff

View File

@ -7,56 +7,48 @@ const ALLOWED_TYPES = [
"image/jpg", "image/jpg",
"image/jpeg", "image/jpeg",
]; ];
export const PaymentRequestSchema = (expenseTypes,isItself) => { export const PaymentRequestSchema = (expenseTypes, isItself) => {
return z return z.object({
.object({ title: z.string().min(1, { message: "Project is required" }),
title: z.string().min(1, { message: "Project is required" }), projectId: z.string().min(1, { message: "Project is required" }),
projectId: z.string().min(1, { message: "Project is required" }), expenseCategoryId: z
expenseCategoryId: z .string()
.string() .min(1, { message: "Expense Category is required" }),
.min(1, { message: "Expense Category is required" }), currencyId: z.string().min(1, { message: "Currency is required" }),
currencyId: z dueDate: z.string().min(1, { message: "Date is required" }),
.string() description: z.string().min(1, { message: "Description is required" }),
.min(1, { message: "Currency is required" }), payee: z.string().min(1, { message: "Supplier name is required" }),
dueDate: z.string().min(1, { message: "Date is required" }), isAdvancePayment: z.boolean().optional(),
description: z.string().min(1, { message: "Description is required" }), amount: z.coerce
payee: z.string().min(1, { message: "Supplier name is required" }), .number({
isAdvancePayment: z.boolean().optional(), invalid_type_error: "Amount is required and must be a number",
amount: z.coerce })
.number({ .min(1, "Amount must be Enter")
invalid_type_error: "Amount is required and must be a number", .refine((val) => /^\d+(\.\d{1,2})?$/.test(val.toString()), {
message: "Amount must have at most 2 decimal places",
}),
billAttachments: z
.array(
z.object({
fileName: z.string().min(1, { message: "Filename 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),
}) })
.min(1, "Amount must be Enter") )
.refine((val) => /^\d+(\.\d{1,2})?$/.test(val.toString()), { .optional(),
message: "Amount must have at most 2 decimal places", });
}), };
billAttachments: z
.array(
z.object({
fileName: z.string().min(1, { message: "Filename 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),
})
).refine((data)=>{
if(isItself){
payee.z.string().optional();
}
}),
})
};
export const defaultPaymentRequest = { export const defaultPaymentRequest = {
title:"", title: "",
description: "", description: "",
payee: "", payee: "",
currencyId: "", currencyId: "",
@ -64,11 +56,10 @@ export const defaultPaymentRequest = {
dueDate: "", dueDate: "",
projectId: "", projectId: "",
expenseCategoryId: "", expenseCategoryId: "",
isAdvancePayment:boolean, isAdvancePayment: boolean,
billAttachments: [], billAttachments: [],
}; };
export const SearchPaymentRequestSchema = z.object({ export const SearchPaymentRequestSchema = z.object({
projectIds: z.array(z.string()).optional(), projectIds: z.array(z.string()).optional(),
statusIds: z.array(z.string()).optional(), statusIds: z.array(z.string()).optional(),
@ -91,7 +82,6 @@ export const defaultPaymentRequestFilter = {
endDate: null, endDate: null,
}; };
export const PaymentRequestActionScheam = ( export const PaymentRequestActionScheam = (
isTransaction = false, isTransaction = false,
transactionDate transactionDate
@ -103,14 +93,13 @@ export const PaymentRequestActionScheam = (
paymentRequestId: z.string().nullable().optional(), paymentRequestId: z.string().nullable().optional(),
paidAt: z.string().nullable().optional(), paidAt: z.string().nullable().optional(),
paidById: z.string().nullable().optional(), paidById: z.string().nullable().optional(),
}) })
.superRefine((data, ctx) => { .superRefine((data, ctx) => {
if (isTransaction) { if (isTransaction) {
if (!data.paymentRequestId?.trim()) { if (!data.paymentRequestId?.trim()) {
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
path: ["reimburseTransactionId"], path: ["paymentRequestId"],
message: "Reimburse Transaction ID is required", message: "Reimburse Transaction ID is required",
}); });
} }

View File

@ -111,7 +111,7 @@ const ViewPaymentRequest = ({ requestId }) => {
}; };
return ( return (
<form className="container px-3" onSubmit={handleSubmit(onSubmit)}> <form className="container-xl px-3" onSubmit={handleSubmit(onSubmit)}>
<div className="col-12 mb-1"> <div className="col-12 mb-1">
<h5 className="fw-semibold m-0">Payment Request Details</h5> <h5 className="fw-semibold m-0">Payment Request Details</h5>
<hr /> <hr />
@ -325,14 +325,14 @@ const ViewPaymentRequest = ({ requestId }) => {
<label className="fw-semibold form-label">Description : </label> <label className="fw-semibold form-label">Description : </label>
<div className="text-muted">{data?.description}</div> <div className="text-muted">{data?.description}</div>
</div> </div>
<div className="col-6 text-start"> <div className="col-12 text-start">
<label className="form-label me-2 mb-2 fw-semibold"> <label className="form-label me-2 mb-2 fw-semibold">
Attachment : Attachment :
</label> </label>
<div className="d-flex flex-wrap gap-2"> <div className="d-flex flex-wrap gap-2">
{data?.documents?.length > 0 ? ( {data?.attachments?.length > 0 ? (
data?.documents?.map((doc) => { data?.attachments?.map((doc) => {
const isImage = doc?.contentType?.includes("image"); const isImage = doc?.contentType?.includes("image");
return ( return (

View File

@ -54,7 +54,7 @@ const Timeline = ({ items = [], transparent = true }) => {
<div className="d-flex flex-wrap align-items-center mb-2"> <div className="d-flex flex-wrap align-items-center mb-2">
<ul className="list-unstyled users-list d-flex align-items-center avatar-group m-0"> <ul className="list-unstyled users-list d-flex align-items-center avatar-group m-0">
{item.users.map((user, i) => ( {item.users.map((user, i) => (
<li key={i} className="avatar me-1" title={user.name}> <li key={i} className="m-0" title={user.name}>
{user.avatar ? ( {user.avatar ? (
<img <img
src={user.avatar} src={user.avatar}
@ -64,7 +64,7 @@ const Timeline = ({ items = [], transparent = true }) => {
height="32" height="32"
/> />
) : ( ) : (
<Avatar <Avatar size="xs"
firstName={user.firstName} firstName={user.firstName}
lastName={user.lastName} lastName={user.lastName}
/> />
@ -75,7 +75,7 @@ const Timeline = ({ items = [], transparent = true }) => {
{item.users?.length === 1 && ( {item.users?.length === 1 && (
<div className="m-0"> <div className="m-0">
<p className="mb-0 small fw-medium">{`${item.users[0].firstName} ${item.users[0].lastName}`}</p> <p className="mb-0 text-xs fw-medium">{`${item.users[0].firstName} ${item.users[0].lastName}`}</p>
<small>{item.users[0].role}</small> <small>{item.users[0].role}</small>
</div> </div>
)} )}

View File

@ -5,6 +5,19 @@ import { queryClient } from "../layouts/AuthLayout";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import moment from "moment"; import moment from "moment";
export const usePayee =()=>{
return useQuery({
queryKey:["payee"],
queryFn:async()=>{
const resp = await ExpenseRepository.GetPayee();
return resp.data;
}
})
}
// -------------------Query------------------------------------------------------ // -------------------Query------------------------------------------------------
const cleanFilter = (filter) => { const cleanFilter = (filter) => {

View File

@ -8,6 +8,7 @@ import PaymentRequestList from "../../components/PaymentRequest/PaymentRequestLi
import PaymentRequestFilterPanel from "../../components/PaymentRequest/PaymentRequestFilterPanel"; import PaymentRequestFilterPanel from "../../components/PaymentRequest/PaymentRequestFilterPanel";
import { defaultPaymentRequestFilter,SearchPaymentRequestSchema } from "../../components/PaymentRequest/PaymentRequestSchema"; import { defaultPaymentRequestFilter,SearchPaymentRequestSchema } from "../../components/PaymentRequest/PaymentRequestSchema";
import ViewPaymentRequest from "../../components/PaymentRequest/ViewPaymentRequest"; import ViewPaymentRequest from "../../components/PaymentRequest/ViewPaymentRequest";
import PreviewDocument from "../../components/Expenses/PreviewDocument";
export const PaymentRequestContext = createContext(); export const PaymentRequestContext = createContext();
export const usePaymentRequestContext = () => { export const usePaymentRequestContext = () => {
@ -25,12 +26,16 @@ const PaymentRequestPage = () => {
const [ViewRequest,setVieRequest] = useState({view:false,requestId:null}) const [ViewRequest,setVieRequest] = useState({view:false,requestId:null})
const { setOffcanvasContent, setShowTrigger } = useFab(); const { setOffcanvasContent, setShowTrigger } = useFab();
const [filters, setFilters] = useState(defaultPaymentRequestFilter); const [filters, setFilters] = useState(defaultPaymentRequestFilter);
const [ViewDocument, setDocumentView] = useState({
IsOpen: false,
Image: null,
});
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const contextValue = { const contextValue = {
setManageRequest, setManageRequest,
setVieRequest setVieRequest,
setDocumentView
}; };
useEffect(() => { useEffect(() => {
@ -120,7 +125,7 @@ const PaymentRequestPage = () => {
{ViewRequest.view && ( {ViewRequest.view && (
<GlobalModel <GlobalModel
isOpen isOpen
size="xl"
modalType="top" modalType="top"
closeModal={() => setVieRequest({ requestId: null, view: false })} closeModal={() => setVieRequest({ requestId: null, view: false })}
> >
@ -128,6 +133,17 @@ const PaymentRequestPage = () => {
</GlobalModel> </GlobalModel>
)} )}
{ViewDocument.IsOpen && (
<GlobalModel
isOpen
size="md"
key={ViewDocument.Image ?? "doc"}
closeModal={() => setDocumentView({ IsOpen: false, Image: null })}
>
<PreviewDocument imageUrl={ViewDocument.Image} />
</GlobalModel>
)}
</div> </div>
</PaymentRequestContext.Provider> </PaymentRequestContext.Provider>
); );

View File

@ -1,37 +1,50 @@
import { api } from "../utils/axiosClient"; import { api } from "../utils/axiosClient";
const ExpenseRepository = { const ExpenseRepository = {
GetPayee: () => api.get("/api/Expense/payment-request/payee"),
//#region Expense //#region Expense
GetExpenseList: (pageSize, pageNumber, filter, searchString) => { GetExpenseList: (pageSize, pageNumber, filter, searchString) => {
const payloadJsonString = JSON.stringify(filter); const payloadJsonString = JSON.stringify(filter);
return api.get(`/api/expense/list?pageSize=${pageSize}&pageNumber=${pageNumber}&filter=${payloadJsonString}&searchString=${searchString}`); return api.get(
`/api/expense/list?pageSize=${pageSize}&pageNumber=${pageNumber}&filter=${payloadJsonString}&searchString=${searchString}`
);
}, },
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, data) => api.put(`/api/Expense/edit/${id}`, data), UpdateExpense: (id, data) => api.put(`/api/Expense/edit/${id}`, data),
DeleteExpense: (id) => api.delete(`/api/Expense/delete/${id}`), DeleteExpense: (id) => api.delete(`/api/Expense/delete/${id}`),
ActionOnExpense: (data) => api.post('/api/expense/action', data), ActionOnExpense: (data) => api.post("/api/expense/action", data),
GetExpenseFilter: () => api.get('/api/Expense/filter'), GetExpenseFilter: () => api.get("/api/Expense/filter"),
//#endregion //#endregion
//#region Payment Request //#region Payment Request
GetPaymentRequestList: (pageSize, pageNumber, filter, isActive, searchString) => { GetPaymentRequestList: (
pageSize,
pageNumber,
filter,
isActive,
searchString
) => {
const payloadJsonString = JSON.stringify(filter); const payloadJsonString = JSON.stringify(filter);
return api.get(`/api/Expense/get/payment-requests/list?isActive=${isActive}&pageSize=${pageSize}&pageNumber=${pageNumber}&filter=${payloadJsonString}&searchString=${searchString}`); return api.get(
`/api/Expense/get/payment-requests/list?isActive=${isActive}&pageSize=${pageSize}&pageNumber=${pageNumber}&filter=${payloadJsonString}&searchString=${searchString}`
);
}, },
CreatePaymentRequest: (data) => api.post("/api/expense/payment-request/create", data), CreatePaymentRequest: (data) =>
UpdatePaymentRequest: (id, data) => api.put(`/api/Expense/payment-request/edit/${id}`, data), api.post("/api/expense/payment-request/create", data),
GetPaymentRequest: (id) => api.get(`/api/Expense/get/payment-request/details/${id}`), UpdatePaymentRequest: (id, data) =>
GetPaymentRequestFilter: () => api.get('/api/Expense/payment-request/filter'), api.put(`/api/Expense/payment-request/edit/${id}`, data),
ActionOnPaymentRequest: (data) => api.post('/api/Expense/payment-request/action', data), GetPaymentRequest: (id) =>
api.get(`/api/Expense/get/payment-request/details/${id}`),
GetPaymentRequestFilter: () => api.get("/api/Expense/payment-request/filter"),
ActionOnPaymentRequest: (data) =>
api.post("/api/Expense/payment-request/action", data),
//#endregion //#endregion
//#region Advance Payment //#region Advance Payment
GetTranctionList: () => api.get(`/get/transactions/${employeeId}`) GetTranctionList: () => api.get(`/get/transactions/${employeeId}`),
//#endregion //#endregion
} };
export default ExpenseRepository; export default ExpenseRepository;