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) => {
reset(defaultPayment);
onClose();
};
return (
@ -96,6 +95,19 @@ const AddPayment = ({ onClose }) => {
<small className="danger-text">{errors.amount.message}</small>
)}
</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">
{" "}
@ -128,16 +140,19 @@ const AddPayment = ({ onClose }) => {
<i className="bx bx-history bx-sm me-1"></i>History
</div>
<div className="row text-start">
{data.receivedInvoicePayments.map((payment, index) => (
<div className="row text-start mx-2">
{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=" p-2 border-start border-warning">
<div className="d-flex justify-content-between">
<p className="mb-1">
<strong>Date:</strong>{" "}
<div className="row">
<div className="col-12 col-md-6 d-flex justify-content-between align-items-center ">
<div>
<small className="fw-semibold me-1">
Transaction Date:
</small>{" "}
{formatUTCToLocalTime(payment.paymentReceivedDate)}
</p>{" "}
<span className="text-secondary ">
</div>
<span className="fs-semibold d-block d-md-none">
{formatFigure(payment.amount, {
type: "currency",
currency: "INR",
@ -145,6 +160,18 @@ const AddPayment = ({ onClose }) => {
</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 className="row">
<div className="col-12 col-md-6">
<p className="mb-1">
@ -152,17 +179,14 @@ const AddPayment = ({ onClose }) => {
{payment.transactionId}
</p>
</div>
<div className="col-12 col-md-6">
<div className="mb-0 d-flex align-items-center">
<small className="fw-semibold">Received By:</small>{" "}
<Avatar
size="xs"
firstName={payment?.createdBy?.firstName}
lastName={payment?.createdBy?.lastName}
/>{" "}
{payment?.createdBy?.firstName}{" "}
{payment.createdBy?.lastName}
</div>
<div className="col-12 ">
<span className="fs-semibold d-none d-md-block">
{formatFigure(payment.amount, {
type: "currency",
currency: "INR",
})}
</span>
<p className="text-tiny m-0 mt-1">{payment?.comment}</p>
</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 { formatFigure } from "../../utils/appUtils";
import Avatar from "../common/Avatar";
import PaymentHistoryTable from "./PaymentHistoryTable";
import Comment from "./Comment";
const ViewCollection = () => {
const { viewCollection } = useCollectionContext();
@ -14,61 +16,65 @@ const ViewCollection = () => {
<div className="container p-3">
<p className="fs-5 fw-semibold">Collection Details</p>
<div className="text-start ">
<div className="mb-3">
<p className="mb-1 fs-5">{data?.title}</p>
<p className="mb-3">{data?.description}</p>
<div className="row mb-3 px-1">
<div className="col-10">
<p className="mb-1 fs-6">{data?.title}</p>
</div>
<div className="col-2">
<span class="badge bg-label-primary">
{data?.isActive ? "Active" : "Inactive"}
</span>
</div>
</div>
<div className="row mb-3">
<div className="col-md-6">
<strong>Invoice Number:</strong> {data?.invoiceNumber}
<div className="col-md-6 d-flex">
<p className="m-0 fw-semibold me-1">Project:</p>{" "}
{data?.project?.name}
</div>
<div className="col-md-6">
<strong>E-Invoice Number:</strong> {data?.eInvoiceNumber}
</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 className="row mb-3">
<div className="col-md-6">
<strong>Project:</strong> {data?.project?.name}
</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>{" "}
<div className="col-md-6 d-flex">
<p className="m-0 fw-semibold me-1">Invoice Date:</p>{" "}
{formatUTCToLocalTime(data?.invoiceDate)}
</div>
<div className="col-md-6">
<strong>Client Submitted Date:</strong>{" "}
<div className="col-md-6 d-flex">
<p className="m-0 fw-semibold me-1">Client Submitted Date:</p>{" "}
{formatUTCToLocalTime(data?.clientSubmitedDate)}
</div>
</div>
<div className="row mb-3">
<div className="col-md-6">
<strong>Expected Payment Date:</strong>{" "}
<div className="col-md-6 d-flex">
<p className="m-0 fw-semibold me-1">Expected Payment Date:</p>{" "}
{formatUTCToLocalTime(data?.exceptedPaymentDate)}
</div>
<div className="col-md-6">
<strong>Mark as Completed:</strong>{" "}
<div className="col-md-6 d-flex">
<p className="m-0 fw-semibold me-1">Mark as Completed:</p>{" "}
{data?.markAsCompleted ? "Yes" : "No"}
</div>
</div>
<div className="row mb-3">
<div className="col-md-6">
<strong>Basic Amount:</strong>{" "}
<div className="col-md-6 d-flex">
<p className="m-0 fw-semibold me-1">Basic Amount:</p>{" "}
{formatFigure(data?.basicAmount, {
type: "currency",
currency: "INR",
})}
</div>
<div className="col-md-6">
<strong>Tax Amount:</strong>{" "}
<div className="col-md-6 d-flex">
<p className="m-0 fw-semibold me-1">Tax Amount:</p>{" "}
{formatFigure(data?.taxAmount, {
type: "currency",
currency: "INR",
@ -77,76 +83,85 @@ const ViewCollection = () => {
</div>
<div className="row mb-3">
<div className="col-md-6">
<strong>Balance Amount:</strong>{" "}
<div className="col-md-6 d-flex">
<p className="m-0 fw-semibold me-1">Balance Amount:</p>{" "}
{formatFigure(data?.balanceAmount, {
type: "currency",
currency: "INR",
})}
</div>
<div className="col-md-6">
<strong>Created At:</strong> {formatUTCToLocalTime(data?.createdAt)}
<div className="col-md-6 d-flex">
<p className="m-0 fw-semibold me-1">Created At:</p>{" "}
{formatUTCToLocalTime(data?.createdAt)}
</div>
</div>
<div className="row mb-3">
<div className="col-md-6">
<strong>Created By:</strong>{" "}
<div className="col-md-6 d-flex align-items-center">
<p className="m-0 fw-semibold">Created By:</p>{" "}
<div className="d-flex align-items-center">
<Avatar
size="xs"
firstName={data.createdBy?.firstName}
lastName={data.createdBy?.lastName}
/>
{data?.createdBy?.firstName} {data?.createdBy?.lastName} (
{data?.createdBy?.jobRoleName})
{data?.createdBy?.firstName} {data?.createdBy?.lastName}
</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>
{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="">Sr,No</th>
<th>Transaction ID</th>
<th> Received Date</th>
<th>Amount</th>
<th>Received By</th>
</tr>
</thead>
<tbody>
{data.receivedInvoicePayments.map((payment, index) => (
<tr key={payment.id}>
<td>{index + 1}</td>
<td>{payment.transactionId}</td>
<td>{formatUTCToLocalTime(payment.paymentReceivedDate)}</td>
<td className="text-end">
{formatFigure(payment.amount, {
type: "currency",
currency: "INR",
})}
</td>
<td>
<div className="d-flex align-items-center">
<Avatar
size="xs"
firstName={payment.createdBy?.firstName}
lastName={payment.createdBy?.lastName}
/>
{payment.createdBy?.firstName}{" "}
{payment.createdBy?.lastName}
<div className="container px-1 ">
{/* Tabs Navigation */}
<ul className="nav nav-tabs" role="tablist">
<li className="nav-item">
<button
className="nav-link active"
id="details-tab"
data-bs-toggle="tab"
data-bs-target="#details"
type="button"
role="tab"
>
Comments ({data?.comments?.length ?? '0'})
</button>
</li>
<li className="nav-item">
<button
className="nav-link"
id="payments-tab"
data-bs-toggle="tab"
data-bs-target="#payments"
type="button"
role="tab"
>
Payments History
</button>
</li>
</ul>
<div className="tab-content py-1 border-top-0 ">
<div
className="tab-pane fade show active"
id="details"
role="tabpanel"
>
<Comment invoice={data}/>
</div>
{/* Payments History Tab Content */}
<div className="tab-pane fade" id="payments" role="tabpanel">
<div className="row text-start">
<PaymentHistoryTable data={data} />
</div>
</div>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
);

View File

@ -77,6 +77,7 @@ export const paymentSchema = z.object({
paymentReceivedDate: z.string().min(1, { message: "Date is required" }),
transactionId: z.string().min(1, "Transaction ID is required"),
amount: z.number().min(1, "Amount must be greater than zero"),
comment:z.string().min(1,{message:"Comment required"})
});
// Default Value
@ -84,4 +85,10 @@ export const defaultPayment = {
paymentReceivedDate: null,
transactionId: "",
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 (
<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}`}>
{generateAvatarText(firstName, lastName)}
</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),
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",
currency = "INR",
locale = "en-US",
notation = "compact",
notation = "standard", // standard or compact
compactDisplay = "short",
minimumFractionDigits = 0,
maximumFractionDigits = 2,