Compare commits
17 Commits
main
...
Collection
Author | SHA1 | Date | |
---|---|---|---|
6e89fbd680 | |||
9648d1a98b | |||
aa947b791b | |||
51cca64dd5 | |||
ca88928850 | |||
962286a4da | |||
76df08e921 | |||
0052fed1e6 | |||
e2035e1fd8 | |||
7176a86913 | |||
a26e4d1dc2 | |||
f91c7d6da1 | |||
272645f0b4 | |||
301684a12b | |||
9288ac1fbc | |||
376a2a397f | |||
58c2fbdf1b |
@ -4,17 +4,20 @@ import OrganizationModal from "./components/Organization/OrganizationModal";
|
||||
import { useAuthModal, useModal } from "./hooks/useAuth";
|
||||
import SwitchTenant from "./pages/authentication/SwitchTenant";
|
||||
import ChangePasswordPage from "./pages/authentication/ChangePassword";
|
||||
import NewCollection from "./components/collections/ManageCollection";
|
||||
|
||||
const ModalProvider = () => {
|
||||
const { isOpen, onClose } = useOrganizationModal();
|
||||
const { isOpen: isAuthOpen } = useAuthModal();
|
||||
const {isOpen:isChangePass} = useModal("ChangePassword")
|
||||
const {isOpen:isCollectionNew} = useModal("newCollection");
|
||||
|
||||
return (
|
||||
<>
|
||||
{isOpen && <OrganizationModal />}
|
||||
{isAuthOpen && <SwitchTenant />}
|
||||
{isChangePass && <ChangePasswordPage /> }
|
||||
{isCollectionNew && <NewCollection/>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -34,6 +34,8 @@ const Header = () => {
|
||||
const isDashboardPath =
|
||||
/^\/dashboard$/.test(location.pathname) || /^\/$/.test(location.pathname);
|
||||
const isProjectPath = /^\/projects$/.test(location.pathname);
|
||||
const isCollectionPath =
|
||||
/^\/collection$/.test(location.pathname) || /^\/$/.test(location.pathname);
|
||||
|
||||
const showProjectDropdown = (pathname) => {
|
||||
const isDirectoryPath = /^\/directory$/.test(pathname);
|
||||
@ -216,7 +218,7 @@ const Header = () => {
|
||||
className="dropdown-menu"
|
||||
style={{ overflow: "auto", maxHeight: "300px" }}
|
||||
>
|
||||
{isDashboardPath && (
|
||||
{(isDashboardPath|| isCollectionPath) &&(
|
||||
<li>
|
||||
<button
|
||||
className="dropdown-item"
|
||||
|
266
src/components/collections/AddPayment.jsx
Normal file
266
src/components/collections/AddPayment.jsx
Normal file
@ -0,0 +1,266 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import React, { useState } from "react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { defaultPayment, paymentSchema } from "./collectionSchema";
|
||||
import Label from "../common/Label";
|
||||
import DatePicker from "../common/DatePicker";
|
||||
import { formatDate } from "date-fns";
|
||||
import { useCollectionContext } from "../../pages/collections/CollectionPage";
|
||||
import { useAddPayment, useCollection } from "../../hooks/useCollections";
|
||||
import { formatFigure, localToUtc } from "../../utils/appUtils";
|
||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||
import Avatar from "../common/Avatar";
|
||||
import { PaymentHistorySkeleton } from "./CollectionSkeleton";
|
||||
import { usePaymentAjustmentHead } from "../../hooks/masterHook/useMaster";
|
||||
|
||||
const AddPayment = ({ onClose }) => {
|
||||
const { addPayment } = useCollectionContext();
|
||||
const { data, isLoading, isError, error } = useCollection(
|
||||
addPayment?.invoiceId
|
||||
);
|
||||
const {
|
||||
data: paymentTypes,
|
||||
isLoading: isPaymentTypeLoading,
|
||||
isError: isPaymentTypeError,
|
||||
error: paymentError,
|
||||
} = usePaymentAjustmentHead(true);
|
||||
const methods = useForm({
|
||||
resolver: zodResolver(paymentSchema),
|
||||
defaultValues: defaultPayment,
|
||||
});
|
||||
const {
|
||||
control,
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors },
|
||||
} = methods;
|
||||
const { mutate: AddPayment, isPending } = useAddPayment(() => {
|
||||
handleClose();
|
||||
});
|
||||
const onSubmit = (formData) => {
|
||||
const payload = {
|
||||
...formData,
|
||||
paymentReceivedDate: localToUtc(formData.paymentReceivedDate),
|
||||
invoiceId: addPayment.invoiceId,
|
||||
};
|
||||
AddPayment(payload);
|
||||
};
|
||||
const handleClose = (formData) => {
|
||||
reset(defaultPayment);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container pb-3">
|
||||
<div className="text-black fs-5 mb-2">Add Payment</div>
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="p-0 text-start">
|
||||
<div className="row px-md-1 px-0">
|
||||
<div className="col-12 col-md-6 mb-2">
|
||||
<Label required>TransanctionId</Label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
{...register("transactionId")}
|
||||
/>
|
||||
{errors.transactionId && (
|
||||
<small className="danger-text">
|
||||
{errors.transactionId.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-6 mb-2">
|
||||
<Label required>Transaction Date </Label>
|
||||
<DatePicker
|
||||
name="paymentReceivedDate"
|
||||
control={control}
|
||||
minDate={
|
||||
data?.clientSubmitedDate
|
||||
? new Date(
|
||||
new Date(data?.clientSubmitedDate).setDate(
|
||||
new Date(data?.clientSubmitedDate).getDate() + 1
|
||||
)
|
||||
)
|
||||
: null
|
||||
}
|
||||
maxDate={new Date()}
|
||||
/>
|
||||
{errors.paymentReceivedDate && (
|
||||
<small className="danger-text">
|
||||
{errors.paymentReceivedDate.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-6 mb-2">
|
||||
<Label
|
||||
htmlFor="paymentAdjustmentHeadId"
|
||||
className="form-label"
|
||||
required
|
||||
>
|
||||
Payment Adjustment Head
|
||||
</Label>
|
||||
<select
|
||||
className="form-select form-select-sm "
|
||||
{...register("paymentAdjustmentHeadId")}
|
||||
>
|
||||
{isPaymentTypeLoading ? (
|
||||
<option>Loading..</option>
|
||||
) : (
|
||||
<>
|
||||
<option value="">Select Payment Head</option>
|
||||
{paymentTypes?.data
|
||||
?.sort((a, b) => a.name.localeCompare(b.name))
|
||||
?.map((type) => (
|
||||
<option key={type.id} value={type.id}>
|
||||
{type.name}
|
||||
</option>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</select>
|
||||
{errors.paymentAdjustmentHeadId && (
|
||||
<small className="danger-text">
|
||||
{errors.paymentAdjustmentHeadId.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-6 mb-2">
|
||||
<Label htmlFor="amount" className="form-label" required>
|
||||
Amount
|
||||
</Label>
|
||||
<input
|
||||
type="number"
|
||||
id="amount"
|
||||
className="form-control form-control-sm"
|
||||
min="1"
|
||||
step="0.01"
|
||||
inputMode="decimal"
|
||||
{...register("amount", { valueAsNumber: true })}
|
||||
/>
|
||||
{errors.amount && (
|
||||
<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">
|
||||
{" "}
|
||||
<button
|
||||
type="reset"
|
||||
className="btn btn-label-secondary btn-sm mt-3"
|
||||
onClick={() => {
|
||||
handleClose();
|
||||
onClose();
|
||||
}}
|
||||
disabled={isPending}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary btn-sm mt-3"
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending ? "Please Wait..." : "Submit"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
|
||||
{isLoading ? (
|
||||
<PaymentHistorySkeleton />
|
||||
) : (
|
||||
data?.receivedInvoicePayments?.length > 0 && (
|
||||
<div className="mt-1 text-start">
|
||||
<div className="mb-2 text-secondry fs-6">
|
||||
<i className="bx bx-history bx-sm me-1"></i>History
|
||||
</div>
|
||||
|
||||
<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="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)}
|
||||
</div>
|
||||
<span className="fs-semibold d-block d-md-none">
|
||||
{formatFigure(payment.amount, {
|
||||
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">
|
||||
Updated 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">
|
||||
<small className="fw-semibold">
|
||||
Transaction ID:
|
||||
</small>{" "}
|
||||
{payment.transactionId}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-12 ">
|
||||
<div className="d-flex justify-content-between">
|
||||
<span>{payment?.paymentAdjustmentHead?.name}</span>
|
||||
<span className="fs-semibold d-none d-md-block">
|
||||
{formatFigure(payment.amount, {
|
||||
type: "currency",
|
||||
currency: "INR",
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-tiny m-0 mt-1">
|
||||
{payment?.comment}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddPayment;
|
267
src/components/collections/CollectionList.jsx
Normal file
267
src/components/collections/CollectionList.jsx
Normal file
@ -0,0 +1,267 @@
|
||||
import React, { useState } from "react";
|
||||
import { useCollections } from "../../hooks/useCollections";
|
||||
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
||||
import { formatFigure, localToUtc, useDebounce } from "../../utils/appUtils";
|
||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||
import Pagination from "../common/Pagination";
|
||||
import { useCollectionContext } from "../../pages/collections/CollectionPage";
|
||||
import { CollectionTableSkeleton } from "./CollectionSkeleton";
|
||||
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||
|
||||
const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const selectedProject = useSelectedProject()
|
||||
const searchDebounce = useDebounce(searchString, 500);
|
||||
|
||||
const { data, isLoading, isError, error } = useCollections(
|
||||
ITEMS_PER_PAGE,
|
||||
currentPage,
|
||||
localToUtc(fromDate),
|
||||
localToUtc(toDate),
|
||||
isPending,
|
||||
true,
|
||||
selectedProject,
|
||||
searchDebounce
|
||||
);
|
||||
const { setProcessedPayment, setAddPayment, setViewCollection } =
|
||||
useCollectionContext();
|
||||
|
||||
const paginate = (page) => {
|
||||
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
|
||||
setCurrentPage(page);
|
||||
}
|
||||
};
|
||||
|
||||
const collectionColumns = [
|
||||
{
|
||||
key: "invoiceDate",
|
||||
label: "Invoice Date",
|
||||
getValue: (col) => (
|
||||
<span
|
||||
className="text-truncate d-inline-block"
|
||||
style={{ maxWidth: "200px" }}
|
||||
>
|
||||
{formatUTCToLocalTime(col.invoiceDate)}
|
||||
</span>
|
||||
),
|
||||
align: "text-start",
|
||||
},
|
||||
{
|
||||
key: "invoiceId",
|
||||
label: "Invoice Id",
|
||||
getValue: (col) => (
|
||||
<span
|
||||
className="text-truncate d-inline-block"
|
||||
style={{ maxWidth: "200px" }}
|
||||
>
|
||||
{col?.invoiceNumber ?? "-"}
|
||||
</span>
|
||||
),
|
||||
align: "text-start",
|
||||
},
|
||||
{
|
||||
key: "project",
|
||||
label: "Project",
|
||||
getValue: (col) => (
|
||||
<span
|
||||
className="text-truncate d-inline-block"
|
||||
style={{ maxWidth: "200px" }}
|
||||
>
|
||||
{col?.project?.name ?? "-"}
|
||||
</span>
|
||||
),
|
||||
align: "text-start",
|
||||
},
|
||||
{
|
||||
key: "submittedDate",
|
||||
label: "Submitted Date",
|
||||
getValue: (col) => (
|
||||
<span
|
||||
className="text-truncate d-inline-block"
|
||||
style={{ maxWidth: "200px" }}
|
||||
>
|
||||
{formatUTCToLocalTime(col.createdAt)}
|
||||
</span>
|
||||
),
|
||||
align: "text-center",
|
||||
},
|
||||
{
|
||||
key: "expectedSubmittedDate",
|
||||
label: "Expected Payment Date",
|
||||
getValue: (col) => (
|
||||
<span
|
||||
className="text-truncate d-inline-block"
|
||||
style={{ maxWidth: "200px" }}
|
||||
>
|
||||
{formatUTCToLocalTime(col.exceptedPaymentDate) ?? "-"}
|
||||
</span>
|
||||
),
|
||||
align: "text-center",
|
||||
},
|
||||
{
|
||||
key: "amount",
|
||||
label: "Amount",
|
||||
getValue: (col) => (
|
||||
<span
|
||||
className="text-truncate d-inline-block"
|
||||
style={{ maxWidth: "200px" }}
|
||||
>
|
||||
{formatFigure(col?.basicAmount, {
|
||||
type: "currency",
|
||||
currency: "INR",
|
||||
}) ?? 0}
|
||||
</span>
|
||||
),
|
||||
align: "text-end",
|
||||
},
|
||||
{
|
||||
key: "balance",
|
||||
label: "Balance",
|
||||
getValue: (col) => (
|
||||
<span
|
||||
className="text-truncate d-inline-block py-3"
|
||||
style={{ maxWidth: "200px" }}
|
||||
>
|
||||
{formatFigure(col?.balanceAmount, {
|
||||
type: "currency",
|
||||
currency: "INR",
|
||||
}) ?? 0}
|
||||
</span>
|
||||
),
|
||||
align: "text-end",
|
||||
},
|
||||
];
|
||||
|
||||
if (isLoading) return <CollectionTableSkeleton />;
|
||||
if (isError) return <p>{error.message}</p>;
|
||||
|
||||
return (
|
||||
<div className="card ">
|
||||
<div
|
||||
className="card-datatable table-responsive page-min-h"
|
||||
id="horizontal-example"
|
||||
>
|
||||
<div className="dataTables_wrapper no-footer mx-5 pb-2">
|
||||
<table className="table dataTable text-nowrap">
|
||||
<thead>
|
||||
<tr className="table_header_border">
|
||||
{collectionColumns.map((col) => (
|
||||
<th key={col.key} className={col.align}>
|
||||
{col.label}
|
||||
</th>
|
||||
))}
|
||||
<th className="sticky-action-column bg-white text-center">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Array.isArray(data?.data) && data.data.length > 0 ? (
|
||||
data.data.map((row, i) => (
|
||||
<tr key={i}>
|
||||
{collectionColumns.map((col) => (
|
||||
<td key={col.key} className={col.align}>
|
||||
{col.getValue(row)}
|
||||
</td>
|
||||
))}
|
||||
<td
|
||||
className="sticky-action-column text-center"
|
||||
style={{ padding: "12px 8px" }}
|
||||
>
|
||||
<div className="dropdown z-2">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<i
|
||||
className="bx bx-dots-vertical-rounded bx-sm text-muted"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-offset="0,8"
|
||||
data-bs-placement="top"
|
||||
data-bs-custom-class="tooltip-dark"
|
||||
title="More Action"
|
||||
></i>
|
||||
</button>
|
||||
|
||||
<ul className="dropdown-menu dropdown-menu-end">
|
||||
{/* View */}
|
||||
<li>
|
||||
<a
|
||||
className="dropdown-item cursor-pointer"
|
||||
onClick={() => setViewCollection(row.id)}
|
||||
>
|
||||
<i className="bx bx-show me-2 text-primary"></i>
|
||||
<span>View</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{!row?.markAsCompleted && (
|
||||
<>
|
||||
{/* Add Payment */}
|
||||
<li>
|
||||
<a
|
||||
className="dropdown-item cursor-pointer"
|
||||
onClick={() =>
|
||||
setAddPayment({
|
||||
isOpen: true,
|
||||
invoiceId: row.id,
|
||||
})
|
||||
}
|
||||
>
|
||||
<i className="bx bx-wallet me-2 text-warning"></i>
|
||||
<span>Add Payment</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{/* Mark Payment */}
|
||||
<li>
|
||||
<a
|
||||
className="dropdown-item cursor-pointer"
|
||||
onClick={() =>
|
||||
setProcessedPayment({
|
||||
isOpen: true,
|
||||
invoiceId: row.id,
|
||||
})
|
||||
}
|
||||
>
|
||||
<i className="bx bx-check-circle me-2 text-success"></i>
|
||||
<span>Mark Payment</span>
|
||||
</a>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr style={{ height: "200px" }}>
|
||||
<td
|
||||
colSpan={collectionColumns.length + 1}
|
||||
className="text-center border-0 align-middle"
|
||||
>
|
||||
No Collections Found
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
{data?.data?.length > 0 && (
|
||||
<div className="d-flex justify-content-start mt-2">
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={data?.totalPages}
|
||||
onPageChange={paginate}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollectionList;
|
211
src/components/collections/CollectionSkeleton.jsx
Normal file
211
src/components/collections/CollectionSkeleton.jsx
Normal file
@ -0,0 +1,211 @@
|
||||
import React from "react";
|
||||
|
||||
const SkeletonLine = ({ height = 20, width = "100%", className = "" }) => (
|
||||
<div
|
||||
className={`skeleton mb-2 ${className}`}
|
||||
style={{
|
||||
height,
|
||||
width,
|
||||
}}
|
||||
></div>
|
||||
);
|
||||
|
||||
export const PaymentHistorySkeleton = ({ count = 2 }) => {
|
||||
return (
|
||||
<div className="row text-start">
|
||||
{[...Array(count)].map((_, idx) => (
|
||||
<div className="col-12 mb-2" key={idx}>
|
||||
<div className="p-2 border-start border-warning">
|
||||
{/* Top Row: Date + Amount */}
|
||||
<div className="d-flex justify-content-between align-items-center">
|
||||
<SkeletonLine width="150px" height={18} /> {/* Date */}
|
||||
<SkeletonLine width="100px" height={18} /> {/* Amount */}
|
||||
</div>
|
||||
|
||||
<div className="row mt-1">
|
||||
{/* Transaction ID */}
|
||||
<div className="col-12 col-md-6">
|
||||
<SkeletonLine width="80%" height={16} />
|
||||
</div>
|
||||
|
||||
{/* Received By (Avatar + Name) */}
|
||||
<div className="col-12 col-md-6 d-flex align-items-center gap-2">
|
||||
<SkeletonLine
|
||||
width="30px"
|
||||
height={30}
|
||||
className="rounded-circle"
|
||||
/>{" "}
|
||||
{/* Avatar */}
|
||||
<SkeletonLine width="120px" height={16} /> {/* Name */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CollectionDetailsSkeleton = () => {
|
||||
return (
|
||||
<div className="container p-3">
|
||||
{/* Title */}
|
||||
<SkeletonLine height={24} width="200px" className="mx-auto" />
|
||||
|
||||
{/* Header Row */}
|
||||
<div className="row mb-3 px-1">
|
||||
<div className="col-10">
|
||||
<SkeletonLine height={20} />
|
||||
</div>
|
||||
<div className="col-2 d-flex justify-content-end">
|
||||
<SkeletonLine height={20} width="60px" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Project */}
|
||||
<div className="row mb-3">
|
||||
<div className="col-md-6">
|
||||
<SkeletonLine width="60%" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Invoice & E-Invoice */}
|
||||
<div className="row mb-3">
|
||||
<div className="col-md-6">
|
||||
<SkeletonLine />
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<SkeletonLine />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Invoice Date & Client Submitted */}
|
||||
<div className="row mb-3">
|
||||
<div className="col-md-6">
|
||||
<SkeletonLine />
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<SkeletonLine />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expected Payment & Mark as Completed */}
|
||||
<div className="row mb-3">
|
||||
<div className="col-md-6">
|
||||
<SkeletonLine />
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<SkeletonLine width="40%" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Basic & Tax Amount */}
|
||||
<div className="row mb-3">
|
||||
<div className="col-md-6">
|
||||
<SkeletonLine />
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<SkeletonLine />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Balance & Created At */}
|
||||
<div className="row mb-3">
|
||||
<div className="col-md-6">
|
||||
<SkeletonLine />
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<SkeletonLine />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Created By */}
|
||||
<div className="row mb-3">
|
||||
<div className="col-md-6 d-flex align-items-center">
|
||||
<SkeletonLine
|
||||
width="40px"
|
||||
height={40}
|
||||
className="me-2 rounded-circle"
|
||||
/>
|
||||
<SkeletonLine width="100px" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="row mb-3">
|
||||
<div className="col-12">
|
||||
<SkeletonLine height={50} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Attachments */}
|
||||
<div className="row mb-3">
|
||||
<div className="col-12 d-flex gap-2 flex-wrap">
|
||||
{[...Array(3)].map((_, idx) => (
|
||||
<SkeletonLine key={idx} height={60} width="80px" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="row mb-2">
|
||||
<div className="col-12 d-flex gap-2">
|
||||
<SkeletonLine height={35} width="120px" />
|
||||
<SkeletonLine height={35} width="150px" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab Content (Comments / Payments) */}
|
||||
<SkeletonLine height={200} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CollectionTableSkeleton = () => {
|
||||
const columnCount = 8;
|
||||
|
||||
return (
|
||||
<div className="card ">
|
||||
<div
|
||||
className="card-datatable table-responsive page-min-h"
|
||||
id="horizontal-example"
|
||||
>
|
||||
<div className="dataTables_wrapper no-footer mx-5 pb-2">
|
||||
<table className="table dataTable text-nowrap">
|
||||
<thead>
|
||||
<tr >
|
||||
{[...Array(columnCount - 1)].map((_, i) => (
|
||||
<th key={i}>
|
||||
<SkeletonLine height={15} width="80px" />
|
||||
</th>
|
||||
))}
|
||||
<th className="d-flex justify-content-center">
|
||||
<SkeletonLine height={16} width="40px" />
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[...Array(8)].map((_, rowIdx) => (
|
||||
<tr key={rowIdx}>
|
||||
{[...Array(columnCount - 1)].map((_, colIdx) => (
|
||||
<td key={colIdx}>
|
||||
<SkeletonLine height={33} />
|
||||
</td>
|
||||
))}
|
||||
<td className="d-flex justify-content-center">
|
||||
<SkeletonLine height={16} width="20px" />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* Pagination Skeleton */}
|
||||
<div className="d-flex justify-content-end mt-2">
|
||||
<SkeletonLine height={30} width="200px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
85
src/components/collections/Comment.jsx
Normal file
85
src/components/collections/Comment.jsx
Normal file
@ -0,0 +1,85 @@
|
||||
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,
|
||||
reset,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: zodResolver(CommentSchema),
|
||||
defaultValues: { comment: "" },
|
||||
});
|
||||
|
||||
const { mutate: AddComment, isPending } = useAddComment(() => {
|
||||
reset();
|
||||
});
|
||||
|
||||
const onSubmit = (formData) => {
|
||||
const payload = { ...formData, invoiceId: invoice?.id };
|
||||
AddComment(payload);
|
||||
};
|
||||
return (
|
||||
<div className="row pt-1 px-2">
|
||||
<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>
|
||||
{invoice?.comments?.length > 0 ? (
|
||||
invoice.comments.map((comment, index) => (
|
||||
<div
|
||||
className="border-start border-primary ps-1 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>
|
||||
|
||||
<div className="ms-9"> <p className="mb-1">{comment?.comment}</p></div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-2"> <p className="text-muted">No comments yet.</p></div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Comment;
|
446
src/components/collections/ManageCollection.jsx
Normal file
446
src/components/collections/ManageCollection.jsx
Normal file
@ -0,0 +1,446 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useModal } from "../../hooks/useAuth";
|
||||
import Modal from "../common/Modal";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import Label from "../common/Label";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { defaultCollection, newCollection } from "./collectionSchema";
|
||||
import SelectMultiple from "../common/SelectMultiple";
|
||||
import { useProjectName } from "../../hooks/useProjects";
|
||||
import DatePicker from "../common/DatePicker";
|
||||
import {
|
||||
useCollection,
|
||||
useCreateCollection,
|
||||
useUpdateCollection,
|
||||
} from "../../hooks/useCollections";
|
||||
import { formatFileSize, localToUtc } from "../../utils/appUtils";
|
||||
import { useCollectionContext } from "../../pages/collections/CollectionPage";
|
||||
import { formatDate } from "../../utils/dateUtils";
|
||||
|
||||
const ManageCollection = ({ collectionId, onClose }) => {
|
||||
const { data, isError, isLoading, error } = useCollection(collectionId);
|
||||
const { projectNames, projectLoading } = useProjectName(true);
|
||||
const methods = useForm({
|
||||
resolver: zodResolver(newCollection),
|
||||
defaultValues: defaultCollection,
|
||||
});
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
register,
|
||||
setValue,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors },
|
||||
} = methods;
|
||||
|
||||
const { mutate: createNewCollection, isPending } = useCreateCollection(() => {
|
||||
handleClose();
|
||||
});
|
||||
const { mutate: UpdateCollection } = useUpdateCollection(() => {
|
||||
handleClose();
|
||||
});
|
||||
|
||||
const files = watch("attachments");
|
||||
const onFileChange = async (e) => {
|
||||
const newFiles = Array.from(e.target.files);
|
||||
if (newFiles.length === 0) return;
|
||||
|
||||
const existingFiles = watch("attachments") || [];
|
||||
|
||||
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: "",
|
||||
isActive: true,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const combinedFiles = [
|
||||
...existingFiles,
|
||||
...parsedFiles.filter(
|
||||
(newFile) =>
|
||||
!existingFiles.some(
|
||||
(f) =>
|
||||
f.fileName === newFile.fileName && f.fileSize === newFile.fileSize
|
||||
)
|
||||
),
|
||||
];
|
||||
|
||||
setValue("attachments", 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]);
|
||||
reader.onerror = (error) => reject(error);
|
||||
});
|
||||
const removeFile = (index) => {
|
||||
if (collectionId) {
|
||||
const newFiles = files.map((file, i) => {
|
||||
if (file.documentId !== index) return file;
|
||||
return {
|
||||
...file,
|
||||
isActive: false,
|
||||
};
|
||||
});
|
||||
setValue("attachments", newFiles, { shouldValidate: true });
|
||||
} else {
|
||||
const newFiles = files.filter((_, i) => i !== index);
|
||||
setValue("attachments", newFiles, { shouldValidate: true });
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = (formData) => {
|
||||
const payload = {
|
||||
...formData,
|
||||
clientSubmitedDate: localToUtc(formData.clientSubmitedDate),
|
||||
invoiceDate: localToUtc(formData.invoiceDate),
|
||||
exceptedPaymentDate: localToUtc(formData.exceptedPaymentDate),
|
||||
};
|
||||
|
||||
if (collectionId) {
|
||||
UpdateCollection({
|
||||
collectionId,
|
||||
payload: { ...payload, id: collectionId },
|
||||
});
|
||||
} else {
|
||||
createNewCollection(payload);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
reset(defaultCollection);
|
||||
onClose();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (data && collectionId) {
|
||||
reset({
|
||||
projectId: data?.project?.id,
|
||||
invoiceNumber: data?.invoiceNumber,
|
||||
eInvoiceNumber: data?.eInvoiceNumber,
|
||||
title: data?.title,
|
||||
clientSubmitedDate: formatDate(data?.clientSubmitedDate),
|
||||
invoiceDate: formatDate(data?.invoiceDate),
|
||||
exceptedPaymentDate: formatDate(data?.exceptedPaymentDate),
|
||||
taxAmount: data?.taxAmount,
|
||||
basicAmount: data?.basicAmount,
|
||||
description: data?.description,
|
||||
attachments: data?.attachments,
|
||||
attachments: data.attachments
|
||||
? data.attachments.map((doc) => ({
|
||||
fileName: doc.fileName,
|
||||
base64Data: null,
|
||||
contentType: doc.contentType,
|
||||
documentId: doc.documentId,
|
||||
fileSize: 0,
|
||||
description: "",
|
||||
preSignedUrl: doc.preSignedUrl,
|
||||
isActive: doc.isActive ?? true,
|
||||
}))
|
||||
: [],
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
if (isLoading) return <div>Loading....</div>;
|
||||
if (isError) return <div>{error.message}</div>;
|
||||
return (
|
||||
<div className="container pb-3">
|
||||
<div className="text-black fs-5 mb-2">
|
||||
{collectionId ? "Update" : "New"} Collection
|
||||
</div>
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="p-0 text-start">
|
||||
<div className="row px-md-1 px-0">
|
||||
<div className="col-12 col-md-6 mb-2">
|
||||
<Label className="form-label" required>
|
||||
Select Project
|
||||
</Label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
{...register("projectId")}
|
||||
>
|
||||
<option value="">Select Project</option>
|
||||
{projectLoading ? (
|
||||
<option>Loading...</option>
|
||||
) : (
|
||||
projectNames?.map((project) => (
|
||||
<option key={project.id} value={project.id}>
|
||||
{project.name}
|
||||
</option>
|
||||
))
|
||||
)}
|
||||
</select>
|
||||
{errors.projectId && (
|
||||
<small className="danger-text">
|
||||
{errors.projectId.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 col-md-6 mb-2">
|
||||
<Label required>Title</Label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
{...register("title")}
|
||||
/>
|
||||
{errors.title && (
|
||||
<small className="danger-text">{errors.title.message}</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 col-md-6 mb-2">
|
||||
<Label required>Invoice Number</Label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
{...register("invoiceNumber")}
|
||||
/>
|
||||
{errors.invoiceId && (
|
||||
<small className="danger-text">
|
||||
{errors.invoiceId.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 col-md-6 mb-2">
|
||||
<Label required>E-Invoice Number</Label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
{...register("eInvoiceNumber")}
|
||||
/>
|
||||
{errors.invoiceId && (
|
||||
<small className="danger-text">
|
||||
{errors.invoiceId.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 col-md-6 mb-2">
|
||||
<Label required>Invoice Date</Label>
|
||||
<DatePicker
|
||||
name="invoiceDate"
|
||||
control={control}
|
||||
maxDate={new Date()}
|
||||
/>
|
||||
{errors.invoiceDate && (
|
||||
<small className="danger-text">
|
||||
{errors.invoiceDate.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 col-md-6 mb-2">
|
||||
<Label required>Expected Payment Date</Label>
|
||||
<DatePicker
|
||||
name="exceptedPaymentDate"
|
||||
control={control}
|
||||
minDate={watch("invoiceDate")}
|
||||
/>
|
||||
{errors.exceptedPaymentDate && (
|
||||
<small className="danger-text">
|
||||
{errors.exceptedPaymentDate.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 col-md-6 mb-2">
|
||||
<Label required>Submission Date (Client)</Label>
|
||||
<DatePicker
|
||||
name="clientSubmitedDate"
|
||||
control={control}
|
||||
maxDate={new Date()}
|
||||
/>
|
||||
{errors.exceptedPaymentDate && (
|
||||
<small className="danger-text">
|
||||
{errors.exceptedPaymentDate.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-6 mb-2">
|
||||
<Label htmlFor="basicAmount" className="form-label" required>
|
||||
Basic Amount
|
||||
</Label>
|
||||
<input
|
||||
type="number"
|
||||
id="basicAmount"
|
||||
className="form-control form-control-sm"
|
||||
min="1"
|
||||
step="0.01"
|
||||
inputMode="decimal"
|
||||
{...register("basicAmount", { valueAsNumber: true })}
|
||||
/>
|
||||
{errors.basicAmount && (
|
||||
<small className="danger-text">
|
||||
{errors.basicAmount.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 col-md-6 mb-2">
|
||||
<Label htmlFor="taxAmount" className="form-label" required>
|
||||
Tax Amount
|
||||
</Label>
|
||||
<input
|
||||
type="number"
|
||||
id="taxAmount"
|
||||
className="form-control form-control-sm"
|
||||
min="1"
|
||||
step="0.01"
|
||||
inputMode="decimal"
|
||||
{...register("taxAmount", { valueAsNumber: true })}
|
||||
/>
|
||||
{errors.taxAmount && (
|
||||
<small className="danger-text">
|
||||
{errors.taxAmount.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 my-2 text-start mb-2">
|
||||
<div className="col-md-12">
|
||||
<Label htmlFor="description" className="form-label" required>
|
||||
Description
|
||||
</Label>
|
||||
<textarea
|
||||
id="description"
|
||||
className="form-control form-control-sm"
|
||||
{...register("description")}
|
||||
rows="2"
|
||||
></textarea>
|
||||
{errors.description && (
|
||||
<small className="danger-text">
|
||||
{errors.description.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<Label className="form-label" required>
|
||||
Upload Bill{" "}
|
||||
</Label>
|
||||
|
||||
<div
|
||||
className="border border-secondary border-dashed rounded p-4 text-center bg-textMuted position-relative text-black"
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => document.getElementById("attachments").click()}
|
||||
>
|
||||
<i className="bx bx-cloud-upload d-block bx-lg "> </i>
|
||||
<span className="text-muted d-block">
|
||||
Click to select or click here to browse
|
||||
</span>
|
||||
<small className="text-muted">
|
||||
(PDF, JPG, PNG,Doc,docx,xls,xlsx max 5MB)
|
||||
</small>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
id="attachments"
|
||||
accept="
|
||||
.pdf,
|
||||
.doc,
|
||||
.docx,
|
||||
.xls,
|
||||
.xlsx,
|
||||
.jpg,
|
||||
.jpeg,
|
||||
.png
|
||||
"
|
||||
multiple
|
||||
style={{ display: "none" }}
|
||||
{...register("attachments")}
|
||||
onChange={(e) => {
|
||||
onFileChange(e);
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{errors.attachments && (
|
||||
<small className="danger-text">
|
||||
{errors.attachments.message}
|
||||
</small>
|
||||
)}
|
||||
{files.length > 0 && (
|
||||
<div className="d-block">
|
||||
{files
|
||||
.filter((file) => {
|
||||
if (collectionId) {
|
||||
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(collectionId ? file.documentId : idx);
|
||||
}}
|
||||
></i>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{Array.isArray(errors.attachments) &&
|
||||
errors.attachments.map((fileError, index) => (
|
||||
<div key={index} className="danger-text small mt-1">
|
||||
{
|
||||
(fileError?.fileSize?.message ||
|
||||
fileError?.contentType?.message ||
|
||||
fileError?.base64Data?.message,
|
||||
fileError?.documentId?.message)
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="d-flex justify-content-end gap-3">
|
||||
{" "}
|
||||
<button
|
||||
type="reset"
|
||||
className="btn btn-label-secondary btn-sm mt-3"
|
||||
onClick={handleClose}
|
||||
disabled={isPending}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary btn-sm mt-3"
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending ? "Please Wait..." : "Submit"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageCollection;
|
53
src/components/collections/PaymentHistoryTable.jsx
Normal file
53
src/components/collections/PaymentHistoryTable.jsx
Normal file
@ -0,0 +1,53 @@
|
||||
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="pt-1 data-tabe table-responsive">
|
||||
<table className="table table-bordered table-responsive 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"> Payment Adjustment-Head</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-start">{payment?.paymentAdjustmentHead?.name ?? "--"}</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 py-1">
|
||||
|
||||
{payment.createdBy?.firstName}{" "}
|
||||
{payment.createdBy?.lastName}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
):(<div className='text-center py-2'><p>No History</p></div>)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PaymentHistoryTable
|
253
src/components/collections/ViewCollection.jsx
Normal file
253
src/components/collections/ViewCollection.jsx
Normal file
@ -0,0 +1,253 @@
|
||||
import React, { useState } from "react";
|
||||
import { useCollectionContext } from "../../pages/collections/CollectionPage";
|
||||
import { useCollection } from "../../hooks/useCollections";
|
||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||
import { formatFigure, getIconByFileType } from "../../utils/appUtils";
|
||||
import Avatar from "../common/Avatar";
|
||||
import PaymentHistoryTable from "./PaymentHistoryTable";
|
||||
import Comment from "./Comment";
|
||||
import { CollectionDetailsSkeleton } from "./CollectionSkeleton";
|
||||
|
||||
const ViewCollection = ({ onClose }) => {
|
||||
const [activeTab, setActiveTab] = useState("payments");
|
||||
const { viewCollection, setCollection, setDocumentView } =
|
||||
useCollectionContext();
|
||||
const { data, isLoading, isError, error } = useCollection(viewCollection);
|
||||
|
||||
const handleEdit = () => {
|
||||
setCollection({ isOpen: true, invoiceId: viewCollection });
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (isLoading) return <CollectionDetailsSkeleton />;
|
||||
if (isError) return <div>{error.message}</div>;
|
||||
|
||||
return (
|
||||
<div className="container p-3">
|
||||
<p className="fs-5 fw-semibold">Collection Details</p>
|
||||
<div className="row text-start ">
|
||||
<div className="col-12 mb-3 d-flex justify-content-between">
|
||||
<div className="d-flex">
|
||||
<label
|
||||
className=" me-2 mb-0 fw-semibold"
|
||||
style={{ minWidth: "130px" }}
|
||||
>
|
||||
Project :
|
||||
</label>
|
||||
<div className="text-muted">{data?.project?.name}</div>
|
||||
</div>
|
||||
<div>
|
||||
{" "}
|
||||
<span
|
||||
className={`badge bg-label-${
|
||||
data?.isActive ? "primary" : "danger"
|
||||
}`}
|
||||
>
|
||||
{data?.isActive ? "Active" : "Inactive"}
|
||||
</span>
|
||||
{!data?.receivedInvoicePayments && (
|
||||
<span onClick={handleEdit} className="ms-2 cursor-pointer">
|
||||
<i className="bx bx-edit text-primary bx-sm"></i>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<div className="row mb-2 text-wrap">
|
||||
<div className="col-4 fw-semibold">Title :</div>
|
||||
<div className="col-8 text-wrap">{data?.title}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<div className="row mb-4 align-items-end">
|
||||
<div className="col-4 fw-semibold">Invoice Number:</div>
|
||||
<div className="col-8">{data?.invoiceNumber}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Row 2: E-Invoice Number + Project */}
|
||||
<div className="col-md-6">
|
||||
<div className="row mb-4 align-items-end">
|
||||
<div className="col-4 fw-semibold">E-Invoice Number:</div>
|
||||
<div className="col-8">{data?.eInvoiceNumber}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 3: Invoice Date + Client Submitted Date */}
|
||||
<div className="col-md-6">
|
||||
<div className="row mb-4 align-items-end">
|
||||
<div className="col-4 fw-semibold">Invoice Date:</div>
|
||||
<div className="col-8">
|
||||
{formatUTCToLocalTime(data?.invoiceDate)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<div className="row mb-4 align-items-end">
|
||||
<div className="col-4 fw-semibold">Client Submitted Date:</div>
|
||||
<div className="col-8">
|
||||
{formatUTCToLocalTime(data?.clientSubmitedDate)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Row 4: Expected Payment Date + Mark as Completed */}
|
||||
<div className="col-md-6">
|
||||
<div className="row mb-4 align-items-end">
|
||||
<div className="col-4 fw-semibold">Expected Payment Date:</div>
|
||||
<div className="col-8">
|
||||
{formatUTCToLocalTime(data?.exceptedPaymentDate)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 5: Basic Amount + Tax Amount */}
|
||||
<div className="col-md-6">
|
||||
<div className="row mb-4 align-items-end">
|
||||
<div className="col-4 fw-semibold">Basic Amount :</div>
|
||||
<div className="col-8">
|
||||
{formatFigure(data?.basicAmount, {
|
||||
type: "currency",
|
||||
currency: "INR",
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<div className="row mb-4 align-items-end">
|
||||
<div className="col-4 fw-semibold">Tax Amount :</div>
|
||||
<div className="col-8">
|
||||
{formatFigure(data?.taxAmount, {
|
||||
type: "currency",
|
||||
currency: "INR",
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Row 6: Balance Amount + Created At */}
|
||||
<div className="col-md-6">
|
||||
<div className="row mb-4 align-items-end">
|
||||
<div className="col-4 fw-semibold">Balance Amount :</div>
|
||||
<div className="col-8">
|
||||
{formatFigure(data?.balanceAmount, {
|
||||
type: "currency",
|
||||
currency: "INR",
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<div className="row mb-4 align-items-end">
|
||||
<div className="col-4 fw-semibold">Created At :</div>
|
||||
<div className="col-8">{formatUTCToLocalTime(data?.createdAt)}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Row 7: Created By */}
|
||||
<div className="col-md-6">
|
||||
<div className="row mb-4 align-items-center">
|
||||
<div className="col-4 fw-semibold">Created By :</div>
|
||||
<div className="col-8 d-flex align-items-center">
|
||||
<Avatar
|
||||
size="xs"
|
||||
firstName={data.createdBy?.firstName}
|
||||
lastName={data.createdBy?.lastName}
|
||||
/>
|
||||
<span className="ms-1 text-muted">
|
||||
{data?.createdBy?.firstName} {data?.createdBy?.lastName}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Description */}
|
||||
<div className="col-12 my-1 mb-2">
|
||||
<div className=" me-2 mb-0 fw-semibold">Description :</div>
|
||||
<div className="text-muted">{data?.description}</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 text-start">
|
||||
<label className=" me-2 mb-2 fw-semibold">Attachment :</label>
|
||||
|
||||
<div className="d-flex flex-wrap gap-2">
|
||||
{data?.attachments?.map((doc) => {
|
||||
const isImage = doc.contentType?.includes("image");
|
||||
|
||||
return (
|
||||
<div
|
||||
key={doc.documentId}
|
||||
className="border rounded hover-scale p-2 d-flex flex-column align-items-center"
|
||||
style={{
|
||||
width: "80px",
|
||||
cursor: isImage ? "pointer" : "default",
|
||||
}}
|
||||
onClick={() => {
|
||||
if (isImage) {
|
||||
setDocumentView({
|
||||
IsOpen: true,
|
||||
Image: doc.preSignedUrl,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<i
|
||||
className={`bx ${getIconByFileType(doc.contentType)}`}
|
||||
style={{ fontSize: "30px" }}
|
||||
></i>
|
||||
<small
|
||||
className="text-center text-tiny text-truncate w-100"
|
||||
title={doc.fileName}
|
||||
>
|
||||
{doc.fileName}
|
||||
</small>
|
||||
</div>
|
||||
);
|
||||
}) ?? "No Attachment"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container px-1">
|
||||
{/* Tabs Navigation */}
|
||||
<ul className="nav nav-tabs" role="tablist">
|
||||
<li className="nav-item">
|
||||
<button
|
||||
className={`nav-link ${
|
||||
activeTab === "payments" ? "active" : ""
|
||||
}`}
|
||||
onClick={() => setActiveTab("payments")}
|
||||
type="button"
|
||||
>
|
||||
<i className="bx bx-history bx-sm me-1"></i> Payments History
|
||||
</button>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<button
|
||||
className={`nav-link ${
|
||||
activeTab === "details" ? "active" : ""
|
||||
}`}
|
||||
onClick={() => setActiveTab("details")}
|
||||
type="button"
|
||||
>
|
||||
<i className="bx bx-message-dots bx-sm me-2"></i> Comments (
|
||||
{data?.comments?.length ?? "0"})
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="tab-content px-1 py-0 border-top-0">
|
||||
{activeTab === "payments" && (
|
||||
<div className="tab-pane fade show active">
|
||||
<PaymentHistoryTable data={data} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "details" && (
|
||||
<div className="tab-pane fade show active">
|
||||
<Comment invoice={data} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewCollection;
|
101
src/components/collections/collectionSchema.jsx
Normal file
101
src/components/collections/collectionSchema.jsx
Normal file
@ -0,0 +1,101 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
||||
const ALLOWED_TYPES = [
|
||||
"application/pdf",
|
||||
"application/doc",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"image/png",
|
||||
"image/jpg",
|
||||
"image/jpeg",
|
||||
"application/vnd.ms-excel",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
];
|
||||
|
||||
|
||||
export const newCollection = z.object({
|
||||
title: z.string().trim().min(1, { message: "Title is required" }),
|
||||
projectId: z.string().trim().min(1, { message: "Project is required" }),
|
||||
invoiceDate: z.string().min(1, { message: "Date is required" }),
|
||||
description: z.string().trim().optional(),
|
||||
clientSubmitedDate: z.string().min(1, { message: "Date is required" }),
|
||||
exceptedPaymentDate: z.string().min(1, { message: "Date is required" }),
|
||||
invoiceNumber: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, { message: "Invoice is required" })
|
||||
.max(17, { message: "Invalid Number" }),
|
||||
eInvoiceNumber: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, { message: "E-Invoice No is required" }),
|
||||
taxAmount: z.coerce
|
||||
.number({
|
||||
invalid_type_error: "Amount is required and must be a number",
|
||||
})
|
||||
.min(1, "Amount must be Enter")
|
||||
.refine((val) => /^\d+(\.\d{1,2})?$/.test(val.toString()), {
|
||||
message: "Amount must have at most 2 decimal places",
|
||||
}),
|
||||
basicAmount: z.coerce
|
||||
.number({
|
||||
invalid_type_error: "Amount is required and must be a number",
|
||||
})
|
||||
.min(1, "Amount must be Enter")
|
||||
.refine((val) => /^\d+(\.\d{1,2})?$/.test(val.toString()), {
|
||||
message: "Amount must have at most 2 decimal places",
|
||||
}),
|
||||
attachments: 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),
|
||||
})
|
||||
)
|
||||
.nonempty({ message: "At least one file attachment is required" }),
|
||||
});
|
||||
|
||||
export const defaultCollection = {
|
||||
projectId: "",
|
||||
invoiceNumber: " ",
|
||||
eInvoiceNumber: "",
|
||||
title: "",
|
||||
clientSubmitedDate: null,
|
||||
invoiceDate: null,
|
||||
exceptedPaymentDate: null,
|
||||
taxAmount: "",
|
||||
basicAmount: "",
|
||||
description: "",
|
||||
attachments: [],
|
||||
};
|
||||
|
||||
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"}),
|
||||
paymentAdjustmentHeadId:z.string().min(1,{message:"Payment Type required"})
|
||||
});
|
||||
|
||||
// Default Value
|
||||
export const defaultPayment = {
|
||||
paymentReceivedDate: null,
|
||||
transactionId: "",
|
||||
amount: 0,
|
||||
comment:"",
|
||||
paymentAdjustmentHeadId:""
|
||||
};
|
||||
|
||||
|
||||
export const CommentSchema = z.object({
|
||||
comment:z.string().min(1,{message:"Comment required"})
|
||||
})
|
@ -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>
|
||||
|
@ -13,19 +13,18 @@ const ConfirmModal = ({
|
||||
if (!isOpen) return null;
|
||||
|
||||
const TypeofIcon = () => {
|
||||
if (type === "delete") {
|
||||
return (
|
||||
<i
|
||||
className="bx bx-x-circle text-danger"
|
||||
style={{ fontSize: "60px" }}
|
||||
></i>
|
||||
);
|
||||
switch (type) {
|
||||
case "delete":
|
||||
return <i className="bx bx-x-circle text-danger" style={{ fontSize: "60px" }}></i>;
|
||||
case "success":
|
||||
return <i className="bx bx-check-circle text-success" style={{ fontSize: "60px" }}></i>;
|
||||
case "warning":
|
||||
return <i className="bx bx-error-circle text-warning" style={{ fontSize: "60px" }}></i>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const modalSize = type === "delete" ? "sm" : "md";
|
||||
|
||||
return (
|
||||
<div
|
||||
className="modal fade show"
|
||||
@ -33,22 +32,24 @@ const ConfirmModal = ({
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div className={`modal-dialog modal-${modalSize} modal-dialog-top`}>
|
||||
<div className="modal-dialog modal-sm modal-dialog-top">
|
||||
<div className="modal-content">
|
||||
<div className="modal-body py-1 px-2">
|
||||
<div className="d-flex justify-content-between mb-4 pt-2">
|
||||
{header && <strong className="mb-0">{header}</strong>}
|
||||
<button
|
||||
type="button"
|
||||
className="btn-close "
|
||||
className="btn-close"
|
||||
aria-label="Close"
|
||||
onClick={onClose}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-4 col-sm-2">{TypeofIcon()}</div>
|
||||
<div className="col-8 col-sm-10 py-sm-2 py-1 text-sm-start">
|
||||
<div className="col-4 col-sm-3 d-flex justify-content-center align-items-start">
|
||||
{TypeofIcon()}
|
||||
</div>
|
||||
<div className="col-8 col-sm-9 py-sm-2 py-1 text-sm-start">
|
||||
<span className="fs-6">{message}</span>
|
||||
<div className="d-flex justify-content-end mt-4">
|
||||
<button
|
||||
@ -59,7 +60,7 @@ const ConfirmModal = ({
|
||||
{loading ? "Please Wait..." : "Yes"}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary ms-4 btn-sm"
|
||||
className="btn btn-secondary ms-3 btn-sm"
|
||||
onClick={onClose}
|
||||
disabled={loading}
|
||||
>
|
||||
@ -68,6 +69,7 @@ const ConfirmModal = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -88,7 +88,6 @@ export default DateRangePicker;
|
||||
|
||||
|
||||
|
||||
|
||||
export const DateRangePicker1 = ({
|
||||
startField = "startDate",
|
||||
endField = "endDate",
|
||||
@ -98,6 +97,7 @@ export const DateRangePicker1 = ({
|
||||
resetSignal,
|
||||
defaultRange = true,
|
||||
maxDate = null,
|
||||
howManyDay = 6,
|
||||
...rest
|
||||
}) => {
|
||||
const inputRef = useRef(null);
|
||||
@ -107,12 +107,13 @@ export const DateRangePicker1 = ({
|
||||
field: { ref },
|
||||
} = useController({ name: startField, control });
|
||||
|
||||
// Apply default range
|
||||
const applyDefaultDates = () => {
|
||||
const today = new Date();
|
||||
const past = new Date();
|
||||
past.setDate(today.getDate() - 6);
|
||||
const past = new Date(today.getTime());
|
||||
past.setDate(today.getDate() - howManyDay);
|
||||
|
||||
const format = (d) => flatpickr.formatDate(d, "d-m-Y");
|
||||
const format = (d) => window.flatpickr.formatDate(d, "d-m-Y");
|
||||
const formattedStart = format(past);
|
||||
const formattedEnd = format(today);
|
||||
|
||||
@ -127,15 +128,19 @@ export const DateRangePicker1 = ({
|
||||
useEffect(() => {
|
||||
if (!inputRef.current || inputRef.current._flatpickr) return;
|
||||
|
||||
const instance = flatpickr(inputRef.current, {
|
||||
if (defaultRange && !getValues(startField) && !getValues(endField)) {
|
||||
applyDefaultDates();
|
||||
}
|
||||
|
||||
const instance = window.flatpickr(inputRef.current, {
|
||||
mode: "range",
|
||||
dateFormat: "d-m-Y",
|
||||
allowInput: allowText,
|
||||
maxDate ,
|
||||
maxDate,
|
||||
onChange: (selectedDates) => {
|
||||
if (selectedDates.length === 2) {
|
||||
const [start, end] = selectedDates;
|
||||
const format = (d) => flatpickr.formatDate(d, "d-m-Y");
|
||||
const format = (d) => window.flatpickr.formatDate(d, "d-m-Y");
|
||||
setValue(startField, format(start), { shouldValidate: true });
|
||||
setValue(endField, format(end), { shouldValidate: true });
|
||||
} else {
|
||||
@ -148,12 +153,10 @@ export const DateRangePicker1 = ({
|
||||
|
||||
const currentStart = getValues(startField);
|
||||
const currentEnd = getValues(endField);
|
||||
if (defaultRange && !currentStart && !currentEnd) {
|
||||
applyDefaultDates();
|
||||
} else if (currentStart && currentEnd) {
|
||||
if (currentStart && currentEnd) {
|
||||
instance.setDate([
|
||||
flatpickr.parseDate(currentStart, "d-m-Y"),
|
||||
flatpickr.parseDate(currentEnd, "d-m-Y"),
|
||||
window.flatpickr.parseDate(currentStart, "d-m-Y"),
|
||||
window.flatpickr.parseDate(currentEnd, "d-m-Y"),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -161,20 +164,19 @@ export const DateRangePicker1 = ({
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (resetSignal !== undefined) {
|
||||
if (defaultRange) {
|
||||
applyDefaultDates();
|
||||
} else {
|
||||
setValue(startField, "", { shouldValidate: true });
|
||||
setValue(endField, "", { shouldValidate: true });
|
||||
if (resetSignal !== undefined) {
|
||||
if (defaultRange) {
|
||||
applyDefaultDates();
|
||||
} else {
|
||||
setValue(startField, "", { shouldValidate: true });
|
||||
setValue(endField, "", { shouldValidate: true });
|
||||
|
||||
if (inputRef.current?._flatpickr) {
|
||||
inputRef.current._flatpickr.clear();
|
||||
if (inputRef.current?._flatpickr) {
|
||||
inputRef.current._flatpickr.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [resetSignal, defaultRange, setValue, startField, endField]);
|
||||
|
||||
}, [resetSignal, defaultRange, setValue, startField, endField]);
|
||||
|
||||
const start = getValues(startField);
|
||||
const end = getValues(endField);
|
||||
@ -186,7 +188,7 @@ export const DateRangePicker1 = ({
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
placeholder={placeholder}
|
||||
defaultValue={formattedValue}
|
||||
value={formattedValue}
|
||||
ref={(el) => {
|
||||
inputRef.current = el;
|
||||
ref(el);
|
||||
|
@ -16,10 +16,10 @@ import ManageDocumentCategory from "./ManageDocumentCategory";
|
||||
import ManageDocumentType from "./ManageDocumentType";
|
||||
import ManageServices from "./Services/ManageServices";
|
||||
import ServiceGroups from "./Services/ServicesGroups";
|
||||
import ManagePaymentHead from "./paymentAdjustmentHead/ManagePaymentHead";
|
||||
|
||||
const MasterModal = ({ modaldata, closeModal }) => {
|
||||
if (!modaldata?.modalType || modaldata.modalType === "delete") {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -58,24 +58,20 @@ const MasterModal = ({ modaldata, closeModal }) => {
|
||||
"Edit-Document Type": (
|
||||
<ManageDocumentType data={item} onClose={closeModal} />
|
||||
),
|
||||
"Services": (
|
||||
<ManageServices onClose={closeModal} />
|
||||
),
|
||||
"Edit-Services": (
|
||||
<ManageServices data={item} onClose={closeModal} />
|
||||
),
|
||||
"Manage-Services": (
|
||||
<ServiceGroups service={item} onClose={closeModal}/>
|
||||
),
|
||||
Services: <ManageServices onClose={closeModal} />,
|
||||
"Edit-Services": <ManageServices data={item} onClose={closeModal} />,
|
||||
"Manage-Services": <ServiceGroups service={item} onClose={closeModal} />,
|
||||
"Payment Adjustment Head": <ManagePaymentHead onClose={closeModal} />,
|
||||
"Edit-Payment Adjustment Head": <ManagePaymentHead data={item} onClose={closeModal} />
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-2 p-md-1">
|
||||
<div className="text-center">
|
||||
<p className="fs-5 fw-semibold" >{`${masterType, " ", modalType}`}</p>
|
||||
<div className="p-2 p-md-1">
|
||||
<div className="text-center">
|
||||
<p className="fs-5 fw-semibold">{`${(masterType, " ", modalType)}`}</p>
|
||||
</div>
|
||||
{modalComponents[modalType] || null}
|
||||
</div>
|
||||
{ modalComponents[modalType] || null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,107 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import Label from "../../common/Label";
|
||||
import { useCreatePaymentAjustmentHead, useUpdatePaymentAjustmentHead } from "../../../hooks/masterHook/useMaster";
|
||||
|
||||
export const simpleFormSchema = z.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
description: z.string().min(1, "Description is required"),
|
||||
});
|
||||
|
||||
const ManagePaymentHead = ({ data, onClose }) => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: zodResolver(simpleFormSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
description: "",
|
||||
},
|
||||
});
|
||||
|
||||
const {mutate:CreateAjustmentHead,isPending} = useCreatePaymentAjustmentHead(()=>{
|
||||
handleClose?.()
|
||||
});
|
||||
const {mutate:UpdateAjustmentHead,isPending:isUpdating} = useUpdatePaymentAjustmentHead(()=>{
|
||||
handleClose?.()
|
||||
})
|
||||
const onSubmit = (formData) => {
|
||||
if(data){
|
||||
let id = data?.id;
|
||||
const payload = {
|
||||
...formData,
|
||||
id:id,
|
||||
}
|
||||
UpdateAjustmentHead({id:id,payload:payload})
|
||||
}else{
|
||||
let payload={
|
||||
...formData
|
||||
}
|
||||
CreateAjustmentHead(payload)
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset({
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
const handleClose = () => {
|
||||
reset();
|
||||
onClose();
|
||||
};
|
||||
return (
|
||||
<div className="row text-start">
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="p-4">
|
||||
<div className="mb-3">
|
||||
<Label htmlFor="name" required>
|
||||
Name
|
||||
</Label>
|
||||
<input
|
||||
type="text"
|
||||
{...register("name")}
|
||||
className={`form-control ${errors.name ? "is-invalid" : ""}`}
|
||||
/>
|
||||
{errors.name && (
|
||||
<div className="invalid-feedback">{errors.name.message}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<Label htmlFor="description" required>
|
||||
Description
|
||||
</Label>
|
||||
<textarea
|
||||
{...register("description")}
|
||||
className={`form-control ${errors.description ? "is-invalid" : ""}`}
|
||||
/>
|
||||
{errors.description && (
|
||||
<div className="invalid-feedback">{errors.description.message}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="d-flex flex-row justify-content-end gap-2">
|
||||
<button
|
||||
className="btn btn-sm btn-label-secondary"
|
||||
onClick={handleClose} disabled={isUpdating || isPending}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="btn btn-sm btn-primary" disabled={isUpdating || isPending}>
|
||||
{isPending || isUpdating ? "Please Wait" :data ? "Update":"Submit"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManagePaymentHead;
|
@ -10,6 +10,14 @@ import {
|
||||
} from "@tanstack/react-query";
|
||||
import showToast from "../../services/toastService";
|
||||
|
||||
export const usePaymentAjustmentHead = (isActive) => {
|
||||
return useQuery({
|
||||
queryKey: ["paymentType",isActive],
|
||||
queryFn: async () => await MasterRespository.getPaymentAdjustmentHead(isActive),
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
export const useServices = () => {
|
||||
return useQuery({
|
||||
queryKey: ["services"],
|
||||
@ -30,7 +38,6 @@ export const useActivitiesByGroups = (groupId) => {
|
||||
queryFn: async () => await MasterRespository.getActivitesByGroup(groupId),
|
||||
|
||||
enabled: !!groupId,
|
||||
|
||||
});
|
||||
};
|
||||
export const useGlobalServices = () => {
|
||||
@ -296,6 +303,8 @@ const fetchMasterData = async (masterType) => {
|
||||
return (await MasterRespository.getDocumentTypes()).data;
|
||||
case "Document Category":
|
||||
return (await MasterRespository.getDocumentCategories()).data;
|
||||
case "Payment Adjustment Head":
|
||||
return (await MasterRespository.getPaymentAdjustmentHead(true)).data;
|
||||
case "Status":
|
||||
return [
|
||||
{
|
||||
@ -448,8 +457,6 @@ export const useUpdateApplicationRole = (onSuccessCallback) => {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
//-----Create work Category-------------------------------
|
||||
export const useCreateWorkCategory = (onSuccessCallback) => {
|
||||
const queryClient = useQueryClient();
|
||||
@ -703,13 +710,11 @@ export const useCreateService = (onSuccessCallback) => {
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (payload) => {
|
||||
|
||||
const resp = await MasterRespository.createService(payload);
|
||||
|
||||
return resp.data;
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ["masterData", "Services"] });
|
||||
|
||||
showToast(data?.message || "Service added successfully", "success");
|
||||
@ -717,8 +722,12 @@ export const useCreateService = (onSuccessCallback) => {
|
||||
if (onSuccessCallback) onSuccessCallback(data?.data);
|
||||
},
|
||||
onError: (error) => {
|
||||
|
||||
showToast( error?.response?.data?.message || error?.message || "Something went wrong", "error");
|
||||
showToast(
|
||||
error?.response?.data?.message ||
|
||||
error?.message ||
|
||||
"Something went wrong",
|
||||
"error"
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -741,7 +750,12 @@ export const useUpdateService = (onSuccessCallback) => {
|
||||
if (onSuccessCallback) onSuccessCallback(data);
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(error?.response?.data?.message || error?.message || "Something went wrong", "error");
|
||||
showToast(
|
||||
error?.response?.data?.message ||
|
||||
error?.message ||
|
||||
"Something went wrong",
|
||||
"error"
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -759,15 +773,22 @@ export const useCreateActivityGroup = (onSuccessCallback) => {
|
||||
queryKey: ["groups"],
|
||||
});
|
||||
|
||||
showToast( data?.message ||
|
||||
data?.response?.data?.message || "Activity Group created successfully.",
|
||||
showToast(
|
||||
data?.message ||
|
||||
data?.response?.data?.message ||
|
||||
"Activity Group created successfully.",
|
||||
"success"
|
||||
);
|
||||
|
||||
if (onSuccessCallback) onSuccessCallback(data);
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(error?.response?.data?.message || error?.message || "Something went wrong", "error");
|
||||
showToast(
|
||||
error?.response?.data?.message ||
|
||||
error?.message ||
|
||||
"Something went wrong",
|
||||
"error"
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -775,8 +796,8 @@ export const useUpdateActivityGroup = (onSuccessCallback) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({id,payload}) => {
|
||||
const response = await MasterRespository.updateActivityGrop(id,payload);
|
||||
mutationFn: async ({ id, payload }) => {
|
||||
const response = await MasterRespository.updateActivityGrop(id, payload);
|
||||
return response;
|
||||
},
|
||||
onSuccess: (data, variables) => {
|
||||
@ -786,7 +807,8 @@ export const useUpdateActivityGroup = (onSuccessCallback) => {
|
||||
|
||||
showToast(
|
||||
data?.message ||
|
||||
data?.response?.data?.message|| "Activity Group Updated successfully.",
|
||||
data?.response?.data?.message ||
|
||||
"Activity Group Updated successfully.",
|
||||
"success"
|
||||
);
|
||||
|
||||
@ -812,7 +834,12 @@ export const useCreateActivity = (onSuccessCallback) => {
|
||||
if (onSuccessCallback) onSuccessCallback(data);
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(error?.response?.data?.message || error?.message || "Something went wrong", "error");
|
||||
showToast(
|
||||
error?.response?.data?.message ||
|
||||
error?.message ||
|
||||
"Something went wrong",
|
||||
"error"
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -834,7 +861,12 @@ export const useUpdateActivity = (onSuccessCallback) => {
|
||||
if (onSuccessCallback) onSuccessCallback(data);
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(error?.response?.data?.message || error?.message || "Something went wrong", "error");
|
||||
showToast(
|
||||
error?.response?.data?.message ||
|
||||
error?.message ||
|
||||
"Something went wrong",
|
||||
"error"
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -968,7 +1000,52 @@ export const useUpdateDocumentType = (onSuccessCallback) => {
|
||||
},
|
||||
});
|
||||
};
|
||||
// -Delete Master --------
|
||||
// ------------------------------x-x--------x-x------------------------------------
|
||||
|
||||
// ==============================Payment Adjustment Head =============================
|
||||
export const useCreatePaymentAjustmentHead = (onSuccessCallback) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (payload) => {
|
||||
const resp = await MasterRespository.createPaymentAjustmentHead(payload);
|
||||
return resp.data;
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["masterData", "Payment Adjustment Head"],
|
||||
});
|
||||
showToast("Payment Ajustment Head successfully", "success");
|
||||
if (onSuccessCallback) onSuccessCallback(data);
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(error.message || "Something went wrong", "error");
|
||||
},
|
||||
});
|
||||
};
|
||||
export const useUpdatePaymentAjustmentHead = (onSuccessCallback) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ id, payload }) => {
|
||||
const resp = await MasterRespository.updatePaymentAjustmentHead(id, payload);
|
||||
return resp.data;
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["masterData", "Payment Adjustment Head"],
|
||||
});
|
||||
showToast("Payment Ajustment Head Updated successfully", "success");
|
||||
if (onSuccessCallback) onSuccessCallback(data);
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(error.message || "Something went wrong", "error");
|
||||
},
|
||||
});
|
||||
};
|
||||
// ====================x=x====================x=x==================================
|
||||
|
||||
// --------Delete Master --------
|
||||
export const useDeleteMasterItem = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@ -1001,14 +1078,12 @@ export const useDeleteMasterItem = () => {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const useDeleteServiceGroup =()=>{
|
||||
const queryClient = useQueryClient();
|
||||
export const useDeleteServiceGroup = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (id)=>await MasterRespository.deleteActivityGroup(id),
|
||||
onSuccess: ({_,variable}) => {
|
||||
|
||||
mutationFn: async (id) => await MasterRespository.deleteActivityGroup(id),
|
||||
onSuccess: ({ _, variable }) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["groups"] });
|
||||
|
||||
showToast(`Group deleted successfully.`, "success");
|
||||
@ -1022,15 +1097,13 @@ export const useDeleteServiceGroup =()=>{
|
||||
showToast(message, "error");
|
||||
},
|
||||
});
|
||||
}
|
||||
export const useDeleteActivity =()=>{
|
||||
const queryClient = useQueryClient();
|
||||
};
|
||||
export const useDeleteActivity = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (id)=>await MasterRespository.deleteActivity(id),
|
||||
onSuccess: ({_,variable}) => {
|
||||
|
||||
|
||||
mutationFn: async (id) => await MasterRespository.deleteActivity(id),
|
||||
onSuccess: ({ _, variable }) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["activties"] });
|
||||
|
||||
showToast(`Acivity deleted successfully.`, "success");
|
||||
@ -1044,4 +1117,4 @@ export const useDeleteActivity =()=>{
|
||||
showToast(message, "error");
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -32,6 +32,16 @@ export const useModal = (modalType) => {
|
||||
|
||||
return { isOpen, onOpen, onClose, onToggle };
|
||||
};
|
||||
export const useSubscription = (frequency) => {
|
||||
return useQuery({
|
||||
queryKey: ["subscriptionPlans", frequency],
|
||||
queryFn: async () => {
|
||||
debugger
|
||||
const resp = await AuthRepository.getSubscription(frequency);
|
||||
return resp.data;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// -------------------APIHook-------------------------------------
|
||||
|
||||
@ -85,8 +95,8 @@ export const useAuthModal = () => {
|
||||
|
||||
export const useLogout = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const naviget = useNavigate()
|
||||
const dispatch = useDispatch()
|
||||
const naviget = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
@ -99,12 +109,12 @@ export const useLogout = () => {
|
||||
},
|
||||
|
||||
onSuccess: (data) => {
|
||||
queryClient.clear()
|
||||
queryClient.clear();
|
||||
removeSession();
|
||||
dispatch(cacheProfileData(null))
|
||||
dispatch(cacheProfileData(null));
|
||||
|
||||
// window.location.href = "/auth/login";
|
||||
naviget("/auth/login",{replace:true})
|
||||
naviget("/auth/login", { replace: true });
|
||||
if (onSuccessCallBack) onSuccessCallBack();
|
||||
},
|
||||
|
||||
|
152
src/hooks/useCollections.js
Normal file
152
src/hooks/useCollections.js
Normal file
@ -0,0 +1,152 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { CollectionRepository } from "../repositories/ColllectionRepository";
|
||||
import showToast from "../services/toastService";
|
||||
|
||||
export const useCollections = (
|
||||
pageSize,
|
||||
pageNumber,
|
||||
fromDate,
|
||||
toDate,
|
||||
isPending,
|
||||
isActive,
|
||||
projectId,
|
||||
searchString
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: [
|
||||
"collections",
|
||||
pageSize,
|
||||
pageNumber,
|
||||
fromDate,
|
||||
toDate,
|
||||
isPending,
|
||||
isActive,
|
||||
projectId,
|
||||
searchString,
|
||||
],
|
||||
|
||||
queryFn: async () => {
|
||||
const response = await CollectionRepository.getCollections(
|
||||
pageSize,
|
||||
pageNumber,
|
||||
fromDate,
|
||||
toDate,
|
||||
isPending,
|
||||
isActive,
|
||||
projectId,
|
||||
searchString
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
keepPreviousData: true,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const useCollection =(collectionId)=>{
|
||||
return useQuery({
|
||||
queryKey:["collection",collectionId],
|
||||
queryFn:async()=> {
|
||||
const resp = await CollectionRepository.getCollection(collectionId);
|
||||
return resp.data
|
||||
},
|
||||
enabled:!!collectionId
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// =========================Mutation======================
|
||||
|
||||
export const useCreateCollection = (onSuccessCallBack) => {
|
||||
const clinent = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (payload) =>
|
||||
await CollectionRepository.createNewCollection(payload),
|
||||
onSuccess: (_, variables) => {
|
||||
showToast("New Collection created Successfully", "success");
|
||||
clinent.invalidateQueries({ queryKey: ["collections"] });
|
||||
if (onSuccessCallBack) onSuccessCallBack();
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(
|
||||
error.response.data.message || error.message || "Something Went wrong"
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useMarkedPaymentReceived = (onSuccessCallBack) => {
|
||||
const client = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (payload) =>
|
||||
await CollectionRepository.markPaymentReceived(payload),
|
||||
onSuccess: async () => {
|
||||
client.invalidateQueries({ queryKey: ["collections"] });
|
||||
showToast("Payment Received marked Successfully", "success");
|
||||
if (onSuccessCallBack) onSuccessCallBack();
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(
|
||||
error.response.data.message || error.message || "Something Went wrong"
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useAddPayment = (onSuccessCallBack) => {
|
||||
const client = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (payload) => CollectionRepository.makeReceivePayment(payload),
|
||||
onSuccess: () => {
|
||||
client.invalidateQueries({ queryKey: ["collections"] });
|
||||
client.invalidateQueries({ queryKey: ["collection"] });
|
||||
showToast("Payment Received marked Successfully", "success");
|
||||
if (onSuccessCallBack) onSuccessCallBack();
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(
|
||||
error?.response?.data?.message || error.message || "Something Went wrong"
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
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"
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateCollection =(onSuccessCallBack)=>{
|
||||
const client = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn:async({collectionId,payload})=> await CollectionRepository.updateCollection(collectionId,payload),
|
||||
onSuccess: () => {
|
||||
client.invalidateQueries({ queryKey: ["collections"] });
|
||||
client.invalidateQueries({ queryKey: ["collection"] });
|
||||
showToast("Collection Updated Successfully", "success");
|
||||
if (onSuccessCallBack) onSuccessCallBack();
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(
|
||||
error?.response?.data?.message || error.message || "Something Went wrong"
|
||||
);
|
||||
},
|
||||
})
|
||||
}
|
@ -153,7 +153,7 @@ export const useProjectsAllocationByEmployee = (employeeId) => {
|
||||
return { projectList, loading: isLoading, error, refetch };
|
||||
};
|
||||
|
||||
export const useProjectName = () => {
|
||||
export const useProjectName = (provideAll=false) => {
|
||||
const {
|
||||
data = [],
|
||||
isLoading,
|
||||
@ -161,9 +161,9 @@ export const useProjectName = () => {
|
||||
refetch,
|
||||
isError,
|
||||
} = useQuery({
|
||||
queryKey: ["basicProjectNameList"],
|
||||
queryKey: ["basicProjectNameList",provideAll],
|
||||
queryFn: async () => {
|
||||
const res = await ProjectRepository.projectNameList();
|
||||
const res = await ProjectRepository.projectNameList(provideAll);
|
||||
return res.data || res;
|
||||
},
|
||||
onError: (error) => {
|
||||
|
@ -206,8 +206,8 @@ const LandingPage = () => {
|
||||
navigation={false}
|
||||
modules={[EffectFlip, Autoplay, Pagination, Navigation]}
|
||||
className="mySwiper"
|
||||
onSlideChange={() => console.log("slide change")}
|
||||
onSwiper={(swiper) => console.log(swiper)}
|
||||
onSlideChange={() => {}}
|
||||
onSwiper={(swiper) => {}}
|
||||
>
|
||||
<SwiperSlide>
|
||||
<SwaperSlideContent
|
||||
|
@ -2,37 +2,28 @@ import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import { Link } from "react-router-dom";
|
||||
import PlanCardSkeleton from "./PlanCardSkeleton";
|
||||
import { useSubscription } from "../../hooks/useAuth";
|
||||
|
||||
const SubscriptionPlans = () => {
|
||||
const [plans, setPlans] = useState([]);
|
||||
const [frequency, setFrequency] = useState(1);
|
||||
const { data, isLoading, isError, error } = useSubscription(frequency);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPlans = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const res = await axios.get(
|
||||
`http://localhost:5032/api/market/list/subscription-plan?frequency=${frequency}`,
|
||||
{ headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
setPlans(res.data?.data || []);
|
||||
} catch (err) {
|
||||
console.error("Error fetching plans:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchPlans();
|
||||
}, [frequency]);
|
||||
|
||||
|
||||
const frequencyLabel = (freq) => {
|
||||
switch (freq) {
|
||||
case 0: return "1 mo";
|
||||
case 1: return "3 mo";
|
||||
case 2: return "6 mo";
|
||||
case 3: return "1 yr";
|
||||
default: return "mo";
|
||||
case 0:
|
||||
return "1 mo";
|
||||
case 1:
|
||||
return "3 mo";
|
||||
case 2:
|
||||
return "6 mo";
|
||||
case 3:
|
||||
return "1 yr";
|
||||
default:
|
||||
return "mo";
|
||||
}
|
||||
};
|
||||
|
||||
@ -41,38 +32,49 @@ const SubscriptionPlans = () => {
|
||||
{/* Frequency Switcher */}
|
||||
<div className="text-center mb-4">
|
||||
<div className="btn-group" role="group" aria-label="Plan frequency">
|
||||
{["Monthly", "Quarterly", "Half-Yearly", "Yearly"].map((label, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
type="button"
|
||||
className={`btn btn-${frequency === idx ? "primary" : "outline-secondary"}`}
|
||||
onClick={() => setFrequency(idx)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
{["Monthly", "Quarterly", "Half-Yearly", "Yearly"].map(
|
||||
(label, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
type="button"
|
||||
className={`btn btn-${
|
||||
frequency === idx ? "primary" : "outline-secondary"
|
||||
}`}
|
||||
onClick={() => setFrequency(idx)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cards */}
|
||||
<div className="row g-4 mt-10">
|
||||
{loading ? (
|
||||
{isLoading ? (
|
||||
// Show 3 skeletons
|
||||
<>
|
||||
<PlanCardSkeleton />
|
||||
<PlanCardSkeleton />
|
||||
<PlanCardSkeleton />
|
||||
</>
|
||||
) : plans.length === 0 ? (
|
||||
) : data.length === 0 ? (
|
||||
<div className="text-center">No plans found</div>
|
||||
) : isError ? (
|
||||
<div className="text-start bg-light">
|
||||
<p>{error.message}</p>
|
||||
<p>{error.name}</p>
|
||||
</div>
|
||||
) : (
|
||||
plans.map((plan) => (
|
||||
data.map((plan) => (
|
||||
<div key={plan.id} className="col-xl-4 col-lg-6 col-md-6">
|
||||
<div className="card h-100 shadow-lg border-0 p-3 text-center p-10">
|
||||
{/* Header */}
|
||||
<div className="mb-3">
|
||||
<i className="bx bxs-package text-primary fs-1 mb-2"></i>
|
||||
<p className="card-title fs-3 fw-bold mb-1">{plan.planName}</p>
|
||||
<p className="card-title fs-3 fw-bold mb-1">
|
||||
{plan.planName}
|
||||
</p>
|
||||
<p className="text-muted mb-0 fs-5">{plan.description}</p>
|
||||
</div>
|
||||
|
||||
@ -80,7 +82,9 @@ const SubscriptionPlans = () => {
|
||||
<div className="mb-3">
|
||||
<h4 className="fw-semibold mt-auto mb-0 fs-3">
|
||||
{plan.currency?.symbol} {plan.price}
|
||||
<small className="text-muted ms-1">/ {frequencyLabel(frequency)}</small>
|
||||
<small className="text-muted ms-1">
|
||||
/ {frequencyLabel(frequency)}
|
||||
</small>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
@ -133,7 +137,6 @@ const SubscriptionPlans = () => {
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
195
src/pages/collections/CollectionPage.jsx
Normal file
195
src/pages/collections/CollectionPage.jsx
Normal file
@ -0,0 +1,195 @@
|
||||
import React, { createContext, useContext, useState } from "react";
|
||||
import moment from "moment";
|
||||
import Breadcrumb from "../../components/common/Breadcrumb";
|
||||
import CollectionList from "../../components/collections/CollectionList";
|
||||
import { useModal } from "../../hooks/useAuth";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { DateRangePicker1 } from "../../components/common/DateRangePicker";
|
||||
import { isPending } from "@reduxjs/toolkit";
|
||||
import ConfirmModal from "../../components/common/ConfirmModal";
|
||||
import showToast from "../../services/toastService";
|
||||
import { useMarkedPaymentReceived } from "../../hooks/useCollections";
|
||||
import GlobalModel from "../../components/common/GlobalModel";
|
||||
import AddPayment from "../../components/collections/AddPayment";
|
||||
import ViewCollection from "../../components/collections/ViewCollection";
|
||||
import ManageCollection from "../../components/collections/ManageCollection";
|
||||
import PreviewDocument from "../../components/Expenses/PreviewDocument";
|
||||
|
||||
const CollectionContext = createContext();
|
||||
export const useCollectionContext = () => {
|
||||
const context = useContext(CollectionContext);
|
||||
if (!context) {
|
||||
window.location = "/dashboard";
|
||||
showToast("Out of Context Happend inside Collection Context", "warning");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
const CollectionPage = () => {
|
||||
const [viewCollection, setViewCollection] = useState(null);
|
||||
const [makeCollection, setCollection] = useState({
|
||||
isOpen: false,
|
||||
invoiceId: null,
|
||||
});
|
||||
const [ViewDocument, setDocumentView] = useState({
|
||||
IsOpen: false,
|
||||
Image: null,
|
||||
});
|
||||
const [processedPayment, setProcessedPayment] = useState(null);
|
||||
const [addPayment, setAddPayment] = useState({
|
||||
isOpen: false,
|
||||
invoiceId: null,
|
||||
});
|
||||
const [showPending, setShowPending] = useState(false);
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const methods = useForm({
|
||||
defaultValues: {
|
||||
fromDate: moment().subtract(180, "days").format("DD-MM-YYYY"),
|
||||
toDate: moment().format("DD-MM-YYYY"),
|
||||
},
|
||||
});
|
||||
const { watch } = methods;
|
||||
const [fromDate, toDate] = watch(["fromDate", "toDate"]);
|
||||
const handleToggleActive = (e) => setShowPending(e.target.checked);
|
||||
|
||||
const contextMassager = {
|
||||
setProcessedPayment,
|
||||
setCollection,
|
||||
setAddPayment,
|
||||
addPayment,
|
||||
setViewCollection,
|
||||
viewCollection,
|
||||
setDocumentView,
|
||||
};
|
||||
const { mutate: MarkedReceived, isPending } = useMarkedPaymentReceived(() => {
|
||||
setProcessedPayment(null);
|
||||
});
|
||||
const handleMarkedPayment = (payload) => {
|
||||
MarkedReceived(payload);
|
||||
};
|
||||
return (
|
||||
<CollectionContext.Provider value={contextMassager}>
|
||||
<div className="container-fluid">
|
||||
<Breadcrumb
|
||||
data={[{ label: "Home", link: "/" }, { label: "Collection" }]}
|
||||
/>
|
||||
|
||||
<div className="card my-3 py-2 px-sm-4 px-0">
|
||||
<div className="row px-3">
|
||||
<div className="col-12 col-md-3 mb-1">
|
||||
<FormProvider {...methods}>
|
||||
<DateRangePicker1 howManyDay={180} />
|
||||
</FormProvider>
|
||||
</div>
|
||||
<div className="col-12 col-md-3 d-flex align-items-center gap-2 ">
|
||||
<div className="form-check form-switch text-start align-items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
role="switch"
|
||||
id="inactiveEmployeesCheckbox"
|
||||
checked={showPending}
|
||||
onChange={(e) => setShowPending(e.target.checked)}
|
||||
/>
|
||||
<label
|
||||
className="form-check-label ms-0"
|
||||
htmlFor="inactiveEmployeesCheckbox"
|
||||
>
|
||||
Show Pending
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-6 d-flex justify-content-end gap-4">
|
||||
<div className=" w-md-auto">
|
||||
{" "}
|
||||
<input
|
||||
type="search"
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
placeholder="search Collection"
|
||||
className="form-control form-control-sm"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
type="button"
|
||||
onClick={() => setCollection({ isOpen: true, invoiceId: null })}
|
||||
>
|
||||
<i className="bx bx-plus-circle me-2"></i>
|
||||
<span className="d-none d-md-inline-block">
|
||||
Add New Collection
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CollectionList
|
||||
fromDate={fromDate}
|
||||
toDate={toDate}
|
||||
isPending={showPending}
|
||||
searchString={searchText}
|
||||
/>
|
||||
|
||||
{makeCollection.isOpen && (
|
||||
<GlobalModel
|
||||
isOpen={makeCollection.isOpen}
|
||||
size="lg"
|
||||
closeModal={() => setCollection({ isOpen: false, invoiceId: null })}
|
||||
>
|
||||
<ManageCollection
|
||||
collectionId={makeCollection?.invoiceId ?? null}
|
||||
onClose={() => setCollection({ isOpen: false, invoiceId: null })}
|
||||
/>
|
||||
</GlobalModel>
|
||||
)}
|
||||
|
||||
{addPayment.isOpen && (
|
||||
<GlobalModel
|
||||
size="lg"
|
||||
isOpen={addPayment.isOpen}
|
||||
closeModal={() => setAddPayment({ isOpen: false, invoiceId: null })}
|
||||
>
|
||||
<AddPayment
|
||||
onClose={() => setAddPayment({ isOpen: false, invoiceId: null })}
|
||||
/>
|
||||
</GlobalModel>
|
||||
)}
|
||||
|
||||
{viewCollection && (
|
||||
<GlobalModel
|
||||
size="lg"
|
||||
isOpen={viewCollection}
|
||||
closeModal={() => setViewCollection(null)}
|
||||
>
|
||||
<ViewCollection onClose={() => setViewCollection(null)} />
|
||||
</GlobalModel>
|
||||
)}
|
||||
|
||||
{ViewDocument.IsOpen && (
|
||||
<GlobalModel
|
||||
isOpen
|
||||
size="md"
|
||||
key={ViewDocument.Image ?? "doc"}
|
||||
closeModal={() => setDocumentView({ IsOpen: false, Image: null })}
|
||||
>
|
||||
<PreviewDocument imageUrl={ViewDocument.Image} />
|
||||
</GlobalModel>
|
||||
)}
|
||||
|
||||
<ConfirmModal
|
||||
type="success"
|
||||
header="Payment Successful Received"
|
||||
message="Payment has been recored successfully."
|
||||
isOpen={processedPayment?.isOpen}
|
||||
loading={isPending}
|
||||
onSubmit={() => handleMarkedPayment(processedPayment?.invoiceId)}
|
||||
onClose={() => setProcessedPayment(null)}
|
||||
/>
|
||||
</div>
|
||||
</CollectionContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollectionPage;
|
@ -10,6 +10,7 @@ const AuthRepository = {
|
||||
verifyOTP: (data) => api.postPublic("/api/auth/login-otp", data),
|
||||
register: (data) => api.postPublic("/api/auth/register", data),
|
||||
sendMail: (data) => api.postPublic("/api/auth/sendmail", data),
|
||||
getSubscription:(frequency)=> api.getPublic(`/api/market/list/subscription-plan?frequency=${frequency}`),
|
||||
|
||||
// Protected routes (require auth token)
|
||||
logout: (data) => api.post("/api/auth/logout", data),
|
||||
@ -17,7 +18,9 @@ const AuthRepository = {
|
||||
changepassword: (data) => api.post("/api/auth/change-password", data),
|
||||
appmenu: () => api.get('/api/appmenu/get/menu'),
|
||||
selectTenant: (tenantId) => api.post(`/api/Auth/select-tenant/${tenantId}`),
|
||||
getTenantList: () => api.get("/api/Auth/get/user/tenants"),
|
||||
getTenantList: () => api.get("/api/Auth/get/user/tenants"),
|
||||
|
||||
//
|
||||
|
||||
};
|
||||
|
||||
|
29
src/repositories/ColllectionRepository.jsx
Normal file
29
src/repositories/ColllectionRepository.jsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { api } from "../utils/axiosClient";
|
||||
import { DirectoryRepository } from "./DirectoryRepository";
|
||||
|
||||
export const CollectionRepository = {
|
||||
createNewCollection: (data) =>
|
||||
api.post(`/api/Collection/invoice/create`, data),
|
||||
updateCollection:(id,data)=>{
|
||||
api.put(`/api/Collection/invoice/edit/${id}`,data)
|
||||
},
|
||||
getCollections: (pageSize, pageNumber,fromDate,toDate, isPending,isActive,projectId, searchString) => {
|
||||
let url = `/api/Collection/invoice/list?pageSize=${pageSize}&pageNumber=${pageNumber}&isPending=${isPending}&isActive=${isActive}&searchString=${searchString}`;
|
||||
|
||||
const params = [];
|
||||
if (fromDate) params.push(`fromDate=${fromDate}`);
|
||||
if (toDate) params.push(`toDate=${toDate}`);
|
||||
if(projectId) params.push(`projectId=${projectId}`)
|
||||
|
||||
if (params.length > 0) {
|
||||
url += `&${params.join("&")}`;
|
||||
}
|
||||
return api.get(url);
|
||||
},
|
||||
|
||||
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}`),
|
||||
addComment:(data)=>api.post(`/api/Collection/invoice/add/comment`,data)
|
||||
};
|
||||
|
@ -58,6 +58,7 @@ export const MasterRespository = {
|
||||
"Document Type": (id) => api.delete(`/api/Master/document-type/delete/${id}`),
|
||||
"Document Category": (id) =>
|
||||
api.delete(`/api/Master/document-category/delete/${id}`),
|
||||
"Payment Adjustment Head":(id,isActive)=>api.delete(`/api/Master/payment-adjustment-head/delete/${id}`,(isActive=false)),
|
||||
|
||||
getWorkCategory: () => api.get(`/api/master/work-categories`),
|
||||
createWorkCategory: (data) => api.post(`/api/master/work-category`, data),
|
||||
@ -124,10 +125,15 @@ export const MasterRespository = {
|
||||
api.put(`/api/Master/activity-group/edit/${serviceId}`, data),
|
||||
getActivitesByGroup: (activityGroupId) =>
|
||||
api.get(`api/master/activities?activityGroupId=${activityGroupId}`),
|
||||
deleteActivityGroup:(id)=>api.delete(`/api/Master/activity-group/delete/${id}`),
|
||||
deleteActivityGroup: (id) =>
|
||||
api.delete(`/api/Master/activity-group/delete/${id}`),
|
||||
|
||||
|
||||
deleteActivity:(id)=>api.delete(`/api/Master/activity/delete/${id}`),
|
||||
deleteActivity: (id) => api.delete(`/api/Master/activity/delete/${id}`),
|
||||
|
||||
getOrganizationType: () => api.get("/api/Master/organization-type/list"),
|
||||
|
||||
getPaymentAdjustmentHead: (isActive) =>
|
||||
api.get(`/api/Master/payment-adjustment-head/list?isActive=${isActive}`),
|
||||
createPaymentAjustmentHead:(data)=>api.post(`/api/Master/payment-adjustment-head`, data),
|
||||
updatePaymentAjustmentHead:(id,data)=>api.put(`/api/Master/payment-adjustment-head/edit/${id}`, data)
|
||||
};
|
||||
|
@ -40,7 +40,7 @@ const ProjectRepository = {
|
||||
api.get(`/api/project/allocation-histery/${id}`),
|
||||
updateProjectsByEmployee: (id, data) =>
|
||||
api.post(`/api/project/assign-projects/${id}`, data),
|
||||
projectNameList: () => api.get("/api/project/list/basic"),
|
||||
projectNameList: (provideAll) => api.get(`/api/project/list/basic?provideAll=${provideAll}`),
|
||||
|
||||
getProjectDetails: (id) => api.get(`/api/project/details/${id}`),
|
||||
|
||||
|
@ -53,6 +53,7 @@ import DailyProgrssReport from "../pages/DailyProgressReport/DailyProgrssReport"
|
||||
import ProjectPage from "../pages/project/ProjectPage";
|
||||
import { ComingSoonPage } from "../pages/Misc/ComingSoonPage";
|
||||
import ImageGalleryPage from "../pages/Gallary/ImageGallaryPage";
|
||||
import CollectionPage from "../pages/collections/CollectionPage";
|
||||
const router = createBrowserRouter(
|
||||
[
|
||||
{
|
||||
@ -95,6 +96,7 @@ const router = createBrowserRouter(
|
||||
{ path: "/activities/reports", element: <Reports /> },
|
||||
{ path: "/gallary", element: <ImageGalleryPage /> },
|
||||
{ path: "/expenses", element: <ExpensePage /> },
|
||||
{ path: "/collection", element: <CollectionPage /> },
|
||||
{ path: "/masters", element: <MasterPage /> },
|
||||
{ path: "/tenants", element: <TenantPage /> },
|
||||
{ path: "/tenants/new-tenant", element: <CreateTenant /> },
|
||||
|
@ -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,
|
||||
|
@ -72,7 +72,9 @@ axiosClient.interceptors.response.use(
|
||||
if (status === 401 && !isRefreshRequest) {
|
||||
originalRequest._retry = true;
|
||||
|
||||
const refreshToken = localStorage.getItem("refreshToken") || sessionStorage.getItem("refreshToken");
|
||||
const refreshToken =
|
||||
localStorage.getItem("refreshToken") ||
|
||||
sessionStorage.getItem("refreshToken");
|
||||
|
||||
if (
|
||||
!refreshToken ||
|
||||
@ -87,7 +89,9 @@ axiosClient.interceptors.response.use(
|
||||
try {
|
||||
// Refresh token call
|
||||
const res = await axiosClient.post("/api/Auth/refresh-token", {
|
||||
token: localStorage.getItem("jwtToken") || sessionStorage.getItem("jwtToken"),
|
||||
token:
|
||||
localStorage.getItem("jwtToken") ||
|
||||
sessionStorage.getItem("jwtToken"),
|
||||
refreshToken,
|
||||
});
|
||||
|
||||
@ -144,6 +148,11 @@ export const api = {
|
||||
headers: { ...customHeaders },
|
||||
authRequired: false,
|
||||
}),
|
||||
getPublic: (url, data = {}, customHeaders = {}) =>
|
||||
apiRequest("get", url, data, {
|
||||
headers: { ...customHeaders },
|
||||
authRequired: false,
|
||||
}),
|
||||
|
||||
// Authenticated routes
|
||||
get: (url, params = {}, customHeaders = {}) =>
|
||||
|
Loading…
x
Reference in New Issue
Block a user