integrated comment api added comment and payments history inside view collection

This commit is contained in:
pramod.mahajan 2025-10-14 18:20:36 +05:30
parent e2035e1fd8
commit 0052fed1e6
9 changed files with 315 additions and 109 deletions

View File

@ -42,7 +42,6 @@ const AddPayment = ({ onClose }) => {
}; };
const handleClose = (formData) => { const handleClose = (formData) => {
reset(defaultPayment); reset(defaultPayment);
onClose();
}; };
return ( return (
@ -96,6 +95,19 @@ const AddPayment = ({ onClose }) => {
<small className="danger-text">{errors.amount.message}</small> <small className="danger-text">{errors.amount.message}</small>
)} )}
</div> </div>
<div className="col-12 mb-2">
<Label htmlFor="comment" className="form-label" required>
Comment
</Label>
<textarea
id="comment"
className="form-control form-control-sm"
{...register("comment")}
/>
{errors.comment && (
<small className="danger-text">{errors.comment.message}</small>
)}
</div>
<div className="d-flex justify-content-end gap-3"> <div className="d-flex justify-content-end gap-3">
{" "} {" "}
@ -128,21 +140,36 @@ const AddPayment = ({ onClose }) => {
<i className="bx bx-history bx-sm me-1"></i>History <i className="bx bx-history bx-sm me-1"></i>History
</div> </div>
<div className="row text-start"> <div className="row text-start mx-2">
{data.receivedInvoicePayments.map((payment, index) => ( {data.receivedInvoicePayments.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)).map((payment, index) => (
<div className="col-12 mb-2" key={payment.id}> <div className="col-12 mb-2" key={payment.id}>
<div className=" p-2 border-start border-warning"> <div className=" p-2 border-start border-warning">
<div className="d-flex justify-content-between"> <div className="row">
<p className="mb-1"> <div className="col-12 col-md-6 d-flex justify-content-between align-items-center ">
<strong>Date:</strong>{" "} <div>
{formatUTCToLocalTime(payment.paymentReceivedDate)} <small className="fw-semibold me-1">
</p>{" "} Transaction Date:
<span className="text-secondary "> </small>{" "}
{formatFigure(payment.amount, { {formatUTCToLocalTime(payment.paymentReceivedDate)}
type: "currency", </div>
currency: "INR", <span className="fs-semibold d-block d-md-none">
})} {formatFigure(payment.amount, {
</span> type: "currency",
currency: "INR",
})}
</span>
</div>
<div className="col-12 col-md-6 mb-0 d-flex align-items-center m-0">
<small className="fw-semibold me-2">Received By:</small>{" "}
<Avatar
size="xs"
firstName={payment?.createdBy?.firstName}
lastName={payment?.createdBy?.lastName}
/>{" "}
{payment?.createdBy?.firstName}{" "}
{payment.createdBy?.lastName}
</div>
</div> </div>
<div className="row"> <div className="row">
@ -152,17 +179,14 @@ const AddPayment = ({ onClose }) => {
{payment.transactionId} {payment.transactionId}
</p> </p>
</div> </div>
<div className="col-12 col-md-6"> <div className="col-12 ">
<div className="mb-0 d-flex align-items-center"> <span className="fs-semibold d-none d-md-block">
<small className="fw-semibold">Received By:</small>{" "} {formatFigure(payment.amount, {
<Avatar type: "currency",
size="xs" currency: "INR",
firstName={payment?.createdBy?.firstName} })}
lastName={payment?.createdBy?.lastName} </span>
/>{" "} <p className="text-tiny m-0 mt-1">{payment?.comment}</p>
{payment?.createdBy?.firstName}{" "}
{payment.createdBy?.lastName}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,84 @@
import { zodResolver } from "@hookform/resolvers/zod";
import React from "react";
import { useForm } from "react-hook-form";
import { CommentSchema } from "./collectionSchema";
import { useAddComment } from "../../hooks/useCollections";
import Avatar from "../common/Avatar";
import { formatUTCToLocalTime } from "../../utils/dateUtils";
import moment from "moment";
const Comment = ({ invoice }) => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
resolver: zodResolver(CommentSchema),
defaultValues: { comment: "" },
});
const { mutate: AddComment, isPending } = useAddComment(() => {});
const onSubmit = (formData) => {
const payload = { ...formData, invoiceId: invoice?.id };
debugger;
AddComment(payload);
};
return (
<div className="row">
{invoice?.comments?.length > 0 ? (
invoice.comments.map((comment, index) => (
<div
className="border-start border-primary ps-3 py-2 mb-3"
key={comment.id || index}
>
<div className="d-flex justify-content-between align-items-center mb-1">
<div className="d-flex align-items-center">
<Avatar
size="xs"
firstName={comment?.createdBy?.firstName}
lastName={comment?.createdBy?.lastName}
/>
<span className="ms-1 fw-semibold">
{comment?.createdBy?.firstName} {comment?.createdBy?.lastName}
</span>
</div>
<small className="text-secondary">
{moment.utc(comment?.createdAt).local().fromNow()}
</small>
</div>
<p className="mb-1">{comment?.comment}</p>
</div>
))
) : (
<p className="text-muted">No comments yet.</p>
)}
<form onSubmit={handleSubmit(onSubmit)}>
<div className="col-12">
<textarea
className="form-control "
rows={3}
{...register("comment")}
/>
{errors.comment && (
<small className="danger-text">{errors.comment.message}</small>
)}
</div>
<div className="d-flex justify-content-end p-2">
<button
className="btn btn-sm btn-primary"
type="submit"
disabled={isPending}
>
{isPending ? "Please wait..." : "Submit"}
</button>
</div>
</form>
</div>
);
};
export default Comment;

View File

@ -0,0 +1,56 @@
import React from 'react'
import { formatUTCToLocalTime } from '../../utils/dateUtils'
import { formatFigure } from '../../utils/appUtils'
import Avatar from '../common/Avatar'
const PaymentHistoryTable = ({data}) => {
return (
<div>
{data?.receivedInvoicePayments?.length > 0 && (
<div className="mt-4">
<p className="fw-semibold fs-6">Received Payments</p>
<table className="table table-bordered mt-2">
<thead className="table-light">
<tr>
<th className="text-center">Sr.No</th>
<th className="text-center">Transaction ID</th>
<th className="text-center"> Received Date</th>
<th className="text-center">Amount</th>
<th className="text-center">Updated By</th>
</tr>
</thead>
<tbody>
{data.receivedInvoicePayments.map((payment, index) => (
<tr key={payment.id}>
<td className="text-center">{index + 1}</td>
<td ><span className="mx-2">{payment.transactionId}</span></td>
<td className="text-center">{formatUTCToLocalTime(payment.paymentReceivedDate)}</td>
<td className="text-end">
<span className="px-1">{formatFigure(payment.amount, {
type: "currency",
currency: "INR",
})}</span>
</td>
<td>
<div className="d-flex align-items-center mx-2">
<Avatar
size="xs"
firstName={payment.createdBy?.firstName}
lastName={payment.createdBy?.lastName}
/>
{payment.createdBy?.firstName}{" "}
{payment.createdBy?.lastName}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)
}
export default PaymentHistoryTable

View File

@ -4,6 +4,8 @@ import { useCollection } from "../../hooks/useCollections";
import { formatUTCToLocalTime } from "../../utils/dateUtils"; import { formatUTCToLocalTime } from "../../utils/dateUtils";
import { formatFigure } from "../../utils/appUtils"; import { formatFigure } from "../../utils/appUtils";
import Avatar from "../common/Avatar"; import Avatar from "../common/Avatar";
import PaymentHistoryTable from "./PaymentHistoryTable";
import Comment from "./Comment";
const ViewCollection = () => { const ViewCollection = () => {
const { viewCollection } = useCollectionContext(); const { viewCollection } = useCollectionContext();
@ -14,61 +16,65 @@ const ViewCollection = () => {
<div className="container p-3"> <div className="container p-3">
<p className="fs-5 fw-semibold">Collection Details</p> <p className="fs-5 fw-semibold">Collection Details</p>
<div className="text-start "> <div className="text-start ">
<div className="mb-3"> <div className="row mb-3 px-1">
<p className="mb-1 fs-5">{data?.title}</p> <div className="col-10">
<p className="mb-3">{data?.description}</p> <p className="mb-1 fs-6">{data?.title}</p>
</div>
<div className="row mb-3">
<div className="col-md-6">
<strong>Invoice Number:</strong> {data?.invoiceNumber}
</div> </div>
<div className="col-md-6"> <div className="col-2">
<strong>E-Invoice Number:</strong> {data?.eInvoiceNumber} <span class="badge bg-label-primary">
{data?.isActive ? "Active" : "Inactive"}
</span>
</div>
</div>
<div className="row mb-3">
<div className="col-md-6 d-flex">
<p className="m-0 fw-semibold me-1">Project:</p>{" "}
{data?.project?.name}
</div>
</div>
<div className="row mb-3">
<div className="col-md-6 d-flex">
<p className="m-0 fw-semibold me-1">Invoice Number:</p>{" "}
{data?.invoiceNumber}
</div>
<div className="col-md-6 d-flex">
<p className="m-0 fw-semibold me-1">E-Invoice Number:</p>{" "}
{data?.eInvoiceNumber}
</div> </div>
</div> </div>
<div className="row mb-3"> <div className="row mb-3">
<div className="col-md-6"> <div className="col-md-6 d-flex">
<strong>Project:</strong> {data?.project?.name} <p className="m-0 fw-semibold me-1">Invoice Date:</p>{" "}
</div>
<div className="col-md-6">
<strong>Status:</strong> {data?.isActive ? "Active" : "Inactive"}
</div>
</div>
<div className="row mb-3">
<div className="col-md-6">
<strong>Invoice Date:</strong>{" "}
{formatUTCToLocalTime(data?.invoiceDate)} {formatUTCToLocalTime(data?.invoiceDate)}
</div> </div>
<div className="col-md-6"> <div className="col-md-6 d-flex">
<strong>Client Submitted Date:</strong>{" "} <p className="m-0 fw-semibold me-1">Client Submitted Date:</p>{" "}
{formatUTCToLocalTime(data?.clientSubmitedDate)} {formatUTCToLocalTime(data?.clientSubmitedDate)}
</div> </div>
</div> </div>
<div className="row mb-3"> <div className="row mb-3">
<div className="col-md-6"> <div className="col-md-6 d-flex">
<strong>Expected Payment Date:</strong>{" "} <p className="m-0 fw-semibold me-1">Expected Payment Date:</p>{" "}
{formatUTCToLocalTime(data?.exceptedPaymentDate)} {formatUTCToLocalTime(data?.exceptedPaymentDate)}
</div> </div>
<div className="col-md-6"> <div className="col-md-6 d-flex">
<strong>Mark as Completed:</strong>{" "} <p className="m-0 fw-semibold me-1">Mark as Completed:</p>{" "}
{data?.markAsCompleted ? "Yes" : "No"} {data?.markAsCompleted ? "Yes" : "No"}
</div> </div>
</div> </div>
<div className="row mb-3"> <div className="row mb-3">
<div className="col-md-6"> <div className="col-md-6 d-flex">
<strong>Basic Amount:</strong>{" "} <p className="m-0 fw-semibold me-1">Basic Amount:</p>{" "}
{formatFigure(data?.basicAmount, { {formatFigure(data?.basicAmount, {
type: "currency", type: "currency",
currency: "INR", currency: "INR",
})} })}
</div> </div>
<div className="col-md-6"> <div className="col-md-6 d-flex">
<strong>Tax Amount:</strong>{" "} <p className="m-0 fw-semibold me-1">Tax Amount:</p>{" "}
{formatFigure(data?.taxAmount, { {formatFigure(data?.taxAmount, {
type: "currency", type: "currency",
currency: "INR", currency: "INR",
@ -77,76 +83,85 @@ const ViewCollection = () => {
</div> </div>
<div className="row mb-3"> <div className="row mb-3">
<div className="col-md-6"> <div className="col-md-6 d-flex">
<strong>Balance Amount:</strong>{" "} <p className="m-0 fw-semibold me-1">Balance Amount:</p>{" "}
{formatFigure(data?.balanceAmount, { {formatFigure(data?.balanceAmount, {
type: "currency", type: "currency",
currency: "INR", currency: "INR",
})} })}
</div> </div>
<div className="col-md-6"> <div className="col-md-6 d-flex">
<strong>Created At:</strong> {formatUTCToLocalTime(data?.createdAt)} <p className="m-0 fw-semibold me-1">Created At:</p>{" "}
{formatUTCToLocalTime(data?.createdAt)}
</div> </div>
</div> </div>
<div className="row mb-3"> <div className="row mb-3">
<div className="col-md-6"> <div className="col-md-6 d-flex align-items-center">
<strong>Created By:</strong>{" "} <p className="m-0 fw-semibold">Created By:</p>{" "}
<div className="d-flex align-items-center"> <div className="d-flex align-items-center">
<Avatar <Avatar
size="xs" size="xs"
firstName={data.createdBy?.firstName} firstName={data.createdBy?.firstName}
lastName={data.createdBy?.lastName} lastName={data.createdBy?.lastName}
/> />
{data?.createdBy?.firstName} {data?.createdBy?.lastName} ( {data?.createdBy?.firstName} {data?.createdBy?.lastName}
{data?.createdBy?.jobRoleName})
</div>{" "} </div>{" "}
</div> </div>
{/* <div className="col-md-6"><strong>Updated At:</strong> {data?.updatedAt ? formatUTCToLocalTime(data?.updatedAt) : "-"}</div> */}
<div className="col-12">
<p className="m-0 fw-semibold">Description : </p>
{data?.description}
</div>
</div> </div>
{data?.receivedInvoicePayments?.length > 0 && ( <div className="container px-1 ">
<div className="mt-4"> {/* Tabs Navigation */}
<p className="fw-semibold fs-6">Received Payments</p> <ul className="nav nav-tabs" role="tablist">
<table className="table table-bordered mt-2"> <li className="nav-item">
<thead className="table-light"> <button
<tr> className="nav-link active"
<th className="">Sr,No</th> id="details-tab"
<th>Transaction ID</th> data-bs-toggle="tab"
<th> Received Date</th> data-bs-target="#details"
<th>Amount</th> type="button"
<th>Received By</th> role="tab"
</tr> >
</thead> Comments ({data?.comments?.length ?? '0'})
<tbody> </button>
{data.receivedInvoicePayments.map((payment, index) => ( </li>
<tr key={payment.id}> <li className="nav-item">
<td>{index + 1}</td> <button
<td>{payment.transactionId}</td> className="nav-link"
<td>{formatUTCToLocalTime(payment.paymentReceivedDate)}</td> id="payments-tab"
<td className="text-end"> data-bs-toggle="tab"
{formatFigure(payment.amount, { data-bs-target="#payments"
type: "currency", type="button"
currency: "INR", role="tab"
})} >
</td> Payments History
<td> </button>
<div className="d-flex align-items-center"> </li>
<Avatar </ul>
size="xs"
firstName={payment.createdBy?.firstName} <div className="tab-content py-1 border-top-0 ">
lastName={payment.createdBy?.lastName} <div
/> className="tab-pane fade show active"
{payment.createdBy?.firstName}{" "} id="details"
{payment.createdBy?.lastName} role="tabpanel"
</div> >
</td>
</tr> <Comment invoice={data}/>
))} </div>
</tbody>
</table> {/* Payments History Tab Content */}
<div className="tab-pane fade" id="payments" role="tabpanel">
<div className="row text-start">
<PaymentHistoryTable data={data} />
</div>
</div>
</div> </div>
)} </div>
</div> </div>
</div> </div>
); );

View File

@ -77,6 +77,7 @@ export const paymentSchema = z.object({
paymentReceivedDate: z.string().min(1, { message: "Date is required" }), paymentReceivedDate: z.string().min(1, { message: "Date is required" }),
transactionId: z.string().min(1, "Transaction ID is required"), transactionId: z.string().min(1, "Transaction ID is required"),
amount: z.number().min(1, "Amount must be greater than zero"), amount: z.number().min(1, "Amount must be greater than zero"),
comment:z.string().min(1,{message:"Comment required"})
}); });
// Default Value // Default Value
@ -84,4 +85,10 @@ export const defaultPayment = {
paymentReceivedDate: null, paymentReceivedDate: null,
transactionId: "", transactionId: "",
amount: 0, amount: 0,
comment:""
}; };
export const CommentSchema = z.object({
comment:z.string().min(1,{message:"Comment required"})
})

View File

@ -51,7 +51,7 @@ const Avatar = ({ firstName, lastName, size = "sm", classAvatar }) => {
return ( return (
<div className="avatar-wrapper p-1"> <div className="avatar-wrapper p-1">
<div className={`avatar avatar-${size} me-2 ${classAvatar}`}> <div className={`avatar avatar-${size} ${classAvatar}`}>
<span className={`avatar-initial rounded-circle ${bgClass}`}> <span className={`avatar-initial rounded-circle ${bgClass}`}>
{generateAvatarText(firstName, lastName)} {generateAvatarText(firstName, lastName)}
</span> </span>

View File

@ -110,3 +110,22 @@ export const useAddPayment = (onSuccessCallBack) => {
}, },
}); });
}; };
export const useAddComment = (onSuccessCallBack) => {
const client = useQueryClient();
return useMutation({
mutationFn: (payload) => CollectionRepository.addComment(payload),
onSuccess: () => {
client.invalidateQueries({ queryKey: ["collections"] });
client.invalidateQueries({ queryKey: ["collection"] });
showToast("Comment Successfully", "success");
if (onSuccessCallBack) onSuccessCallBack();
},
onError: (error) => {
showToast(
error?.response?.data?.message || error.message || "Something Went wrong"
);
},
});
};

View File

@ -19,6 +19,7 @@ export const CollectionRepository = {
makeReceivePayment:(data)=> api.post(`/api/Collection/invoice/payment/received`,data), makeReceivePayment:(data)=> api.post(`/api/Collection/invoice/payment/received`,data),
markPaymentReceived:(invoiceId)=>api.put(`/api/Collection/invoice/marked/completed/${invoiceId}`), markPaymentReceived:(invoiceId)=>api.put(`/api/Collection/invoice/marked/completed/${invoiceId}`),
getCollection:(id)=>api.get(`/api/Collection/invoice/details/${id}`) getCollection:(id)=>api.get(`/api/Collection/invoice/details/${id}`),
addComment:(data)=>api.post(`/api/Collection/invoice/add/comment`,data)
}; };

View File

@ -113,7 +113,7 @@ export const formatFigure = (
type = "number", type = "number",
currency = "INR", currency = "INR",
locale = "en-US", locale = "en-US",
notation = "compact", notation = "standard", // standard or compact
compactDisplay = "short", compactDisplay = "short",
minimumFractionDigits = 0, minimumFractionDigits = 0,
maximumFractionDigits = 2, maximumFractionDigits = 2,