Merge pull request 'Project_Branch_Management : New Feature Service Project - Branch Management' (#519) from Project_Branch_Management into main
Reviewed-on: #519 merged
This commit is contained in:
commit
d4582c101a
6
public/assets/vendor/css/core.css
vendored
6
public/assets/vendor/css/core.css
vendored
@ -89,7 +89,7 @@
|
|||||||
);
|
);
|
||||||
--bs-root-font-size: 16px;
|
--bs-root-font-size: 16px;
|
||||||
--bs-body-font-family: var(--bs-font-sans-serif);
|
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||||
--bs-body-font-size: 0.875rem;
|
--bs-body-font-size: 0.85rem;
|
||||||
--bs-body-font-weight: 400;
|
--bs-body-font-weight: 400;
|
||||||
--bs-body-line-height: 1.375;
|
--bs-body-line-height: 1.375;
|
||||||
--bs-body-color: #646e78;
|
--bs-body-color: #646e78;
|
||||||
@ -9060,7 +9060,7 @@ img[data-app-light-img][data-app-dark-img] {
|
|||||||
}
|
}
|
||||||
.table th {
|
.table th {
|
||||||
color: var(--bs-heading-color);
|
color: var(--bs-heading-color);
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8025rem;
|
||||||
letter-spacing: 0.2px;
|
letter-spacing: 0.2px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
@ -20345,7 +20345,7 @@ li:not(:first-child) .dropdown-item,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fs-6 {
|
.fs-6 {
|
||||||
font-size: 0.9375rem !important;
|
font-size: 0.8375rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fs-tiny {
|
.fs-tiny {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
import React, { useEffect, useMemo } from "react";
|
import React, { useEffect, useMemo } from "react";
|
||||||
import { useExpenseTransactions } from "../../hooks/useExpense";
|
import { useExpenseAllTransactionsList, useExpenseTransactions } from "../../hooks/useExpense";
|
||||||
import Error from "../common/Error";
|
import Error from "../common/Error";
|
||||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||||
import Loader, { SpinnerLoader } from "../common/Loader";
|
import Loader, { SpinnerLoader } from "../common/Loader";
|
||||||
@ -11,11 +11,10 @@ import { employee } from "../../data/masters";
|
|||||||
import { useAdvancePaymentContext } from "../../pages/AdvancePayment/AdvancePaymentPage";
|
import { useAdvancePaymentContext } from "../../pages/AdvancePayment/AdvancePaymentPage";
|
||||||
import { formatFigure } from "../../utils/appUtils";
|
import { formatFigure } from "../../utils/appUtils";
|
||||||
|
|
||||||
const AdvancePaymentList = ({ employeeId }) => {
|
const AdvancePaymentList = ({ employeeId, searchString }) => {
|
||||||
const { setBalance } = useAdvancePaymentContext();
|
const { setBalance } = useAdvancePaymentContext();
|
||||||
const { data, isError, isLoading, error, isFetching } =
|
const { data, isError, isLoading, error, isFetching } =
|
||||||
useExpenseTransactions(employeeId, { enabled: !!employeeId });
|
useExpenseTransactions(employeeId, { enabled: !!employeeId });
|
||||||
|
|
||||||
const records = Array.isArray(data) ? data : [];
|
const records = Array.isArray(data) ? data : [];
|
||||||
|
|
||||||
let currentBalance = 0;
|
let currentBalance = 0;
|
||||||
@ -85,7 +84,7 @@ const AdvancePaymentList = ({ employeeId }) => {
|
|||||||
key: "date",
|
key: "date",
|
||||||
label: (
|
label: (
|
||||||
<>
|
<>
|
||||||
Date
|
Date
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
align: "text-start",
|
align: "text-start",
|
||||||
|
|||||||
100
src/components/AdvancePayment/AdvancePaymentList1.jsx
Normal file
100
src/components/AdvancePayment/AdvancePaymentList1.jsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Avatar from "../../components/common/Avatar"; // <-- ADD THIS
|
||||||
|
import { useExpenseAllTransactionsList } from '../../hooks/useExpense';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { formatFigure } from '../../utils/appUtils';
|
||||||
|
|
||||||
|
const AdvancePaymentList1 = ({ searchString }) => {
|
||||||
|
|
||||||
|
const { data, isError, isLoading, error } =
|
||||||
|
useExpenseAllTransactionsList(searchString);
|
||||||
|
|
||||||
|
const rows = data || [];
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
key: "employee",
|
||||||
|
label: "Employee Name",
|
||||||
|
align: "text-start",
|
||||||
|
customRender: (r) => (
|
||||||
|
<div className="d-flex align-items-center gap-2" onClick={() => navigate(`/advance-payment/${r.id}`)}
|
||||||
|
style={{ cursor: "pointer" }}>
|
||||||
|
<Avatar firstName={r.firstName} lastName={r.lastName} />
|
||||||
|
|
||||||
|
<span className="fw-medium">
|
||||||
|
{r.firstName} {r.lastName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "jobRoleName",
|
||||||
|
label: "Job Role",
|
||||||
|
align: "text-start",
|
||||||
|
customRender: (r) => (
|
||||||
|
<span className="fw-semibold">
|
||||||
|
{r.jobRoleName}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "balanceAmount",
|
||||||
|
label: "Balance (₹)",
|
||||||
|
align: "text-end",
|
||||||
|
customRender: (r) => (
|
||||||
|
<span className="fw-semibold fs-6">
|
||||||
|
{formatFigure(r.balanceAmount, {
|
||||||
|
// type: "currency",
|
||||||
|
currency: "INR",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isLoading) return <p className="text-center py-4">Loading...</p>;
|
||||||
|
if (isError) return <p className="text-center py-4 text-danger">{error.message}</p>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card-datatable" id="payment-request-table">
|
||||||
|
<div className="mx-2">
|
||||||
|
<table className="table border-top dataTable text-nowrap align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{columns.map((col) => (
|
||||||
|
<th key={col.key} className={`sorting ${col.align}`}>
|
||||||
|
{col.label}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{rows.length > 0 ? (
|
||||||
|
rows.map((row) => (
|
||||||
|
<tr key={row.id} className="align-middle" style={{ height: "40px" }}>
|
||||||
|
{columns.map((col) => (
|
||||||
|
<td key={col.key} className={`d-table-cell ${col.align} py-3`}>
|
||||||
|
{col.customRender
|
||||||
|
? col.customRender(row)
|
||||||
|
: col.getValue(row)}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={columns.length} className="text-center border-0 py-3">
|
||||||
|
No Employees Found
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AdvancePaymentList1;
|
||||||
@ -2,15 +2,19 @@ import React, { useState } from "react";
|
|||||||
import HorizontalBarChart from "../Charts/HorizontalBarChart";
|
import HorizontalBarChart from "../Charts/HorizontalBarChart";
|
||||||
import { useProjects } from "../../hooks/useProjects";
|
import { useProjects } from "../../hooks/useProjects";
|
||||||
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
||||||
|
import { useProjectCompletionStatus } from "../../hooks/useDashboard_Data";
|
||||||
|
|
||||||
const ProjectCompletionChart = () => {
|
const ProjectCompletionChart = () => {
|
||||||
|
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const { data: projects, isLoading: loading, isError, error } = useProjects(50,currentPage);
|
const {
|
||||||
// Bar chart logic
|
data: projects,
|
||||||
const projectNames = projects?.data?.map((p) => p.name) || [];
|
isLoading: loading,
|
||||||
|
isError,
|
||||||
|
error,
|
||||||
|
} = useProjectCompletionStatus();
|
||||||
|
const projectNames = projects?.map((p) => p.name) || [];
|
||||||
const projectProgress =
|
const projectProgress =
|
||||||
projects?.data?.map((p) => {
|
projects?.map((p) => {
|
||||||
const completed = p.completedWork || 0;
|
const completed = p.completedWork || 0;
|
||||||
const planned = p.plannedWork || 1;
|
const planned = p.plannedWork || 1;
|
||||||
const percent = planned ? (completed / planned) * 100 : 0;
|
const percent = planned ? (completed / planned) * 100 : 0;
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import React, { useEffect, useState } from "react";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { defaultExpense, ExpenseSchema } from "./ExpenseSchema";
|
import { defaultExpense, ExpenseSchema } from "./ExpenseSchema";
|
||||||
import { formatFileSize, localToUtc } from "../../utils/appUtils";
|
import { formatFileSize, localToUtc } from "../../utils/appUtils";
|
||||||
import { useProjectName } from "../../hooks/useProjects";
|
import { useProjectName } from "../../hooks/useProjects";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { changeMaster } from "../../slices/localVariablesSlice";
|
import { changeMaster } from "../../slices/localVariablesSlice";
|
||||||
import useMaster, {
|
import useMaster, {
|
||||||
@ -32,6 +32,11 @@ import Label from "../common/Label";
|
|||||||
import EmployeeSearchInput from "../common/EmployeeSearchInput";
|
import EmployeeSearchInput from "../common/EmployeeSearchInput";
|
||||||
import Filelist from "./Filelist";
|
import Filelist from "./Filelist";
|
||||||
import { DEFAULT_CURRENCY } from "../../utils/constants";
|
import { DEFAULT_CURRENCY } from "../../utils/constants";
|
||||||
|
import SelectEmployeeServerSide, {
|
||||||
|
SelectProjectField,
|
||||||
|
} from "../common/Forms/SelectFieldServerSide";
|
||||||
|
import { useAllocationServiceProjectTeam } from "../../hooks/useServiceProject";
|
||||||
|
import { AppFormController } from "../../hooks/appHooks/useAppForm";
|
||||||
|
|
||||||
const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||||
const {
|
const {
|
||||||
@ -40,6 +45,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
error: ExpenseErrorLoad,
|
error: ExpenseErrorLoad,
|
||||||
} = useExpense(expenseToEdit);
|
} = useExpense(expenseToEdit);
|
||||||
const [expenseCategory, setExpenseCategory] = useState();
|
const [expenseCategory, setExpenseCategory] = useState();
|
||||||
|
const [selectedEmployees, setSelectedEmployees] = useState([]);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const {
|
const {
|
||||||
expenseCategories,
|
expenseCategories,
|
||||||
@ -83,11 +89,11 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
loading: StatusLoadding,
|
loading: StatusLoadding,
|
||||||
error: stausError,
|
error: stausError,
|
||||||
} = useExpenseStatus();
|
} = useExpenseStatus();
|
||||||
const {
|
// const {
|
||||||
data: employees,
|
// data: employees,
|
||||||
isLoading: EmpLoading,
|
// isLoading: EmpLoading,
|
||||||
isError: isEmployeeError,
|
// isError: isEmployeeError,
|
||||||
} = useEmployeesNameByProject(selectedproject);
|
// } = useEmployeesNameByProject(selectedproject);
|
||||||
|
|
||||||
const files = watch("billAttachments");
|
const files = watch("billAttachments");
|
||||||
const onFileChange = async (e) => {
|
const onFileChange = async (e) => {
|
||||||
@ -150,6 +156,14 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { mutate: AllocationTeam, isPending1 } =
|
||||||
|
useAllocationServiceProjectTeam(() => {
|
||||||
|
setSelectedEmployees([]);
|
||||||
|
setSeletingEmp({
|
||||||
|
employee: null,
|
||||||
|
isOpen: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (expenseToEdit && data) {
|
if (expenseToEdit && data) {
|
||||||
reset({
|
reset({
|
||||||
@ -180,7 +194,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
: [],
|
: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [data, reset, employees]);
|
}, [data, reset]);
|
||||||
const { mutate: ExpenseUpdate, isPending } = useUpdateExpense(() =>
|
const { mutate: ExpenseUpdate, isPending } = useUpdateExpense(() =>
|
||||||
handleClose()
|
handleClose()
|
||||||
);
|
);
|
||||||
@ -223,7 +237,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
</h5>
|
</h5>
|
||||||
<form id="expenseForm" onSubmit={handleSubmit(onSubmit)}>
|
<form id="expenseForm" onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="row my-2 text-start">
|
<div className="row my-2 text-start">
|
||||||
<div className="col-md-6">
|
{/* <div className="col-md-6">
|
||||||
<Label className="form-label" required>
|
<Label className="form-label" required>
|
||||||
Select Project
|
Select Project
|
||||||
</Label>
|
</Label>
|
||||||
@ -245,6 +259,23 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
{errors.projectId && (
|
{errors.projectId && (
|
||||||
<small className="danger-text">{errors.projectId.message}</small>
|
<small className="danger-text">{errors.projectId.message}</small>
|
||||||
)}
|
)}
|
||||||
|
</div> */}
|
||||||
|
<div className="col-12 col-md-6 mb-2">
|
||||||
|
<SelectProjectField
|
||||||
|
label="Project"
|
||||||
|
required
|
||||||
|
placeholder="Select Project"
|
||||||
|
value={watch("projectId")}
|
||||||
|
onChange={(val) =>
|
||||||
|
setValue("projectId", val, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldValidate: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{errors.projectId && (
|
||||||
|
<small className="danger-text">{errors.projectId.message}</small>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
@ -307,14 +338,28 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="col-12 col-md-6 text-start">
|
<div className="col-12 col-md-6 text-start">
|
||||||
<Label className="form-label" required>
|
{/* <Label className="form-label" required>
|
||||||
Paid By{" "}
|
Paid By{" "}
|
||||||
</Label>
|
</Label> */}
|
||||||
<EmployeeSearchInput
|
{/* <EmployeeSearchInput
|
||||||
control={control}
|
control={control}
|
||||||
name="paidById"
|
name="paidById"
|
||||||
projectId={null}
|
projectId={null}
|
||||||
forAll={expenseToEdit ? true : false}
|
forAll={true}
|
||||||
|
/> */}
|
||||||
|
|
||||||
|
<AppFormController
|
||||||
|
name="paidById"
|
||||||
|
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<SelectEmployeeServerSide
|
||||||
|
label="Paid By" required
|
||||||
|
value={field.value}
|
||||||
|
onChange={field.onChange}
|
||||||
|
isFullObject={false} // because using ID
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -423,10 +468,9 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
<small className="danger-text">{errors.gstNumber.message}</small>
|
<small className="danger-text">{errors.gstNumber.message}</small>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-md-6 text-start ">
|
<div className="col-md-6 text-start ">
|
||||||
<Label htmlFor="currencyId" className="form-label" required>
|
<Label htmlFor="currencyId" className="form-label" required>
|
||||||
Select Currency
|
Select Currency
|
||||||
</Label>
|
</Label>
|
||||||
@ -452,24 +496,26 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
<small className="danger-text">{errors.currencyId.message}</small>
|
<small className="danger-text">{errors.currencyId.message}</small>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{expenseCategory?.noOfPersonsRequired && (
|
{expenseCategory?.noOfPersonsRequired && (
|
||||||
<div className="col-md-6 text-start">
|
<div className="col-md-6 text-start">
|
||||||
<Label className="form-label" required>No. of Persons</Label>
|
<Label className="form-label" required>
|
||||||
<input
|
No. of Persons
|
||||||
type="number"
|
</Label>
|
||||||
id="noOfPersons"
|
<input
|
||||||
className="form-control form-control-sm"
|
type="number"
|
||||||
{...register("noOfPersons")}
|
id="noOfPersons"
|
||||||
inputMode="numeric"
|
className="form-control form-control-sm"
|
||||||
/>
|
{...register("noOfPersons")}
|
||||||
{errors.noOfPersons && (
|
inputMode="numeric"
|
||||||
<small className="danger-text">
|
/>
|
||||||
{errors.noOfPersons.message}
|
{errors.noOfPersons && (
|
||||||
</small>
|
<small className="danger-text">
|
||||||
)}
|
{errors.noOfPersons.message}
|
||||||
</div>
|
</small>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="row my-2 text-start">
|
<div className="row my-2 text-start">
|
||||||
<div className="col-md-12">
|
<div className="col-md-12">
|
||||||
|
|||||||
@ -1,54 +1,80 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
|
|
||||||
const PreviewDocument = ({ imageUrl }) => {
|
const PreviewDocument = ({ imageUrl }) => {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [rotation, setRotation] = useState(0);
|
const [rotation, setRotation] = useState(0);
|
||||||
|
const [scale, setScale] = useState(1);
|
||||||
|
|
||||||
|
const zoomIn = () => setScale((prev) => Math.min(prev + 0.2, 3));
|
||||||
|
const zoomOut = () => setScale((prev) => Math.max(prev - 0.2, 0.4));
|
||||||
|
const resetAll = () => {
|
||||||
|
setRotation(0);
|
||||||
|
setScale(1);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="d-flex justify-content-start">
|
<div className="d-flex justify-content-start gap-3 mb-2">
|
||||||
<i
|
<i
|
||||||
className="bx bx-rotate-right cursor-pointer"
|
className="bx bx-rotate-right cursor-pointer fs-4"
|
||||||
|
title="Rotate"
|
||||||
onClick={() => setRotation((prev) => prev + 90)}
|
onClick={() => setRotation((prev) => prev + 90)}
|
||||||
></i>
|
></i>
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="position-relative d-flex flex-column justify-content-center align-items-center"
|
|
||||||
style={{ minHeight: "80vh" }}
|
|
||||||
>
|
|
||||||
|
|
||||||
{loading && (
|
|
||||||
<div className="text-secondary text-center mb-2">Loading...</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mb-3 d-flex justify-content-center align-items-center">
|
<i
|
||||||
<img
|
className="bx bx-zoom-in cursor-pointer fs-4"
|
||||||
src={imageUrl}
|
title="Zoom In"
|
||||||
alt="Full View"
|
onClick={zoomIn}
|
||||||
className="img-fluid"
|
></i>
|
||||||
style={{
|
|
||||||
maxHeight: "80vh",
|
<i
|
||||||
objectFit: "contain",
|
className="bx bx-zoom-out cursor-pointer fs-4"
|
||||||
display: loading ? "none" : "block",
|
title="Zoom Out"
|
||||||
transform: `rotate(${rotation}deg)`,
|
onClick={zoomOut}
|
||||||
transition: "transform 0.3s ease",
|
></i>
|
||||||
}}
|
|
||||||
onLoad={() => setLoading(false)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="position-absolute bottom-0 start-0 justify-content-center gap-2">
|
<div
|
||||||
<button
|
className="position-relative d-flex flex-column justify-content-center align-items-center overflow-hidden"
|
||||||
className="btn btn-outline-secondary"
|
style={{ minHeight: "80vh" }}
|
||||||
onClick={() => setRotation(0)}
|
>
|
||||||
title="Reset Rotation"
|
{loading && (
|
||||||
>
|
<div className="text-secondary text-center mb-2">
|
||||||
<i className="bx bx-reset"></i> Reset
|
Loading...
|
||||||
</button>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mb-3 d-flex justify-content-center align-items-center">
|
||||||
|
<img
|
||||||
|
src={imageUrl}
|
||||||
|
alt="Full View"
|
||||||
|
className="img-fluid"
|
||||||
|
style={{
|
||||||
|
maxHeight: "80vh",
|
||||||
|
objectFit: "contain",
|
||||||
|
display: loading ? "none" : "block",
|
||||||
|
transform: `rotate(${rotation}deg) scale(${scale})`,
|
||||||
|
transition: "transform 0.3s ease",
|
||||||
|
cursor: "grab",
|
||||||
|
}}
|
||||||
|
onLoad={() => setLoading(false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="position-absolute bottom-0 start-0 m-2">
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-secondary"
|
||||||
|
onClick={resetAll}
|
||||||
|
>
|
||||||
|
<i className="bx bx-reset"></i> Reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default PreviewDocument;
|
export default PreviewDocument;
|
||||||
|
|||||||
@ -50,8 +50,11 @@ const Header = () => {
|
|||||||
const isRecurringExpense = /^\/recurring-payment$/.test(pathname);
|
const isRecurringExpense = /^\/recurring-payment$/.test(pathname);
|
||||||
const isAdvancePayment = /^\/advance-payment$/.test(pathname);
|
const isAdvancePayment = /^\/advance-payment$/.test(pathname);
|
||||||
const isServiceProjectPage = /^\/service-projects\/[0-9a-fA-F-]{36}$/.test(pathname);
|
const isServiceProjectPage = /^\/service-projects\/[0-9a-fA-F-]{36}$/.test(pathname);
|
||||||
|
const isAdvancePayment1 =
|
||||||
|
/^\/advance-payment(\/[0-9a-fA-F-]{36})?$/.test(pathname);
|
||||||
|
|
||||||
return !(isDirectoryPath || isProfilePage || isExpensePage || isPaymentRequest || isRecurringExpense || isAdvancePayment ||isServiceProjectPage);
|
|
||||||
|
return !(isDirectoryPath || isProfilePage || isExpensePage || isPaymentRequest || isRecurringExpense || isAdvancePayment ||isServiceProjectPage || isAdvancePayment1);
|
||||||
};
|
};
|
||||||
const allowedProjectStatusIds = [
|
const allowedProjectStatusIds = [
|
||||||
"603e994b-a27f-4e5d-a251-f3d69b0498ba",
|
"603e994b-a27f-4e5d-a251-f3d69b0498ba",
|
||||||
|
|||||||
@ -25,8 +25,7 @@ const Sidebar = () => {
|
|||||||
/>
|
/>
|
||||||
</span> */}
|
</span> */}
|
||||||
|
|
||||||
<a
|
<small
|
||||||
href="/"
|
|
||||||
className="app-brand-link fw-bold navbar-brand text-green fs-6"
|
className="app-brand-link fw-bold navbar-brand text-green fs-6"
|
||||||
>
|
>
|
||||||
<span className="app-brand-logo demo">
|
<span className="app-brand-logo demo">
|
||||||
@ -35,12 +34,13 @@ const Sidebar = () => {
|
|||||||
<span className="text-blue">OnField</span>
|
<span className="text-blue">OnField</span>
|
||||||
<span>Work</span>
|
<span>Work</span>
|
||||||
<span className="text-dark">.com</span>
|
<span className="text-dark">.com</span>
|
||||||
</a>
|
</small>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<a className="layout-menu-toggle menu-link text-large ms-auto">
|
<small className="layout-menu-toggle menu-link text-large ms-auto">
|
||||||
|
|
||||||
<i className="bx bx-chevron-left bx-sm d-flex align-items-center justify-content-center"></i>
|
<i className="bx bx-chevron-left bx-sm d-flex align-items-center justify-content-center"></i>
|
||||||
</a>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="menu-inner-shadow"></div>
|
<div className="menu-inner-shadow"></div>
|
||||||
|
|||||||
@ -40,7 +40,6 @@ const ActionPaymentRequest = ({ requestId }) => {
|
|||||||
error: PaymentModeError,
|
error: PaymentModeError,
|
||||||
} = usePaymentMode();
|
} = usePaymentMode();
|
||||||
|
|
||||||
console.log("Kartik", data)
|
|
||||||
|
|
||||||
const IsReview = useHasUserPermission(REVIEW_EXPENSE);
|
const IsReview = useHasUserPermission(REVIEW_EXPENSE);
|
||||||
const [imageLoaded, setImageLoaded] = useState({});
|
const [imageLoaded, setImageLoaded] = useState({});
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import Filelist from "../Expenses/Filelist";
|
|||||||
import InputSuggestions from "../common/InputSuggestion";
|
import InputSuggestions from "../common/InputSuggestion";
|
||||||
import { useProfile } from "../../hooks/useProfile";
|
import { useProfile } from "../../hooks/useProfile";
|
||||||
import { blockUI } from "../../utils/blockUI";
|
import { blockUI } from "../../utils/blockUI";
|
||||||
|
import { SelectProjectField } from "../common/Forms/SelectFieldServerSide";
|
||||||
|
|
||||||
function ManagePaymentRequest({ closeModal, requestToEdit = null }) {
|
function ManagePaymentRequest({ closeModal, requestToEdit = null }) {
|
||||||
const {
|
const {
|
||||||
@ -234,10 +235,10 @@ function ManagePaymentRequest({ closeModal, requestToEdit = null }) {
|
|||||||
{/* Project and Category */}
|
{/* Project and Category */}
|
||||||
<div className="row my-2 text-start">
|
<div className="row my-2 text-start">
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<Label className="form-label" required>
|
{/* <Label className="form-label" required>
|
||||||
Select Project
|
Select Project
|
||||||
</Label>
|
</Label> */}
|
||||||
<select
|
{/* <select
|
||||||
className="form-select form-select-sm"
|
className="form-select form-select-sm"
|
||||||
{...register("projectId")}
|
{...register("projectId")}
|
||||||
disabled={
|
disabled={
|
||||||
@ -254,7 +255,23 @@ function ManagePaymentRequest({ closeModal, requestToEdit = null }) {
|
|||||||
</option>
|
</option>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</select>
|
</select> */}
|
||||||
|
<SelectProjectField
|
||||||
|
label="Project"
|
||||||
|
required
|
||||||
|
placeholder="Select Project"
|
||||||
|
value={watch("projectId")}
|
||||||
|
onChange={(val) =>
|
||||||
|
setValue("projectId", val, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldValidate: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
disabled={
|
||||||
|
data?.recurringPayment?.isVariable && !isDraft && !isProcessed
|
||||||
|
}
|
||||||
|
/>
|
||||||
{errors.projectId && (
|
{errors.projectId && (
|
||||||
<small className="danger-text">{errors.projectId.message}</small>
|
<small className="danger-text">{errors.projectId.message}</small>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -85,7 +85,7 @@ const PaymentRequestList = ({ filters, filterData, removeFilterChip, clearFilter
|
|||||||
key: "paymentRequestUID",
|
key: "paymentRequestUID",
|
||||||
label: "Request ID",
|
label: "Request ID",
|
||||||
align: "text-start mx-2",
|
align: "text-start mx-2",
|
||||||
getValue: (e) => e.paymentRequestUID || "N/A",
|
getValue: (e) => <div className="d-flex"><span>{e.paymentRequestUID || "N/A"}</span> {e.isAdvancePayment && <span class="ms-1 badge bg-label-warning text-xxs" >Adv</span>}</div>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "title",
|
key: "title",
|
||||||
|
|||||||
@ -148,7 +148,7 @@ const ViewPaymentRequest = ({ requestId }) => {
|
|||||||
<div className="col-12 col-sm-6 ">
|
<div className="col-12 col-sm-6 ">
|
||||||
<div className="row ">
|
<div className="row ">
|
||||||
<div className="col-12 d-flex justify-content-between mb-6">
|
<div className="col-12 d-flex justify-content-between mb-6">
|
||||||
<div className="d-flex align-items-center"><span className="fw-semibold">PR No : </span><span className="fw-semibold ms-2"> {data?.paymentRequestUID}</span></div>
|
<div className="d-flex align-items-center"><span className="fw-semibold">PR No : </span><span className="fw-semibold ms-2"> {data?.paymentRequestUID}</span> {data.isAdvancePayment && <span class="ms-1 badge bg-label-warning text-xs" >Advance</span>}</div>
|
||||||
<span
|
<span
|
||||||
className={`badge bg-label-${getColorNameFromHex(data?.expenseStatus?.color) || "secondary"
|
className={`badge bg-label-${getColorNameFromHex(data?.expenseStatus?.color) || "secondary"
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
@ -82,7 +82,6 @@ const EditActivityModal = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!workItem) return;
|
if (!workItem) return;
|
||||||
console.log(workItem)
|
|
||||||
reset({
|
reset({
|
||||||
activityID: String(
|
activityID: String(
|
||||||
workItem?.workItem?.activityId || workItem?.activityMaster?.id
|
workItem?.workItem?.activityId || workItem?.activityMaster?.id
|
||||||
|
|||||||
@ -8,7 +8,12 @@ const ProjectCardView = ({ data, currentPage, totalPages, paginate }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="row page-min-h">
|
<div className="row page-min-h">
|
||||||
{data?.length === 0 && (
|
{data?.length === 0 && (
|
||||||
<p className="text-center text-muted">No projects found.</p>
|
<div
|
||||||
|
className="col-12 d-flex justify-content-center align-items-center"
|
||||||
|
style={{ minHeight: "250px" }}
|
||||||
|
>
|
||||||
|
<p className="text-center text-muted m-0">No Infra projects found.</p>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{data?.map((project) => (
|
{data?.map((project) => (
|
||||||
|
|||||||
@ -126,8 +126,8 @@ const ProjectListView = ({ data, currentPage, totalPages, paginate }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card page-min-h py-4 px-6 shadow-sm">
|
<div className="card page-min-h py-4 px-6 shadow-sm">
|
||||||
<div className="table-responsive text-nowrap page-min-h">
|
<div className="table-responsive text-nowrap">
|
||||||
<table className="table table-hover align-middle m-0">
|
<table className="table table-hover align-middle m-0">
|
||||||
<thead className="border-bottom ">
|
<thead className="border-bottom ">
|
||||||
<tr>
|
<tr>
|
||||||
{projectColumns.map((col) => (
|
{projectColumns.map((col) => (
|
||||||
@ -143,77 +143,94 @@ const ProjectListView = ({ data, currentPage, totalPages, paginate }) => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{data?.map((project) => (
|
{data?.length > 0 ? (
|
||||||
<tr key={project.id}>
|
data?.map((project) => (
|
||||||
{projectColumns.map((col) => (
|
<tr key={project.id}>
|
||||||
<td
|
{projectColumns.map((col) => (
|
||||||
key={col.key}
|
<td
|
||||||
colSpan={col.colSpan}
|
key={col.key}
|
||||||
className={`${col.className} py-5`}
|
colSpan={col.colSpan}
|
||||||
style={{ paddingTop: "20px", paddingBottom: "20px" }}
|
className={`${col.className} py-5`}
|
||||||
>
|
style={{ paddingTop: "20px", paddingBottom: "20px" }}
|
||||||
{col.getValue
|
|
||||||
? col.getValue(project)
|
|
||||||
: project[col.key] || "N/A"}
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
<td
|
|
||||||
className={`mx-2 ${
|
|
||||||
canManageProject ? "d-sm-table-cell" : "d-none"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<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
|
{col.getValue
|
||||||
className="bx bx-dots-vertical-rounded bx-sm text-muted"
|
? col.getValue(project)
|
||||||
data-bs-toggle="tooltip"
|
: project[col.key] || "N/A"}
|
||||||
data-bs-offset="0,8"
|
</td>
|
||||||
data-bs-placement="top"
|
))}
|
||||||
data-bs-custom-class="tooltip-dark"
|
<td
|
||||||
title="More Action"
|
className={`mx-2 ${canManageProject ? "d-sm-table-cell" : "d-none"
|
||||||
></i>
|
}`}
|
||||||
</button>
|
>
|
||||||
<ul className="dropdown-menu dropdown-menu-end">
|
<div className="dropdown z-2">
|
||||||
<li>
|
<button
|
||||||
<a
|
type="button"
|
||||||
aria-label="click to View details"
|
className="btn btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0"
|
||||||
className="dropdown-item cursor-pointer"
|
data-bs-toggle="dropdown"
|
||||||
>
|
aria-expanded="false"
|
||||||
<i className="bx bx-detail me-2"></i>
|
>
|
||||||
<span className="align-left">View details</span>
|
<i
|
||||||
</a>
|
className="bx bx-dots-vertical-rounded bx-sm text-muted"
|
||||||
</li>
|
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">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
aria-label="click to View details"
|
||||||
|
className="dropdown-item cursor-pointer"
|
||||||
|
>
|
||||||
|
<i className="bx bx-detail me-2"></i>
|
||||||
|
<span className="align-left">View details</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
className="dropdown-item cursor-pointer"
|
className="dropdown-item cursor-pointer"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setMangeProject({
|
setMangeProject({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
Project: project.id,
|
Project: project.id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<i className="bx bx-pencil me-2"></i>
|
<i className="bx bx-pencil me-2"></i>
|
||||||
<span className="align-left">Modify</span>
|
<span className="align-left">Modify</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li onClick={() => handleViewActivities(project.id)}>
|
<li onClick={() => handleViewActivities(project.id)}>
|
||||||
<a className="dropdown-item cursor-pointer">
|
<a className="dropdown-item cursor-pointer">
|
||||||
<i className="bx bx-task me-2"></i>
|
<i className="bx bx-task me-2"></i>
|
||||||
<span className="align-left">Activities</span>
|
<span className="align-left">Activities</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<tr
|
||||||
|
className="no-hover"
|
||||||
|
style={{
|
||||||
|
pointerEvents: "none",
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
colSpan={projectColumns.length + 1}
|
||||||
|
className="text-center align-middle"
|
||||||
|
style={{ height: "300px", borderBottom: "none" }}
|
||||||
|
>
|
||||||
|
No Infra projects available
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import InputSuggestions from "../common/InputSuggestion";
|
|||||||
import { useEmployeesName } from "../../hooks/useEmployees";
|
import { useEmployeesName } from "../../hooks/useEmployees";
|
||||||
import PmsEmployeeInputTag from "../common/PmsEmployeeInputTag";
|
import PmsEmployeeInputTag from "../common/PmsEmployeeInputTag";
|
||||||
import HoverPopup from "../common/HoverPopup";
|
import HoverPopup from "../common/HoverPopup";
|
||||||
|
import { SelectProjectField } from "../common/Forms/SelectFieldServerSide";
|
||||||
|
|
||||||
const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
|
const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
|
||||||
const {
|
const {
|
||||||
@ -131,7 +132,7 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
|
|||||||
}
|
}
|
||||||
}, [currencyData, requestToEdit, setValue]);
|
}, [currencyData, requestToEdit, setValue]);
|
||||||
|
|
||||||
const StrikeDate = watch("strikeDate")
|
const StrikeDate = watch("strikeDate");
|
||||||
|
|
||||||
const onSubmit = (fromdata) => {
|
const onSubmit = (fromdata) => {
|
||||||
let payload = {
|
let payload = {
|
||||||
@ -163,10 +164,7 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
|
|||||||
{/* Project and Category */}
|
{/* Project and Category */}
|
||||||
<div className="row my-2 text-start">
|
<div className="row my-2 text-start">
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<Label className="form-label" required>
|
{/* <select
|
||||||
Select Project
|
|
||||||
</Label>
|
|
||||||
<select
|
|
||||||
className="form-select form-select-sm"
|
className="form-select form-select-sm"
|
||||||
{...register("projectId")}
|
{...register("projectId")}
|
||||||
>
|
>
|
||||||
@ -180,7 +178,19 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
|
|||||||
</option>
|
</option>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</select>
|
</select> */}
|
||||||
|
<SelectProjectField
|
||||||
|
label="Select Project"
|
||||||
|
required
|
||||||
|
placeholder="Select Project"
|
||||||
|
value={watch("projectId")}
|
||||||
|
onChange={(val) =>
|
||||||
|
setValue("projectId", val, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldValidate: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
{errors.projectId && (
|
{errors.projectId && (
|
||||||
<small className="danger-text">{errors.projectId.message}</small>
|
<small className="danger-text">{errors.projectId.message}</small>
|
||||||
)}
|
)}
|
||||||
@ -235,7 +245,7 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-md-6 mt-2">
|
<div className="col-md-6 mt-2">
|
||||||
<div className="d-flex justify-content-start align-items-center gap-2">
|
<div className="d-flex justify-content-start align-items-center text-nowrap gap-2">
|
||||||
<Label htmlFor="isVariable" className="form-label mb-0" required>
|
<Label htmlFor="isVariable" className="form-label mb-0" required>
|
||||||
Payment Type
|
Payment Type
|
||||||
</Label>
|
</Label>
|
||||||
@ -243,13 +253,16 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
|
|||||||
title="Payment Type"
|
title="Payment Type"
|
||||||
id="payment_type"
|
id="payment_type"
|
||||||
content={
|
content={
|
||||||
<p>
|
<div className=" w-50">
|
||||||
Choose whether the payment amount varies or remains fixed each cycle.
|
<p>
|
||||||
<br />
|
Choose whether the payment amount varies or remains fixed
|
||||||
<strong>Is Variable:</strong> Amount changes per cycle.
|
each cycle.
|
||||||
<br />
|
<br />
|
||||||
<strong>Fixed:</strong> Amount stays constant.
|
<strong>Is Variable:</strong> Amount changes per cycle.
|
||||||
</p>
|
<br />
|
||||||
|
<strong>Fixed:</strong> Amount stays constant.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<i className="bx bx-info-circle bx-sm text-muted cursor-pointer"></i>
|
<i className="bx bx-info-circle bx-sm text-muted cursor-pointer"></i>
|
||||||
@ -270,7 +283,10 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
|
|||||||
checked={field.value === true}
|
checked={field.value === true}
|
||||||
onChange={() => field.onChange(true)}
|
onChange={() => field.onChange(true)}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="isVariableTrue" className="form-check-label">
|
<Label
|
||||||
|
htmlFor="isVariableTrue"
|
||||||
|
className="form-check-label"
|
||||||
|
>
|
||||||
Is Variable
|
Is Variable
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
@ -283,7 +299,10 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
|
|||||||
checked={field.value === false}
|
checked={field.value === false}
|
||||||
onChange={() => field.onChange(false)}
|
onChange={() => field.onChange(false)}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="isVariableFalse" className="form-check-label">
|
<Label
|
||||||
|
htmlFor="isVariableFalse"
|
||||||
|
className="form-check-label"
|
||||||
|
>
|
||||||
Fixed
|
Fixed
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
@ -295,7 +314,6 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
|
|||||||
<small className="danger-text">{errors.isVariable.message}</small>
|
<small className="danger-text">{errors.isVariable.message}</small>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Date and Amount */}
|
{/* Date and Amount */}
|
||||||
@ -391,11 +409,12 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
|
|||||||
id="frequency"
|
id="frequency"
|
||||||
content={
|
content={
|
||||||
<p>
|
<p>
|
||||||
Defines how often payments or billing occur, such as monthly, quarterly, or annually.
|
Defines how often payments or billing occur, such as
|
||||||
|
monthly, quarterly, or annually.
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<i className="bx bx-info-circle bx-sm text-muted cursor-pointer"></i>
|
<i className="bx bx-info-circle bx-xs text-muted cursor-pointer"></i>
|
||||||
</HoverPopup>
|
</HoverPopup>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -444,10 +463,13 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
|
|||||||
|
|
||||||
{/* Payment Buffer Days and End Date */}
|
{/* Payment Buffer Days and End Date */}
|
||||||
<div className="row my-2 text-start">
|
<div className="row my-2 text-start">
|
||||||
|
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<div className="d-flex justify-content-start align-items-center gap-2">
|
<div className="d-flex justify-content-start align-items-center text-nowrap gap-2">
|
||||||
<Label htmlFor="paymentBufferDays" className="form-label mb-0" required>
|
<Label
|
||||||
|
htmlFor="paymentBufferDays"
|
||||||
|
className="form-label mb-0 "
|
||||||
|
required
|
||||||
|
>
|
||||||
Payment Buffer Days
|
Payment Buffer Days
|
||||||
</Label>
|
</Label>
|
||||||
<HoverPopup
|
<HoverPopup
|
||||||
@ -455,11 +477,12 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
|
|||||||
id="payment_buffer_days"
|
id="payment_buffer_days"
|
||||||
content={
|
content={
|
||||||
<p>
|
<p>
|
||||||
Number of extra days allowed after the due date before payment is considered late.
|
Number of extra days allowed after the due date before
|
||||||
|
payment is considered late.
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<i className="bx bx-info-circle bx-sm text-muted cursor-pointer"></i>
|
<i className="bx bx-info-circle bx-xs text-muted cursor-pointer"></i>
|
||||||
</HoverPopup>
|
</HoverPopup>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -480,9 +503,8 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<div className="d-flex justify-content-start align-items-center gap-2">
|
<div className="d-flex justify-content-start align-items-center text-nowrap gap-2">
|
||||||
<Label htmlFor="endDate" className="form-label mb-0" required>
|
<Label htmlFor="endDate" className="form-label mb-0" required>
|
||||||
End Date
|
End Date
|
||||||
</Label>
|
</Label>
|
||||||
@ -495,7 +517,7 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
|
|||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<i className="bx bx-info-circle bx-sm text-muted cursor-pointer"></i>
|
<i className="bx bx-info-circle bx-xs text-muted cursor-pointer"></i>
|
||||||
</HoverPopup>
|
</HoverPopup>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -510,10 +532,8 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
|
|||||||
<small className="danger-text">{errors.endDate.message}</small>
|
<small className="danger-text">{errors.endDate.message}</small>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div className="row my-2 text-start">
|
<div className="row my-2 text-start">
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<Label htmlFor="notifyTo" className="form-label" required>
|
<Label htmlFor="notifyTo" className="form-label" required>
|
||||||
@ -572,8 +592,8 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
|
|||||||
{createPending || isPending
|
{createPending || isPending
|
||||||
? "Please wait...."
|
? "Please wait...."
|
||||||
: requestToEdit
|
: requestToEdit
|
||||||
? "Update"
|
? "Update"
|
||||||
: "Save as Draft"}
|
: "Save as Draft"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -166,7 +166,7 @@ const RecurringExpenseList = ({ search, filterStatuses }) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
console.log("Tanish",filteredData)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{IsDeleteModalOpen && (
|
{IsDeleteModalOpen && (
|
||||||
|
|||||||
@ -4,24 +4,49 @@ import {
|
|||||||
getJobStatusBadge,
|
getJobStatusBadge,
|
||||||
getNextBadgeColor,
|
getNextBadgeColor,
|
||||||
} from "../../utils/appUtils";
|
} from "../../utils/appUtils";
|
||||||
import { useServiceProjectJobs } from "../../hooks/useServiceProject";
|
import { useServiceProjectJobs, useUpdateServiceProjectJob } from "../../hooks/useServiceProject";
|
||||||
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
import { ITEMS_PER_PAGE, JOBS_STATUS_IDS } from "../../utils/constants";
|
||||||
import EmployeeAvatarGroup from "../common/EmployeeAvatarGroup";
|
import EmployeeAvatarGroup from "../common/EmployeeAvatarGroup";
|
||||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||||
import { SpinnerLoader } from "../common/Loader";
|
import { SpinnerLoader } from "../common/Loader";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import ProjectPage from "../../pages/project/ProjectPage";
|
import ProjectPage from "../../pages/project/ProjectPage";
|
||||||
import { useServiceProjectJobContext } from "./Jobs";
|
import { useServiceProjectJobContext } from "./Jobs";
|
||||||
|
import ConfirmModal from "../common/ConfirmModal";
|
||||||
|
|
||||||
const JobList = () => {
|
const JobList = ({ isArchive }) => {
|
||||||
const { setSelectedJob, setManageJob } = useServiceProjectJobContext();
|
const { setSelectedJob, setManageJob } = useServiceProjectJobContext();
|
||||||
|
const { mutate: UpdateJob,isPending } = useUpdateServiceProjectJob(() => {
|
||||||
|
});
|
||||||
const { projectId } = useParams();
|
const { projectId } = useParams();
|
||||||
const { data, isLoading, isError, error } = useServiceProjectJobs(
|
const { data, isLoading, isError, error } = useServiceProjectJobs(
|
||||||
ITEMS_PER_PAGE,
|
ITEMS_PER_PAGE,
|
||||||
1,
|
1,
|
||||||
true,
|
true,
|
||||||
projectId
|
projectId,
|
||||||
|
isArchive
|
||||||
);
|
);
|
||||||
|
const [isArchiveModalOpen, setIsArchiveModalOpen] = useState(false);
|
||||||
|
const [archiveJobId, setArchiveJobId] = useState(null);
|
||||||
|
const [isArchiveAction, setIsArchiveAction] = useState(true);
|
||||||
|
|
||||||
|
const handleArchive = () => {
|
||||||
|
const payload = [
|
||||||
|
{
|
||||||
|
op: "replace",
|
||||||
|
path: "/isArchive",
|
||||||
|
value: isArchiveAction,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
UpdateJob({
|
||||||
|
id: archiveJobId,
|
||||||
|
payload,
|
||||||
|
isArchiveAction,
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsArchiveModalOpen(false);
|
||||||
|
setArchiveJobId(null);
|
||||||
|
};
|
||||||
|
|
||||||
const jobGrid = [
|
const jobGrid = [
|
||||||
{
|
{
|
||||||
@ -35,12 +60,17 @@ const JobList = () => {
|
|||||||
label: "Title",
|
label: "Title",
|
||||||
getValue: (e) => (
|
getValue: (e) => (
|
||||||
<span
|
<span
|
||||||
className="fw-semibold text-truncate d-inline-block cursor-pointer"
|
className={`fw-semibold text-truncate d-inline-block ${!isArchive ? "cursor-pointer" : ""}`}
|
||||||
style={{
|
style={{
|
||||||
maxWidth: "100%",
|
maxWidth: "100%",
|
||||||
width: "210px",
|
width: "210px",
|
||||||
}}
|
}}
|
||||||
onClick={() => setSelectedJob({ showCanvas: true, job: e?.id })}
|
onClick={() => {
|
||||||
|
if (!isArchive) {
|
||||||
|
setSelectedJob({ showCanvas: true, job: e?.id });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title={e?.title}
|
||||||
>
|
>
|
||||||
{e?.title}
|
{e?.title}
|
||||||
</span>
|
</span>
|
||||||
@ -49,6 +79,7 @@ const JobList = () => {
|
|||||||
className: "text-start",
|
className: "text-start",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
key: "dueDate",
|
key: "dueDate",
|
||||||
label: "Due On",
|
label: "Due On",
|
||||||
@ -86,92 +117,155 @@ const JobList = () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const canArchive = (statusId) => {
|
||||||
|
const closedId = JOBS_STATUS_IDS.find((s) => s.label === "Closed")?.id;
|
||||||
|
const reviewDoneId = JOBS_STATUS_IDS.find((s) => s.label === "Review Done")?.id;
|
||||||
|
|
||||||
|
return statusId === closedId || statusId === reviewDoneId;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dataTables_wrapper dt-bootstrap5 no-footer table-responsive">
|
<>
|
||||||
<table
|
{isArchiveModalOpen && (
|
||||||
className="datatables-users table border-top dataTable no-footer dtr-column text-nowrap table-responsive"
|
<ConfirmModal
|
||||||
aria-describedby="DataTables_Table_0_info"
|
isOpen={isArchiveModalOpen}
|
||||||
>
|
type={isArchiveAction ? "archive" : "Un-archive"}
|
||||||
<thead>
|
header={isArchiveAction ? "Archive Job" : "Restore Job"}
|
||||||
<tr>
|
message={
|
||||||
{jobGrid.map((col) => (
|
isArchiveAction
|
||||||
<th
|
? "Are you sure you want to archive this job?"
|
||||||
key={col.key}
|
: "Are you sure you want to restore this job?"
|
||||||
className={`${col.align || "text-center"} ${
|
}
|
||||||
col.className || ""
|
onSubmit={handleArchive}
|
||||||
}`}
|
onClose={() => setIsArchiveModalOpen(false)}
|
||||||
scope="col"
|
loading={isPending}
|
||||||
>
|
/>
|
||||||
<div className={col.className}>{col.label}</div>
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
<div className="dataTables_wrapper dt-bootstrap5 no-footer table-responsive">
|
||||||
|
<table
|
||||||
|
className="datatables-users table border-top dataTable no-footer dtr-column text-nowrap table-responsive"
|
||||||
|
aria-describedby="DataTables_Table_0_info"
|
||||||
|
>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{jobGrid.map((col) => (
|
||||||
|
<th
|
||||||
|
key={col.key}
|
||||||
|
className={`${col.align || "text-center"} ${col.className || ""
|
||||||
|
}`}
|
||||||
|
scope="col"
|
||||||
|
>
|
||||||
|
<div className={col.className}>{col.label}</div>
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
<th className="sorting_disabled text-center" aria-label="Actions">
|
||||||
|
Actions
|
||||||
</th>
|
</th>
|
||||||
))}
|
</tr>
|
||||||
<th className="sorting_disabled text-center" aria-label="Actions">
|
</thead>
|
||||||
Actions
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
{Array.isArray(data?.data) && data.data.length > 0 ? (
|
{Array.isArray(data?.data) && data.data.length > 0 ? (
|
||||||
data.data.map((row, i) => (
|
data.data.map((row, i) => (
|
||||||
<tr key={i} className="text-start">
|
<tr key={i} className="text-start">
|
||||||
{jobGrid.map((col) => (
|
{jobGrid.map((col) => (
|
||||||
<td
|
<td
|
||||||
key={col.key}
|
key={col.key}
|
||||||
className={col.className}
|
className={col.className}
|
||||||
onClick={() =>
|
// onClick={() =>
|
||||||
setSelectedJob({ showCanvas: true, job: row?.id })
|
// setSelectedJob({ showCanvas: true, job: row?.id })
|
||||||
}
|
// }
|
||||||
>
|
onClick={() => {
|
||||||
{col.getValue(row)}
|
if (!isArchive) {
|
||||||
</td>
|
setSelectedJob({ showCanvas: true, job: e?.id });
|
||||||
))}
|
|
||||||
<td>
|
|
||||||
<div className="dropdown text-center">
|
|
||||||
<button
|
|
||||||
className="btn btn-icon dropdown-toggle hide-arrow"
|
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
>
|
|
||||||
<i className="bx bx-dots-vertical-rounded bx-md"></i>
|
|
||||||
</button>
|
|
||||||
<div className="dropdown-menu dropdown-menu-end">
|
|
||||||
<button
|
|
||||||
className="dropdown-item py-1"
|
|
||||||
onClick={() =>
|
|
||||||
setSelectedJob({ showCanvas: true, job: row?.id })
|
|
||||||
}
|
}
|
||||||
>
|
}}
|
||||||
<i className="bx bx-detail bx-sm"></i> View
|
|
||||||
</button>
|
>
|
||||||
|
{col.getValue(row)}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
<td>
|
||||||
|
<div className="dropdown text-center">
|
||||||
|
<button
|
||||||
|
className="btn btn-icon dropdown-toggle hide-arrow"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
>
|
||||||
|
<i className="bx bx-dots-vertical-rounded bx-md"></i>
|
||||||
|
</button>
|
||||||
|
<div className="dropdown-menu dropdown-menu-end">
|
||||||
|
|
||||||
|
{!isArchive && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className="dropdown-item py-1"
|
||||||
|
onClick={() =>
|
||||||
|
setSelectedJob({ showCanvas: true, job: row?.id })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<i className="bx bx-detail bx-sm"></i> View
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="dropdown-item py-1"
|
||||||
|
onClick={() =>
|
||||||
|
setManageJob({ isOpen: true, jobId: row?.id })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<i className="bx bx-edit bx-sm"></i> Edit
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isArchive && (
|
||||||
|
<button
|
||||||
|
className="dropdown-item py-1"
|
||||||
|
onClick={() => {
|
||||||
|
setArchiveJobId(row.id);
|
||||||
|
setIsArchiveAction(false);
|
||||||
|
setIsArchiveModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i className="bx bx-reset bx-sm"></i> Restore
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isArchive && canArchive(row?.status?.id) && (
|
||||||
|
<button
|
||||||
|
className="dropdown-item py-1"
|
||||||
|
onClick={() => {
|
||||||
|
setArchiveJobId(row.id);
|
||||||
|
setIsArchiveAction(true);
|
||||||
|
setIsArchiveModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i className="bx bx-archive bx-sm"></i> Archive
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
className="dropdown-item py-1"
|
|
||||||
onClick={() =>
|
|
||||||
setManageJob({ isOpen: true, jobId: row?.id })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<i className="bx bx-edit bx-sm"></i> Edit
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<tr style={{ height: "200px" }}>
|
||||||
|
<td
|
||||||
|
colSpan={jobGrid.length + 1}
|
||||||
|
className="text-center border-0 align-middle"
|
||||||
|
>
|
||||||
|
{isLoading ? <SpinnerLoader /> : "Not Found Jobs."}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))
|
)}
|
||||||
) : (
|
</tbody>
|
||||||
<tr style={{ height: "200px" }}>
|
</table>
|
||||||
<td
|
</div>
|
||||||
colSpan={jobGrid.length + 1}
|
</>
|
||||||
className="text-center border-0 align-middle"
|
|
||||||
>
|
|
||||||
{isLoading ? <SpinnerLoader /> : "Not Found Jobs."}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -5,8 +5,8 @@ import { useServiceProjects } from "../../hooks/useServiceProject";
|
|||||||
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
||||||
import OffcanvasComponent from "../common/OffcanvasComponent";
|
import OffcanvasComponent from "../common/OffcanvasComponent";
|
||||||
import showToast from "../../services/toastService";
|
import showToast from "../../services/toastService";
|
||||||
import ManageJob from "./ManageJob";
|
import ManageJob from "./ServiceProjectJob/ManageJob";
|
||||||
import ManageJobTicket from "./ManageJobTicket";
|
import ManageJobTicket from "./ServiceProjectJob/ManageJobTicket";
|
||||||
import GlobalModel from "../common/GlobalModel";
|
import GlobalModel from "../common/GlobalModel";
|
||||||
import PreviewDocument from "../Expenses/PreviewDocument";
|
import PreviewDocument from "../Expenses/PreviewDocument";
|
||||||
|
|
||||||
@ -21,6 +21,8 @@ export const useServiceProjectJobContext = () => {
|
|||||||
};
|
};
|
||||||
const Jobs = () => {
|
const Jobs = () => {
|
||||||
const [manageJob, setManageJob] = useState({ isOpen: false, jobId: null });
|
const [manageJob, setManageJob] = useState({ isOpen: false, jobId: null });
|
||||||
|
const [showArchive, setShowArchive] = useState(false);
|
||||||
|
|
||||||
const [showCanvas, setShowCanvas] = useState(false);
|
const [showCanvas, setShowCanvas] = useState(false);
|
||||||
const [selectedProject, setSelectedProject] = useState(null);
|
const [selectedProject, setSelectedProject] = useState(null);
|
||||||
const [selectJob, setSelectedJob] = useState({
|
const [selectJob, setSelectedJob] = useState({
|
||||||
@ -58,21 +60,34 @@ const Jobs = () => {
|
|||||||
<ManageJob Job={manageJob.jobId} />
|
<ManageJob Job={manageJob.jobId} />
|
||||||
</OffcanvasComponent>
|
</OffcanvasComponent>
|
||||||
<div className="card page-min-h my-2 px-7 pb-4">
|
<div className="card page-min-h my-2 px-7 pb-4">
|
||||||
<div className="row">
|
<div className="row align-items-center py-4">
|
||||||
<div className="col-12 py-2 d-flex justify-content-end ">
|
|
||||||
<div className="px-2">
|
{/* LEFT — Tabs */}
|
||||||
<button
|
<div className="col-12 col-md-6 text-start">
|
||||||
className="btn btn-sm btn-primary"
|
<button
|
||||||
onClick={() => setManageJob({ isOpen: true, jobId: null })}
|
type="button"
|
||||||
>
|
className={`btn btn-sm ${showArchive ? "btn-secondary" : "btn-outline-secondary"}`}
|
||||||
<i className="bx bx-plus-circle bx-md me-2"></i>New Job
|
onClick={() => setShowArchive(!showArchive)}
|
||||||
</button>
|
>
|
||||||
</div>
|
<i className="bx bx-archive bx-sm me-1 mt-1"></i> Archived
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<JobList filterByProject={selectedProject} />
|
{/* RIGHT — New Job button */}
|
||||||
|
<div className="col-12 col-md-6 d-flex justify-content-md-end mt-2 mt-md-0">
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-primary"
|
||||||
|
onClick={() => setManageJob({ isOpen: true, jobId: null })}
|
||||||
|
>
|
||||||
|
<i className="bx bx-plus-circle bx-md me-2"></i>New Job
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Job List */}
|
||||||
|
<JobList filterByProject={selectedProject} isArchive={showArchive} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</JonContext.Provider>
|
</JonContext.Provider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -192,8 +192,8 @@ const ManageServiceProject = ({ serviceProjectId, onClose }) => {
|
|||||||
{...register("statusId")}
|
{...register("statusId")}
|
||||||
>
|
>
|
||||||
<option>Select Service</option>
|
<option>Select Service</option>
|
||||||
{PROJECT_STATUS.map((status) => (
|
{PROJECT_STATUS?.map((status) => (
|
||||||
<option value={status.id}>{status.label}</option>
|
<option key={status.id} value={status.id}>{status.label}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
{errors?.statusId && (
|
{errors?.statusId && (
|
||||||
|
|||||||
95
src/components/ServiceProject/ServiceProfile.jsx
Normal file
95
src/components/ServiceProject/ServiceProfile.jsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { formatUTCToLocalTime } from '../../utils/dateUtils'
|
||||||
|
|
||||||
|
const ServiceProfile = ({data,setIsOpenModal}) => {
|
||||||
|
return (
|
||||||
|
<div className="card mb-4 h-100">
|
||||||
|
<div className="card-header text-start">
|
||||||
|
<h5 className="card-action-title mb-0 ps-1">
|
||||||
|
{" "}
|
||||||
|
<i className="fa fa-building rounded-circle text-primary"></i>
|
||||||
|
<span className="ms-2 fw-bold">Project Profile</span>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<ul className="list-unstyled my-3 ps-0 text-start">
|
||||||
|
|
||||||
|
<li className="d-flex mb-3">
|
||||||
|
<div className="d-flex align-items-start" style={{ minWidth: "150px" }}>
|
||||||
|
<i className="bx bx-cog"></i>
|
||||||
|
<span className="fw-medium mx-2 text-nowrap">Name:</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-grow-1 text-start text-wrap">
|
||||||
|
{data.name}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li className="d-flex mb-3">
|
||||||
|
<div className="d-flex align-items-center" style={{ width: '150px' }}>
|
||||||
|
<i className="bx bx-fingerprint"></i>
|
||||||
|
<span className="fw-medium mx-2">Nick Name:</span>
|
||||||
|
</div>
|
||||||
|
<span>{data.shortName}</span>
|
||||||
|
</li>
|
||||||
|
<li className="d-flex mb-3">
|
||||||
|
<div className="d-flex align-items-center" style={{ width: '150px' }}>
|
||||||
|
<i className="bx bx-check"></i>
|
||||||
|
<span className="fw-medium mx-2">Assign Date:</span>
|
||||||
|
</div>
|
||||||
|
<span>
|
||||||
|
{data.assignedDate ? formatUTCToLocalTime(data.assignedDate) : "NA"}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li className="d-flex mb-3">
|
||||||
|
<div className="d-flex align-items-center" style={{ width: '150px' }}>
|
||||||
|
<i className="bx bx-trophy"></i>
|
||||||
|
<span className="fw-medium mx-2">Status:</span>
|
||||||
|
</div>
|
||||||
|
<span>{data?.status.status}</span>
|
||||||
|
</li>
|
||||||
|
<li className="d-flex mb-3">
|
||||||
|
<div className="d-flex align-items-center" style={{ width: '150px' }}>
|
||||||
|
<i className="bx bx-user"></i>
|
||||||
|
<span className="fw-medium mx-2">Contact:</span>
|
||||||
|
</div>
|
||||||
|
<span>{data.contactName}</span>
|
||||||
|
</li>
|
||||||
|
<li className="d-flex mb-3">
|
||||||
|
{/* Label section with icon */}
|
||||||
|
<div className="d-flex align-items-start" style={{ minWidth: "150px" }}>
|
||||||
|
<i className="bx bx-flag mt-1"></i>
|
||||||
|
<span className="fw-medium mx-2 text-nowrap">Address:</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content section that wraps nicely */}
|
||||||
|
<div className="flex-grow-1 text-start text-wrap">
|
||||||
|
{data.address}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li className="d-flex justify-content-center mt-4">
|
||||||
|
|
||||||
|
<a className="d-flex justify-content-center mt-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-sm btn-primary"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#edit-project-modal"
|
||||||
|
onClick={() => setIsOpenModal(true)}
|
||||||
|
>
|
||||||
|
Modify Details
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ServiceProfile
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { useBranchDetails } from "../../../hooks/useServiceProject";
|
||||||
|
import { SpinnerLoader } from "../../common/Loader";
|
||||||
|
import Error from "../../common/Error";
|
||||||
|
import { BranchDetailsSkeleton } from "../ServiceProjectSeketon";
|
||||||
|
|
||||||
|
const BranchDetails = ({ branch }) => {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const { data, isLoading, isError, error } = useBranchDetails(branch);
|
||||||
|
|
||||||
|
if (isLoading) return <BranchDetailsSkeleton />;
|
||||||
|
if (isError) return <Error error={error} />;
|
||||||
|
|
||||||
|
let contactInfo = [];
|
||||||
|
try {
|
||||||
|
contactInfo = JSON.parse(data?.contactInformation || "[]");
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
const googleMapUrl = data?.googleMapUrl || data?.locationLink;
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
if (!googleMapUrl) return;
|
||||||
|
await navigator.clipboard.writeText(googleMapUrl);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-100">
|
||||||
|
<div className="d-flex mb-2 align-items-center">
|
||||||
|
<i className="bx bx-buildings bx-sm me-2 text-primary"></i>
|
||||||
|
<span className="fw-semibold">Branch Details</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DetailRow label="Branch Name" value={data?.branchName} />
|
||||||
|
|
||||||
|
<DetailRow label="Type" value={data?.branchType} />
|
||||||
|
|
||||||
|
<DetailRow label="Address" value={data?.address} />
|
||||||
|
|
||||||
|
{/* Contact persons */}
|
||||||
|
{contactInfo.map((person, index) => (
|
||||||
|
<div key={index} className="mb-2">
|
||||||
|
<div className="fw-medium text-primary">{person.contactPerson}</div>
|
||||||
|
<DetailRow label="Role" value={person.designation} />
|
||||||
|
<DetailRow label="Emails" value={person.contactEmails.join(", ")} />
|
||||||
|
<DetailRow label="Phone" value={person.contactNumbers.join(", ")} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Map Link */}
|
||||||
|
{googleMapUrl && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<a
|
||||||
|
href={googleMapUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary text-decoration-underline"
|
||||||
|
>
|
||||||
|
View on Google Maps
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="btn btn-xs btn-secondry border ms-2"
|
||||||
|
onClick={handleCopy}
|
||||||
|
>
|
||||||
|
<i className={`bx bx-xs me-1 ${copied ? "bxs-copy-alt":"bx-copy-alt"} `} ></i> {copied ? "Copied!" : "Copy"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DetailRow = ({ label, value }) => (
|
||||||
|
<div className="d-flex mb-1">
|
||||||
|
<div className="text-secondary" style={{ width: "90px", flexShrink: 0 }}>
|
||||||
|
{label}:
|
||||||
|
</div>
|
||||||
|
<div className="" style={{ wordBreak: "break-word" }}>
|
||||||
|
{value || "N/A"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default BranchDetails;
|
||||||
@ -0,0 +1,358 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { useProjectName } from "../../../hooks/useProjects";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import Label from "../../common/Label";
|
||||||
|
import {
|
||||||
|
useBranchDetails,
|
||||||
|
useBranchTypes,
|
||||||
|
useCreateBranch,
|
||||||
|
useServiceProjects,
|
||||||
|
useUpdateBranch,
|
||||||
|
} from "../../../hooks/useServiceProject";
|
||||||
|
import { useAppForm } from "../../../hooks/appHooks/useAppForm";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { BranchSchema, defaultBranches } from "../ServiceProjectSchema";
|
||||||
|
import InputSuggessionField from "../../common/Forms/InputSuggesstionField";
|
||||||
|
import InputSuggestions from "../../common/InputSuggestion";
|
||||||
|
|
||||||
|
const ManageBranch = ({ closeModal, BranchToEdit = null }) => {
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
error: requestError,
|
||||||
|
} = useBranchDetails(BranchToEdit);
|
||||||
|
const { data: branchTypes } = useBranchTypes();
|
||||||
|
const [contacts, setContacts] = React.useState([
|
||||||
|
{
|
||||||
|
contactPerson: "",
|
||||||
|
designation: "",
|
||||||
|
contactEmails: [""],
|
||||||
|
contactNumbers: [""],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { projectId } = useParams();
|
||||||
|
const schema = BranchSchema();
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
control,
|
||||||
|
watch,
|
||||||
|
handleSubmit,
|
||||||
|
setValue,
|
||||||
|
reset,
|
||||||
|
formState: { errors },
|
||||||
|
} = useAppForm({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
...defaultBranches,
|
||||||
|
projectId: projectId || "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
reset();
|
||||||
|
closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (BranchToEdit && data) {
|
||||||
|
reset({
|
||||||
|
branchName: data.branchName || "",
|
||||||
|
projectId: data.project?.id || projectId || "",
|
||||||
|
address: data.address || "",
|
||||||
|
branchType: data.branchType || "",
|
||||||
|
googleMapUrl: data.googleMapUrl || "",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.contactInformation) {
|
||||||
|
try {
|
||||||
|
setContacts(JSON.parse(data.contactInformation));
|
||||||
|
} catch {
|
||||||
|
setContacts([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [data, reset]);
|
||||||
|
|
||||||
|
const { mutate: CreateServiceBranch, isPending: createPending } =
|
||||||
|
useCreateBranch(() => {
|
||||||
|
handleClose();
|
||||||
|
});
|
||||||
|
const { mutate: ServiceBranchUpdate, isPending } = useUpdateBranch(() =>
|
||||||
|
handleClose()
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSubmit = (formdata) => {
|
||||||
|
let payload = {
|
||||||
|
...data,
|
||||||
|
...formdata,
|
||||||
|
projectId,
|
||||||
|
contactInformation: JSON.stringify(contacts), // ← important
|
||||||
|
};
|
||||||
|
|
||||||
|
if (BranchToEdit) {
|
||||||
|
ServiceBranchUpdate({ id: data.id, payload });
|
||||||
|
} else {
|
||||||
|
CreateServiceBranch(payload);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container p-3">
|
||||||
|
<h5 className="m-0">
|
||||||
|
{BranchToEdit ? "Update Branch" : "Create Branch"}
|
||||||
|
</h5>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div className="row my-2 text-start">
|
||||||
|
<div className="col-md-6">
|
||||||
|
<Label htmlFor="branchName" className="form-label" required>
|
||||||
|
Branch Name
|
||||||
|
</Label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="branchName"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
{...register("branchName")}
|
||||||
|
placeholder="Enter Branch"
|
||||||
|
/>
|
||||||
|
{errors.branchName && (
|
||||||
|
<small className="danger-text">{errors.branchName.message}</small>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<Label htmlFor="branchType" className="form-label" required>
|
||||||
|
Branch Type
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<InputSuggestions
|
||||||
|
organizationList={branchTypes}
|
||||||
|
value={watch("branchType") || ""}
|
||||||
|
onChange={(val) =>
|
||||||
|
setValue("branchType", val, { shouldValidate: true })
|
||||||
|
}
|
||||||
|
error={errors.branchType?.message}
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row my-2 text-start">
|
||||||
|
<div className="col-md-6">
|
||||||
|
<Label htmlFor="googleMapUrl" className="form-label">
|
||||||
|
Google Map URL
|
||||||
|
</Label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="googleMapUrl"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
{...register("googleMapUrl")}
|
||||||
|
/>
|
||||||
|
{errors.googleMapUrl && (
|
||||||
|
<small className="danger-text">
|
||||||
|
{errors.googleMapUrl.message}
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row my-2 text-start">
|
||||||
|
<div className="col-12">
|
||||||
|
<Label className="form-label" required>
|
||||||
|
Contact Persons
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
{contacts.map((item, index) => (
|
||||||
|
<div key={index} className="border rounded p-2 mb-3">
|
||||||
|
<div className="d-flex justify-content-end py-1">
|
||||||
|
{" "}
|
||||||
|
<div className="col-md-1 d-flex align-items-center">
|
||||||
|
<i
|
||||||
|
className="bx bx-trash text-danger cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setContacts(contacts.filter((_, i) => i !== index))
|
||||||
|
}
|
||||||
|
></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="row mb-2">
|
||||||
|
<div className=" col-md-6">
|
||||||
|
<Label className="form-label">Contact Name</Label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Contact Name"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
value={item.contactPerson}
|
||||||
|
onChange={(e) => {
|
||||||
|
const list = [...contacts];
|
||||||
|
list[index].contactPerson = e.target.value;
|
||||||
|
setContacts(list);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-md-6">
|
||||||
|
<Label className="form-label">Designation</Label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Designation"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
value={item.designation}
|
||||||
|
onChange={(e) => {
|
||||||
|
const list = [...contacts];
|
||||||
|
list[index].designation = e.target.value;
|
||||||
|
setContacts(list);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Numbers Section */}
|
||||||
|
<Label className="form-label">Contact Numbers</Label>
|
||||||
|
|
||||||
|
{item.contactNumbers.map((num, numIndex) => (
|
||||||
|
<div
|
||||||
|
key={numIndex}
|
||||||
|
className="d-flex gap-2 mb-2 align-items-center"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Number"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
maxLength={10}
|
||||||
|
value={num}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value.replace(/\D/g, ""); // remove non-digit characters
|
||||||
|
const list = [...contacts];
|
||||||
|
list[index].contactNumbers[numIndex] = value;
|
||||||
|
setContacts(list);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Show PLUS only on last row */}
|
||||||
|
{numIndex === item.contactNumbers.length - 1 ? (
|
||||||
|
<i
|
||||||
|
className="bx bx-plus-circle text-primary cursor-pointer fs-5"
|
||||||
|
onClick={() => {
|
||||||
|
const list = [...contacts];
|
||||||
|
list[index].contactNumbers.push("");
|
||||||
|
setContacts(list);
|
||||||
|
}}
|
||||||
|
></i>
|
||||||
|
) : (
|
||||||
|
<i
|
||||||
|
className="bx bx-minus-circle text-danger cursor-pointer fs-5"
|
||||||
|
onClick={() => {
|
||||||
|
const list = [...contacts];
|
||||||
|
list[index].contactNumbers.splice(numIndex, 1);
|
||||||
|
setContacts(list);
|
||||||
|
}}
|
||||||
|
></i>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
{/* Emails Section */}
|
||||||
|
<Label className="form-label">Contact Emails</Label>
|
||||||
|
|
||||||
|
{item.contactEmails.map((email, emailIndex) => (
|
||||||
|
<div
|
||||||
|
key={emailIndex}
|
||||||
|
className="d-flex gap-2 mb-2 align-items-center"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="Email"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => {
|
||||||
|
const list = [...contacts];
|
||||||
|
list[index].contactEmails[emailIndex] = e.target.value;
|
||||||
|
setContacts(list);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Show PLUS only on the last row */}
|
||||||
|
{emailIndex === item.contactEmails.length - 1 ? (
|
||||||
|
<i
|
||||||
|
className="bx bx-plus-circle text-primary cursor-pointer fs-5"
|
||||||
|
onClick={() => {
|
||||||
|
const list = [...contacts];
|
||||||
|
list[index].contactEmails.push("");
|
||||||
|
setContacts(list);
|
||||||
|
}}
|
||||||
|
></i>
|
||||||
|
) : (
|
||||||
|
<i
|
||||||
|
className="bx bx-minus-circle text-danger cursor-pointer fs-5"
|
||||||
|
onClick={() => {
|
||||||
|
const list = [...contacts];
|
||||||
|
list[index].contactEmails.splice(emailIndex, 1);
|
||||||
|
setContacts(list);
|
||||||
|
}}
|
||||||
|
></i>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-sm btn-primary mt-2"
|
||||||
|
onClick={() =>
|
||||||
|
setContacts([
|
||||||
|
...contacts,
|
||||||
|
{
|
||||||
|
contactPerson: "",
|
||||||
|
designation: "",
|
||||||
|
contactEmails: [""], // ← important
|
||||||
|
contactNumbers: [""], // ← important
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<i className="bx bx-plus"></i> Add Contact
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row my-2 text-start">
|
||||||
|
<div className="col-12">
|
||||||
|
<Label htmlFor="address" className="form-label" required>
|
||||||
|
Address
|
||||||
|
</Label>
|
||||||
|
<textarea
|
||||||
|
id="address"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
{...register("address")}
|
||||||
|
/>
|
||||||
|
{errors.address && (
|
||||||
|
<small className="danger-text">{errors.address.message}</small>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="d-flex justify-content-end gap-3">
|
||||||
|
<button
|
||||||
|
type="reset"
|
||||||
|
onClick={handleClose}
|
||||||
|
className="btn btn-label-secondary btn-sm mt-3"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="submit" className="btn btn-primary btn-sm mt-3">
|
||||||
|
{isPending ? "Please wait..." : "Submit"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ManageBranch;
|
||||||
@ -0,0 +1,283 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import GlobalModel from "../../common/GlobalModel";
|
||||||
|
import ManageBranch from "./ManageBranch";
|
||||||
|
import { useBranches, useDeleteBranch } from "../../../hooks/useServiceProject";
|
||||||
|
import { ITEMS_PER_PAGE } from "../../../utils/constants";
|
||||||
|
import { useDebounce } from "../../../utils/appUtils";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import Pagination from "../../common/Pagination";
|
||||||
|
import ConfirmModal from "../../common/ConfirmModal";
|
||||||
|
import { SpinnerLoader } from "../../common/Loader";
|
||||||
|
|
||||||
|
const ServiceBranch = () => {
|
||||||
|
const { projectId } = useParams();
|
||||||
|
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
|
const [showInactive, setShowInactive] = useState(false);
|
||||||
|
const [manageState, setManageState] = useState({
|
||||||
|
IsOpen: false,
|
||||||
|
branchId: null,
|
||||||
|
});
|
||||||
|
const { mutate: DeleteBranch, isPending } = useDeleteBranch();
|
||||||
|
const [deletingId, setDeletingId] = useState(null);
|
||||||
|
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const debouncedSearch = useDebounce(search, 500);
|
||||||
|
|
||||||
|
const { data, isLoading, isError, error } = useBranches(
|
||||||
|
projectId,
|
||||||
|
!showInactive,
|
||||||
|
ITEMS_PER_PAGE - 12,
|
||||||
|
currentPage,
|
||||||
|
debouncedSearch
|
||||||
|
);
|
||||||
|
|
||||||
|
const paginate = (page) => {
|
||||||
|
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
|
||||||
|
setCurrentPage(page);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
key: "branchName",
|
||||||
|
label: "Name",
|
||||||
|
align: "text-start",
|
||||||
|
getValue: (e) => e?.branchName || "N/A",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleDelete = (id) => {
|
||||||
|
setDeletingId(id);
|
||||||
|
DeleteBranch(
|
||||||
|
{ id, isActive: showInactive },
|
||||||
|
{
|
||||||
|
onSettled: () => {
|
||||||
|
setDeletingId(null);
|
||||||
|
setIsDeleteModalOpen(false);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{IsDeleteModalOpen && (
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={IsDeleteModalOpen}
|
||||||
|
type={!showInactive ? "delete" : "undo"}
|
||||||
|
header={!showInactive ? "Delete Branch" : "Restore Branch"}
|
||||||
|
message={
|
||||||
|
!showInactive
|
||||||
|
? "Are you sure you want delete?"
|
||||||
|
: "Are you sure you want restore?"
|
||||||
|
}
|
||||||
|
onSubmit={handleDelete}
|
||||||
|
onClose={() => setIsDeleteModalOpen(false)}
|
||||||
|
loading={isPending}
|
||||||
|
paramData={deletingId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="card h-100 table-responsive px-sm-4">
|
||||||
|
<div className="card-datatable" id="payment-request-table">
|
||||||
|
{/* Header Section */}
|
||||||
|
<div className="row align-items-center justify-content-between mt-3 mx-1">
|
||||||
|
<div className="col-md-4 col-sm-12 ms-n3 text-start ">
|
||||||
|
<h5 className="mb-0">
|
||||||
|
<i className="bx bx-buildings text-primary"></i>
|
||||||
|
<span className="ms-2 fw-bold">Branchs</span>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Flex container for toggle + button */}
|
||||||
|
<div className="col-md-8 col-sm-12 text-end">
|
||||||
|
<div className="d-flex justify-content-end gap-2">
|
||||||
|
<div className="form-check form-switch d-inline-flex align-items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="form-check-input mt-1"
|
||||||
|
id="inactiveEmployeesCheckbox"
|
||||||
|
checked={showInactive}
|
||||||
|
onChange={() => setShowInactive(!showInactive)}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="inactiveEmployeesCheckbox"
|
||||||
|
className="ms-2 text-xs"
|
||||||
|
>
|
||||||
|
{!showInactive ? "Show Deleted" : "Hide Deleted"}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="d-flex justify-content-end">
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-primary"
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
setManageState({
|
||||||
|
IsOpen: true,
|
||||||
|
branchId: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<i className="bx bx-sm bx-plus-circle me-2"></i>
|
||||||
|
Add Branch
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx-2 mt-3">
|
||||||
|
<table className="table border-top text-nowrap align-middle table-borderless">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{columns.map((col) => (
|
||||||
|
<th key={col.key} className={col.align}>
|
||||||
|
{col.label}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
<th className="text-center">Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{isLoading && (
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colSpan={columns.length + 1}
|
||||||
|
className="text-center py-5"
|
||||||
|
style={{
|
||||||
|
height: "200px",
|
||||||
|
verticalAlign: "middle",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="d-flex justify-content-center align-items-center w-100 h-100">
|
||||||
|
<SpinnerLoader />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isError && (
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colSpan={columns.length + 1}
|
||||||
|
className="text-center text-danger py-5"
|
||||||
|
>
|
||||||
|
{error?.message || "Error loading branches"}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading &&
|
||||||
|
!isError &&
|
||||||
|
data?.data?.length > 0 &&
|
||||||
|
data.data.map((branch) => (
|
||||||
|
<tr key={branch.id} style={{ height: "35px" }}>
|
||||||
|
{columns.map((col) => (
|
||||||
|
<td key={col.key} className={`${col.align} py-3`}>
|
||||||
|
{col.getValue(branch)}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
<td className="text-center">
|
||||||
|
<div className="dropdown z-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-xs btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0 m-0"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
>
|
||||||
|
<i className="bx bx-dots-vertical-rounded text-muted p-0"></i>
|
||||||
|
</button>
|
||||||
|
<ul className="dropdown-menu dropdown-menu-end w-auto">
|
||||||
|
{!showInactive ? (
|
||||||
|
<>
|
||||||
|
<li
|
||||||
|
onClick={() =>
|
||||||
|
setManageState({
|
||||||
|
IsOpen: true,
|
||||||
|
branchId: branch.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<a className="dropdown-item px-2 cursor-pointer py-1">
|
||||||
|
<i className="bx bx-edit text-primary bx-xs me-2"></i>
|
||||||
|
Modify
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
onClick={() => {
|
||||||
|
setIsDeleteModalOpen(true);
|
||||||
|
setDeletingId(branch.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<a className="dropdown-item px-2 cursor-pointer py-1">
|
||||||
|
<i className="bx bx-trash text-danger bx-xs me-2"></i>
|
||||||
|
Delete
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<li
|
||||||
|
onClick={() => {
|
||||||
|
setIsDeleteModalOpen(true);
|
||||||
|
setDeletingId(branch.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<a className="dropdown-item px-2 cursor-pointer py-1">
|
||||||
|
<i className="bx bx-undo text-danger me-2"></i>
|
||||||
|
Restore
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{!isLoading &&
|
||||||
|
!isError &&
|
||||||
|
(!data?.data || data.data.length === 0) && (
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colSpan={columns.length + 1}
|
||||||
|
className="text-center py-12"
|
||||||
|
>
|
||||||
|
No Branch Found
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{data?.data?.length > 0 && (
|
||||||
|
<Pagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={data.totalPages}
|
||||||
|
onPageChange={paginate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{manageState.IsOpen && (
|
||||||
|
<GlobalModel
|
||||||
|
isOpen
|
||||||
|
size="md"
|
||||||
|
closeModal={() =>
|
||||||
|
setManageState({ IsOpen: false, branchId: null })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ManageBranch
|
||||||
|
key={manageState.branchId ?? "new"}
|
||||||
|
BranchToEdit={manageState.branchId}
|
||||||
|
closeModal={() =>
|
||||||
|
setManageState({ IsOpen: false, branchId: null })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</GlobalModel>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ServiceBranch;
|
||||||
@ -1,17 +1,16 @@
|
|||||||
import SelectField from "../common/Forms/SelectField";
|
import SelectField from "../../common/Forms/SelectField";
|
||||||
import { useJobStatus } from "../../hooks/masterHook/useMaster";
|
import Error from "../../common/Error";
|
||||||
import { SpinnerLoader } from "../common/Loader";
|
|
||||||
import Error from "../common/Error";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import {
|
||||||
AppFormController,
|
AppFormController,
|
||||||
AppFormProvider,
|
AppFormProvider,
|
||||||
useAppForm,
|
useAppForm,
|
||||||
} from "../../hooks/appHooks/useAppForm";
|
} from "../../../hooks/appHooks/useAppForm";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { closePopup } from "../../slices/localVariablesSlice";
|
import { closePopup } from "../../../slices/localVariablesSlice";
|
||||||
import { useUpdateServiceProjectJob } from "../../hooks/useServiceProject";
|
import { useUpdateServiceProjectJob } from "../../../hooks/useServiceProject";
|
||||||
|
import { useJobStatus } from "../../../hooks/masterHook/useMaster";
|
||||||
|
|
||||||
export const ChangeStatusSchema = z.object({
|
export const ChangeStatusSchema = z.object({
|
||||||
statusId: z.string().min(1, { message: "Please select status" }),
|
statusId: z.string().min(1, { message: "Please select status" }),
|
||||||
@ -32,7 +31,7 @@ const ChangeStatus = ({ statusId, projectId, jobId, popUpId }) => {
|
|||||||
} = methods;
|
} = methods;
|
||||||
|
|
||||||
const { mutate: UpdateStatus, isPending } = useUpdateServiceProjectJob(() => {
|
const { mutate: UpdateStatus, isPending } = useUpdateServiceProjectJob(() => {
|
||||||
// handleClose();
|
handleClose();
|
||||||
});
|
});
|
||||||
const onSubmit = (formData) => {
|
const onSubmit = (formData) => {
|
||||||
const payload =
|
const payload =
|
||||||
@ -53,6 +52,11 @@ const ChangeStatus = ({ statusId, projectId, jobId, popUpId }) => {
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<AppFormProvider {...methods}>
|
<AppFormProvider {...methods}>
|
||||||
|
<div className="d-flex mb-2">
|
||||||
|
<span className="fs-6 fw-medium">
|
||||||
|
Change Status
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<form className="row text-start" onSubmit={handleSubmit(onSubmit)}>
|
<form className="row text-start" onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<AppFormController
|
<AppFormController
|
||||||
@ -1,16 +1,16 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import Avatar from "../common/Avatar";
|
import Avatar from "../../common/Avatar";
|
||||||
import { useAppForm } from "../../hooks/appHooks/useAppForm";
|
import { useAppForm } from "../../../hooks/appHooks/useAppForm";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { JobCommentSchema } from "./ServiceProjectSchema";
|
import { JobCommentSchema } from "../ServiceProjectSchema";
|
||||||
import {
|
import {
|
||||||
useAddCommentJob,
|
useAddCommentJob,
|
||||||
useJobComments,
|
useJobComments,
|
||||||
} from "../../hooks/useServiceProject";
|
} from "../../../hooks/useServiceProject";
|
||||||
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
import { ITEMS_PER_PAGE } from "../../../utils/constants";
|
||||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
import { formatUTCToLocalTime } from "../../../utils/dateUtils";
|
||||||
import Filelist from "../Expenses/Filelist";
|
import Filelist from "../../Expenses/Filelist";
|
||||||
import { formatFileSize, getIconByFileType } from "../../utils/appUtils";
|
import { formatFileSize, getIconByFileType } from "../../../utils/appUtils";
|
||||||
|
|
||||||
const JobComments = ({ data }) => {
|
const JobComments = ({ data }) => {
|
||||||
const {
|
const {
|
||||||
@ -150,7 +150,7 @@ const JobComments = ({ data }) => {
|
|||||||
type="submit"
|
type="submit"
|
||||||
disabled={!watch("comment")?.trim() || isPending}
|
disabled={!watch("comment")?.trim() || isPending}
|
||||||
>
|
>
|
||||||
Submit
|
Send
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@ -161,46 +161,48 @@ const JobComments = ({ data }) => {
|
|||||||
const user = item?.createdBy;
|
const user = item?.createdBy;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="d-flex align-items-start mt-2 mx-0 px-0">
|
||||||
key={item.id}
|
<Avatar
|
||||||
className="list-group-item border-0 border-bottom p-0"
|
size="xs"
|
||||||
>
|
firstName={user?.firstName}
|
||||||
<div className="d-flex align-items-start mt-2 mx-0 px-0">
|
lastName={user?.lastName}
|
||||||
<Avatar
|
/>
|
||||||
firstName={user?.firstName}
|
|
||||||
lastName={user?.lastName}
|
<div className="w-100">
|
||||||
/>
|
<div className="d-flex flex-row align-items-center gap-3 w-100">
|
||||||
<div className="">
|
<span className="fw-semibold">
|
||||||
<div className="d-flex flex-row gap-3">
|
{user?.firstName} {user?.lastName}
|
||||||
<span className="fw-semibold">
|
</span>
|
||||||
{user?.firstName} {user?.lastName}
|
|
||||||
</span>
|
<span className="text-secondary">
|
||||||
<span className="text-secondary">
|
<em>{formatUTCToLocalTime(item?.createdAt, true)}</em>
|
||||||
<em>{formatUTCToLocalTime(item?.createdAt, true)}</em>
|
</span>
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted text-secondary">
|
|
||||||
{user?.jobRoleName}
|
<div className="text-muted text-secondary">
|
||||||
</div>
|
{user?.jobRoleName}
|
||||||
<div className="text-wrap">
|
</div>
|
||||||
<p className="mb-1 mt-2 text-muted">{item.comment}</p>
|
|
||||||
<div className="d-flex flex-wrap jusify-content-end gap-2 gap-sm-6 ">
|
<div className="text-wrap">
|
||||||
{item.attachments?.map((file) => (
|
<p className="mb-1 mt-2 text-muted">{item.comment}</p>
|
||||||
<div className="d-flex align-items-center">
|
|
||||||
<i
|
<div className="d-flex flex-wrap jusify-content-end gap-2 gap-sm-6">
|
||||||
className={`bx bx-xxl ${getIconByFileType(
|
{item.attachments?.map((file) => (
|
||||||
file?.contentType
|
<div className="d-flex align-items-center">
|
||||||
)} fs-3`}
|
<i
|
||||||
></i>
|
className={`bx bx-xxl ${getIconByFileType(
|
||||||
<div className="d-flex flex-column">
|
file?.contentType
|
||||||
<p className="m-0">{file.fileName}</p>
|
)} fs-3`}
|
||||||
<small className="text-secondary">
|
></i>
|
||||||
{formatFileSize(file.fileSize)}
|
<div className="d-flex flex-column">
|
||||||
</small>
|
<p className="m-0">{file.fileName}</p>
|
||||||
</div>
|
<small className="text-secondary">
|
||||||
|
{formatFileSize(file.fileSize)}
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import Avatar from "../common/Avatar";
|
import Avatar from "../../common/Avatar";
|
||||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
import { formatUTCToLocalTime } from "../../../utils/dateUtils";
|
||||||
|
|
||||||
|
|
||||||
const JobStatusLog = ({ data }) => {
|
const JobStatusLog = ({ data }) => {
|
||||||
return (
|
return (
|
||||||
@ -1,30 +1,32 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import Breadcrumb from "../common/Breadcrumb";
|
import Breadcrumb from "../../common/Breadcrumb";
|
||||||
import Label from "../common/Label";
|
import Label from "../../common/Label";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { defaultJobValue, jobSchema } from "./ServiceProjectSchema";
|
import { defaultJobValue, jobSchema } from "../ServiceProjectSchema";
|
||||||
import {
|
import {
|
||||||
|
useBranches,
|
||||||
useCreateServiceProjectJob,
|
useCreateServiceProjectJob,
|
||||||
useJobTags,
|
useJobTags,
|
||||||
useServiceProjectJobDetails,
|
useServiceProjectJobDetails,
|
||||||
useServiceProjects,
|
useServiceProjects,
|
||||||
useUpdateServiceProjectJob,
|
useUpdateServiceProjectJob,
|
||||||
} from "../../hooks/useServiceProject";
|
} from "../../../hooks/useServiceProject";
|
||||||
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
import { ITEMS_PER_PAGE } from "../../../utils/constants";
|
||||||
import DatePicker from "../common/DatePicker";
|
import DatePicker from "../../common/DatePicker";
|
||||||
import PmsEmployeeInputTag from "../common/PmsEmployeeInputTag";
|
import PmsEmployeeInputTag from "../../common/PmsEmployeeInputTag";
|
||||||
import TagInput from "../common/TagInput";
|
import TagInput from "../../common/TagInput";
|
||||||
import { localToUtc } from "../../utils/appUtils";
|
import { localToUtc } from "../../../utils/appUtils";
|
||||||
import SelectField from "../common/Forms/SelectField";
|
import SelectField from "../../common/Forms/SelectField";
|
||||||
import {
|
import {
|
||||||
AppFormController,
|
AppFormController,
|
||||||
AppFormProvider,
|
AppFormProvider,
|
||||||
useAppForm,
|
useAppForm,
|
||||||
} from "../../hooks/appHooks/useAppForm";
|
} from "../../../hooks/appHooks/useAppForm";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch } from "react-redux";
|
||||||
import { useJobStatus } from "../../hooks/masterHook/useMaster";
|
import { useJobStatus } from "../../../hooks/masterHook/useMaster";
|
||||||
import { useServiceProjectJobContext } from "./Jobs";
|
import { useServiceProjectJobContext } from "../Jobs";
|
||||||
|
import { SelectFieldSearch } from "../../common/Forms/SelectFieldServerSide";
|
||||||
|
|
||||||
const ManageJob = ({ Job }) => {
|
const ManageJob = ({ Job }) => {
|
||||||
const { setManageJob, setSelectedJob } = useServiceProjectJobContext();
|
const { setManageJob, setSelectedJob } = useServiceProjectJobContext();
|
||||||
@ -40,6 +42,7 @@ const ManageJob = ({ Job }) => {
|
|||||||
watch,
|
watch,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
|
setValue,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = methods;
|
} = methods;
|
||||||
|
|
||||||
@ -162,6 +165,7 @@ const ManageJob = ({ Job }) => {
|
|||||||
dueDate: JobData.dueDate ?? null,
|
dueDate: JobData.dueDate ?? null,
|
||||||
tags: JobData.tags ?? [],
|
tags: JobData.tags ?? [],
|
||||||
statusId: JobData.status.id,
|
statusId: JobData.status.id,
|
||||||
|
projectBranchId : JobData?.projectBranch?.id
|
||||||
});
|
});
|
||||||
}, [JobData, Job, projectId]);
|
}, [JobData, Job, projectId]);
|
||||||
return (
|
return (
|
||||||
@ -199,7 +203,7 @@ const ManageJob = ({ Job }) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-12 col-md-6 mb-2 mb-md-4">
|
<div className="col-12 col-md-6 mb-2 mb-md-4">
|
||||||
<Label required>Select Employee</Label>
|
<Label>Select Employee</Label>
|
||||||
<PmsEmployeeInputTag
|
<PmsEmployeeInputTag
|
||||||
control={control}
|
control={control}
|
||||||
name="assignees"
|
name="assignees"
|
||||||
@ -238,7 +242,20 @@ const ManageJob = ({ Job }) => {
|
|||||||
name="tags"
|
name="tags"
|
||||||
label="Tag"
|
label="Tag"
|
||||||
placeholder="Enter Tag"
|
placeholder="Enter Tag"
|
||||||
required
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-12 col-md-6 mb-2 mb-md-4">
|
||||||
|
<SelectFieldSearch
|
||||||
|
label="Select Branch"
|
||||||
|
placeholder="Select Branch"
|
||||||
|
value={watch("projectBranchId")}
|
||||||
|
onChange={(val) => setValue("projectBranchId", val)}
|
||||||
|
valueKey="id"
|
||||||
|
labelKey="branchName"
|
||||||
|
hookParams={[projectId, true, 10, 1]}
|
||||||
|
useFetchHook={useBranches}
|
||||||
|
isMultiple={false}
|
||||||
|
disabled={Job}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
@ -1,25 +1,27 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { useServiceProjectJobDetails } from "../../hooks/useServiceProject";
|
import { useServiceProjectJobDetails } from "../../../hooks/useServiceProject";
|
||||||
import { SpinnerLoader } from "../common/Loader";
|
import { SpinnerLoader } from "../../common/Loader";
|
||||||
import Error from "../common/Error";
|
import Error from "../../common/Error";
|
||||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
import { formatUTCToLocalTime } from "../../../utils/dateUtils";
|
||||||
import Avatar from "../common/Avatar";
|
import Avatar from "../../common/Avatar";
|
||||||
import EmployeeAvatarGroup from "../common/EmployeeAvatarGroup";
|
import EmployeeAvatarGroup from "../../common/EmployeeAvatarGroup";
|
||||||
import JobStatusLog from "./JobStatusLog";
|
import { daysLeft, getJobStatusBadge } from "../../../utils/appUtils";
|
||||||
import JobComments from "./JobComments";
|
import HoverPopup from "../../common/HoverPopup";
|
||||||
import { daysLeft, getJobStatusBadge } from "../../utils/appUtils";
|
|
||||||
import HoverPopup from "../common/HoverPopup";
|
|
||||||
import ChangeStatus from "./ChangeStatus";
|
import ChangeStatus from "./ChangeStatus";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { STATUS_JOB_CLOSED } from "../../utils/constants";
|
import { STATUS_JOB_CLOSED } from "../../../utils/constants";
|
||||||
import Tooltip from "../common/Tooltip";
|
import Tooltip from "../../common/Tooltip";
|
||||||
|
import BranchDetails from "../ServiceProjectBranch/BranchDetails";
|
||||||
|
import { JobDetailsSkeleton } from "../ServiceProjectSeketon";
|
||||||
|
import JobComments from "./JobComments";
|
||||||
|
import JobStatusLog from "./JobStatusLog";
|
||||||
|
|
||||||
const ManageJobTicket = ({ Job }) => {
|
const ManageJobTicket = ({ Job }) => {
|
||||||
const { projectId } = useParams();
|
const { projectId } = useParams();
|
||||||
const { data, isLoading, isError, error } = useServiceProjectJobDetails(
|
const { data, isLoading, isError, error } = useServiceProjectJobDetails(
|
||||||
Job?.job
|
Job?.job
|
||||||
);
|
);
|
||||||
|
const drawerRef = useRef();
|
||||||
const tabsData = [
|
const tabsData = [
|
||||||
{
|
{
|
||||||
id: "comment",
|
id: "comment",
|
||||||
@ -37,7 +39,7 @@ const ManageJobTicket = ({ Job }) => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (isLoading) return <SpinnerLoader />;
|
if (isLoading) return <JobDetailsSkeleton />;
|
||||||
if (isError)
|
if (isError)
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -45,7 +47,11 @@ const ManageJobTicket = ({ Job }) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div className="row text-start">
|
<div
|
||||||
|
className=" text-start position-relative"
|
||||||
|
ref={drawerRef}
|
||||||
|
style={{ overflow: "visible" }}
|
||||||
|
>
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<h6 className="fs-5 fw-semibold">{data?.title}</h6>
|
<h6 className="fs-5 fw-semibold">{data?.title}</h6>
|
||||||
<div className="d-flex justify-content-between align-items-start flex-wrap mb-2">
|
<div className="d-flex justify-content-between align-items-start flex-wrap mb-2">
|
||||||
@ -54,16 +60,16 @@ const ManageJobTicket = ({ Job }) => {
|
|||||||
{data?.jobTicketUId || "N/A"}
|
{data?.jobTicketUId || "N/A"}
|
||||||
</p>
|
</p>
|
||||||
<div className="d-flex flex-column align-items-end gap-3 mb-3">
|
<div className="d-flex flex-column align-items-end gap-3 mb-3">
|
||||||
<div className="d-flex flex-row gap-2">
|
<div className="d-flex flex-row gap-2 position-relative">
|
||||||
<span className={`badge ${getJobStatusBadge(data?.status?.id)}`}>
|
<span className={`badge ${getJobStatusBadge(data?.status?.id)}`}>
|
||||||
{data?.status?.displayName}
|
{data?.status?.displayName}
|
||||||
</span>
|
</span>
|
||||||
{STATUS_JOB_CLOSED !== data?.status?.id && (
|
{STATUS_JOB_CLOSED !== data?.status?.id && (
|
||||||
<HoverPopup
|
<HoverPopup
|
||||||
id="STATUS_CHANEG"
|
id="STATUS_CHANEG"
|
||||||
title="Change Status"
|
|
||||||
Mode="click"
|
Mode="click"
|
||||||
className=""
|
className=""
|
||||||
|
align="right"
|
||||||
content={
|
content={
|
||||||
<ChangeStatus
|
<ChangeStatus
|
||||||
statusId={data?.status?.id}
|
statusId={data?.status?.id}
|
||||||
@ -90,23 +96,33 @@ const ManageJobTicket = ({ Job }) => {
|
|||||||
<p>{data?.description || "N/A"}</p>
|
<p>{data?.description || "N/A"}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="d-flex justify-content-between mb-4">
|
<div className="d-flex justify-content-between align-items-center mb-4">
|
||||||
<div className="d-flex flex-row gap-1 fw-medium">
|
<div className="d-flex flex-row gap-1 text-secondry">
|
||||||
<i className="bx bx-calendar"></i>{" "}
|
<i className="bx bx-calendar"></i>{" "}
|
||||||
<span>
|
<span>
|
||||||
Created Date : {formatUTCToLocalTime(data?.createdAt, true)}
|
Created Date : {formatUTCToLocalTime(data?.createdAt, true)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="d-flex flex-row gap-2 text-wraps">
|
||||||
|
{data?.tags?.map((tag, ind) => (
|
||||||
|
<span
|
||||||
|
key={`${ind}0-${tag?.name}`}
|
||||||
|
className="badge bg-label-primary"
|
||||||
|
>
|
||||||
|
{tag?.name}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="d-flex justify-content-md-between ">
|
<div className="d-flex justify-content-md-between ">
|
||||||
<div className="d-flex flex-row gap-5">
|
<div className="d-flex flex-row gap-5">
|
||||||
<span className="fw-medium">
|
<span className="text-secondry">
|
||||||
<i className="bx bx-calendar"></i> Start Date :{" "}
|
<i className="bx bx-calendar"></i> Start Date :{" "}
|
||||||
{formatUTCToLocalTime(data?.startDate)}
|
{formatUTCToLocalTime(data?.startDate)}
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
<i className="bx bx-right-arrow-alt"></i>{" "}
|
<i className="bx bx-right-arrow-alt"></i>{" "}
|
||||||
<span className="fw-medium">
|
<span className="text-secondry">
|
||||||
<i className="bx bx-calendar"></i> Due on :{" "}
|
<i className="bx bx-calendar"></i> Due on :{" "}
|
||||||
{formatUTCToLocalTime(data?.startDate)}
|
{formatUTCToLocalTime(data?.startDate)}
|
||||||
</span>
|
</span>
|
||||||
@ -116,7 +132,7 @@ const ManageJobTicket = ({ Job }) => {
|
|||||||
const { days, color } = daysLeft(data?.startDate, data?.dueDate);
|
const { days, color } = daysLeft(data?.startDate, data?.dueDate);
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
<span className="fw-medium me-1">Days Left:</span>
|
<span className="text-secondry me-1">Days Left:</span>
|
||||||
<span className={`badge bg-${color}`}>
|
<span className={`badge bg-${color}`}>
|
||||||
{days !== null ? `${days} days` : "N/A"}
|
{days !== null ? `${days} days` : "N/A"}
|
||||||
</span>
|
</span>
|
||||||
@ -124,61 +140,82 @@ const ManageJobTicket = ({ Job }) => {
|
|||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
<div className="d-block mt-4 mb-3">
|
{data?.projectBranch && (
|
||||||
<div className="row align-items-start align-items-md-start gap-2 mb-1">
|
<div className="d-flex gap-3 my-2 position-relative" ref={drawerRef} >
|
||||||
<div className="col-12 col-md-auto">
|
<span className="text-secondary">
|
||||||
<small className="fs-6 fw-medium">Created By</small>
|
<i className="bx bx-buildings"></i> Branch Name:
|
||||||
</div>
|
</span>
|
||||||
<div className="col d-flex flex-row align-items-center ">
|
|
||||||
|
<HoverPopup
|
||||||
|
id="BRANCH_DETAILS"
|
||||||
|
Mode="click"
|
||||||
|
align="auto"
|
||||||
|
boundaryRef={drawerRef}
|
||||||
|
content={<BranchDetails branch={data?.projectBranch?.id} />}
|
||||||
|
>
|
||||||
|
<span className="text-decoration-underline cursor-pointer">
|
||||||
|
{data?.projectBranch?.branchName}
|
||||||
|
</span>
|
||||||
|
</HoverPopup>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div className="border-top my-1">
|
||||||
|
<p className="m-0 py-1">
|
||||||
|
<i className="bx bx-group"></i> Peoples
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Created By */}
|
||||||
|
<div className="d-flex justify-content-between align-items-start w-100">
|
||||||
|
<p className="text-secondary m-0 me-3">Created By</p>
|
||||||
|
|
||||||
|
<div className="flex-grow-1 d-flex align-items-center gap-2">
|
||||||
<Avatar
|
<Avatar
|
||||||
size="xs"
|
size="xs"
|
||||||
firstName={data?.createdBy?.firstName}
|
firstName={data?.createdBy?.firstName}
|
||||||
lastName={data?.createdBy?.lastName}
|
lastName={data?.createdBy?.lastName}
|
||||||
/>{" "}
|
/>
|
||||||
<div className="d-flex flex-row align-items-center">
|
<div className="d-flex flex-column">
|
||||||
<p className="m-0">{`${data?.createdBy?.firstName} ${data?.createdBy?.lastName}`}</p>
|
<p className="m-0 text-truncate">
|
||||||
<small className="text-secondary ms-1">
|
{data?.createdBy?.firstName} {data?.createdBy?.lastName}
|
||||||
({data?.createdBy?.jobRoleName})
|
</p>
|
||||||
|
<small className="text-secondary text-xs">
|
||||||
|
{data?.createdBy?.jobRoleName}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{data?.assignees?.length > 0 && (
|
{/* Assigned To */}
|
||||||
<div className="row align-items-start align-items-md-start gap-2">
|
<div className="d-flex flex-column flex-md-row align-items-start w-100 mt-2">
|
||||||
<div className="col-12 col-md-auto">
|
<p className="text-secondary m-0 me-3">Assigned To</p>
|
||||||
<small className="fs-6 fw-medium">Assigned To</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col">
|
<div className="flex-grow-1">
|
||||||
<div className="row gap-4">
|
<div className="d-flex flex-wrap gap-3">
|
||||||
{data?.assignees?.map((emp) => (
|
{data?.assignees?.map((emp) => (
|
||||||
<div
|
<div key={emp.id} className="d-flex align-items-center">
|
||||||
key={emp.id}
|
<Avatar
|
||||||
className="col-6 col-sm-6 col-md-4 col-lg-4"
|
size="xs"
|
||||||
>
|
firstName={emp.firstName}
|
||||||
<div className="d-flex align-items-center gap-2">
|
lastName={emp.lastName}
|
||||||
<Avatar
|
/>
|
||||||
size="xs"
|
|
||||||
firstName={emp.firstName}
|
|
||||||
lastName={emp.lastName}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="d-flex flex-column">
|
<div className="d-flex flex-column ms-2 text-truncate">
|
||||||
<span className=" text-truncate">
|
<span className="text-truncate">
|
||||||
{emp.firstName} {emp.lastName}
|
{emp.firstName} {emp.lastName}
|
||||||
</span>
|
</span>
|
||||||
<small className="text-secondary text-truncate">
|
<small className="text-secondary text-xs text-truncate">
|
||||||
{emp.jobRoleName}
|
{emp.jobRoleName}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useAppForm } from "../../../hooks/appHooks/useAppForm";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { JobCommentSchema } from "../ServiceProjectSchema";
|
||||||
|
import Avatar from "../../common/Avatar";
|
||||||
|
import Filelist from "../../Expenses/Filelist";
|
||||||
|
|
||||||
|
const UpdateJobComment = () => {
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
watch,
|
||||||
|
reset,
|
||||||
|
setValue,
|
||||||
|
formState: { errors },
|
||||||
|
} = useAppForm({
|
||||||
|
resolver: zodResolver(JobCommentSchema),
|
||||||
|
defaultValues: { comment: "", attachments: [] },
|
||||||
|
});
|
||||||
|
const onSubmit = () => {};
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div className="d-flex">
|
||||||
|
<Avatar firstName={"A"} lastName={"D"} />
|
||||||
|
|
||||||
|
<div className="flex-grow-1">
|
||||||
|
<textarea
|
||||||
|
className="form-control"
|
||||||
|
rows={3}
|
||||||
|
placeholder="Write your comment..."
|
||||||
|
{...register("comment")}
|
||||||
|
></textarea>
|
||||||
|
|
||||||
|
{errors?.comment && (
|
||||||
|
<small className="danger-text">{errors?.comment?.message}</small>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* <div className="flex-grow-1 ms-10 mt-2">
|
||||||
|
{files?.length > 0 && (
|
||||||
|
<Filelist files={} removeFile={removeFile} />
|
||||||
|
)}
|
||||||
|
</div> */}
|
||||||
|
<div className="d-flex justify-content-end gap-2 align-items-center text-end mt-3 ms-10 ms-md-0">
|
||||||
|
<div
|
||||||
|
onClick={() => document.getElementById("attachments").click()}
|
||||||
|
className="cursor-pointer"
|
||||||
|
style={{ whiteSpace: "nowrap" }}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".pdf,.jpg,.jpeg,.png"
|
||||||
|
id="attachments"
|
||||||
|
multiple
|
||||||
|
className="d-none"
|
||||||
|
{...register("attachments")}
|
||||||
|
onChange={(e) => {
|
||||||
|
onFileChange(e);
|
||||||
|
e.target.value = "";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<i className="bx bx-sm bx-paperclip mb-1 me-1"></i>
|
||||||
|
Add Attachment
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="btn btn-primary btn-sm px-1 py-1"
|
||||||
|
type="submit"
|
||||||
|
disabled={!watch("comment")?.trim() || isPending}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UpdateJobComment;
|
||||||
@ -4,18 +4,27 @@ import { useServiceProject } from "../../hooks/useServiceProject";
|
|||||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||||
import ManageServiceProject from "./ManageServiceProject";
|
import ManageServiceProject from "./ManageServiceProject";
|
||||||
import GlobalModel from "../common/GlobalModel";
|
import GlobalModel from "../common/GlobalModel";
|
||||||
|
import { SpinnerLoader } from "../common/Loader";
|
||||||
|
import ServiceBranch from "./ServiceProjectBranch/ServiceBranch";
|
||||||
|
import ServiceProfile from "./ServiceProfile";
|
||||||
|
|
||||||
const ServiceProjectProfile = () => {
|
const ServiceProjectProfile = () => {
|
||||||
const { projectId } = useParams();
|
const { projectId } = useParams();
|
||||||
const [IsOpenModal, setIsOpenModal] = useState(false);
|
const [IsOpenModal, setIsOpenModal] = useState(false);
|
||||||
const { data, isLoading, isError, error } = useServiceProject(projectId);
|
const { data, isLoading, isError, error } = useServiceProject(projectId);
|
||||||
if (isLoading) {
|
if (isLoading)
|
||||||
return <div className="">Loadng.</div>;
|
return (
|
||||||
}
|
<div className="py-8">
|
||||||
|
<SpinnerLoader />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{IsOpenModal && (
|
{IsOpenModal && (
|
||||||
<GlobalModel isOpen={IsOpenModal} closeModal={() => setIsOpenModal(false)}>
|
<GlobalModel
|
||||||
|
isOpen={IsOpenModal}
|
||||||
|
closeModal={() => setIsOpenModal(false)}
|
||||||
|
>
|
||||||
<ManageServiceProject
|
<ManageServiceProject
|
||||||
serviceProjectId={projectId}
|
serviceProjectId={projectId}
|
||||||
onClose={() => setIsOpenModal(false)}
|
onClose={() => setIsOpenModal(false)}
|
||||||
@ -24,98 +33,13 @@ const ServiceProjectProfile = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="row py-2">
|
<div className="row py-2">
|
||||||
<div className="col-md-6 col-lg-4 order-2 mb-6">
|
<div className="col-md-6 col-lg-5 order-2 mb-6">
|
||||||
<div className="card mb-4">
|
<ServiceProfile data={data} setIsOpenModal={setIsOpenModal}/>
|
||||||
<div className="card-header text-start">
|
|
||||||
<h5 className="card-action-title mb-0 ps-1">
|
|
||||||
{" "}
|
|
||||||
<i className="fa fa-building rounded-circle text-primary"></i>
|
|
||||||
<span className="ms-2 fw-bold">Project Profile</span>
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<div className="card-body">
|
|
||||||
<ul className="list-unstyled my-3 ps-0 text-start">
|
|
||||||
|
|
||||||
<li className="d-flex mb-3">
|
|
||||||
<div className="d-flex align-items-start" style={{ minWidth: "150px" }}>
|
|
||||||
<i className="bx bx-cog"></i>
|
|
||||||
<span className="fw-medium mx-2 text-nowrap">Name:</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content section that wraps nicely */}
|
|
||||||
<div className="flex-grow-1 text-start text-wrap">
|
|
||||||
{data.name}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li className="d-flex mb-3">
|
|
||||||
<div className="d-flex align-items-center" style={{ width: '150px' }}>
|
|
||||||
<i className="bx bx-fingerprint"></i>
|
|
||||||
<span className="fw-medium mx-2">Nick Name:</span>
|
|
||||||
</div>
|
|
||||||
<span>{data.shortName}</span>
|
|
||||||
</li>
|
|
||||||
<li className="d-flex mb-3">
|
|
||||||
<div className="d-flex align-items-center" style={{ width: '150px' }}>
|
|
||||||
<i className="bx bx-check"></i>
|
|
||||||
<span className="fw-medium mx-2">Assign Date:</span>
|
|
||||||
</div>
|
|
||||||
<span>
|
|
||||||
{data.assignedDate ? formatUTCToLocalTime(data.assignedDate) : "NA"}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li className="d-flex mb-3">
|
|
||||||
<div className="d-flex align-items-center" style={{ width: '150px' }}>
|
|
||||||
<i className="bx bx-trophy"></i>
|
|
||||||
<span className="fw-medium mx-2">Status:</span>
|
|
||||||
</div>
|
|
||||||
<span>{data?.status.status}</span>
|
|
||||||
</li>
|
|
||||||
<li className="d-flex mb-3">
|
|
||||||
<div className="d-flex align-items-center" style={{ width: '150px' }}>
|
|
||||||
<i className="bx bx-user"></i>
|
|
||||||
<span className="fw-medium mx-2">Contact:</span>
|
|
||||||
</div>
|
|
||||||
<span>{data.contactName}</span>
|
|
||||||
</li>
|
|
||||||
<li className="d-flex mb-3">
|
|
||||||
{/* Label section with icon */}
|
|
||||||
<div className="d-flex align-items-start" style={{ minWidth: "150px" }}>
|
|
||||||
<i className="bx bx-flag mt-1"></i>
|
|
||||||
<span className="fw-medium mx-2 text-nowrap">Address:</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content section that wraps nicely */}
|
|
||||||
<div className="flex-grow-1 text-start text-wrap">
|
|
||||||
{data.address}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<li className="d-flex justify-content-center mt-4"> {/* Added mt-4 for some top margin */}
|
|
||||||
|
|
||||||
<a className="d-flex justify-content-center mt-4"> {/* Added mt-4 for some top margin */}
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-sm btn-primary"
|
|
||||||
data-bs-toggle="modal"
|
|
||||||
data-bs-target="#edit-project-modal"
|
|
||||||
onClick={() => setIsOpenModal(true)}
|
|
||||||
>
|
|
||||||
Modify Details
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="col-md-6 col-lg-7 order-2 mb-6">
|
||||||
|
<ServiceBranch />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -50,6 +50,10 @@ export const defaultProjectValues = {
|
|||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//#region JobSchema
|
//#region JobSchema
|
||||||
|
|
||||||
export const TagSchema = z.object({
|
export const TagSchema = z.object({
|
||||||
@ -70,6 +74,8 @@ export const jobSchema = z.object({
|
|||||||
tags: z.array(TagSchema).optional().default([]),
|
tags: z.array(TagSchema).optional().default([]),
|
||||||
|
|
||||||
statusId: z.string().optional().nullable(),
|
statusId: z.string().optional().nullable(),
|
||||||
|
|
||||||
|
projectBranchId: z.string().optional().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
||||||
@ -109,6 +115,52 @@ export const defaultJobValue = {
|
|||||||
startDate: null,
|
startDate: null,
|
||||||
dueDate: null,
|
dueDate: null,
|
||||||
tags: [],
|
tags: [],
|
||||||
|
branchId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//#region Branch
|
||||||
|
|
||||||
|
export const BranchSchema = () =>
|
||||||
|
z.object({
|
||||||
|
projectId: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, { message: "Project is required" }),
|
||||||
|
|
||||||
|
branchName: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, { message: "Branch Name is required" }),
|
||||||
|
|
||||||
|
contactInformation: z.string().optional(),
|
||||||
|
address: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, { message: "Address is required" }),
|
||||||
|
|
||||||
|
branchType: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, { message: "Branch Type is required" }),
|
||||||
|
|
||||||
|
googleMapUrl: z
|
||||||
|
.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const defaultBranches = {
|
||||||
|
branchName: "",
|
||||||
|
projectId: "",
|
||||||
|
contactInformation: "",
|
||||||
|
address: "",
|
||||||
|
branchType: "",
|
||||||
|
googleMapUrl: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
//#endregion
|
||||||
138
src/components/ServiceProject/ServiceProjectSeketon.jsx
Normal file
138
src/components/ServiceProject/ServiceProjectSeketon.jsx
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const SkeletonLine = ({ height = 18, width = "100%", className = "" }) => (
|
||||||
|
<div
|
||||||
|
className={`skeleton ${className}`}
|
||||||
|
style={{
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
borderRadius: "4px",
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const BranchDetailsSkeleton = () => {
|
||||||
|
return (
|
||||||
|
<div className="w-100">
|
||||||
|
<div className="d-flex mb-3">
|
||||||
|
<SkeletonLine height={22} width="280px" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row mb-2">
|
||||||
|
<div className="col-4">
|
||||||
|
<SkeletonLine height={16} width="70%" />
|
||||||
|
</div>
|
||||||
|
<div className="col-8">
|
||||||
|
<SkeletonLine height={16} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row mb-2">
|
||||||
|
<div className="col-4">
|
||||||
|
<SkeletonLine height={16} width="70%" />
|
||||||
|
</div>
|
||||||
|
<div className="col-8">
|
||||||
|
<SkeletonLine height={16} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row mb-2">
|
||||||
|
<div className="col-4">
|
||||||
|
<SkeletonLine height={16} width="70%" />
|
||||||
|
</div>
|
||||||
|
<div className="col-8">
|
||||||
|
<SkeletonLine height={16} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row mb-2">
|
||||||
|
<div className="col-4">
|
||||||
|
<SkeletonLine height={16} width="70%" />
|
||||||
|
</div>
|
||||||
|
<div className="col-8">
|
||||||
|
<SkeletonLine height={16} width="90%" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row mb-2">
|
||||||
|
<div className="col-4">
|
||||||
|
<SkeletonLine height={16} width="70%" />
|
||||||
|
</div>
|
||||||
|
<div className="col-8 d-flex gap-2 align-items-center">
|
||||||
|
<SkeletonLine height={16} width="60%" />
|
||||||
|
<SkeletonLine height={16} width="20px" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const JobDetailsSkeleton = () => {
|
||||||
|
return (
|
||||||
|
<div className="row text-start">
|
||||||
|
<div className="col-12">
|
||||||
|
{/* Title */}
|
||||||
|
<SkeletonLine height={24} width="50%" />
|
||||||
|
|
||||||
|
{/* Job ID + Status */}
|
||||||
|
<div className="d-flex justify-content-between align-items-start flex-wrap mb-3 mt-2">
|
||||||
|
<SkeletonLine height={18} width="30%" />
|
||||||
|
<div className="d-flex flex-row gap-2">
|
||||||
|
<SkeletonLine height={22} width="70px" />
|
||||||
|
<SkeletonLine height={22} width="22px" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<SkeletonLine height={40} width="100%" />
|
||||||
|
|
||||||
|
{/* Created Date */}
|
||||||
|
<div className="d-flex my-3">
|
||||||
|
<SkeletonLine height={16} width="40%" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Start / Due Date */}
|
||||||
|
<div className="d-flex justify-content-between mb-4">
|
||||||
|
<SkeletonLine height={16} width="50%" />
|
||||||
|
<SkeletonLine height={22} width="70px" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Branch Name */}
|
||||||
|
<div className="d-flex flex-row gap-3 my-2">
|
||||||
|
<SkeletonLine height={16} width="30%" />
|
||||||
|
<SkeletonLine height={16} width="40%" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Created By */}
|
||||||
|
<div className="row align-items-center my-3">
|
||||||
|
<div className="col-12 col-md-auto mb-2">
|
||||||
|
<SkeletonLine height={16} width="80px" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col d-flex align-items-center gap-2">
|
||||||
|
<SkeletonLine height={30} width="30px" /> {/* Avatar */}
|
||||||
|
<SkeletonLine height={16} width="40%" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Assigned To */}
|
||||||
|
<div className="row mt-2">
|
||||||
|
<div className="col-12 col-md-auto mb-2">
|
||||||
|
<SkeletonLine height={16} width="90px" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="mt-4">
|
||||||
|
<div className="d-flex gap-3 mb-3">
|
||||||
|
<SkeletonLine height={35} width="80px" />
|
||||||
|
<SkeletonLine height={35} width="80px" />
|
||||||
|
<SkeletonLine height={35} width="80px" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SkeletonLine height={150} width="100%" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -26,13 +26,6 @@ const ServiceProjectCard = ({ project, isCore = true }) => {
|
|||||||
const ManageProject = useHasUserPermission(MANAGE_PROJECT);
|
const ManageProject = useHasUserPermission(MANAGE_PROJECT);
|
||||||
const { setMangeProject, setManageServiceProject } = useProjectContext();
|
const { setMangeProject, setManageServiceProject } = useProjectContext();
|
||||||
|
|
||||||
const getProgress = (planned, completed) => {
|
|
||||||
return (completed * 100) / planned + "%";
|
|
||||||
};
|
|
||||||
const getProgressInNumber = (planned, completed) => {
|
|
||||||
return (completed * 100) / planned;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => setShowModal(false);
|
const handleClose = () => setShowModal(false);
|
||||||
|
|
||||||
const handleViewProject = () => {
|
const handleViewProject = () => {
|
||||||
@ -43,10 +36,6 @@ const ServiceProjectCard = ({ project, isCore = true }) => {
|
|||||||
navigate(`/service-projects/${project.id}`);
|
navigate(`/service-projects/${project.id}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const handleViewActivities = () => {
|
|
||||||
dispatch(setProjectId(project.id));
|
|
||||||
navigate(`/activities/records?project=${project.id}`);
|
|
||||||
};
|
|
||||||
const handleManage = () => {
|
const handleManage = () => {
|
||||||
if (isCore) {
|
if (isCore) {
|
||||||
setMangeProject({
|
setMangeProject({
|
||||||
@ -68,6 +57,8 @@ const ServiceProjectCard = ({ project, isCore = true }) => {
|
|||||||
DeleteProject(projectId, false);
|
DeleteProject(projectId, false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
@ -98,7 +89,7 @@ const ServiceProjectCard = ({ project, isCore = true }) => {
|
|||||||
>
|
>
|
||||||
{project?.shortName ? project?.shortName : project?.name}
|
{project?.shortName ? project?.shortName : project?.name}
|
||||||
</h5>
|
</h5>
|
||||||
<div className="client-info text-body">
|
<div className="client-info text-body text-start">
|
||||||
<span>{project?.shortName ? project?.name : ""}</span>
|
<span>{project?.shortName ? project?.name : ""}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -138,14 +129,6 @@ const ServiceProjectCard = ({ project, isCore = true }) => {
|
|||||||
<span className="align-left">Modify</span>
|
<span className="align-left">Modify</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{isCore && (
|
|
||||||
<li onClick={handleViewActivities}>
|
|
||||||
<a className="dropdown-item">
|
|
||||||
<i className="bx bx-task me-2"></i>
|
|
||||||
<span className="align-left">Activities</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
{!isCore && (
|
{!isCore && (
|
||||||
<li
|
<li
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
|
|||||||
@ -0,0 +1,209 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { MANAGE_PROJECT, PROJECT_STATUS } from "../../../utils/constants";
|
||||||
|
import { useProjects } from "../../../hooks/useProjects";
|
||||||
|
import { formatNumber, formatUTCToLocalTime } from "../../../utils/dateUtils";
|
||||||
|
import ProgressBar from "../../common/ProgressBar";
|
||||||
|
import {
|
||||||
|
getProjectStatusColor,
|
||||||
|
getProjectStatusName,
|
||||||
|
} from "../../../utils/projectStatus";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { setProjectId } from "../../../slices/localVariablesSlice";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useHasUserPermission } from "../../../hooks/useHasUserPermission";
|
||||||
|
import { useProjectContext } from "../../../pages/project/ProjectPage";
|
||||||
|
import usePagination from "../../../hooks/usePagination";
|
||||||
|
import Pagination from "../../common/Pagination";
|
||||||
|
|
||||||
|
const ServiceProjectList = ({
|
||||||
|
data,
|
||||||
|
currentPage,
|
||||||
|
totalPages,
|
||||||
|
paginate,
|
||||||
|
isCore = true,
|
||||||
|
}) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { setMangeProject, setManageServiceProject } = useProjectContext();
|
||||||
|
const handleClose = () => setShowModal(false);
|
||||||
|
|
||||||
|
// check Permissions
|
||||||
|
const canManageProject = useHasUserPermission(MANAGE_PROJECT);
|
||||||
|
|
||||||
|
const projectColumns = [
|
||||||
|
{
|
||||||
|
key: "projectName",
|
||||||
|
label: "Project Name",
|
||||||
|
className: "text-start py-3",
|
||||||
|
getValue: (p) => (
|
||||||
|
<div
|
||||||
|
className="text-primary cursor-pointer fw-bold py-3"
|
||||||
|
onClick={() => {
|
||||||
|
dispatch(setProjectId(p.id));
|
||||||
|
navigate(`/service-projects/${p.id}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{p.shortName ? `${p.name} (${p.shortName})` : p.name}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "client.contactPerson",
|
||||||
|
label: "Contact Person",
|
||||||
|
className: "text-start small",
|
||||||
|
getValue: (p) => p.client?.contactPerson || "N/A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "assignedDate",
|
||||||
|
label: "Assign Date",
|
||||||
|
className: "text-center small",
|
||||||
|
getValue: (p) => formatUTCToLocalTime(p.assignedDate),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "status",
|
||||||
|
label: "Status",
|
||||||
|
className: "text-center small",
|
||||||
|
getValue: (p) => (
|
||||||
|
<span className={`badge ${getProjectStatusColor(p.status?.id)}`}>
|
||||||
|
{p.status?.status}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleViewProject = (p) => {
|
||||||
|
if (isCore) {
|
||||||
|
dispatch(setProjectId(p.id));
|
||||||
|
navigate(`/projects/details`);
|
||||||
|
} else {
|
||||||
|
navigate(`/service-projects/${p.id}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleManage = (p) => {
|
||||||
|
if (isCore) {
|
||||||
|
setMangeProject({
|
||||||
|
isOpen: true,
|
||||||
|
Project: p.id,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setManageServiceProject({
|
||||||
|
isOpen: true,
|
||||||
|
project: p.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="card page-min-h py-4 px-6 shadow-sm">
|
||||||
|
<div className="table-responsive text-nowrap page-min-h">
|
||||||
|
<table className="table table-hover align-middle m-0">
|
||||||
|
<thead className="border-bottom ">
|
||||||
|
<tr>
|
||||||
|
{projectColumns.map((col) => (
|
||||||
|
<th
|
||||||
|
key={col.key}
|
||||||
|
colSpan={col.colSpan}
|
||||||
|
className={`${col.className} table_header_border`}
|
||||||
|
>
|
||||||
|
{col.label}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
<th className="text-center py-3">Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data?.length > 0 ? (
|
||||||
|
data.map((project) => (
|
||||||
|
<tr key={project.id}>
|
||||||
|
{projectColumns.map((col) => (
|
||||||
|
<td
|
||||||
|
key={col.key}
|
||||||
|
colSpan={col.colSpan}
|
||||||
|
className={`${col.className} py-5`}
|
||||||
|
style={{ paddingTop: "20px", paddingBottom: "20px" }}
|
||||||
|
>
|
||||||
|
{col.getValue
|
||||||
|
? col.getValue(project)
|
||||||
|
: project[col.key] || "N/A"}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
<td
|
||||||
|
className={`mx-2 ${
|
||||||
|
canManageProject ? "d-sm-table-cell" : "d-none"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<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">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
aria-label="click to View details"
|
||||||
|
className="dropdown-item"
|
||||||
|
onClick={() => handleViewProject(project)}
|
||||||
|
>
|
||||||
|
<i className="bx bx-detail me-2"></i>
|
||||||
|
<span className="align-left">View details</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
className="dropdown-item"
|
||||||
|
onClick={() => handleManage(project)}
|
||||||
|
>
|
||||||
|
<i className="bx bx-pencil me-2"></i>
|
||||||
|
<span className="align-left">Modify</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<tr
|
||||||
|
className="no-hover"
|
||||||
|
style={{
|
||||||
|
pointerEvents: "none",
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
colSpan={projectColumns.length + 1}
|
||||||
|
className="text-center align-middle"
|
||||||
|
style={{ height: "300px", borderBottom: "none" }}
|
||||||
|
>
|
||||||
|
No Service projects available
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Pagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={totalPages}
|
||||||
|
paginate={paginate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ServiceProjectList;
|
||||||
@ -162,7 +162,7 @@ const ServiceProjectTeamAllocation = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="col-12 d-flex flex-row gap-2 flex-wrap">
|
<div className="col-12 d-flex flex-row gap-2 flex-wrap">
|
||||||
{selectedEmployees.map((e) => (
|
{selectedEmployees.map((e) => (
|
||||||
<EmployeeChip handleRemove={handleRemove} employee={e} />
|
<EmployeeChip key={`${e.id}-emp`} handleRemove={handleRemove} employee={e} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -104,8 +104,7 @@ const SubScriptionHistory = ({ tenantId }) => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="dropdown-item py-1"
|
className="dropdown-item py-1"
|
||||||
onClick={() =>
|
onClick={() =>{}
|
||||||
console.log("Download clicked for", item.id)
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<i className="bx bx-cloud-download bx-sm"></i> Download
|
<i className="bx bx-cloud-download bx-sm"></i> Download
|
||||||
|
|||||||
@ -93,11 +93,9 @@ const TenantForm = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onSubmitTenant = (data) => {
|
const onSubmitTenant = (data) => {
|
||||||
console.log("Tenant Data:", data);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmitSubScription = (data) => {
|
const onSubmitSubScription = (data) => {
|
||||||
console.log("Subscription Data:", data);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const newTenantConfig = [
|
const newTenantConfig = [
|
||||||
|
|||||||
@ -27,7 +27,7 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
|
|||||||
|
|
||||||
const selectedProject = useSelectedProject();
|
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(
|
||||||
selectedProject,
|
selectedProject,
|
||||||
searchDebounce,
|
searchDebounce,
|
||||||
@ -40,7 +40,6 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
|
|||||||
);
|
);
|
||||||
const { setProcessedPayment, setAddPayment, setViewCollection } =
|
const { setProcessedPayment, setAddPayment, setViewCollection } =
|
||||||
useCollectionContext();
|
useCollectionContext();
|
||||||
|
|
||||||
const paginate = (page) => {
|
const paginate = (page) => {
|
||||||
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
|
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
|
||||||
setCurrentPage(page);
|
setCurrentPage(page);
|
||||||
@ -113,6 +112,16 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
|
|||||||
),
|
),
|
||||||
align: "text-center",
|
align: "text-center",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "status",
|
||||||
|
label: "Status",
|
||||||
|
getValue: (col) => (
|
||||||
|
<span className={`badge bg-label-${col?.isActive ? "primary" : "danger"}`}>
|
||||||
|
{col?.isActive ? "Active" : "Inactive"}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
align: "text-center",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "amount",
|
key: "amount",
|
||||||
label: "Total Amount",
|
label: "Total Amount",
|
||||||
@ -129,6 +138,7 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
|
|||||||
),
|
),
|
||||||
align: "text-end",
|
align: "text-end",
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
key: "balance",
|
key: "balance",
|
||||||
label: "Balance",
|
label: "Balance",
|
||||||
|
|||||||
@ -16,10 +16,16 @@ import {
|
|||||||
import { formatFileSize, localToUtc } from "../../utils/appUtils";
|
import { formatFileSize, localToUtc } from "../../utils/appUtils";
|
||||||
import { useCollectionContext } from "../../pages/collections/CollectionPage";
|
import { useCollectionContext } from "../../pages/collections/CollectionPage";
|
||||||
import { formatDate } from "../../utils/dateUtils";
|
import { formatDate } from "../../utils/dateUtils";
|
||||||
|
import { SelectProjectField } from "../common/Forms/SelectFieldServerSide";
|
||||||
|
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
||||||
|
import { useOrganizationsList } from "../../hooks/useOrganization";
|
||||||
|
|
||||||
const ManageCollection = ({ collectionId, onClose }) => {
|
const ManageCollection = ({ collectionId, onClose }) => {
|
||||||
const { data, isError, isLoading, error } = useCollection(collectionId);
|
const { data, isError, isLoading, error } = useCollection(collectionId);
|
||||||
const { projectNames, projectLoading } = useProjectName(true);
|
const { projectNames, projectLoading } = useProjectName(true);
|
||||||
|
const { data: organization, isLoading: isLoadingOrganization } =
|
||||||
|
useOrganizationsList(ITEMS_PER_PAGE, 1, true);
|
||||||
|
|
||||||
const methods = useForm({
|
const methods = useForm({
|
||||||
resolver: zodResolver(newCollection),
|
resolver: zodResolver(newCollection),
|
||||||
defaultValues: defaultCollection,
|
defaultValues: defaultCollection,
|
||||||
@ -138,6 +144,7 @@ const ManageCollection = ({ collectionId, onClose }) => {
|
|||||||
taxAmount: data?.taxAmount,
|
taxAmount: data?.taxAmount,
|
||||||
basicAmount: data?.basicAmount,
|
basicAmount: data?.basicAmount,
|
||||||
description: data?.description,
|
description: data?.description,
|
||||||
|
billedToId: data?.billedToId,
|
||||||
attachments: data.attachments
|
attachments: data.attachments
|
||||||
? data.attachments.map((doc) => ({
|
? data.attachments.map((doc) => ({
|
||||||
fileName: doc.fileName,
|
fileName: doc.fileName,
|
||||||
@ -163,36 +170,61 @@ const ManageCollection = ({ collectionId, onClose }) => {
|
|||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="p-0 text-start">
|
<form onSubmit={handleSubmit(onSubmit)} className="p-0 text-start">
|
||||||
<div className="row px-md-1 px-0">
|
<div className="row px-md-1 px-0">
|
||||||
<div className="col-12 col-md-6 mb-2">
|
<div className="col-12 col-md-6 mb-2">
|
||||||
<Label className="form-label" required>
|
<SelectProjectField
|
||||||
Select Project
|
label="Project"
|
||||||
</Label>
|
required
|
||||||
<select
|
placeholder="Select Project"
|
||||||
className="form-select form-select-sm"
|
value={watch("projectId")}
|
||||||
{...register("projectId")}
|
onChange={(val) =>
|
||||||
>
|
setValue("projectId", val, {
|
||||||
<option value="">Select Project</option>
|
shouldDirty: true,
|
||||||
{projectLoading ? (
|
shouldValidate: true,
|
||||||
<option>Loading...</option>
|
})
|
||||||
) : (
|
}
|
||||||
projectNames?.map((project) => (
|
/>
|
||||||
<option key={project.id} value={project.id}>
|
|
||||||
{project.name}
|
|
||||||
</option>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
{errors.projectId && (
|
{errors.projectId && (
|
||||||
<small className="danger-text">
|
<small className="danger-text">{errors.projectId.message}</small>
|
||||||
{errors.projectId.message}
|
|
||||||
</small>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="col-12 col-md-6 mb-2">
|
||||||
|
<Label htmlFor="name" required>
|
||||||
|
Bill To
|
||||||
|
</Label>
|
||||||
|
<div className="d-flex align-items-center gap-2">
|
||||||
|
<select
|
||||||
|
className="select2 form-select form-select flex-grow-1"
|
||||||
|
aria-label="Default select example"
|
||||||
|
{...register("billedToId", {
|
||||||
|
required: "Client is required",
|
||||||
|
valueAsNumber: false,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<option>Loading...</option>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<option value="">Select Client</option>
|
||||||
|
{organization?.data?.map((org) => (
|
||||||
|
<option key={org.id} value={org.id}>
|
||||||
|
{org.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{errors?.clientId && (
|
||||||
|
<span className="danger-text">{errors.billedToId.message}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="col-12 col-md-6 mb-2">
|
<div className="col-12 col-md-6 mb-2">
|
||||||
<Label required>Title</Label>
|
<Label required>Title</Label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control"
|
||||||
{...register("title")}
|
{...register("title")}
|
||||||
/>
|
/>
|
||||||
{errors.title && (
|
{errors.title && (
|
||||||
@ -203,7 +235,7 @@ const ManageCollection = ({ collectionId, onClose }) => {
|
|||||||
<Label required>Invoice Number</Label>
|
<Label required>Invoice Number</Label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control"
|
||||||
{...register("invoiceNumber")}
|
{...register("invoiceNumber")}
|
||||||
/>
|
/>
|
||||||
{errors.invoiceId && (
|
{errors.invoiceId && (
|
||||||
@ -216,7 +248,7 @@ const ManageCollection = ({ collectionId, onClose }) => {
|
|||||||
<Label required>E-Invoice Number</Label>
|
<Label required>E-Invoice Number</Label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control"
|
||||||
{...register("eInvoiceNumber")}
|
{...register("eInvoiceNumber")}
|
||||||
/>
|
/>
|
||||||
{errors.invoiceId && (
|
{errors.invoiceId && (
|
||||||
@ -232,6 +264,7 @@ const ManageCollection = ({ collectionId, onClose }) => {
|
|||||||
name="invoiceDate"
|
name="invoiceDate"
|
||||||
control={control}
|
control={control}
|
||||||
maxDate={new Date()}
|
maxDate={new Date()}
|
||||||
|
size="md"
|
||||||
/>
|
/>
|
||||||
{errors.invoiceDate && (
|
{errors.invoiceDate && (
|
||||||
<small className="danger-text">
|
<small className="danger-text">
|
||||||
@ -246,6 +279,7 @@ const ManageCollection = ({ collectionId, onClose }) => {
|
|||||||
name="exceptedPaymentDate"
|
name="exceptedPaymentDate"
|
||||||
control={control}
|
control={control}
|
||||||
minDate={watch("invoiceDate")}
|
minDate={watch("invoiceDate")}
|
||||||
|
size="md"
|
||||||
/>
|
/>
|
||||||
{errors.exceptedPaymentDate && (
|
{errors.exceptedPaymentDate && (
|
||||||
<small className="danger-text">
|
<small className="danger-text">
|
||||||
@ -260,6 +294,7 @@ const ManageCollection = ({ collectionId, onClose }) => {
|
|||||||
name="clientSubmitedDate"
|
name="clientSubmitedDate"
|
||||||
control={control}
|
control={control}
|
||||||
maxDate={new Date()}
|
maxDate={new Date()}
|
||||||
|
size="md"
|
||||||
/>
|
/>
|
||||||
{errors.exceptedPaymentDate && (
|
{errors.exceptedPaymentDate && (
|
||||||
<small className="danger-text">
|
<small className="danger-text">
|
||||||
@ -275,7 +310,7 @@ const ManageCollection = ({ collectionId, onClose }) => {
|
|||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
id="basicAmount"
|
id="basicAmount"
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control"
|
||||||
min="1"
|
min="1"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
inputMode="decimal"
|
inputMode="decimal"
|
||||||
@ -294,7 +329,7 @@ const ManageCollection = ({ collectionId, onClose }) => {
|
|||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
id="taxAmount"
|
id="taxAmount"
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control"
|
||||||
min="1"
|
min="1"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
inputMode="decimal"
|
inputMode="decimal"
|
||||||
@ -313,7 +348,7 @@ const ManageCollection = ({ collectionId, onClose }) => {
|
|||||||
</Label>
|
</Label>
|
||||||
<textarea
|
<textarea
|
||||||
id="description"
|
id="description"
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control"
|
||||||
{...register("description")}
|
{...register("description")}
|
||||||
rows="2"
|
rows="2"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|||||||
@ -25,7 +25,6 @@ const ViewCollection = ({ onClose }) => {
|
|||||||
|
|
||||||
if (isLoading) return <CollectionDetailsSkeleton />;
|
if (isLoading) return <CollectionDetailsSkeleton />;
|
||||||
if (isError) return <div>{error.message}</div>;
|
if (isError) return <div>{error.message}</div>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container p-3">
|
<div className="container p-3">
|
||||||
<p className="fs-5 fw-semibold">Collection Details</p>
|
<p className="fs-5 fw-semibold">Collection Details</p>
|
||||||
@ -43,9 +42,8 @@ const ViewCollection = ({ onClose }) => {
|
|||||||
<div>
|
<div>
|
||||||
{" "}
|
{" "}
|
||||||
<span
|
<span
|
||||||
className={`badge bg-label-${
|
className={`badge bg-label-${data?.isActive ? "primary" : "danger"
|
||||||
data?.isActive ? "primary" : "danger"
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{data?.isActive ? "Active" : "Inactive"}
|
{data?.isActive ? "Active" : "Inactive"}
|
||||||
</span>
|
</span>
|
||||||
@ -214,9 +212,8 @@ const ViewCollection = ({ onClose }) => {
|
|||||||
<ul className="nav nav-tabs" role="tablist">
|
<ul className="nav nav-tabs" role="tablist">
|
||||||
<li className="nav-item">
|
<li className="nav-item">
|
||||||
<button
|
<button
|
||||||
className={`nav-link ${
|
className={`nav-link ${activeTab === "payments" ? "active" : ""
|
||||||
activeTab === "payments" ? "active" : ""
|
}`}
|
||||||
}`}
|
|
||||||
onClick={() => setActiveTab("payments")}
|
onClick={() => setActiveTab("payments")}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
@ -225,9 +222,8 @@ const ViewCollection = ({ onClose }) => {
|
|||||||
</li>
|
</li>
|
||||||
<li className="nav-item">
|
<li className="nav-item">
|
||||||
<button
|
<button
|
||||||
className={`nav-link ${
|
className={`nav-link ${activeTab === "details" ? "active" : ""
|
||||||
activeTab === "details" ? "active" : ""
|
}`}
|
||||||
}`}
|
|
||||||
onClick={() => setActiveTab("details")}
|
onClick={() => setActiveTab("details")}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -19,6 +19,7 @@ export const newCollection = z.object({
|
|||||||
invoiceDate: z.string().min(1, { message: "Date is required" }),
|
invoiceDate: z.string().min(1, { message: "Date is required" }),
|
||||||
description: z.string().trim().optional(),
|
description: z.string().trim().optional(),
|
||||||
clientSubmitedDate: z.string().min(1, { message: "Date is required" }),
|
clientSubmitedDate: z.string().min(1, { message: "Date is required" }),
|
||||||
|
billedToId: z.string().min(1, { message: "Date is required" }),
|
||||||
exceptedPaymentDate: z.string().min(1, { message: "Date is required" }),
|
exceptedPaymentDate: z.string().min(1, { message: "Date is required" }),
|
||||||
invoiceNumber: z
|
invoiceNumber: z
|
||||||
.string()
|
.string()
|
||||||
@ -75,6 +76,7 @@ export const defaultCollection = {
|
|||||||
taxAmount: "",
|
taxAmount: "",
|
||||||
basicAmount: "",
|
basicAmount: "",
|
||||||
description: "",
|
description: "",
|
||||||
|
billedToId:"",
|
||||||
attachments: [],
|
attachments: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,45 +1,44 @@
|
|||||||
import React from 'react'
|
import React from "react";
|
||||||
|
|
||||||
export const EmployeeChip = ({handleRemove,employee}) => {
|
export const EmployeeChip = ({ handleRemove, employee }) => {
|
||||||
return(
|
return (
|
||||||
<span
|
<span
|
||||||
key={employee?.id}
|
key={employee?.id}
|
||||||
className="tagify__tag d-inline-flex align-items-center me-1 mb-1"
|
className="tagify__tag d-inline-flex align-items-center me-1 mb-1"
|
||||||
role="listitem"
|
role="listitem"
|
||||||
>
|
>
|
||||||
<div className="d-flex align-items-center">
|
<div className="d-flex align-items-center">
|
||||||
{employee?.photo ? (
|
{employee?.photo ? (
|
||||||
<span className="tagify__tag__avatar-wrap me-1">
|
<span className="tagify__tag__avatar-wrap me-1">
|
||||||
<img
|
<img
|
||||||
src={employee?.avataremployeerl || "/defaemployeelt-avatar.png"}
|
src={employee?.avataremployeerl || "/defaemployeelt-avatar.png"}
|
||||||
alt={`${employee?.firstName || ""} ${employee?.lastName || ""}`}
|
alt={`${employee?.firstName || ""} ${employee?.lastName || ""}`}
|
||||||
style={{ width: 12, height: 12, objectFit: "cover" }}
|
style={{ width: 12, height: 12, objectFit: "cover" }}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<div className="avatar avatar-xs me-2">
|
<div className="avatar avatar-xs me-2">
|
||||||
<span className="avatar-initial roemployeended-circle bg-label-secondary">
|
<span className="avatar-initial roemployeended-circle bg-label-secondary">
|
||||||
{employee?.firstName?.[0] || ""}
|
{employee?.firstName?.[0] || ""}
|
||||||
{employee?.lastName?.[0] || ""}
|
{employee?.lastName?.[0] || ""}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="d-flex flex-colemployeemn">
|
<div className="d-flex flex-colemployeemn">
|
||||||
<span className="tagify__tag-text">
|
<span className="tagify__tag-text">
|
||||||
{employee?.firstName} {employee?.lastName}
|
{employee?.firstName} {employee?.lastName}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<bemployeetton
|
|
||||||
type="bemployeetton"
|
|
||||||
className="tagify__tag__removeBtn border-none"
|
|
||||||
onClick={() => handleRemove(employee?.id)}
|
|
||||||
aria-label={`Remove ${employee?.firstName}`}
|
|
||||||
title="Remove"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
<bemployeetton
|
||||||
|
type="bemployeetton"
|
||||||
|
className="tagify__tag__removeBtn border-none"
|
||||||
|
onClick={() => handleRemove(employee?.id)}
|
||||||
|
aria-label={`Remove ${employee?.firstName}`}
|
||||||
|
title="Remove"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@ -17,9 +17,13 @@ const ConfirmModal = ({
|
|||||||
case "delete":
|
case "delete":
|
||||||
return <i className="bx bx-x-circle text-danger" style={{ fontSize: "60px" }}></i>;
|
return <i className="bx bx-x-circle text-danger" style={{ fontSize: "60px" }}></i>;
|
||||||
case "success":
|
case "success":
|
||||||
return <i className="bx bx-check-circle text-success" style={{ fontSize: "60px" }}></i>;
|
return <i className="bx bx-archive-in text-warning" style={{ fontSize: "60px" }}></i>;
|
||||||
case "warning":
|
case "archive":
|
||||||
return <i className="bx bx-error-circle text-warning" style={{ fontSize: "60px" }}></i>;
|
return <i className="bx bx-archive-in text-warning" style={{ fontSize: "60px" }}></i>;
|
||||||
|
case "Un-archive":
|
||||||
|
return <i className="bx bx-archive-out text-warning" style={{ fontSize: "60px" }}></i>;
|
||||||
|
case "undo":
|
||||||
|
return <i className="bx bx-undo text-info" style={{ fontSize: "50px" }}></i>;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import Avatar from "./Avatar";
|
|||||||
const EmployeeSearchInput = ({
|
const EmployeeSearchInput = ({
|
||||||
control,
|
control,
|
||||||
name,
|
name,
|
||||||
|
size = "sm",
|
||||||
projectId,
|
projectId,
|
||||||
placeholder,
|
placeholder,
|
||||||
forAll,
|
forAll,
|
||||||
@ -46,7 +47,7 @@ const EmployeeSearchInput = ({
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={`form-control form-control-sm`}
|
className={`form-control form-control-sm-${size}`}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|||||||
@ -2,15 +2,18 @@ import React, { useEffect, useRef, useState } from "react";
|
|||||||
import Label from "../Label";
|
import Label from "../Label";
|
||||||
|
|
||||||
const InputSuggessionField = ({
|
const InputSuggessionField = ({
|
||||||
organizationList = [],
|
suggesstionList = [],
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
error,
|
error,
|
||||||
disabled=false
|
disabled = false,
|
||||||
|
label = "Label",
|
||||||
|
placeholder = "Please Enter",
|
||||||
|
required = false,
|
||||||
|
isLoading = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const dropdownRef = useRef(null);
|
const dropdownRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event) => {
|
const handleClickOutside = (event) => {
|
||||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||||
@ -21,12 +24,12 @@ const InputSuggessionField = ({
|
|||||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const selectedOption = options.find((opt) => opt[valueKey] === value);
|
const selectedOption = suggesstionList.find((opt) => opt === value);
|
||||||
|
|
||||||
const displayText = selectedOption ? selectedOption[labelKey] : placeholder;
|
const displayText = selectedOption ? selectedOption : placeholder;
|
||||||
|
|
||||||
const handleSelect = (option) => {
|
const handleSelect = (option) => {
|
||||||
onChange(option[valueKey]);
|
onChange(option);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -57,7 +60,7 @@ const InputSuggessionField = ({
|
|||||||
|
|
||||||
{open && !isLoading && (
|
{open && !isLoading && (
|
||||||
<ul
|
<ul
|
||||||
className="dropdown-menu w-100 shadow-sm show animate__fadeIn"
|
className="dropdown-menu w-100 shadow-sm show animate__fadeIn h-64 overflow-auto rounded overflow-x-hidden"
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: "100%",
|
top: "100%",
|
||||||
@ -68,16 +71,14 @@ const InputSuggessionField = ({
|
|||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{options.map((option, i) => (
|
{suggesstionList.map((option, i) => (
|
||||||
<li key={i}>
|
<li key={i}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`dropdown-item ${
|
className={`dropdown-item ${option === value ? "active" : ""}`}
|
||||||
option[valueKey] === value ? "active" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => handleSelect(option)}
|
onClick={() => handleSelect(option)}
|
||||||
>
|
>
|
||||||
{option[labelKey]}
|
{option}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import Label from "../Label";
|
|||||||
import { useDebounce } from "../../../utils/appUtils";
|
import { useDebounce } from "../../../utils/appUtils";
|
||||||
import { useEmployeesName } from "../../../hooks/useEmployees";
|
import { useEmployeesName } from "../../../hooks/useEmployees";
|
||||||
import { useProjectBothName } from "../../../hooks/useProjects";
|
import { useProjectBothName } from "../../../hooks/useProjects";
|
||||||
|
import EmployeeRepository from "../../../repositories/EmployeeRepository";
|
||||||
|
|
||||||
const SelectEmployeeServerSide = ({
|
const SelectEmployeeServerSide = ({
|
||||||
label = "Select",
|
label = "Select",
|
||||||
@ -18,6 +19,7 @@ const SelectEmployeeServerSide = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [searchText, setSearchText] = useState("");
|
const [searchText, setSearchText] = useState("");
|
||||||
const debounce = useDebounce(searchText, 300);
|
const debounce = useDebounce(searchText, 300);
|
||||||
|
const [forcedSelected, setForcedSelected] = useState(null);
|
||||||
|
|
||||||
const { data, isLoading } = useEmployeesName(
|
const { data, isLoading } = useEmployeesName(
|
||||||
projectId,
|
projectId,
|
||||||
@ -34,22 +36,15 @@ const SelectEmployeeServerSide = ({
|
|||||||
return `${emp.firstName || ""} ${emp.lastName || ""}`.trim();
|
return `${emp.firstName || ""} ${emp.lastName || ""}`.trim();
|
||||||
};
|
};
|
||||||
|
|
||||||
/** -----------------------------
|
|
||||||
* SELECTED OPTION (SINGLE)
|
|
||||||
* ----------------------------- */
|
|
||||||
let selectedSingle = null;
|
let selectedSingle = null;
|
||||||
|
|
||||||
if (!isMultiple) {
|
if (!isMultiple) {
|
||||||
if (isFullObject && value) selectedSingle = value;
|
if (isFullObject && value) selectedSingle = value;
|
||||||
else if (!isFullObject && value)
|
else if (!isFullObject && value)
|
||||||
selectedSingle = options.find((o) => o[valueKey] === value);
|
selectedSingle =
|
||||||
|
options.find((o) => o[valueKey] === value) || forcedSelected;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** -----------------------------
|
|
||||||
* SELECTED OPTION (MULTIPLE)
|
|
||||||
* ----------------------------- */
|
|
||||||
let selectedList = [];
|
let selectedList = [];
|
||||||
|
|
||||||
if (isMultiple && Array.isArray(value)) {
|
if (isMultiple && Array.isArray(value)) {
|
||||||
if (isFullObject) selectedList = value;
|
if (isFullObject) selectedList = value;
|
||||||
else {
|
else {
|
||||||
@ -57,54 +52,61 @@ const SelectEmployeeServerSide = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Main button label */
|
|
||||||
const displayText = !isMultiple
|
const displayText = !isMultiple
|
||||||
? getDisplayName(selectedSingle) || placeholder
|
? getDisplayName(selectedSingle) || placeholder
|
||||||
: selectedList.length > 0
|
: selectedList.length > 0
|
||||||
? selectedList.map((e) => getDisplayName(e)).join(", ")
|
? selectedList.map((e) => getDisplayName(e)).join(", ")
|
||||||
: placeholder;
|
: placeholder;
|
||||||
|
|
||||||
/** -----------------------------
|
|
||||||
* HANDLE OUTSIDE CLICK
|
|
||||||
* ----------------------------- */
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (e) => {
|
const handleClickOutside = (e) => {
|
||||||
if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
|
if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/** -----------------------------
|
|
||||||
* HANDLE SELECT
|
|
||||||
* ----------------------------- */
|
|
||||||
const handleSelect = (option) => {
|
const handleSelect = (option) => {
|
||||||
if (!isMultiple) {
|
if (!isMultiple) {
|
||||||
// SINGLE SELECT
|
|
||||||
if (isFullObject) onChange(option);
|
if (isFullObject) onChange(option);
|
||||||
else onChange(option[valueKey]);
|
else onChange(option[valueKey]);
|
||||||
|
setOpen(false);
|
||||||
} else {
|
} else {
|
||||||
// MULTIPLE SELECT
|
|
||||||
let updated = [];
|
let updated = [];
|
||||||
|
|
||||||
const exists = selectedList.some((e) => e[valueKey] === option[valueKey]);
|
const exists = selectedList.some((e) => e[valueKey] === option[valueKey]);
|
||||||
|
updated = exists
|
||||||
if (exists) {
|
? selectedList.filter((e) => e[valueKey] !== option[valueKey])
|
||||||
// remove
|
: [...selectedList, option];
|
||||||
updated = selectedList.filter((e) => e[valueKey] !== option[valueKey]);
|
|
||||||
} else {
|
|
||||||
// add
|
|
||||||
updated = [...selectedList, option];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFullObject) onChange(updated);
|
if (isFullObject) onChange(updated);
|
||||||
else onChange(updated.map((x) => x[valueKey]));
|
else onChange(updated.map((x) => x[valueKey]));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!value || isFullObject) return;
|
||||||
|
|
||||||
|
const exists = options.some((o) => o[valueKey] === value);
|
||||||
|
if (exists) return;
|
||||||
|
|
||||||
|
const loadSingleEmployee = async () => {
|
||||||
|
try {
|
||||||
|
const emp = await EmployeeRepository.getEmployeeName(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
value
|
||||||
|
);
|
||||||
|
setForcedSelected(emp.data[0]);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load selected employee", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadSingleEmployee();
|
||||||
|
}, [value, options, isFullObject, valueKey]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-3 position-relative" ref={dropdownRef}>
|
<div className="mb-3 position-relative" ref={dropdownRef}>
|
||||||
{label && (
|
{label && (
|
||||||
@ -126,7 +128,6 @@ const SelectEmployeeServerSide = ({
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* DROPDOWN */}
|
|
||||||
{open && (
|
{open && (
|
||||||
<ul
|
<ul
|
||||||
className="dropdown-menu w-100 shadow-sm show animate__fadeIn h-64 overflow-auto rounded"
|
className="dropdown-menu w-100 shadow-sm show animate__fadeIn h-64 overflow-auto rounded"
|
||||||
@ -137,10 +138,10 @@ const SelectEmployeeServerSide = ({
|
|||||||
zIndex: 1050,
|
zIndex: 1050,
|
||||||
marginTop: "4px",
|
marginTop: "4px",
|
||||||
borderRadius: "0.375rem",
|
borderRadius: "0.375rem",
|
||||||
overflow: "hidden",
|
padding: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="p-1">
|
<li className="p-1 sticky-top bg-white" style={{ zIndex: 10 }}>
|
||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
value={searchText}
|
value={searchText}
|
||||||
@ -148,7 +149,7 @@ const SelectEmployeeServerSide = ({
|
|||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</li>
|
||||||
|
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<li className="dropdown-item text-muted text-center">Loading...</li>
|
<li className="dropdown-item text-muted text-center">Loading...</li>
|
||||||
@ -168,10 +169,12 @@ const SelectEmployeeServerSide = ({
|
|||||||
selectedSingle[valueKey] === option[valueKey];
|
selectedSingle[valueKey] === option[valueKey];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={option[valueKey]}>
|
<li key={option[valueKey]} className="px-1 rounded">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`dropdown-item ${isActive ? "active" : ""}`}
|
className={`dropdown-item rounded ${
|
||||||
|
isActive ? "active" : ""
|
||||||
|
}`}
|
||||||
onClick={() => handleSelect(option)}
|
onClick={() => handleSelect(option)}
|
||||||
>
|
>
|
||||||
{getDisplayName(option)}
|
{getDisplayName(option)}
|
||||||
@ -184,6 +187,7 @@ const SelectEmployeeServerSide = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SelectEmployeeServerSide;
|
export default SelectEmployeeServerSide;
|
||||||
|
|
||||||
export const SelectProjectField = ({
|
export const SelectProjectField = ({
|
||||||
@ -196,6 +200,7 @@ export const SelectProjectField = ({
|
|||||||
isFullObject = false,
|
isFullObject = false,
|
||||||
isMultiple = false,
|
isMultiple = false,
|
||||||
isAllProject = false,
|
isAllProject = false,
|
||||||
|
disabled
|
||||||
}) => {
|
}) => {
|
||||||
const [searchText, setSearchText] = useState("");
|
const [searchText, setSearchText] = useState("");
|
||||||
const debounce = useDebounce(searchText, 300);
|
const debounce = useDebounce(searchText, 300);
|
||||||
@ -211,9 +216,6 @@ export const SelectProjectField = ({
|
|||||||
return `${project.name || ""}`.trim();
|
return `${project.name || ""}`.trim();
|
||||||
};
|
};
|
||||||
|
|
||||||
/** -----------------------------
|
|
||||||
* SELECTED OPTION (SINGLE)
|
|
||||||
* ----------------------------- */
|
|
||||||
let selectedSingle = null;
|
let selectedSingle = null;
|
||||||
|
|
||||||
if (!isMultiple) {
|
if (!isMultiple) {
|
||||||
@ -222,9 +224,6 @@ export const SelectProjectField = ({
|
|||||||
selectedSingle = options.find((o) => o[valueKey] === value);
|
selectedSingle = options.find((o) => o[valueKey] === value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** -----------------------------
|
|
||||||
* SELECTED OPTION (MULTIPLE)
|
|
||||||
* ----------------------------- */
|
|
||||||
let selectedList = [];
|
let selectedList = [];
|
||||||
|
|
||||||
if (isMultiple && Array.isArray(value)) {
|
if (isMultiple && Array.isArray(value)) {
|
||||||
@ -297,6 +296,7 @@ export const SelectProjectField = ({
|
|||||||
open ? "show" : ""
|
open ? "show" : ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setOpen((prev) => !prev)}
|
onClick={() => setOpen((prev) => !prev)}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<span className={`text-truncate ${!displayText ? "text-muted" : ""}`}>
|
<span className={`text-truncate ${!displayText ? "text-muted" : ""}`}>
|
||||||
{displayText}
|
{displayText}
|
||||||
@ -337,6 +337,217 @@ export const SelectProjectField = ({
|
|||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!isLoading &&
|
||||||
|
options.map((option) => {
|
||||||
|
const isActive = isMultiple
|
||||||
|
? selectedList.some((x) => x[valueKey] === option[valueKey])
|
||||||
|
: selectedSingle &&
|
||||||
|
selectedSingle[valueKey] === option[valueKey];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={option[valueKey]} className="px-1 rounded w-full">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`dropdown-item rounded d-block text-truncate w-100 ${
|
||||||
|
isActive ? "active" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => handleSelect(option)}
|
||||||
|
>
|
||||||
|
{getDisplayName(option)}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SelectFieldSearch = ({
|
||||||
|
label = "Select",
|
||||||
|
placeholder = "Select ",
|
||||||
|
required = false,
|
||||||
|
value = null,
|
||||||
|
onChange,
|
||||||
|
valueKey = "id",
|
||||||
|
labelKey = "name",
|
||||||
|
disabled = false,
|
||||||
|
isFullObject = false,
|
||||||
|
isMultiple = false,
|
||||||
|
hookParams,
|
||||||
|
useFetchHook,
|
||||||
|
}) => {
|
||||||
|
const [searchText, setSearchText] = useState("");
|
||||||
|
const debounce = useDebounce(searchText, 300);
|
||||||
|
|
||||||
|
const { data, isLoading } = useFetchHook(...hookParams, debounce);
|
||||||
|
const options = data?.data ?? [];
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const dropdownRef = useRef(null);
|
||||||
|
|
||||||
|
const getDisplayName = (entity) => {
|
||||||
|
if (!entity) return "";
|
||||||
|
return `${entity[labelKey] || ""}`.trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** -----------------------------
|
||||||
|
* SELECTED OPTION (SINGLE)
|
||||||
|
* ----------------------------- */
|
||||||
|
let selectedSingle = null;
|
||||||
|
|
||||||
|
if (!isMultiple) {
|
||||||
|
if (isFullObject && value) selectedSingle = value;
|
||||||
|
else if (!isFullObject && value)
|
||||||
|
selectedSingle = options.find((o) => o[valueKey] === value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** -----------------------------
|
||||||
|
* SELECTED OPTION (MULTIPLE)
|
||||||
|
* ----------------------------- */
|
||||||
|
let selectedList = [];
|
||||||
|
if (isMultiple && Array.isArray(value)) {
|
||||||
|
if (isFullObject) selectedList = value;
|
||||||
|
else {
|
||||||
|
selectedList = options.filter((opt) => value.includes(opt[valueKey]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Main button label */
|
||||||
|
const displayText = !isMultiple
|
||||||
|
? getDisplayName(selectedSingle) || placeholder
|
||||||
|
: selectedList.length > 0
|
||||||
|
? selectedList.map((e) => getDisplayName(e)).join(", ")
|
||||||
|
: placeholder;
|
||||||
|
|
||||||
|
/** -----------------------------
|
||||||
|
* HANDLE OUTSIDE CLICK
|
||||||
|
* ----------------------------- */
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (e) => {
|
||||||
|
if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// MERGED OPTIONS TO ENSURE SELECTED VALUE APPEARS EVEN IF NOT IN SEARCH RESULT
|
||||||
|
const [mergedOptions, setMergedOptions] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let finalList = [...options];
|
||||||
|
|
||||||
|
if (!isMultiple && value && !isFullObject) {
|
||||||
|
// already selected option inside options?
|
||||||
|
const exists = options.some((o) => o[valueKey] === value);
|
||||||
|
|
||||||
|
// if selected item not found, try to get from props (value) as fallback
|
||||||
|
if (!exists && typeof value === "object") {
|
||||||
|
finalList.unshift(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMultiple && Array.isArray(value)) {
|
||||||
|
value.forEach((val) => {
|
||||||
|
const id = isFullObject ? val[valueKey] : val;
|
||||||
|
const exists = options.some((o) => o[valueKey] === id);
|
||||||
|
|
||||||
|
if (!exists && typeof val === "object") {
|
||||||
|
finalList.unshift(val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setMergedOptions(finalList);
|
||||||
|
}, [options, value]);
|
||||||
|
|
||||||
|
/** -----------------------------
|
||||||
|
* HANDLE SELECT
|
||||||
|
* ----------------------------- */
|
||||||
|
const handleSelect = (option) => {
|
||||||
|
if (!isMultiple) {
|
||||||
|
// SINGLE SELECT
|
||||||
|
if (isFullObject) onChange(option);
|
||||||
|
else onChange(option[valueKey]);
|
||||||
|
} else {
|
||||||
|
// MULTIPLE SELECT
|
||||||
|
let updated = [];
|
||||||
|
|
||||||
|
const exists = selectedList.some((e) => e[valueKey] === option[valueKey]);
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
// remove
|
||||||
|
updated = selectedList.filter((e) => e[valueKey] !== option[valueKey]);
|
||||||
|
} else {
|
||||||
|
// add
|
||||||
|
updated = [...selectedList, option];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFullObject) onChange(updated);
|
||||||
|
else onChange(updated.map((x) => x[valueKey]));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-3 position-relative" ref={dropdownRef}>
|
||||||
|
{label && (
|
||||||
|
<Label className="form-label" required={required}>
|
||||||
|
{label}
|
||||||
|
</Label>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* MAIN BUTTON */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`select2-icons form-select d-flex align-items-center justify-content-between ${
|
||||||
|
open ? "show" : ""
|
||||||
|
}`}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={() => setOpen((prev) => !prev)}
|
||||||
|
>
|
||||||
|
<span className={`text-truncate ${!displayText ? "text-muted" : ""}`}>
|
||||||
|
{displayText}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* DROPDOWN */}
|
||||||
|
{open && (
|
||||||
|
<ul
|
||||||
|
className="dropdown-menu w-100 shadow-sm show animate__fadeIn h-64 overflow-auto rounded overflow-x-hidden"
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "100%",
|
||||||
|
left: 0,
|
||||||
|
zIndex: 1050,
|
||||||
|
marginTop: "2px",
|
||||||
|
borderRadius: "0.375rem",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="p-1">
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
value={searchText}
|
||||||
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
placeholder="Search..."
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<li className="dropdown-item text-muted text-center">Loading...</li>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && options.length === 0 && (
|
||||||
|
<li className="dropdown-item text-muted text-center">
|
||||||
|
No results found
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
|
||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
options.map((option) => {
|
options.map((option) => {
|
||||||
const isActive = isMultiple
|
const isActive = isMultiple
|
||||||
|
|||||||
@ -28,7 +28,6 @@ const CommentEditor = () => {
|
|||||||
const [value, setValue] = useState("");
|
const [value, setValue] = useState("");
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
console.log("Comment:", value);
|
|
||||||
// Submit or handle content
|
// Submit or handle content
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import {
|
import {
|
||||||
closePopup,
|
closePopup,
|
||||||
@ -6,6 +6,10 @@ import {
|
|||||||
togglePopup,
|
togglePopup,
|
||||||
} from "../../slices/localVariablesSlice";
|
} from "../../slices/localVariablesSlice";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* align: "auto" | "left" | "right"
|
||||||
|
* boundaryRef: optional ref to the drawer/container element to use as boundary
|
||||||
|
*/
|
||||||
const HoverPopup = ({
|
const HoverPopup = ({
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
@ -13,6 +17,8 @@ const HoverPopup = ({
|
|||||||
children,
|
children,
|
||||||
className = "",
|
className = "",
|
||||||
Mode = "hover",
|
Mode = "hover",
|
||||||
|
align = "auto",
|
||||||
|
boundaryRef = null,
|
||||||
}) => {
|
}) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const visible = useSelector((s) => s.localVariables.popups[id] || false);
|
const visible = useSelector((s) => s.localVariables.popups[id] || false);
|
||||||
@ -23,11 +29,9 @@ const HoverPopup = ({
|
|||||||
const handleMouseEnter = () => {
|
const handleMouseEnter = () => {
|
||||||
if (Mode === "hover") dispatch(openPopup(id));
|
if (Mode === "hover") dispatch(openPopup(id));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
const handleMouseLeave = () => {
|
||||||
if (Mode === "hover") dispatch(closePopup(id));
|
if (Mode === "hover") dispatch(closePopup(id));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClick = (e) => {
|
const handleClick = (e) => {
|
||||||
if (Mode === "click") {
|
if (Mode === "click") {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -35,74 +39,159 @@ const HoverPopup = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Close on outside click when using click mode
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Mode !== "click" || !visible) return;
|
if (Mode !== "click" || !visible) return;
|
||||||
|
|
||||||
const handleOutside = (e) => {
|
const handler = (e) => {
|
||||||
if (
|
if (
|
||||||
!popupRef.current?.contains(e.target) &&
|
popupRef.current &&
|
||||||
!triggerRef.current?.contains(e.target)
|
!popupRef.current.contains(e.target) &&
|
||||||
|
triggerRef.current &&
|
||||||
|
!triggerRef.current.contains(e.target)
|
||||||
) {
|
) {
|
||||||
dispatch(closePopup(id));
|
dispatch(closePopup(id));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.addEventListener("click", handleOutside);
|
|
||||||
return () => document.removeEventListener("click", handleOutside);
|
|
||||||
}, [visible, Mode, id]);
|
|
||||||
|
|
||||||
|
document.addEventListener("click", handler);
|
||||||
|
return () => document.removeEventListener("click", handler);
|
||||||
|
}, [Mode, visible, dispatch, id]);
|
||||||
|
|
||||||
|
// Positioning effect: respects align prop and stays inside boundary (drawer)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!visible || !popupRef.current) return;
|
if (!visible || !popupRef.current || !triggerRef.current) return;
|
||||||
|
|
||||||
const popup = popupRef.current;
|
// run in next frame so DOM/layout settles
|
||||||
const rect = popup.getBoundingClientRect();
|
requestAnimationFrame(() => {
|
||||||
|
const popup = popupRef.current;
|
||||||
|
|
||||||
popup.style.left = "50%";
|
// choose boundary: provided boundaryRef or nearest positioned parent (popup.parentElement)
|
||||||
popup.style.right = "auto";
|
const boundaryEl =
|
||||||
popup.style.transform = "translateX(-50%)";
|
(boundaryRef && boundaryRef.current) || popup.parentElement;
|
||||||
|
if (!boundaryEl) return;
|
||||||
|
|
||||||
if (rect.right > window.innerWidth) {
|
const boundaryRect = boundaryEl.getBoundingClientRect();
|
||||||
popup.style.left = "auto";
|
const triggerRect = triggerRef.current.getBoundingClientRect();
|
||||||
popup.style.right = "0";
|
|
||||||
popup.style.transform = "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rect.left < 0) {
|
// reset styles first
|
||||||
popup.style.left = "0";
|
popup.style.left = "";
|
||||||
popup.style.right = "auto";
|
popup.style.right = "";
|
||||||
popup.style.transform = "none";
|
popup.style.transform = "";
|
||||||
}
|
popup.style.top = "";
|
||||||
}, [visible]);
|
|
||||||
|
|
||||||
return (
|
const popupRect = popup.getBoundingClientRect();
|
||||||
<div className="d-inline-block position-relative">
|
const parentRect = boundaryRect; // alias
|
||||||
<div
|
|
||||||
ref={triggerRef}
|
|
||||||
onMouseEnter={handleMouseEnter}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
onClick={handleClick}
|
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{visible && (
|
// Convert trigger center to parent coordinates
|
||||||
<div
|
const triggerCenterX =
|
||||||
ref={popupRef}
|
triggerRect.left + triggerRect.width / 2 - parentRect.left;
|
||||||
className={`bg-white border rounded shadow-sm p-3 w-max position-absolute top-100 mt-2 ${className}`}
|
|
||||||
style={{
|
|
||||||
zIndex: 2000,
|
|
||||||
left: "50%",
|
|
||||||
transform: "translateX(-50%)",
|
|
||||||
}}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
{title && <h6 className="fw-semibold mb-2">{title}</h6>}
|
|
||||||
|
|
||||||
<div>{content}</div>
|
// preferred left so popup center aligns to trigger center:
|
||||||
</div>
|
const preferredLeft = triggerCenterX - popupRect.width / 2;
|
||||||
)}
|
|
||||||
|
// Helpers to set styles in parent's coordinate system:
|
||||||
|
const setLeft = (leftPx) => {
|
||||||
|
popup.style.left = `${leftPx}px`;
|
||||||
|
popup.style.right = "auto";
|
||||||
|
popup.style.transform = "none";
|
||||||
|
};
|
||||||
|
const setRight = (rightPx) => {
|
||||||
|
popup.style.left = "auto";
|
||||||
|
popup.style.right = `${rightPx}px`;
|
||||||
|
popup.style.transform = "none";
|
||||||
|
};
|
||||||
|
|
||||||
|
// If user forced align:
|
||||||
|
if (align === "left") {
|
||||||
|
// align popup's left to parent's left (0)
|
||||||
|
setLeft(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (align === "right") {
|
||||||
|
// align popup's right to parent's right (0)
|
||||||
|
setRight(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (align === "center") {
|
||||||
|
popup.style.left = "50%";
|
||||||
|
popup.style.right = "auto";
|
||||||
|
popup.style.transform = "translateX(-50%)";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// align === "auto": try preferred centered position, but flip fully if overflow
|
||||||
|
// clamp preferredLeft to boundaries so it doesn't render partially outside
|
||||||
|
const leftIfCentered = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(preferredLeft, parentRect.width - popupRect.width)
|
||||||
|
);
|
||||||
|
|
||||||
|
// if centered fits, use it
|
||||||
|
if (leftIfCentered === preferredLeft) {
|
||||||
|
setLeft(leftIfCentered);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if centering would overflow right -> stick popup fully to left (left=0)
|
||||||
|
if (preferredLeft > parentRect.width - popupRect.width) {
|
||||||
|
// place popup so its right aligns to parent's right
|
||||||
|
// i.e., left = parent width - popup width
|
||||||
|
setLeft(parentRect.width - popupRect.width);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if centering would overflow left -> stick popup fully to left=0
|
||||||
|
if (preferredLeft < 0) {
|
||||||
|
setLeft(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback center
|
||||||
|
setLeft(leftIfCentered);
|
||||||
|
});
|
||||||
|
}, [visible, align, boundaryRef]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="d-inline-block position-relative" // <-- ADD THIS !!
|
||||||
|
style={{
|
||||||
|
maxWidth: "calc(700px - 100px)",
|
||||||
|
width: "100%",
|
||||||
|
wordWrap: "break-word",
|
||||||
|
overflow: "visible", // also make sure popup isn't clipped
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="d-inline-block"
|
||||||
|
ref={triggerRef}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
onClick={handleClick}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
|
{visible && (
|
||||||
|
<div
|
||||||
|
ref={popupRef}
|
||||||
|
className={`hover-popup bg-white border rounded shadow-sm p-3 position-absolute mt-2 ${className}`}
|
||||||
|
style={{
|
||||||
|
zIndex: 2000,
|
||||||
|
top: "100%",
|
||||||
|
width: "max-content",
|
||||||
|
minWidth: "120px",
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{title && <h6 className="fw-semibold mb-2">{title}</h6>}
|
||||||
|
<div>{content}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default HoverPopup;
|
export default HoverPopup;
|
||||||
|
|||||||
@ -5,14 +5,13 @@ const InputSuggestions = ({
|
|||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
error,
|
error,
|
||||||
disabled=false
|
disabled = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [filteredList, setFilteredList] = useState([]);
|
const [filteredList, setFilteredList] = useState([]);
|
||||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||||
const handleInputChange = (e) => {
|
const handleInputChange = (e) => {
|
||||||
const val = e.target.value;
|
const val = e.target.value;
|
||||||
onChange(val);
|
onChange(val);
|
||||||
|
|
||||||
const matches = organizationList.filter((org) =>
|
const matches = organizationList.filter((org) =>
|
||||||
org.toLowerCase().includes(val.toLowerCase())
|
org.toLowerCase().includes(val.toLowerCase())
|
||||||
);
|
);
|
||||||
@ -26,7 +25,7 @@ const InputSuggestions = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="position-relative">
|
<div className="mb-3 position-relative">
|
||||||
<input
|
<input
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
value={value}
|
value={value}
|
||||||
@ -39,19 +38,19 @@ const InputSuggestions = ({
|
|||||||
/>
|
/>
|
||||||
{showSuggestions && filteredList.length > 0 && (
|
{showSuggestions && filteredList.length > 0 && (
|
||||||
<ul
|
<ul
|
||||||
className="list-group shadow-sm position-absolute w-100 bg-white border zindex-tooltip"
|
className="dropdown-menu w-100 shadow-sm show animate__fadeIn"
|
||||||
style={{
|
style={{
|
||||||
maxHeight: "180px",
|
maxHeight: "180px",
|
||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
marginTop: "2px",
|
marginTop: "2px",
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
borderRadius:"0px"
|
borderRadius: "0px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{filteredList.map((org) => (
|
{filteredList.map((org) => (
|
||||||
<li
|
<li
|
||||||
key={org}
|
key={org}
|
||||||
className="list-group-item list-group-item-action border-none "
|
className="ropdown-item"
|
||||||
style={{
|
style={{
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
padding: "5px 12px",
|
padding: "5px 12px",
|
||||||
@ -59,17 +58,15 @@ const InputSuggestions = ({
|
|||||||
transition: "background-color 0.2s",
|
transition: "background-color 0.2s",
|
||||||
}}
|
}}
|
||||||
onMouseDown={() => handleSelectSuggestion(org)}
|
onMouseDown={() => handleSelectSuggestion(org)}
|
||||||
onMouseEnter={(e) =>
|
className={`dropdown-item ${
|
||||||
(e.currentTarget.style.backgroundColor = "#f8f9fa")
|
org === value ? "active" : ""
|
||||||
}
|
}`}
|
||||||
onMouseLeave={(e) =>
|
|
||||||
(e.currentTarget.style.backgroundColor = "transparent")
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{org}
|
{org}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && <small className="danger-text">{error}</small>}
|
{error && <small className="danger-text">{error}</small>}
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import { useState, useEffect } from "react";
|
|||||||
import GlobalRepository from "../repositories/GlobalRepository";
|
import GlobalRepository from "../repositories/GlobalRepository";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
|
||||||
export const useDashboard_Data = ({ days, FromDate, projectId }) => {
|
export const useDashboard_Data = ({ days, FromDate, projectId }) => {
|
||||||
const [dashboard_data, setDashboard_Data] = useState([]);
|
const [dashboard_data, setDashboard_Data] = useState([]);
|
||||||
const [isLineChartLoading, setLoading] = useState(false);
|
const [isLineChartLoading, setLoading] = useState(false);
|
||||||
@ -18,11 +17,13 @@ export const useDashboard_Data = ({ days, FromDate, projectId }) => {
|
|||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
days,
|
days,
|
||||||
FromDate: FromDate || '',
|
FromDate: FromDate || "",
|
||||||
projectId: projectId || null,
|
projectId: projectId || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await GlobalRepository.getDashboardProgressionData(payload);
|
const response = await GlobalRepository.getDashboardProgressionData(
|
||||||
|
payload
|
||||||
|
);
|
||||||
setDashboard_Data(response.data);
|
setDashboard_Data(response.data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError("Failed to fetch dashboard data.");
|
setError("Failed to fetch dashboard data.");
|
||||||
@ -38,123 +39,6 @@ export const useDashboard_Data = ({ days, FromDate, projectId }) => {
|
|||||||
return { dashboard_data, loading: isLineChartLoading, error };
|
return { dashboard_data, loading: isLineChartLoading, error };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// export const useDashboard_AttendanceData = (date, projectId) => {
|
|
||||||
// const [dashboard_Attendancedata, setDashboard_AttendanceData] = useState([]);
|
|
||||||
// const [isLineChartLoading, setLoading] = useState(false);
|
|
||||||
// const [error, setError] = useState("");
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// const fetchData = async () => {
|
|
||||||
// setLoading(true);
|
|
||||||
// setError("");
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// const response = await GlobalRepository.getDashboardAttendanceData(date, projectId); // date in 2nd param
|
|
||||||
// setDashboard_AttendanceData(response.data);
|
|
||||||
// } catch (err) {
|
|
||||||
// setError("Failed to fetch dashboard data.");
|
|
||||||
// console.error(err);
|
|
||||||
// } finally {
|
|
||||||
// setLoading(false);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// if (date && projectId !== null) {
|
|
||||||
// fetchData();
|
|
||||||
// }
|
|
||||||
// }, [date, projectId]);
|
|
||||||
|
|
||||||
// return { dashboard_Attendancedata, isLineChartLoading: isLineChartLoading, error };
|
|
||||||
// };
|
|
||||||
|
|
||||||
|
|
||||||
// 🔹 Dashboard Projects Card Data Hook
|
|
||||||
// export const useDashboardProjectsCardData = () => {
|
|
||||||
// const [projectsCardData, setProjectsData] = useState([]);
|
|
||||||
// const [loading, setLoading] = useState(false);
|
|
||||||
// const [error, setError] = useState("");
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// const fetchProjectsData = async () => {
|
|
||||||
// setLoading(true);
|
|
||||||
// setError("");
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// const response = await GlobalRepository.getDashboardProjectsCardData();
|
|
||||||
// setProjectsData(response.data);
|
|
||||||
// } catch (err) {
|
|
||||||
// setError("Failed to fetch projects card data.");
|
|
||||||
// console.error(err);
|
|
||||||
// } finally {
|
|
||||||
// setLoading(false);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// fetchProjectsData();
|
|
||||||
// }, []);
|
|
||||||
|
|
||||||
// return { projectsCardData, loading, error };
|
|
||||||
// };
|
|
||||||
|
|
||||||
// 🔹 Dashboard Teams Card Data Hook
|
|
||||||
// export const useDashboardTeamsCardData = (projectId) => {
|
|
||||||
// const [teamsCardData, setTeamsData] = useState({});
|
|
||||||
// const [loading, setLoading] = useState(false);
|
|
||||||
// const [error, setError] = useState("");
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// const fetchTeamsData = async () => {
|
|
||||||
// setLoading(true);
|
|
||||||
// setError("");
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// const response = await GlobalRepository.getDashboardTeamsCardData(projectId);
|
|
||||||
// setTeamsData(response.data || {});
|
|
||||||
// } catch (err) {
|
|
||||||
// setError("Failed to fetch teams card data.");
|
|
||||||
// console.error("Error fetching teams card data:", err);
|
|
||||||
// setTeamsData({});
|
|
||||||
// } finally {
|
|
||||||
// setLoading(false);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// fetchTeamsData();
|
|
||||||
// }, [projectId]);
|
|
||||||
|
|
||||||
// return { teamsCardData, loading, error };
|
|
||||||
// };
|
|
||||||
|
|
||||||
// export const useDashboardTasksCardData = (projectId) => {
|
|
||||||
// const [tasksCardData, setTasksData] = useState({});
|
|
||||||
// const [loading, setLoading] = useState(false);
|
|
||||||
// const [error, setError] = useState("");
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// const fetchTasksData = async () => {
|
|
||||||
// setLoading(true);
|
|
||||||
// setError("");
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// const response = await GlobalRepository.getDashboardTasksCardData(projectId);
|
|
||||||
// setTasksData(response.data);
|
|
||||||
// } catch (err) {
|
|
||||||
// setError("Failed to fetch tasks card data.");
|
|
||||||
// console.error(err);
|
|
||||||
// setTasksData({});
|
|
||||||
// } finally {
|
|
||||||
// setLoading(false);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// fetchTasksData();
|
|
||||||
// }, [projectId]);
|
|
||||||
|
|
||||||
// return { tasksCardData, loading, error };
|
|
||||||
// };
|
|
||||||
|
|
||||||
|
|
||||||
export const useAttendanceOverviewData = (projectId, days) => {
|
export const useAttendanceOverviewData = (projectId, days) => {
|
||||||
const [attendanceOverviewData, setAttendanceOverviewData] = useState([]);
|
const [attendanceOverviewData, setAttendanceOverviewData] = useState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@ -167,7 +51,10 @@ export const useAttendanceOverviewData = (projectId, days) => {
|
|||||||
setError("");
|
setError("");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await GlobalRepository.getAttendanceOverview(projectId, days);
|
const response = await GlobalRepository.getAttendanceOverview(
|
||||||
|
projectId,
|
||||||
|
days
|
||||||
|
);
|
||||||
setAttendanceOverviewData(response.data);
|
setAttendanceOverviewData(response.data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError("Failed to fetch attendance overview data.");
|
setError("Failed to fetch attendance overview data.");
|
||||||
@ -182,7 +69,6 @@ export const useAttendanceOverviewData = (projectId, days) => {
|
|||||||
return { attendanceOverviewData, loading, error };
|
return { attendanceOverviewData, loading, error };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// -------------------Query----------------------------
|
// -------------------Query----------------------------
|
||||||
|
|
||||||
// export const useDashboard_Data = (days, FromDate, projectId)=>{
|
// export const useDashboard_Data = (days, FromDate, projectId)=>{
|
||||||
@ -199,39 +85,47 @@ export const useAttendanceOverviewData = (projectId, days) => {
|
|||||||
// }
|
// }
|
||||||
// })
|
// })
|
||||||
// }
|
// }
|
||||||
|
export const useProjectCompletionStatus = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["projectCompletionStatus"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const resp = await await GlobalRepository.getProjectCompletionStatus();
|
||||||
|
return resp.data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
export const useDashboard_AttendanceData = (date, projectId) => {
|
export const useDashboard_AttendanceData = (date, projectId) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["dashboardAttendances", date, projectId],
|
queryKey: ["dashboardAttendances", date, projectId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
|
const resp = await await GlobalRepository.getDashboardAttendanceData(
|
||||||
const resp = await await GlobalRepository.getDashboardAttendanceData(date, projectId)
|
date,
|
||||||
|
projectId
|
||||||
|
);
|
||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useDashboardTeamsCardData = (projectId) => {
|
export const useDashboardTeamsCardData = (projectId) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["dashboardTeams", projectId],
|
queryKey: ["dashboardTeams", projectId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
|
const resp = await GlobalRepository.getDashboardTeamsCardData(projectId);
|
||||||
const resp = await GlobalRepository.getDashboardTeamsCardData(projectId)
|
|
||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useDashboardTasksCardData = (projectId) => {
|
export const useDashboardTasksCardData = (projectId) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["dashboardTasks", projectId],
|
queryKey: ["dashboardTasks", projectId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
|
const resp = await GlobalRepository.getDashboardTasksCardData(projectId);
|
||||||
const resp = await GlobalRepository.getDashboardTasksCardData(projectId)
|
|
||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
// export const useAttendanceOverviewData = (projectId, days) => {
|
// export const useAttendanceOverviewData = (projectId, days) => {
|
||||||
// return useQuery({
|
// return useQuery({
|
||||||
// queryKey:["dashboardAttendanceOverView",projectId],
|
// queryKey:["dashboardAttendanceOverView",projectId],
|
||||||
@ -247,29 +141,30 @@ export const useDashboardProjectsCardData = () => {
|
|||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["dashboardProjects"],
|
queryKey: ["dashboardProjects"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
|
|
||||||
const resp = await GlobalRepository.getDashboardProjectsCardData();
|
const resp = await GlobalRepository.getDashboardProjectsCardData();
|
||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useExpenseAnalysis = (projectId, startDate, endDate) => {
|
export const useExpenseAnalysis = (projectId, startDate, endDate) => {
|
||||||
const hasBothDates = !!startDate && !!endDate;
|
const hasBothDates = !!startDate && !!endDate;
|
||||||
const noDatesSelected = !startDate && !endDate;
|
const noDatesSelected = !startDate && !endDate;
|
||||||
|
|
||||||
const shouldFetch =
|
const shouldFetch = noDatesSelected || hasBothDates;
|
||||||
noDatesSelected ||
|
|
||||||
hasBothDates;
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["expenseAnalysis", projectId, startDate, endDate],
|
queryKey: ["expenseAnalysis", projectId, startDate, endDate],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const resp = await GlobalRepository.getExpenseData(projectId, startDate, endDate);
|
const resp = await GlobalRepository.getExpenseData(
|
||||||
|
projectId,
|
||||||
|
startDate,
|
||||||
|
endDate
|
||||||
|
);
|
||||||
return resp.data;
|
return resp.data;
|
||||||
},
|
},
|
||||||
enabled: shouldFetch,
|
enabled: shouldFetch,
|
||||||
refetchOnWindowFocus: true, // refetch when you come back
|
refetchOnWindowFocus: true, // refetch when you come back
|
||||||
refetchOnMount: "always", // always refetch on remount
|
refetchOnMount: "always", // always refetch on remount
|
||||||
staleTime: 0,
|
staleTime: 0,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -280,17 +175,20 @@ export const useExpenseStatus = (projectId) => {
|
|||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const resp = await GlobalRepository.getExpenseStatus(projectId);
|
const resp = await GlobalRepository.getExpenseStatus(projectId);
|
||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useExpenseDataByProject = (projectId, categoryId, months) => {
|
export const useExpenseDataByProject = (projectId, categoryId, months) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["expenseByProject", projectId, categoryId, months],
|
queryKey: ["expenseByProject", projectId, categoryId, months],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const resp = await GlobalRepository.getExpenseDataByProject(projectId, categoryId, months);
|
const resp = await GlobalRepository.getExpenseDataByProject(
|
||||||
|
projectId,
|
||||||
|
categoryId,
|
||||||
|
months
|
||||||
|
);
|
||||||
return resp.data;
|
return resp.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -231,7 +231,7 @@ export const useEmployeesName = (projectId, search, allEmployee) => {
|
|||||||
queryFn: async () =>
|
queryFn: async () =>
|
||||||
await EmployeeRepository.getEmployeeName(projectId, search, allEmployee),
|
await EmployeeRepository.getEmployeeName(projectId, search, allEmployee),
|
||||||
|
|
||||||
staleTime: 5 * 60 * 1000, // Optional: cache for 5 minutes
|
staleTime: 5 * 60 * 1000,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -438,6 +438,15 @@ export const useExpenseTransactions = (employeeId)=>{
|
|||||||
keepPreviousData:true,
|
keepPreviousData:true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
export const useExpenseAllTransactionsList = (searchString) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["transaction", searchString],
|
||||||
|
queryFn: async () => {
|
||||||
|
const resp = await ExpenseRepository.getAllTranctionList(searchString);
|
||||||
|
return resp.data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
// ---------------------------Put Post Recurring Expense---------------------------------------
|
// ---------------------------Put Post Recurring Expense---------------------------------------
|
||||||
|
|||||||
@ -20,14 +20,15 @@ export const useCurrentService = () => {
|
|||||||
|
|
||||||
// ------------------------------Query-------------------
|
// ------------------------------Query-------------------
|
||||||
|
|
||||||
export const useProjects = (pageSize, pageNumber) => {
|
export const useProjects = (pageSize, pageNumber,searchString) => {
|
||||||
const loggedUser = useSelector((store) => store.globalVariables.loginUser);
|
const loggedUser = useSelector((store) => store.globalVariables.loginUser);
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["ProjectsList", pageSize, pageNumber],
|
queryKey: ["ProjectsList", pageSize, pageNumber,searchString],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await ProjectRepository.getProjectList(
|
const response = await ProjectRepository.getProjectList(
|
||||||
pageSize,
|
pageSize,
|
||||||
pageNumber
|
pageNumber,
|
||||||
|
searchString,
|
||||||
);
|
);
|
||||||
return response?.data;
|
return response?.data;
|
||||||
},
|
},
|
||||||
@ -411,7 +412,6 @@ export const useUpdateProject = (onSuccessCallback) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.log(error);
|
|
||||||
showToast(error?.message || "Error while updating project", "error");
|
showToast(error?.message || "Error while updating project", "error");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -8,13 +8,14 @@ import { ServiceProjectRepository } from "../repositories/ServiceProjectReposito
|
|||||||
import showToast from "../services/toastService";
|
import showToast from "../services/toastService";
|
||||||
|
|
||||||
//#region Service Project
|
//#region Service Project
|
||||||
export const useServiceProjects = (pageSize, pageNumber) => {
|
export const useServiceProjects = (pageSize, pageNumber, searchString) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["serviceProjects", pageSize, pageNumber],
|
queryKey: ["serviceProjects", pageSize, pageNumber, searchString],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await ServiceProjectRepository.GetServiceProjects(
|
const response = await ServiceProjectRepository.GetServiceProjects(
|
||||||
pageSize,
|
pageSize,
|
||||||
pageNumber
|
pageNumber,
|
||||||
|
searchString
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
@ -154,16 +155,25 @@ export const useServiceProjectJobs = (
|
|||||||
pageSize,
|
pageSize,
|
||||||
pageNumber,
|
pageNumber,
|
||||||
isActive = true,
|
isActive = true,
|
||||||
project
|
project,
|
||||||
|
isArchive
|
||||||
) => {
|
) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["serviceProjectJobs", pageSize, pageNumber, isActive, project],
|
queryKey: [
|
||||||
|
"serviceProjectJobs",
|
||||||
|
pageSize,
|
||||||
|
pageNumber,
|
||||||
|
isActive,
|
||||||
|
project,
|
||||||
|
isArchive,
|
||||||
|
],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const resp = await ServiceProjectRepository.GetJobList(
|
const resp = await ServiceProjectRepository.GetJobList(
|
||||||
pageSize,
|
pageSize,
|
||||||
pageNumber,
|
pageNumber,
|
||||||
isActive,
|
isActive,
|
||||||
project
|
project,
|
||||||
|
isArchive
|
||||||
);
|
);
|
||||||
return resp.data;
|
return resp.data;
|
||||||
},
|
},
|
||||||
@ -181,7 +191,7 @@ export const useJobComments = (jobId, pageSize, pageNumber) => {
|
|||||||
);
|
);
|
||||||
return resp.data;
|
return resp.data;
|
||||||
},
|
},
|
||||||
enabled:!!jobId,
|
enabled: !!jobId,
|
||||||
|
|
||||||
initialPageParam: pageNumber,
|
initialPageParam: pageNumber,
|
||||||
|
|
||||||
@ -258,18 +268,25 @@ export const useCreateServiceProjectJob = (onSuccessCallback) => {
|
|||||||
export const useUpdateServiceProjectJob = (onSuccessCallback) => {
|
export const useUpdateServiceProjectJob = (onSuccessCallback) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({ id, payload }) => {
|
mutationFn: async ({ id, payload, isArchiveAction = false }) => {
|
||||||
// Call the repository patch
|
|
||||||
const resp = await ServiceProjectRepository.UpdateJob(id, payload);
|
const resp = await ServiceProjectRepository.UpdateJob(id, payload);
|
||||||
|
|
||||||
return resp;
|
return { resp, isArchiveAction };
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
|
||||||
|
onSuccess: ({ isArchiveAction }) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["serviceProjectJobs"] });
|
queryClient.invalidateQueries({ queryKey: ["serviceProjectJobs"] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["service-job"] });
|
queryClient.invalidateQueries({ queryKey: ["service-job"] });
|
||||||
|
|
||||||
if (onSuccessCallback) onSuccessCallback();
|
if (onSuccessCallback) onSuccessCallback();
|
||||||
showToast("Job Updated successfully", "success");
|
|
||||||
|
if (isArchiveAction) {
|
||||||
|
showToast("Job archived successfully", "success");
|
||||||
|
} else {
|
||||||
|
showToast("Job restored successfully", "success");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
showToast(
|
showToast(
|
||||||
error?.response?.data?.message ||
|
error?.response?.data?.message ||
|
||||||
@ -282,3 +299,125 @@ export const useUpdateServiceProjectJob = (onSuccessCallback) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
//#region Branch
|
||||||
|
export const useBranches = (
|
||||||
|
projectId,
|
||||||
|
isActive,
|
||||||
|
pageSize,
|
||||||
|
pageNumber,
|
||||||
|
searchString
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [
|
||||||
|
"branches",
|
||||||
|
projectId,
|
||||||
|
isActive,
|
||||||
|
pageSize,
|
||||||
|
pageNumber,
|
||||||
|
searchString,
|
||||||
|
],
|
||||||
|
queryFn: async () => {
|
||||||
|
const resp = await ServiceProjectRepository.GetBranchList(
|
||||||
|
projectId,
|
||||||
|
isActive,
|
||||||
|
pageSize,
|
||||||
|
pageNumber,
|
||||||
|
searchString
|
||||||
|
);
|
||||||
|
return resp.data;
|
||||||
|
},
|
||||||
|
enabled: !!projectId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useBranchTypes = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["branch_Type"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const resp = await ServiceProjectRepository.GetBranchTypeList();
|
||||||
|
return resp.data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useBranchDetails = (id) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["branch", id],
|
||||||
|
queryFn: async () => {
|
||||||
|
const resp = await ServiceProjectRepository.GetBranchDetail(id);
|
||||||
|
return resp.data;
|
||||||
|
},
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateBranch = (onSuccessCallBack) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (payload) => {
|
||||||
|
await ServiceProjectRepository.CreateBranch(payload);
|
||||||
|
},
|
||||||
|
|
||||||
|
onSuccess: (_, variables) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["branches"] });
|
||||||
|
showToast("Branches Created Successfully", "success");
|
||||||
|
if (onSuccessCallBack) onSuccessCallBack();
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
showToast(
|
||||||
|
error.message || "Something went wrong please try again !",
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateBranch = (onSuccessCallBack) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ id, payload }) =>
|
||||||
|
await ServiceProjectRepository.UpdateBranch(id, payload),
|
||||||
|
|
||||||
|
onSuccess: (_, variables) => {
|
||||||
|
// remove old single-branch cache
|
||||||
|
queryClient.removeQueries({ queryKey: ["branch", variables.id] });
|
||||||
|
|
||||||
|
// refresh list
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["branches"] });
|
||||||
|
|
||||||
|
showToast("Branch updated successfully", "success");
|
||||||
|
onSuccessCallBack?.();
|
||||||
|
},
|
||||||
|
|
||||||
|
onError: () => {
|
||||||
|
showToast("Something went wrong. Please try again later.", "error");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteBranch = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ id, isActive }) =>
|
||||||
|
await ServiceProjectRepository.DeleteBranch(id, isActive),
|
||||||
|
|
||||||
|
onSuccess: (_, variable) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["branches"] });
|
||||||
|
showToast(
|
||||||
|
`Branch ${variable.isActive ? "restored" : "deleted"} successfully`,
|
||||||
|
"success"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
onError: (error) => {
|
||||||
|
showToast(
|
||||||
|
error?.response?.data?.message ||
|
||||||
|
error.message ||
|
||||||
|
"Failed to delete branch",
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@ -13,6 +13,8 @@ import Label from "../../components/common/Label";
|
|||||||
import AdvancePaymentList from "../../components/AdvancePayment/AdvancePaymentList";
|
import AdvancePaymentList from "../../components/AdvancePayment/AdvancePaymentList";
|
||||||
import { employee } from "../../data/masters";
|
import { employee } from "../../data/masters";
|
||||||
import { formatFigure } from "../../utils/appUtils";
|
import { formatFigure } from "../../utils/appUtils";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { useExpenseTransactions } from "../../hooks/useExpense";
|
||||||
|
|
||||||
export const AdvancePaymentContext = createContext();
|
export const AdvancePaymentContext = createContext();
|
||||||
export const useAdvancePaymentContext = () => {
|
export const useAdvancePaymentContext = () => {
|
||||||
@ -25,14 +27,32 @@ export const useAdvancePaymentContext = () => {
|
|||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
const AdvancePaymentPage = () => {
|
const AdvancePaymentPage = () => {
|
||||||
|
const { employeeId } = useParams();
|
||||||
|
|
||||||
|
const { data: transactionData } = useExpenseTransactions(employeeId, {
|
||||||
|
enabled: !!employeeId
|
||||||
|
});
|
||||||
|
|
||||||
|
const employeeName = useMemo(() => {
|
||||||
|
if (Array.isArray(transactionData) && transactionData.length > 0) {
|
||||||
|
const emp = transactionData[0].employee;
|
||||||
|
if (emp) return `${emp.firstName} ${emp.lastName}`;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}, [transactionData]);
|
||||||
|
|
||||||
const [balance, setBalance] = useState(null);
|
const [balance, setBalance] = useState(null);
|
||||||
const { control, reset, watch } = useForm({
|
const { control, reset, watch } = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
employeeId: "",
|
employeeId: employeeId || "",
|
||||||
|
searchString: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectedEmployeeId = watch("employeeId");
|
const selectedEmployeeId = employeeId || watch("employeeId");
|
||||||
|
|
||||||
|
const searchString = watch("searchString");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const selectedEmpoyee = sessionStorage.getItem("transaction-empId");
|
const selectedEmpoyee = sessionStorage.getItem("transaction-empId");
|
||||||
reset({
|
reset({
|
||||||
@ -47,30 +67,20 @@ const AdvancePaymentPage = () => {
|
|||||||
data={[
|
data={[
|
||||||
{ label: "Home", link: "/dashboard" },
|
{ label: "Home", link: "/dashboard" },
|
||||||
{ label: "Finance", link: "/advance-payment" },
|
{ label: "Finance", link: "/advance-payment" },
|
||||||
{ label: "Advance Payment" },
|
{ label: "Advance Payment", link: "/advance-payment" },
|
||||||
]}
|
employeeName && { label: employeeName, link: "" },
|
||||||
|
].filter(Boolean)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="card px-4 py-2 page-min-h ">
|
<div className="card px-4 py-2 page-min-h ">
|
||||||
<div className="row py-1">
|
<div className="row py-1 justify-content-end">
|
||||||
<div className="col-12 col-md-4">
|
|
||||||
<div className="d-block text-start">
|
|
||||||
<EmployeeSearchInput
|
|
||||||
control={control}
|
|
||||||
name="employeeId"
|
|
||||||
projectId={null}
|
|
||||||
forAll={true}
|
|
||||||
placeholder={"Enter Employee Name"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-md-8 d-flex align-items-center justify-content-end">
|
<div className="col-md-8 d-flex align-items-center justify-content-end">
|
||||||
{balance ? (
|
{balance ? (
|
||||||
<>
|
<>
|
||||||
<label className="fs-5 fw-semibold">Current Balance : </label>
|
<label className="fs-5 fw-semibold">Current Balance : </label>
|
||||||
<span
|
<span
|
||||||
className={`${
|
className={`${balance > 0 ? "text-success" : "text-danger"
|
||||||
balance > 0 ? "text-success" : "text-danger"
|
} fs-5 fw-bold ms-1`}
|
||||||
} fs-5 fw-bold ms-1`}
|
|
||||||
>
|
>
|
||||||
{balance > 0 ? (
|
{balance > 0 ? (
|
||||||
<i className="bx bx-plus b-sm"></i>
|
<i className="bx bx-plus b-sm"></i>
|
||||||
@ -88,7 +98,7 @@ const AdvancePaymentPage = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AdvancePaymentList employeeId={selectedEmployeeId} />
|
<AdvancePaymentList employeeId={selectedEmployeeId} searchString={searchString} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AdvancePaymentContext.Provider>
|
</AdvancePaymentContext.Provider>
|
||||||
|
|||||||
34
src/pages/AdvancePayment/AdvancePaymentPage1.jsx
Normal file
34
src/pages/AdvancePayment/AdvancePaymentPage1.jsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Breadcrumb from '../../components/common/Breadcrumb'
|
||||||
|
import AdvancePaymentList1 from '../../components/AdvancePayment/AdvancePaymentList1'
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import EmployeeSearchInput from '../../components/common/EmployeeSearchInput';
|
||||||
|
|
||||||
|
const AdvancePaymentPage1 = () => {
|
||||||
|
const { control, reset, watch } = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
searchString: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const searchString = watch("searchString");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container-fluid">
|
||||||
|
<Breadcrumb
|
||||||
|
data={[
|
||||||
|
{ label: "Home", link: "/dashboard" },
|
||||||
|
{ label: "Finance", link: "/advance-payment" },
|
||||||
|
{ label: "Advance Payment" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<div className="card px-4 py-2 page-min-h">
|
||||||
|
<div className="row py-1">
|
||||||
|
<AdvancePaymentList1 searchString={searchString} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AdvancePaymentPage1
|
||||||
@ -11,14 +11,18 @@ import GlobalModel from "../../components/common/GlobalModel";
|
|||||||
import ManageServiceProject from "../../components/ServiceProject/ManageServiceProject";
|
import ManageServiceProject from "../../components/ServiceProject/ManageServiceProject";
|
||||||
import { SpinnerLoader } from "../../components/common/Loader";
|
import { SpinnerLoader } from "../../components/common/Loader";
|
||||||
import ServiceProjectCard from "../../components/ServiceProject/ServiceProjectTeam/ServiceProjectCard";
|
import ServiceProjectCard from "../../components/ServiceProject/ServiceProjectTeam/ServiceProjectCard";
|
||||||
|
import ServiceProjectList from "../../components/ServiceProject/ServiceProjectTeam/ServiceProjectList";
|
||||||
|
import { useDebounce } from "../../utils/appUtils";
|
||||||
|
|
||||||
const ServiceProjectDisplay = ({ listView ,selectedStatuses }) => {
|
const ServiceProjectDisplay = ({ listView, selectedStatuses, searchTerm }) => {
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
|
||||||
const { manageServiceProject, setManageServiceProject } = useProjectContext();
|
const { manageServiceProject, setManageServiceProject } = useProjectContext();
|
||||||
|
const debouncedSearch = useDebounce(searchTerm, 500);
|
||||||
const { data, isLoading, isError, error } = useServiceProjects(
|
const { data, isLoading, isError, error } = useServiceProjects(
|
||||||
ITEMS_PER_PAGE,
|
ITEMS_PER_PAGE,
|
||||||
currentPage
|
currentPage,
|
||||||
|
debouncedSearch
|
||||||
);
|
);
|
||||||
const paginate = (page) => {
|
const paginate = (page) => {
|
||||||
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
|
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
|
||||||
@ -47,15 +51,20 @@ const ServiceProjectDisplay = ({ listView ,selectedStatuses }) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="">
|
||||||
|
<div className="row">
|
||||||
{listView ? (
|
{listView ? (
|
||||||
<p>List</p>
|
<ServiceProjectList data={filteredProjects}
|
||||||
) : (
|
currentPage={currentPage}
|
||||||
|
totalPages={data?.totalPages}
|
||||||
|
paginate={paginate}
|
||||||
|
isCore={false} />
|
||||||
|
) : filteredProjects?.length > 0 ? (
|
||||||
filteredProjects?.map((project) => (
|
filteredProjects?.map((project) => (
|
||||||
<ServiceProjectCard key={project.id} project={project} isCore={false} />
|
<ServiceProjectCard key={project.id} project={project} isCore={false} />
|
||||||
))
|
))
|
||||||
|
|
||||||
)}
|
):(<div className="d-flex justify-content-center align-items-center page-min-h "><p>No Service projects available</p></div>)}
|
||||||
|
|
||||||
<div className="col-12 d-flex justify-content-start mt-3">
|
<div className="col-12 d-flex justify-content-start mt-3">
|
||||||
<Pagination
|
<Pagination
|
||||||
@ -82,6 +91,7 @@ const ServiceProjectDisplay = ({ listView ,selectedStatuses }) => {
|
|||||||
</GlobalModel>
|
</GlobalModel>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -116,56 +116,60 @@ const CollectionPage = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="card my-3 py-2 px-sm-4 px-2">
|
<div className="card my-3 py-2 px-sm-4 px-2">
|
||||||
<div className="row align-items-center mx-0">
|
<div className="row align-items-center gap-sm-2 gap-md-0 mx-0">
|
||||||
{/* Left side: Date Picker + Show Pending (stacked on mobile) */}
|
<div className="col-12 col-md-4 d-flex flex-column flex-md-row flex-wrap align-items-start">
|
||||||
<div className="col-12 col-md-6 d-flex flex-column flex-md-row flex-wrap align-items-start align-md-items-center gap-2 gap-md-3 mb-3 mb-md-0">
|
<div className="d-inline-flex border rounded-pill overflow-hidden shadow-none">
|
||||||
<FormProvider {...methods}>
|
<button
|
||||||
<DateRangePicker1 howManyDay={180} startField="fromDate"
|
type="button"
|
||||||
endField="toDate" />
|
className={`btn px-2 py-1 rounded-0 text-tiny ${
|
||||||
</FormProvider>
|
!showPending ? "btn-primary text-white" : ""
|
||||||
|
}`}
|
||||||
<div className="form-check form-switch d-flex align-items-center mt-1">
|
onClick={() => setShowPending(false)}
|
||||||
<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-2"
|
|
||||||
htmlFor="inactiveEmployeesCheckbox"
|
|
||||||
>
|
>
|
||||||
Show Completed Collections
|
Show All
|
||||||
</label>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`btn px-2 py-1 rounded-0 text-tiny ${
|
||||||
|
showPending ? "btn-primary text-white" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => setShowPending(true)}
|
||||||
|
>
|
||||||
|
Pending
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right side: Search + Add Button */}
|
<div className="col-12 col-sm-8 d-block d-sm-flex justify-content-end ga-2 align-items-center gap-2">
|
||||||
<div className="col-12 col-sm-6 d-flex justify-content-end align-items-center gap-2">
|
|
||||||
<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 w-auto"
|
className="form-control form-control-sm mt-2 mt-sm-0"
|
||||||
/>
|
/>
|
||||||
|
<div className="d-flex justify-content-between justify-content-sm-between mt-2 mt-sm-0">
|
||||||
|
<FormProvider {...methods} className="me-3">
|
||||||
|
<DateRangePicker1
|
||||||
|
howManyDay={180}
|
||||||
|
startField="fromDate"
|
||||||
|
endField="toDate"
|
||||||
|
/>
|
||||||
|
</FormProvider>
|
||||||
|
|
||||||
{(canCreate || isAdmin) && (
|
{(canCreate || isAdmin) && (
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-primary"
|
className="btn btn-sm btn-primary ms-sm-2"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setCollection({ isOpen: true, invoiceId: null })
|
setCollection({ isOpen: true, invoiceId: null })
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<i className="bx bx-plus-circle me-2"></i>
|
<i className="bx bx-plus-circle me-2"></i>
|
||||||
<span className="d-none d-md-inline-block">
|
<span className="d-none d-md-inline-block">Collection</span>
|
||||||
Add New Collection
|
</button>
|
||||||
</span>
|
)}
|
||||||
</button>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
import React, { createContext, useContext, useEffect, useRef, useState } from "react";
|
||||||
import Breadcrumb from "../../components/common/Breadcrumb";
|
import Breadcrumb from "../../components/common/Breadcrumb";
|
||||||
import {
|
import {
|
||||||
ITEMS_PER_PAGE,
|
ITEMS_PER_PAGE,
|
||||||
@ -36,15 +36,17 @@ const ProjectPage = () => {
|
|||||||
isOpen: false,
|
isOpen: false,
|
||||||
project: null,
|
project: null,
|
||||||
});
|
});
|
||||||
|
const dropdownRef = useRef(null);
|
||||||
const [projectList, setProjectList] = useState([]);
|
const [projectList, setProjectList] = useState([]);
|
||||||
const [listView, setListView] = useState(false);
|
const [listView, setListView] = useState(false);
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [coreProjects, setCoreProjects] = useState(() => {
|
const [coreProjects, setCoreProjects] = useState(() => {
|
||||||
const storedValue = sessionStorage.getItem('whichProjectDisplay');
|
const storedValue = sessionStorage.getItem("whichProjectDisplay");
|
||||||
return storedValue === 'true';
|
return storedValue === "true";
|
||||||
});
|
});
|
||||||
const HasManageProject = useHasUserPermission(MANAGE_PROJECT);
|
const HasManageProject = useHasUserPermission(MANAGE_PROJECT);
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const [selectedStatuses, setSelectedStatuses] = useState(
|
const [selectedStatuses, setSelectedStatuses] = useState(
|
||||||
PROJECT_STATUS.map((s) => s.id)
|
PROJECT_STATUS.map((s) => s.id)
|
||||||
@ -64,12 +66,20 @@ const ProjectPage = () => {
|
|||||||
manageServiceProject,
|
manageServiceProject,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleToggleProject = (value) => {
|
const handleToggleProject = (value) => {
|
||||||
setCoreProjects(value);
|
setCoreProjects(value);
|
||||||
sessionStorage.setItem("whichProjectDisplay", String(value));
|
sessionStorage.setItem("whichProjectDisplay", String(value));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event) => {
|
||||||
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProjectContext.Provider value={contextDispatcher}>
|
<ProjectContext.Provider value={contextDispatcher}>
|
||||||
@ -105,14 +115,11 @@ const ProjectPage = () => {
|
|||||||
>
|
>
|
||||||
Infra Project
|
Infra Project
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{/* RIGHT SIDE — SEARCH + CARD/LIST + DROPDOWN */}
|
{/* RIGHT SIDE — SEARCH + CARD/LIST + DROPDOWN */}
|
||||||
<div className="d-flex flex-wrap align-items-center justify-content-end">
|
<div className="d-flex flex-wrap align-items-center justify-content-end">
|
||||||
|
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<div className="me-2" style={{ minWidth: "200px" }}>
|
<div className="me-2" style={{ minWidth: "200px" }}>
|
||||||
<input
|
<input
|
||||||
@ -131,7 +138,8 @@ const ProjectPage = () => {
|
|||||||
<div className="d-flex gap-2">
|
<div className="d-flex gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`btn btn-sm p-1 ${!listView ? "btn-primary" : "btn-outline-primary"}`}
|
className={`btn btn-sm p-1 ${!listView ? "btn-primary" : "btn-outline-primary"
|
||||||
|
}`}
|
||||||
onClick={() => setListView(false)}
|
onClick={() => setListView(false)}
|
||||||
title="Card View"
|
title="Card View"
|
||||||
>
|
>
|
||||||
@ -140,7 +148,8 @@ const ProjectPage = () => {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`btn btn-sm p-1 ${listView ? "btn-primary" : "btn-outline-primary"}`}
|
className={`btn btn-sm p-1 ${listView ? "btn-primary" : "btn-outline-primary"
|
||||||
|
}`}
|
||||||
onClick={() => setListView(true)}
|
onClick={() => setListView(true)}
|
||||||
title="List View"
|
title="List View"
|
||||||
>
|
>
|
||||||
@ -149,41 +158,74 @@ const ProjectPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Dropdown Filter */}
|
{/* Dropdown Filter */}
|
||||||
<div className="dropdown me-2">
|
<div className="dropdown me-2 position-relative">
|
||||||
<a
|
<div
|
||||||
className="dropdown-toggle hide-arrow cursor-pointer p-1"
|
className="cursor-pointer p-1"
|
||||||
data-bs-toggle="dropdown"
|
onClick={() => setOpen(!open)}
|
||||||
aria-expanded="false"
|
|
||||||
title="Filter"
|
|
||||||
>
|
>
|
||||||
<i className="bx bx-slider-alt fs-5"></i>
|
<i
|
||||||
</a>
|
className={`bx bx-slider-alt fs-5 ${selectedStatuses.length !== PROJECT_STATUS.length ? "text-primary" : ""
|
||||||
|
}`}
|
||||||
|
></i>
|
||||||
|
|
||||||
<ul className="dropdown-menu p-2 text-capitalize">
|
{selectedStatuses.length !== PROJECT_STATUS.length && (
|
||||||
{PROJECT_STATUS.map(({ id, label }) => (
|
<span className="badge bg-warning text-white rounded-pill position-absolute"
|
||||||
<li key={id}>
|
style={{
|
||||||
<div className="form-check">
|
top: "-4px",
|
||||||
<input
|
right: "-4px",
|
||||||
className="form-check-input"
|
fontSize: "0.6rem",
|
||||||
type="checkbox"
|
padding: "2px 5px",
|
||||||
checked={selectedStatuses.includes(id)}
|
}}
|
||||||
onChange={() => handleStatusChange(id)}
|
>
|
||||||
/>
|
{PROJECT_STATUS.length - selectedStatuses.length}
|
||||||
<label className="form-check-label">{label}</label>
|
</span>
|
||||||
</div>
|
)}
|
||||||
</li>
|
</div>
|
||||||
))}
|
|
||||||
</ul>
|
{open && (
|
||||||
|
<ul
|
||||||
|
ref={dropdownRef}
|
||||||
|
className="dropdown-menu show p-2 text-capitalize"
|
||||||
|
onMouseDown={(e) => e.stopPropagation()} // IMPORTANT
|
||||||
|
>
|
||||||
|
{PROJECT_STATUS.map(({ id, label }) => (
|
||||||
|
<li key={id}>
|
||||||
|
<div className="form-check">
|
||||||
|
<input
|
||||||
|
className="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedStatuses.includes(id)}
|
||||||
|
onClick={(e) => e.stopPropagation()} // IMPORTANT
|
||||||
|
onChange={() => handleStatusChange(id)}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
className="form-check-label"
|
||||||
|
onClick={(e) => e.stopPropagation()} // OPTIONAL
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{HasManageProject && (
|
{HasManageProject && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-primary btn-sm d-flex align-items-center my-2"
|
className="btn btn-primary btn-sm d-flex align-items-center my-2"
|
||||||
onClick={() =>
|
onClick={
|
||||||
coreProjects
|
() =>
|
||||||
? setMangeProject({ isOpen: true, Project: null }) // Organization Project → Infra
|
coreProjects
|
||||||
: setManageServiceProject({ isOpen: true, Project: null }) // Service Project
|
? setMangeProject({ isOpen: true, Project: null }) // Organization Project → Infra
|
||||||
|
: setManageServiceProject({
|
||||||
|
isOpen: true,
|
||||||
|
Project: null,
|
||||||
|
}) // Service Project
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<i className="bx bx-plus-circle me-2"></i>
|
<i className="bx bx-plus-circle me-2"></i>
|
||||||
@ -195,11 +237,22 @@ const ProjectPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{coreProjects ? <ProjectsDisplay listView={listView}
|
{coreProjects ? (
|
||||||
searchTerm={searchTerm}
|
<ProjectsDisplay
|
||||||
selectedStatuses={selectedStatuses}
|
listView={listView}
|
||||||
handleStatusChange={handleStatusChange} /> : <ServiceProjectDisplay listView={listView}
|
searchTerm={searchTerm}
|
||||||
selectedStatuses={selectedStatuses} />}
|
selectedStatuses={selectedStatuses}
|
||||||
|
handleStatusChange={handleStatusChange}
|
||||||
|
currentPage={currentPage}
|
||||||
|
setCurrentPage={setCurrentPage}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ServiceProjectDisplay
|
||||||
|
listView={listView}
|
||||||
|
searchTerm={searchTerm}
|
||||||
|
selectedStatuses={selectedStatuses}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ProjectContext.Provider>
|
</ProjectContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { useServiceProjects } from "../../hooks/useServiceProject";
|
|||||||
import { ITEMS_PER_PAGE, PROJECT_STATUS } from "../../utils/constants";
|
import { ITEMS_PER_PAGE, PROJECT_STATUS } from "../../utils/constants";
|
||||||
import usePagination from "../../hooks/usePagination";
|
import usePagination from "../../hooks/usePagination";
|
||||||
import ManageProjectInfo from "../../components/Project/ManageProjectInfo";
|
import ManageProjectInfo from "../../components/Project/ManageProjectInfo";
|
||||||
|
import { useDebounce } from "../../utils/appUtils";
|
||||||
|
|
||||||
const ProjectsDisplay = ({
|
const ProjectsDisplay = ({
|
||||||
listView,
|
listView,
|
||||||
@ -26,8 +27,8 @@ const ProjectsDisplay = ({
|
|||||||
} = useProjectContext();
|
} = useProjectContext();
|
||||||
|
|
||||||
const [projectList, setProjectList] = useState([]);
|
const [projectList, setProjectList] = useState([]);
|
||||||
|
const debouncedSearch = useDebounce(searchTerm, 500);
|
||||||
const { data, isLoading, isError, error } = useProjects(ITEMS_PER_PAGE, 1);
|
const { data, isLoading, isError, error } = useProjects(ITEMS_PER_PAGE, 1, debouncedSearch);
|
||||||
|
|
||||||
const filteredProjects =
|
const filteredProjects =
|
||||||
data?.data?.filter((project) => {
|
data?.data?.filter((project) => {
|
||||||
@ -98,7 +99,7 @@ const ProjectsDisplay = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="">
|
||||||
{listView ? (
|
{listView ? (
|
||||||
<ProjectListView
|
<ProjectListView
|
||||||
data={projectList}
|
data={projectList}
|
||||||
|
|||||||
@ -11,12 +11,13 @@ const EmployeeRepository = {
|
|||||||
// deleteEmployee: ( id ) => api.delete( `/users/${ id }` ),
|
// deleteEmployee: ( id ) => api.delete( `/users/${ id }` ),
|
||||||
getEmployeeProfile: (id) => api.get(`/api/employee/profile/get/${id}`),
|
getEmployeeProfile: (id) => api.get(`/api/employee/profile/get/${id}`),
|
||||||
deleteEmployee: (id, active) => api.delete(`/api/employee/${id}?active=${active}`),
|
deleteEmployee: (id, active) => api.delete(`/api/employee/${id}?active=${active}`),
|
||||||
getEmployeeName: (projectId, search, allEmployee) => {
|
getEmployeeName: (projectId, search, allEmployee,employeeId) => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
if (projectId) params.append("projectId", projectId);
|
if (projectId) params.append("projectId", projectId);
|
||||||
if (search) params.append("searchString", search);
|
if (search) params.append("searchString", search);
|
||||||
if (allEmployee) params.append("allEmployee", allEmployee)
|
if (allEmployee) params.append("allEmployee", allEmployee);
|
||||||
|
if (employeeId) params.append("employeeId", employeeId);
|
||||||
|
|
||||||
const query = params.toString();
|
const query = params.toString();
|
||||||
return api.get(`/api/Employee/basic${query ? `?${query}` : ""}`);
|
return api.get(`/api/Employee/basic${query ? `?${query}` : ""}`);
|
||||||
|
|||||||
@ -44,13 +44,13 @@ const ExpenseRepository = {
|
|||||||
DeletePaymentRequest: () => api.get("delete here come"),
|
DeletePaymentRequest: () => api.get("delete here come"),
|
||||||
CreatePaymentRequestExpense: (data) =>
|
CreatePaymentRequestExpense: (data) =>
|
||||||
api.post("/api/Expense/payment-request/expense/create", data),
|
api.post("/api/Expense/payment-request/expense/create", data),
|
||||||
GetPayee:()=>api.get('/api/Expense/payment-request/payee'),
|
GetPayee: () => api.get('/api/Expense/payment-request/payee'),
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//#region Recurring Expense
|
//#region Recurring Expense
|
||||||
GetRecurringExpenseList:(pageSize, pageNumber, filter,isActive, searchString) => {
|
GetRecurringExpenseList: (pageSize, pageNumber, filter, isActive, searchString) => {
|
||||||
const payloadJsonString = JSON.stringify(filter);
|
const payloadJsonString = JSON.stringify(filter);
|
||||||
return api.get(
|
return api.get(
|
||||||
`/api/expense/get/recurring-payment/list?pageSize=${pageSize}&pageNumber=${pageNumber}&filter=${payloadJsonString}&isActive=${isActive}&searchString=${searchString}`
|
`/api/expense/get/recurring-payment/list?pageSize=${pageSize}&pageNumber=${pageNumber}&filter=${payloadJsonString}&isActive=${isActive}&searchString=${searchString}`
|
||||||
@ -70,9 +70,11 @@ const ExpenseRepository = {
|
|||||||
//#region Advance Payment
|
//#region Advance Payment
|
||||||
GetTranctionList: (employeeId) =>
|
GetTranctionList: (employeeId) =>
|
||||||
api.get(`/api/Expense/get/transactions/${employeeId}`),
|
api.get(`/api/Expense/get/transactions/${employeeId}`),
|
||||||
|
getAllTranctionList: (searchString) =>
|
||||||
|
api.get(`/api/Expense/get/advance-payment/employee/list?searchString=${searchString}`),
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ExpenseRepository;
|
export default ExpenseRepository;
|
||||||
|
|||||||
@ -18,6 +18,8 @@ const GlobalRepository = {
|
|||||||
|
|
||||||
return api.get(`/api/Dashboard/Progression?${params.toString()}`);
|
return api.get(`/api/Dashboard/Progression?${params.toString()}`);
|
||||||
},
|
},
|
||||||
|
getProjectCompletionStatus:()=>api.get(`/api/Dashboard/project-completion-status`),
|
||||||
|
|
||||||
|
|
||||||
getDashboardAttendanceData: (date, projectId) => {
|
getDashboardAttendanceData: (date, projectId) => {
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { api } from "../utils/axiosClient";
|
import { api } from "../utils/axiosClient";
|
||||||
|
|
||||||
const ProjectRepository = {
|
const ProjectRepository = {
|
||||||
getProjectList: (pageSize, pageNumber) =>
|
|
||||||
api.get(`/api/project/list?pageSize=${pageSize}&pageNumber=${pageNumber}`),
|
getProjectList: (pageSize, pageNumber,searchString) =>
|
||||||
|
api.get(`/api/project/list?pageSize=${pageSize}&pageNumber=${pageNumber}&searchString=${searchString}`),
|
||||||
getProjectByprojectId: (projetid) =>
|
getProjectByprojectId: (projetid) =>
|
||||||
api.get(`/api/project/details/${projetid}`),
|
api.get(`/api/project/details/${projetid}`),
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
|
import { isAction } from "@reduxjs/toolkit";
|
||||||
import { api } from "../utils/axiosClient";
|
import { api } from "../utils/axiosClient";
|
||||||
|
|
||||||
export const ServiceProjectRepository = {
|
export const ServiceProjectRepository = {
|
||||||
|
//#region Service Project
|
||||||
CreateServiceProject: (data) => api.post("/api/ServiceProject/create", data),
|
CreateServiceProject: (data) => api.post("/api/ServiceProject/create", data),
|
||||||
GetServiceProjects: (pageSize, pageNumber) =>
|
GetServiceProjects: (pageSize, pageNumber,searchString) =>
|
||||||
api.get(
|
api.get(
|
||||||
`/api/ServiceProject/list?pageSize=${pageSize}&pageNumber=${pageNumber}`
|
`/api/ServiceProject/list?pageSize=${pageSize}&pageNumber=${pageNumber}&searchString=${searchString}`
|
||||||
),
|
),
|
||||||
GetServiceProject: (id) => api.get(`/api/ServiceProject/details/${id}`),
|
GetServiceProject: (id) => api.get(`/api/ServiceProject/details/${id}`),
|
||||||
UpdateServiceProject: (id, data) =>
|
UpdateServiceProject: (id, data) =>
|
||||||
@ -17,12 +19,14 @@ export const ServiceProjectRepository = {
|
|||||||
api.get(
|
api.get(
|
||||||
`/api/ServiceProject/get/allocation/list?projectId=${projectId}&isActive=${isActive} `
|
`/api/ServiceProject/get/allocation/list?projectId=${projectId}&isActive=${isActive} `
|
||||||
),
|
),
|
||||||
|
//#endregion
|
||||||
|
|
||||||
//#region Job
|
//#region Job
|
||||||
|
|
||||||
CreateJob: (data) => api.post(`/api/ServiceProject/job/create`, data),
|
CreateJob: (data) => api.post(`/api/ServiceProject/job/create`, data),
|
||||||
GetJobList: (pageSize, pageNumber, isActive, projectId) =>
|
GetJobList: (pageSize, pageNumber, isActive, projectId,isArchive) =>
|
||||||
api.get(
|
api.get(
|
||||||
`/api/ServiceProject/job/list?pageSize=${pageSize}&pageNumber=${pageNumber}&isActive=${isActive}&projectId=${projectId}`
|
`/api/ServiceProject/job/list?pageSize=${pageSize}&pageNumber=${pageNumber}&isActive=${isActive}&projectId=${projectId}&isArchive=${isArchive}`
|
||||||
),
|
),
|
||||||
GetJobDetails: (id) => api.get(`/api/ServiceProject/job/details/${id}`),
|
GetJobDetails: (id) => api.get(`/api/ServiceProject/job/details/${id}`),
|
||||||
AddComment: (data) => api.post("/api/ServiceProject/job/add/comment", data),
|
AddComment: (data) => api.post("/api/ServiceProject/job/add/comment", data),
|
||||||
@ -35,4 +39,22 @@ export const ServiceProjectRepository = {
|
|||||||
api.patch(`/api/ServiceProject/job/edit/${id}`, patchData, {
|
api.patch(`/api/ServiceProject/job/edit/${id}`, patchData, {
|
||||||
"Content-Type": "application/json-patch+json",
|
"Content-Type": "application/json-patch+json",
|
||||||
}),
|
}),
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region Project Branch
|
||||||
|
CreateBranch: (data) => api.post(`/api/ServiceProject/branch/create`, data),
|
||||||
|
UpdateBranch: (id, data) =>
|
||||||
|
api.put(`/api/ServiceProject/branch/edit/${id}`, data),
|
||||||
|
|
||||||
|
GetBranchList: (projectId, isActive, pageSize, pageNumber, searchString) => {
|
||||||
|
return api.get(
|
||||||
|
`/api/ServiceProject/branch/list/${projectId}?isActive=${isActive}&pageSize=${pageSize}&pageNumber=${pageNumber}&searchString=${searchString}`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
GetBranchDetail: (id) => api.get(`/api/ServiceProject/branch/details/${id}`),
|
||||||
|
DeleteBranch: (id, isActive = false) =>
|
||||||
|
api.delete(`/api/ServiceProject/branch/delete/${id}?isActive=${isActive}`),
|
||||||
|
|
||||||
|
GetBranchTypeList: () => api.get(`/api/serviceproject/branch-type/list`),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -60,7 +60,8 @@ import PaymentRequestPage from "../pages/PaymentRequest/PaymentRequestPage";
|
|||||||
import RecurringExpensePage from "../pages/RecurringExpense/RecurringExpensePage";
|
import RecurringExpensePage from "../pages/RecurringExpense/RecurringExpensePage";
|
||||||
import AdvancePaymentPage from "../pages/AdvancePayment/AdvancePaymentPage";
|
import AdvancePaymentPage from "../pages/AdvancePayment/AdvancePaymentPage";
|
||||||
import ServiceProjectDetail from "../pages/ServiceProject/ServiceProjectDetail";
|
import ServiceProjectDetail from "../pages/ServiceProject/ServiceProjectDetail";
|
||||||
import ManageJob from "../components/ServiceProject/ManageJob";
|
import ManageJob from "../components/ServiceProject/ServiceProjectJob/ManageJob";
|
||||||
|
import AdvancePaymentPage1 from "../pages/AdvancePayment/AdvancePaymentPage1";
|
||||||
const router = createBrowserRouter(
|
const router = createBrowserRouter(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@ -116,7 +117,8 @@ const router = createBrowserRouter(
|
|||||||
{ path: "/expenses", element: <ExpensePage /> },
|
{ path: "/expenses", element: <ExpensePage /> },
|
||||||
{ path: "/payment-request", element: <PaymentRequestPage /> },
|
{ path: "/payment-request", element: <PaymentRequestPage /> },
|
||||||
{ path: "/recurring-payment", element: <RecurringExpensePage /> },
|
{ path: "/recurring-payment", element: <RecurringExpensePage /> },
|
||||||
{ path: "/advance-payment", element: <AdvancePaymentPage /> },
|
{ path: "/advance-payment", element: <AdvancePaymentPage1 /> },
|
||||||
|
{ path: "/advance-payment/:employeeId", element: <AdvancePaymentPage /> },
|
||||||
{ path: "/collection", element: <CollectionPage /> },
|
{ path: "/collection", element: <CollectionPage /> },
|
||||||
{ path: "/masters", element: <MasterPage /> },
|
{ path: "/masters", element: <MasterPage /> },
|
||||||
{ path: "/tenants", element: <TenantPage /> },
|
{ path: "/tenants", element: <TenantPage /> },
|
||||||
|
|||||||
@ -150,6 +150,9 @@ export function startSignalR(loggedUser) {
|
|||||||
queryClient.invalidateQueries(["serviceProjects"]);
|
queryClient.invalidateQueries(["serviceProjects"]);
|
||||||
queryClient.invalidateQueries(["serviceProject"]);
|
queryClient.invalidateQueries(["serviceProject"]);
|
||||||
}
|
}
|
||||||
|
if (keyword === "Project_Branch") {
|
||||||
|
queryClient.invalidateQueries(["branches"]);
|
||||||
|
}
|
||||||
|
|
||||||
if (keyword === "Service_Project_Allocation") {
|
if (keyword === "Service_Project_Allocation") {
|
||||||
queryClient.invalidateQueries(["serviceProjectTeam"]);
|
queryClient.invalidateQueries(["serviceProjectTeam"]);
|
||||||
|
|||||||
@ -210,4 +210,35 @@ export const PAYEE_RECURRING_EXPENSE = [
|
|||||||
//#region Service Project and Jobs
|
//#region Service Project and Jobs
|
||||||
export const STATUS_JOB_CLOSED = "3ddeefb5-ae3c-4e10-a922-35e0a452bb69"
|
export const STATUS_JOB_CLOSED = "3ddeefb5-ae3c-4e10-a922-35e0a452bb69"
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
export const JOBS_STATUS_IDS = [
|
||||||
|
{
|
||||||
|
id: "32d76a02-8f44-4aa0-9b66-c3716c45a918",
|
||||||
|
label: "New",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cfa1886d-055f-4ded-84c6-42a2a8a14a66",
|
||||||
|
label: "Assigned",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "5a6873a5-fed7-4745-a52f-8f61bf3bd72d",
|
||||||
|
label: "In Progress",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "aab71020-2fb8-44d9-9430-c9a7e9bf33b0",
|
||||||
|
label: "Work Done",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ed10ab57-dbaa-4ca5-8ecd-56745dcbdbd7",
|
||||||
|
label: "Review Done",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3ddeefb5-ae3c-4e10-a922-35e0a452bb69",
|
||||||
|
label: "Closed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "75a0c8b8-9c6a-41af-80bf-b35bab722eb2",
|
||||||
|
label: "On Hold",
|
||||||
|
},
|
||||||
|
];
|
||||||
Loading…
x
Reference in New Issue
Block a user