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>
{canDetetExpense(expense) &&
{
canEditExpense(expense) && (
<div className="dropdown z-2">
<button

View File

@ -13,7 +13,7 @@ export const ExpenseSchema = (expenseTypes) => {
return z
.object({
projectId: z.string().min(1, { message: "Project is required" }),
expensesCategoryId: z
expenseCategoryId: z
.string()
.min(1, { message: "Expense type 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;
})
.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">
{/* File icon and info */}
<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 Filelist from "./Filelist";
const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
const {
data,
@ -148,7 +149,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
if (expenseToEdit && data) {
reset({
projectId: data.project.id || "",
expensesCategoryId: data.expensesType.id || "",
expenseCategoryId: data.expensesCategory?.id || "",
paymentModeId: data.paymentMode.id || "",
paidById: data.paidBy.id || "",
transactionDate: data.transactionDate?.slice(0, 10) || "",
@ -245,7 +246,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
<select
className="form-select form-select-sm"
id="expensesCategoryId"
{...register("expensesCategoryId")}
{...register("expenseCategoryId")}
>
<option value="" disabled>
Select Type

View File

@ -1,26 +1,47 @@
import React, { useEffect, useState } from 'react'
import { useCurrencies, useProjectName } from '../../hooks/useProjects';
import Label from '../common/Label';
import { useForm } from 'react-hook-form';
import { useExpenseCategory } from '../../hooks/masterHook/useMaster';
import DatePicker from '../common/DatePicker';
import { useCreatePaymentRequest, usePaymentRequestDetail, useUpdatePaymentRequest } from '../../hooks/useExpense';
import React, { useEffect, useState } from "react";
import { useCurrencies, useProjectName } from "../../hooks/useProjects";
import Label from "../common/Label";
import { useForm } from "react-hook-form";
import { useExpenseCategory } from "../../hooks/masterHook/useMaster";
import DatePicker from "../common/DatePicker";
import {
useCreatePaymentRequest,
usePayee,
usePaymentRequestDetail,
useUpdatePaymentRequest,
} from "../../hooks/useExpense";
import { zodResolver } from '@hookform/resolvers/zod';
import { formatFileSize, localToUtc } from '../../utils/appUtils';
import { defaultPaymentRequest, PaymentRequestSchema } from './PaymentRequestSchema';
import { INR_CURRENCY_CODE } from '../../utils/constants';
import { useProfile } from '../../hooks/useProfile';
import { zodResolver } from "@hookform/resolvers/zod";
import { formatFileSize, localToUtc } from "../../utils/appUtils";
import {
defaultPaymentRequest,
PaymentRequestSchema,
} from "./PaymentRequestSchema";
import { INR_CURRENCY_CODE } from "../../utils/constants";
import { useProfile } from "../../hooks/useProfile";
import Filelist from "../Expenses/Filelist";
import InputSuggestions from "../common/InputSuggestion";
function ManagePaymentRequest({ closeModal, requestToEdit = null }) {
const { data, isLoading, isError, error: requestError } = usePaymentRequestDetail(requestToEdit)
// const data = {}
const {
data,
isLoading,
isError,
error: requestError,
} = usePaymentRequestDetail(requestToEdit);
const { projectNames, loading: projectLoading, error, isError: isProjectError,
const {
projectNames,
loading: projectLoading,
error,
isError: isProjectError,
} = useProjectName();
const { data: currencyData, isLoading: currencyLoading, isError: currencyError } = useCurrencies();
const {
data: currencyData,
isLoading: currencyLoading,
isError: currencyError,
} = useCurrencies();
const {
ExpenseCategories,
@ -29,9 +50,17 @@ function ManagePaymentRequest({ closeModal, requestToEdit = null }) {
} = useExpenseCategory();
const { profile } = useProfile();
const {data:Payees,isLoading:isPayeeLoaing,isError:isPayeeError,error:payeeError} = usePayee()
const schema = PaymentRequestSchema(ExpenseCategories);
const { register, control, watch, handleSubmit, setValue, reset, formState: { errors }, } = useForm({
const {
register,
control,
watch,
handleSubmit,
setValue,
reset,
formState: { errors },
} = useForm({
resolver: zodResolver(schema),
defaultValues: defaultPaymentRequest,
});
@ -104,13 +133,12 @@ function ManagePaymentRequest({ closeModal, requestToEdit = null }) {
closeModal();
};
const { mutate: CreatePaymentRequest, isPending: createPending } = useCreatePaymentRequest(
() => {
const { mutate: CreatePaymentRequest, isPending: createPending } =
useCreatePaymentRequest(() => {
handleClose();
}
);
const { mutate: PaymentRequestUpdate, isPending } = useUpdatePaymentRequest(() =>
handleClose()
});
const { mutate: PaymentRequestUpdate, isPending } = useUpdatePaymentRequest(
() => handleClose()
);
useEffect(() => {
@ -125,7 +153,7 @@ function ManagePaymentRequest({ closeModal, requestToEdit = null }) {
projectId: data.project.id || "",
expenseCategoryId: data.expenseCategory.id || "",
isAdvancePayment: data.isAdvancePayment || false,
billAttachments: data.documents
billAttachments: data.attc
? data.documents.map((doc) => ({
fileName: doc.fileName,
base64Data: null,
@ -137,16 +165,13 @@ function ManagePaymentRequest({ closeModal, requestToEdit = null }) {
isActive: doc.isActive ?? true,
}))
: [],
});
}
}, [data, reset]);
useEffect(() => {
if (!requestToEdit && currencyData && currencyData.length > 0) {
const inrCurrency = currencyData.find(
(c) => c.id === INR_CURRENCY_CODE
);
const inrCurrency = currencyData.find((c) => c.id === INR_CURRENCY_CODE);
if (inrCurrency) {
setValue("currencyId", INR_CURRENCY_CODE, { shouldValidate: true });
}
@ -154,14 +179,17 @@ function ManagePaymentRequest({ closeModal, requestToEdit = null }) {
}, [currencyData, requestToEdit, setValue]);
const onSubmit = (fromdata) => {
let payload = {
...fromdata,
dueDate: localToUtc(fromdata.dueDate),
payee:isItself ? profile?.employeeInfo?.id : fromdata.payee
payee: isItself ? profile?.employeeInfo?.id : fromdata.payee,
};
if (requestToEdit) {
const editPayload = { ...payload, id: data.id, payee:isItself ? profile?.employeeInfo?.id : fromdata.payee };
const editPayload = {
...payload,
id: data.id,
payee: isItself ? profile?.employeeInfo?.id : fromdata.payee,
};
PaymentRequestUpdate({ id: data.id, payload: editPayload });
} else {
CreatePaymentRequest(payload);
@ -169,8 +197,8 @@ function ManagePaymentRequest({ closeModal, requestToEdit = null }) {
};
const handleSetItSelf = (e) => {
setisItself(e.target.value);
setValue('payee',profile?.employeeInfo.id)
}
setValue("payee", `${profile?.employeeInfo.firstName} ${profile?.employeeInfo.lastName}`);
};
return (
<div className="container p-3">
@ -232,7 +260,6 @@ setValue('payee',profile?.employeeInfo.id)
</small>
)}
</div>
</div>
{/* Title and Advance Payment */}
@ -248,9 +275,7 @@ setValue('payee',profile?.employeeInfo.id)
{...register("title")}
/>
{errors.title && (
<small className="danger-text">
{errors.title.message}
</small>
<small className="danger-text">{errors.title.message}</small>
)}
</div>
@ -262,7 +287,8 @@ setValue('payee',profile?.employeeInfo.id)
id="isAdvancePayment"
className="form-select form-select-sm"
{...register("isAdvancePayment", {
setValueAs: (v) => v === "true" ? true : v === "false" ? false : undefined,
setValueAs: (v) =>
v === "true" ? true : v === "false" ? false : undefined,
})}
>
<option value="">Select Option</option>
@ -270,14 +296,13 @@ setValue('payee',profile?.employeeInfo.id)
<option value="false">False</option>
</select>
{errors.isAdvancePayment && (
<small className="danger-text">{errors.isAdvancePayment.message}</small>
<small className="danger-text">
{errors.isAdvancePayment.message}
</small>
)}
</div>
</div>
{/* Date and Amount */}
<div className="row my-2 text-start">
<div className="col-md-6">
@ -288,13 +313,11 @@ setValue('payee',profile?.employeeInfo.id)
name="dueDate"
control={control}
minDate={new Date()}
className='w-100'
className="w-100"
/>
{errors.dueDate && (
<small className="danger-text">
{errors.dueDate.message}
</small>
<small className="danger-text">{errors.dueDate.message}</small>
)}
</div>
@ -323,18 +346,17 @@ setValue('payee',profile?.employeeInfo.id)
<Label htmlFor="payee" className="form-label" required>
Payee (Supplier Name/Transporter Name/Other)
</Label>
<input
type="text"
id="payee"
className="form-control form-control-sm"
{...register("payee")}
disabled={isItself}
<InputSuggestions
organizationList={Payees}
value={watch("payee") || ""}
onChange={(val) =>
setValue("payee", val, { shouldValidate: true })
}
error={errors.payee?.message}
/>
{errors.payee && (
<small className="danger-text">
{errors.payee.message}
</small>
<small className="danger-text">{errors.payee.message}</small>
)}
{/* Checkbox below input */}
@ -352,7 +374,6 @@ setValue('payee',profile?.employeeInfo.id)
</div>
</div>
<div className="col-md-6">
<Label htmlFor="currencyId" className="form-label" required>
Currency
@ -378,7 +399,6 @@ setValue('payee',profile?.employeeInfo.id)
<small className="danger-text">{errors.currencyId.message}</small>
)}
</div>
</div>
{/* Description */}
@ -404,7 +424,7 @@ setValue('payee',profile?.employeeInfo.id)
{/* Upload Document */}
<div className="row my-2 text-start">
<div className="col-md-12">
<Label className="form-label">
<Label className="form-label" required>
Upload Bill{" "}
</Label>
@ -437,42 +457,12 @@ setValue('payee',profile?.employeeInfo.id)
{errors.billAttachments.message}
</small>
)}
{Array.isArray(files) && files.length > 0 && (
<div className="d-block">
{files
.filter((file) => {
if (requestToEdit) {
return file.isActive;
}
return true;
})
.map((file, idx) => (
<a
key={idx}
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">
{file.fileSize ? formatFileSize(file.fileSize) : ""}
</span>
</div>
<i
className="bx bx-trash bx-sm cursor-pointer text-danger"
onClick={(e) => {
e.preventDefault();
removeFile(requestToEdit ? file.documentId : idx);
}}
></i>
</a>
))}
</div>
{files.length > 0 && (
<Filelist
files={files}
removeFile={removeFile}
expenseToEdit={requestToEdit}
/>
)}
{Array.isArray(errors.billAttachments) &&
@ -489,7 +479,6 @@ setValue('payee',profile?.employeeInfo.id)
</div>
</div>
<div className="d-flex justify-content-end gap-3">
<button
type="reset"
disabled={createPending || isPending}
@ -510,10 +499,9 @@ setValue('payee',profile?.employeeInfo.id)
: "Submit"}
</button>
</div>
</form>
</div>
)
);
}
export default ManagePaymentRequest
export default ManagePaymentRequest;

View File

@ -8,16 +8,13 @@ const ALLOWED_TYPES = [
"image/jpeg",
];
export const PaymentRequestSchema = (expenseTypes, isItself) => {
return z
.object({
return z.object({
title: z.string().min(1, { message: "Project is required" }),
projectId: z.string().min(1, { message: "Project is required" }),
expenseCategoryId: z
.string()
.min(1, { message: "Expense Category is required" }),
currencyId: z
.string()
.min(1, { message: "Currency is required" }),
currencyId: z.string().min(1, { message: "Currency is required" }),
dueDate: z.string().min(1, { message: "Date is required" }),
description: z.string().min(1, { message: "Description is required" }),
payee: z.string().min(1, { message: "Supplier name is required" }),
@ -35,9 +32,7 @@ export const PaymentRequestSchema = (expenseTypes,isItself) => {
z.object({
fileName: z.string().min(1, { message: "Filename is required" }),
base64Data: z.string().nullable(),
contentType: z
.string()
.refine((val) => ALLOWED_TYPES.includes(val), {
contentType: z.string().refine((val) => ALLOWED_TYPES.includes(val), {
message: "Only PDF, PNG, JPG, or JPEG files are allowed",
}),
documentId: z.string().optional(),
@ -47,12 +42,9 @@ export const PaymentRequestSchema = (expenseTypes,isItself) => {
description: z.string().optional(),
isActive: z.boolean().default(true),
})
).refine((data)=>{
if(isItself){
payee.z.string().optional();
}
}),
})
)
.optional(),
});
};
export const defaultPaymentRequest = {
@ -68,7 +60,6 @@ export const defaultPaymentRequest = {
billAttachments: [],
};
export const SearchPaymentRequestSchema = z.object({
projectIds: z.array(z.string()).optional(),
statusIds: z.array(z.string()).optional(),
@ -91,7 +82,6 @@ export const defaultPaymentRequestFilter = {
endDate: null,
};
export const PaymentRequestActionScheam = (
isTransaction = false,
transactionDate
@ -103,14 +93,13 @@ export const PaymentRequestActionScheam = (
paymentRequestId: z.string().nullable().optional(),
paidAt: z.string().nullable().optional(),
paidById: z.string().nullable().optional(),
})
.superRefine((data, ctx) => {
if (isTransaction) {
if (!data.paymentRequestId?.trim()) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["reimburseTransactionId"],
path: ["paymentRequestId"],
message: "Reimburse Transaction ID is required",
});
}

View File

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

View File

@ -54,7 +54,7 @@ const Timeline = ({ items = [], transparent = true }) => {
<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">
{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 ? (
<img
src={user.avatar}
@ -64,7 +64,7 @@ const Timeline = ({ items = [], transparent = true }) => {
height="32"
/>
) : (
<Avatar
<Avatar size="xs"
firstName={user.firstName}
lastName={user.lastName}
/>
@ -75,7 +75,7 @@ const Timeline = ({ items = [], transparent = true }) => {
{item.users?.length === 1 && (
<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>
</div>
)}

View File

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

View File

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

View File

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