added collection permission

This commit is contained in:
pramod.mahajan 2025-10-17 10:18:11 +05:30
parent 6e89fbd680
commit 4c059afb72
4 changed files with 278 additions and 193 deletions

View File

@ -1,16 +1,31 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useCollections } from "../../hooks/useCollections"; import { useCollections } from "../../hooks/useCollections";
import { ITEMS_PER_PAGE } from "../../utils/constants"; import {
ADDPAYMENT_COLLECTION,
ADMIN_COLLECTION,
CREATE_COLLECTION,
EDIT_COLLECTION,
ITEMS_PER_PAGE,
VIEW_COLLECTION,
} from "../../utils/constants";
import { formatFigure, localToUtc, useDebounce } from "../../utils/appUtils"; import { formatFigure, localToUtc, useDebounce } from "../../utils/appUtils";
import { formatUTCToLocalTime } from "../../utils/dateUtils"; import { formatUTCToLocalTime } from "../../utils/dateUtils";
import Pagination from "../common/Pagination"; import Pagination from "../common/Pagination";
import { useCollectionContext } from "../../pages/collections/CollectionPage"; import { useCollectionContext } from "../../pages/collections/CollectionPage";
import { CollectionTableSkeleton } from "./CollectionSkeleton"; import { CollectionTableSkeleton } from "./CollectionSkeleton";
import { useSelectedProject } from "../../slices/apiDataManager"; import { useSelectedProject } from "../../slices/apiDataManager";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
const CollectionList = ({ fromDate, toDate, isPending, searchString }) => { const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const selectedProject = useSelectedProject()
const isAdmin = useHasUserPermission(ADMIN_COLLECTION);
const canAddPayment = useHasUserPermission(ADDPAYMENT_COLLECTION);
const canViewCollection = useHasUserPermission(VIEW_COLLECTION);
const canEditCollection = useHasUserPermission(EDIT_COLLECTION);
const canCreate = useHasUserPermission(CREATE_COLLECTION);
const selectedProject = useSelectedProject();
const searchDebounce = useDebounce(searchString, 500); const searchDebounce = useDebounce(searchString, 500);
const { data, isLoading, isError, error } = useCollections( const { data, isLoading, isError, error } = useCollections(
@ -48,7 +63,7 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
}, },
{ {
key: "invoiceId", key: "invoiceId",
label: "Invoice Id", label: "Invoice No",
getValue: (col) => ( getValue: (col) => (
<span <span
className="text-truncate d-inline-block" className="text-truncate d-inline-block"
@ -74,7 +89,7 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
}, },
{ {
key: "submittedDate", key: "submittedDate",
label: "Submitted Date", label: "Submission Date",
getValue: (col) => ( getValue: (col) => (
<span <span
className="text-truncate d-inline-block" className="text-truncate d-inline-block"
@ -100,13 +115,13 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
}, },
{ {
key: "amount", key: "amount",
label: "Amount", label: "Total Amount",
getValue: (col) => ( getValue: (col) => (
<span <span
className="text-truncate d-inline-block" className="text-truncate d-inline-block"
style={{ maxWidth: "200px" }} style={{ maxWidth: "200px" }}
> >
{formatFigure(col?.basicAmount, { {formatFigure(col?.basicAmount + col?.taxAmount, {
type: "currency", type: "currency",
currency: "INR", currency: "INR",
}) ?? 0} }) ?? 0}
@ -150,9 +165,11 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
{col.label} {col.label}
</th> </th>
))} ))}
<th className="sticky-action-column bg-white text-center"> {(isAdmin ||
Action canAddPayment ||
</th> canViewCollection ||
canEditCollection ||
canCreate) && <th>Action</th>}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -164,77 +181,90 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
{col.getValue(row)} {col.getValue(row)}
</td> </td>
))} ))}
<td {(isAdmin || canAddPayment || canViewCollection) && (
className="sticky-action-column text-center" <td
style={{ padding: "12px 8px" }} className="sticky-action-column text-center"
> style={{ padding: "12px 8px" }}
<div className="dropdown z-2"> >
<button <div className="dropdown z-2">
type="button" <button
className="btn btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0" type="button"
data-bs-toggle="dropdown" className="btn btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0"
aria-expanded="false" data-bs-toggle="dropdown"
> aria-expanded="false"
<i >
className="bx bx-dots-vertical-rounded bx-sm text-muted" <i
data-bs-toggle="tooltip" className="bx bx-dots-vertical-rounded bx-sm text-muted"
data-bs-offset="0,8" data-bs-toggle="tooltip"
data-bs-placement="top" data-bs-offset="0,8"
data-bs-custom-class="tooltip-dark" data-bs-placement="top"
title="More Action" data-bs-custom-class="tooltip-dark"
></i> title="More Action"
</button> ></i>
</button>
<ul className="dropdown-menu dropdown-menu-end"> <ul className="dropdown-menu dropdown-menu-end">
{/* View */} {/* View */}
<li> {(isAdmin ||
<a canAddPayment ||
className="dropdown-item cursor-pointer" canViewCollection ||
onClick={() => setViewCollection(row.id)} canEditCollection ||
> canCreate) && (
<i className="bx bx-show me-2 text-primary"></i>
<span>View</span>
</a>
</li>
{!row?.markAsCompleted && (
<>
{/* Add Payment */}
<li> <li>
<a <a
className="dropdown-item cursor-pointer" className="dropdown-item cursor-pointer"
onClick={() => onClick={() => setViewCollection(row.id)}
setAddPayment({
isOpen: true,
invoiceId: row.id,
})
}
> >
<i className="bx bx-wallet me-2 text-warning"></i> <i className="bx bx-show me-2 text-primary"></i>
<span>Add Payment</span> <span>View</span>
</a> </a>
</li> </li>
)}
{/* Mark Payment */} {/* Only if not completed */}
<li> {!row?.markAsCompleted && (
<a <>
className="dropdown-item cursor-pointer" {/* Add Payment */}
onClick={() => {(isAdmin || canAddPayment) && (
setProcessedPayment({ <li>
isOpen: true, <a
invoiceId: row.id, className="dropdown-item cursor-pointer"
}) onClick={() =>
} setAddPayment({
> isOpen: true,
<i className="bx bx-check-circle me-2 text-success"></i> invoiceId: row.id,
<span>Mark Payment</span> })
</a> }
</li> >
</> <i className="bx bx-wallet me-2 text-warning"></i>
)} <span>Add Payment</span>
</ul> </a>
</div> </li>
</td> )}
{/* Mark Payment */}
{isAdmin && (
<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>
)) ))
) : ( ) : (

View File

@ -7,9 +7,13 @@ import Avatar from "../common/Avatar";
import PaymentHistoryTable from "./PaymentHistoryTable"; import PaymentHistoryTable from "./PaymentHistoryTable";
import Comment from "./Comment"; import Comment from "./Comment";
import { CollectionDetailsSkeleton } from "./CollectionSkeleton"; import { CollectionDetailsSkeleton } from "./CollectionSkeleton";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { ADMIN_COLLECTION, EDIT_COLLECTION } from "../../utils/constants";
const ViewCollection = ({ onClose }) => { const ViewCollection = ({ onClose }) => {
const [activeTab, setActiveTab] = useState("payments"); const [activeTab, setActiveTab] = useState("payments");
const isAdmin = useHasUserPermission(ADMIN_COLLECTION);
const canEditCollection = useHasUserPermission(EDIT_COLLECTION);
const { viewCollection, setCollection, setDocumentView } = const { viewCollection, setCollection, setDocumentView } =
useCollectionContext(); useCollectionContext();
const { data, isLoading, isError, error } = useCollection(viewCollection); const { data, isLoading, isError, error } = useCollection(viewCollection);
@ -45,11 +49,12 @@ const ViewCollection = ({ onClose }) => {
> >
{data?.isActive ? "Active" : "Inactive"} {data?.isActive ? "Active" : "Inactive"}
</span> </span>
{!data?.receivedInvoicePayments && ( {(isAdmin || canEditCollection) &&
<span onClick={handleEdit} className="ms-2 cursor-pointer"> !data?.receivedInvoicePayments && (
<i className="bx bx-edit text-primary bx-sm"></i> <span onClick={handleEdit} className="ms-2 cursor-pointer">
</span> <i className="bx bx-edit text-primary bx-sm"></i>
)} </span>
)}
</div> </div>
</div> </div>
<div className="col-md-6"> <div className="col-md-6">
@ -83,7 +88,7 @@ const ViewCollection = ({ onClose }) => {
</div> </div>
<div className="col-md-6"> <div className="col-md-6">
<div className="row mb-4 align-items-end"> <div className="row mb-4 align-items-end">
<div className="col-4 fw-semibold">Client Submitted Date:</div> <div className="col-4 fw-semibold">Client Submission Date:</div>
<div className="col-8"> <div className="col-8">
{formatUTCToLocalTime(data?.clientSubmitedDate)} {formatUTCToLocalTime(data?.clientSubmitedDate)}
</div> </div>

View File

@ -15,6 +15,14 @@ import AddPayment from "../../components/collections/AddPayment";
import ViewCollection from "../../components/collections/ViewCollection"; import ViewCollection from "../../components/collections/ViewCollection";
import ManageCollection from "../../components/collections/ManageCollection"; import ManageCollection from "../../components/collections/ManageCollection";
import PreviewDocument from "../../components/Expenses/PreviewDocument"; import PreviewDocument from "../../components/Expenses/PreviewDocument";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import {
ADDPAYMENT_COLLECTION,
ADMIN_COLLECTION,
CREATE_COLLECTION,
EDIT_COLLECTION,
VIEW_COLLECTION,
} from "../../utils/constants";
const CollectionContext = createContext(); const CollectionContext = createContext();
export const useCollectionContext = () => { export const useCollectionContext = () => {
@ -42,6 +50,11 @@ const CollectionPage = () => {
}); });
const [showPending, setShowPending] = useState(false); const [showPending, setShowPending] = useState(false);
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const isAdmin = useHasUserPermission(ADMIN_COLLECTION);
const canViewCollection = useHasUserPermission(VIEW_COLLECTION);
const canCreate = useHasUserPermission(CREATE_COLLECTION);
const canEditCollection = useHasUserPermission(EDIT_COLLECTION);
const canAddPayment = useHasUserPermission(ADDPAYMENT_COLLECTION);
const methods = useForm({ const methods = useForm({
defaultValues: { defaultValues: {
fromDate: moment().subtract(180, "days").format("DD-MM-YYYY"), fromDate: moment().subtract(180, "days").format("DD-MM-YYYY"),
@ -50,6 +63,7 @@ const CollectionPage = () => {
}); });
const { watch } = methods; const { watch } = methods;
const [fromDate, toDate] = watch(["fromDate", "toDate"]); const [fromDate, toDate] = watch(["fromDate", "toDate"]);
const handleToggleActive = (e) => setShowPending(e.target.checked); const handleToggleActive = (e) => setShowPending(e.target.checked);
const contextMassager = { const contextMassager = {
@ -69,125 +83,153 @@ const CollectionPage = () => {
}; };
return ( return (
<CollectionContext.Provider value={contextMassager}> <CollectionContext.Provider value={contextMassager}>
<div className="container-fluid"> {isAdmin ||
<Breadcrumb canAddPayment ||
data={[{ label: "Home", link: "/" }, { label: "Collection" }]} canEditCollection ||
/> canViewCollection ||
canCreate ? (
<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="card my-3 py-2 px-sm-4 px-0">
<div className="row px-3"> <div className="row px-3">
<div className="col-12 col-md-3 mb-1"> <div className="col-12 col-md-3 mb-1">
<FormProvider {...methods}> <FormProvider {...methods}>
<DateRangePicker1 howManyDay={180} /> <DateRangePicker1 howManyDay={180} />
</FormProvider> </FormProvider>
</div> </div>
<div className="col-12 col-md-3 d-flex align-items-center gap-2 "> <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"> <div className="form-check form-switch text-start align-items-center">
<input <input
type="checkbox" type="checkbox"
className="form-check-input" className="form-check-input"
role="switch" role="switch"
id="inactiveEmployeesCheckbox" id="inactiveEmployeesCheckbox"
checked={showPending} checked={showPending}
onChange={(e) => setShowPending(e.target.checked)} onChange={(e) => setShowPending(e.target.checked)}
/> />
<label <label
className="form-check-label ms-0" className="form-check-label ms-0"
htmlFor="inactiveEmployeesCheckbox" htmlFor="inactiveEmployeesCheckbox"
> >
Show Pending Show Pending
</label> </label>
</div>
</div> </div>
</div>
<div className="col-12 col-md-6 d-flex justify-content-end gap-4"> <div className="col-12 col-md-6 d-flex justify-content-end gap-4">
<div className=" w-md-auto"> <div className=" w-md-auto">
{" "} {" "}
<input <input
type="search" type="search"
value={searchText} value={searchText}
onChange={(e) => setSearchText(e.target.value)} onChange={(e) => setSearchText(e.target.value)}
placeholder="search Collection" placeholder="search Collection"
className="form-control form-control-sm" className="form-control form-control-sm"
/> />
</div>
{isAdmin ||
(isCanCreate && (
<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>
<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> </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> </div>
) : (
<CollectionList <div className="container-fluid">
fromDate={fromDate} <div className="card text-center py-1">
toDate={toDate} <i className="fa-solid fa-triangle-exclamation fs-5" />
isPending={showPending} <p>
searchString={searchText} Access Denied: You don't have permission to perform this action !
/> </p>
</div>
{makeCollection.isOpen && ( </div>
<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> </CollectionContext.Provider>
); );
}; };

View File

@ -3,7 +3,6 @@ export const DURATION_TIME = 10; // minutes
export const ITEMS_PER_PAGE = 20; export const ITEMS_PER_PAGE = 20;
export const OTP_EXPIRY_SECONDS = 300; // OTP time export const OTP_EXPIRY_SECONDS = 300; // OTP time
export const BASE_URL = process.env.VITE_BASE_URL; export const BASE_URL = process.env.VITE_BASE_URL;
// export const BASE_URL = "https://api.marcoaiot.com"; // export const BASE_URL = "https://api.marcoaiot.com";
@ -50,7 +49,7 @@ export const DIRECTORY_ADMIN = "4286a13b-bb40-4879-8c6d-18e9e393beda";
export const DIRECTORY_MANAGER = "62668630-13ce-4f52-a0f0-db38af2230c5"; export const DIRECTORY_MANAGER = "62668630-13ce-4f52-a0f0-db38af2230c5";
export const DIRECTORY_USER = "0f919170-92d4-4337-abd3-49b66fc871bb"; export const DIRECTORY_USER = "0f919170-92d4-4337-abd3-49b66fc871bb";
// ========================Finance=========================================================
// -----------------------Expense---------------------------------------- // -----------------------Expense----------------------------------------
export const VIEW_SELF_EXPENSE = "385be49f-8fde-440e-bdbc-3dffeb8dd116"; export const VIEW_SELF_EXPENSE = "385be49f-8fde-440e-bdbc-3dffeb8dd116";
@ -66,6 +65,16 @@ export const PROCESS_EXPENSE = "ea5a1529-4ee8-4828-80ea-0e23c9d4dd11";
export const EXPENSE_MANAGE = "ea5a1529-4ee8-4828-80ea-0e23c9d4dd11"; export const EXPENSE_MANAGE = "ea5a1529-4ee8-4828-80ea-0e23c9d4dd11";
// --------------------------------Collection----------------------------
export const ADMIN_COLLECTION = "dbf17591-09fe-4c93-9e1a-12db8f5cc5de";
export const VIEW_COLLECTION = "c8d7eea5-4033-4aad-9ebe-76de49896830";
export const CREATE_COLLECTION = "b93141fd-dbd3-4051-8f57-bf25d18e3555";
export const EDIT_COLLECTION = "455187b4-fef1-41f9-b3d0-025d0b6302c3";
export const ADDPAYMENT_COLLECTION = "061d9ccd-85b4-4cb0-be06-2f9f32cebb72";
// ==========================================================================================
export const EXPENSE_REJECTEDBY = [ export const EXPENSE_REJECTEDBY = [
"d1ee5eec-24b6-4364-8673-a8f859c60729", "d1ee5eec-24b6-4364-8673-a8f859c60729",
"965eda62-7907-4963-b4a1-657fb0b2724b", "965eda62-7907-4963-b4a1-657fb0b2724b",
@ -146,4 +155,3 @@ export const PROJECT_STATUS = [
}, },
]; ];
export const DEFAULT_EMPTY_STATUS_ID = "00000000-0000-0000-0000-000000000000"; export const DEFAULT_EMPTY_STATUS_ID = "00000000-0000-0000-0000-000000000000";