added initially expense

This commit is contained in:
pramod.mahajan 2025-11-07 18:15:45 +05:30
parent dcbb4a3997
commit 6a97dcf5f6
18 changed files with 1058 additions and 677 deletions

View File

@ -1,6 +1,5 @@
import React, { useState, useEffect } from "react";
import Chart from "react-apexcharts";
import { useExpenseType } from "../../hooks/masterHook/useMaster";
import { useSelector } from "react-redux";
import { useExpenseDataByProject } from "../../hooks/useDashboard_Data";
import { formatCurrency } from "../../utils/appUtils";
@ -8,6 +7,7 @@ import { formatDate_DayMonth } from "../../utils/dateUtils";
import { useProjectName } from "../../hooks/useProjects";
import { useSelectedProject } from "../../slices/apiDataManager";
import { SpinnerLoader } from "../common/Loader";
import { useExpenseCategory } from "../../hooks/masterHook/useMaster";
const ExpenseByProject = () => {
const projectId = useSelector((store) => store.localVariables.projectId);
@ -19,7 +19,7 @@ const ExpenseByProject = () => {
const [chartData, setChartData] = useState({ categories: [], data: [] });
const selectedProject = useSelectedProject();
const { ExpenseTypes, loading: typeLoading } = useExpenseType();
const {expenseCategories , loading: typeLoading } = useExpenseCategory();
const { data: expenseApiData, isLoading } = useExpenseDataByProject(
projectId,
@ -50,7 +50,7 @@ const ExpenseByProject = () => {
const getSelectedTypeName = () => {
if (!selectedType) return "All Types";
const found = ExpenseTypes.find((t) => t.id === selectedType);
const found = expenseCategories?.find((t) => t.id === selectedType);
return found ? found.name : "All Types";
};
@ -157,7 +157,7 @@ const ExpenseByProject = () => {
style={{ maxWidth: "200px" }}
>
<option value="">All Types</option>
{ExpenseTypes.map((type) => (
{expenseCategories?.map((type) => (
<option key={type.id} value={type.id}>
{type.name}
</option>

View File

@ -0,0 +1,50 @@
const ActiveFilters = ({ filters, optionsLookup = {}, onRemove }) => {
const entries = Object.entries(filters || {});
return (
<div className="d-flex flex-wrap gap-2">
{entries.map(([key, value]) => {
if (!value || (Array.isArray(value) && value.length === 0)) return null;
if (Array.isArray(value)) {
return value.map((v) => {
const label = optionsLookup[key]?.[v] || v;
return (
<span
key={`${key}-${v}`}
className="badge bg-label-primary cursor-pointer"
onClick={() => onRemove(key, v)}
>
{label} <i className="bx bx-x ms-1"></i>
</span>
);
});
}
if (typeof value === "boolean") {
return (
<span
key={key}
className="badge bg-label-success cursor-pointer"
onClick={() => onRemove(key)}
>
{key}: {value ? "Yes" : "No"} <i className="bx bx-x ms-1"></i>
</span>
);
}
return (
<span className="badge bg-primary">
{data?.startDate && data?.endDate
? `${formatUTCToLocalTime(
data.startDate
)} - ${formatUTCToLocalTime(data.endDate)}`
: "No dates"}
</span>
);
})}
</div>
);
};
export default ActiveFilters;

View File

@ -40,44 +40,47 @@ const ExpenseFilterChips = ({ filters, filterData, removeFilterChip }) => {
if (!filterChips.length) return null;
return (
<div className="row">
<div className="col-12">
<div className="d-flex flex-wrap align-items-start gap-1 text-start">
{filterChips.map((chip) => (
<div
key={chip.key}
className="d-flex align-items-center flex-wrap px-2 py-1 "
style={{ fontSize: "0.9rem", maxWidth: "100%" }}
>
{/* Chip Label */}
<span className="fw-semibold me-2">{chip.label}:</span>
<div className="row">
<div className="col-12">
<div className="d-flex flex-wrap align-items-start gap-1 text-start">
{filterChips.map((chip) => (
<div
key={chip.key}
className="d-flex align-items-center flex-wrap px-2 py-1 "
style={{ fontSize: "0.9rem", maxWidth: "100%" }}
>
{/* Chip Label */}
<span className="fw-semibold me-2">{chip.label}:</span>
{/* Chip Items */}
<div className="d-flex flex-wrap align-items-center gap-1">
{chip.items.map((item) => (
<span
key={item.id}
className="d-flex align-items-center bg-light rounded px-2 py-1 text-xs"
>
<span>{item.name}</span>
<button
type="button"
className="btn-close btn-close-white btn-sm ms-2"
style={{
filter: "invert(1) grayscale(1)",
opacity: 0.7,
fontSize: "0.6rem",
}}
onClick={() => removeFilterChip(chip.key, item.id)}
/>
</span>
))}
</div>
</div>
))}
</div>
</div>
{/* Chip Items */}
<div className="d-flex flex-wrap align-items-center gap-1">
{chip.items.map((item) => (
<span
key={item.id}
className="d-flex align-items-center bg-light rounded px-2 py-1 text-xs"
>
<span>{item.name}</span>
<button
type="button"
className="btn-close btn-close-white btn-sm ms-2"
style={{
filter: "invert(1) grayscale(1)",
opacity: 0.7,
fontSize: "0.6rem",
}}
onClick={() => removeFilterChip(chip.key, item.id)}
/>
</span>
))}
</div>
</div>
))}
</div>
</div>
</div>
);
};

View File

@ -46,12 +46,12 @@ const ExpenseFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata }
projectIds: defaultFilter.projectIds || [],
createdByIds: defaultFilter.createdByIds || [],
paidById: defaultFilter.paidById || [],
ExpenseTypeIds: defaultFilter.ExpenseTypeIds || [],
ExpenseCategoryIds: defaultFilter.ExpenseCategoryIds || [],
isTransactionDate: defaultFilter.isTransactionDate ?? true,
startDate: defaultFilter.startDate,
endDate: defaultFilter.endDate,
};
}, [status]);
}, [status, selectedProjectId]);
const methods = useForm({
resolver: zodResolver(SearchSchema),
@ -96,7 +96,7 @@ const ExpenseFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata }
endDate: moment.utc(formData.endDate, "DD-MM-YYYY").toISOString(),
});
handleGroupBy(selectedGroup.id);
closePanel();
// closePanel();
};
const onClear = () => {
@ -105,7 +105,7 @@ const ExpenseFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata }
setSelectedGroup(groupByList[0]);
onApply(defaultFilter);
handleGroupBy(groupByList[0].id);
closePanel();
// closePanel();
if (status) {
navigate("/expenses", { replace: true });
}
@ -148,7 +148,6 @@ const ExpenseFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata }
selectedProjectId,
]);
if (isLoading || isFetching) return <ExpenseFilterSkeleton />;
if (isError && isFetched)
return <div>Something went wrong Here- {error.message} </div>;
@ -181,7 +180,6 @@ const ExpenseFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata }
</button>
</div>
</div>
<label className="fw-semibold">Choose Date Range:</label>
<DateRangePicker1
placeholder="DD-MM-YYYY To DD-MM-YYYY"
startField="startDate"
@ -189,6 +187,7 @@ const ExpenseFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata }
resetSignal={resetKey}
defaultRange={false}
maxDate={new Date()}
className="w-100"
/>
</div>
@ -215,9 +214,9 @@ const ExpenseFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata }
valueKey="id"
/>
<SelectMultiple
name="ExpenseTypeIds"
name="ExpenseCategoryIds"
label="Category :"
options={data.expensesType}
options={data.expenseCategory}
labelKey={(item) => item.name}
valueKey="id"
/>

View File

@ -12,6 +12,7 @@ import {
} from "../../utils/constants";
import {
formatCurrency,
formatFigure,
getColorNameFromHex,
useDebounce,
} from "../../utils/appUtils";
@ -26,14 +27,18 @@ import { useNavigate } from "react-router-dom";
const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
const [deletingId, setDeletingId] = useState(null);
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const { setViewExpense, setManageExpenseModal, filterData, removeFilterChip } = useExpenseContext();
const {
setViewExpense,
setManageExpenseModal,
filterData,
removeFilterChip,
} = useExpenseContext();
const IsExpenseEditable = useHasUserPermission();
const IsExpesneApprpve = useHasUserPermission(APPROVE_EXPENSE);
const [currentPage, setCurrentPage] = useState(1);
const debouncedSearch = useDebounce(searchText, 500);
const navigate = useNavigate();
const { mutate: DeleteExpense, isPending } = useDeleteExpense();
const { data, isLoading, isError, isInitialLoading, error } = useExpenseList(
ITEMS_PER_PAGE,
@ -80,8 +85,9 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
displayField = "Status";
break;
case "submittedBy":
key = `${item?.createdBy?.firstName ?? ""} ${item.createdBy?.lastName ?? ""
}`.trim();
key = `${item?.createdBy?.firstName ?? ""} ${
item.createdBy?.lastName ?? ""
}`.trim();
displayField = "Submitted By";
break;
case "project":
@ -92,8 +98,8 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
key = item?.paymentMode?.name || "Unknown Mode";
displayField = "Payment Mode";
break;
case "expensesType":
key = item?.expensesType?.name || "Unknown Type";
case "expenseCategory":
key = item?.expenseCategory?.name || "Unknown Type";
displayField = "Expense Category";
break;
case "createdAt":
@ -123,9 +129,9 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
align: "text-start mx-2",
},
{
key: "expensesType",
key: "expensesCategory",
label: "Expense Category",
getValue: (e) => e.expensesType?.name || "N/A",
getValue: (e) => e.expenseCategory?.name || "N/A",
align: "text-start",
},
{
@ -139,11 +145,14 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
label: "Submitted By",
align: "text-start",
getValue: (e) =>
`${e.createdBy?.firstName ?? ""} ${e.createdBy?.lastName ?? ""
}`.trim() || "N/A",
`${e.createdBy?.firstName ?? ""} ${
e.createdBy?.lastName ?? ""
}`.trim() || "N/A",
customRender: (e) => (
<div className="d-flex align-items-center cursor-pointer"
onClick={() => navigate(`/employee/${e.createdBy?.id}`)}>
<div
className="d-flex align-items-center cursor-pointer"
onClick={() => navigate(`/employee/${e.createdBy?.id}`)}
>
<Avatar
size="xs"
classAvatar="m-0"
@ -151,8 +160,9 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
lastName={e.createdBy?.lastName}
/>
<span className="text-truncate">
{`${e.createdBy?.firstName ?? ""} ${e.createdBy?.lastName ?? ""
}`.trim() || "N/A"}
{`${e.createdBy?.firstName ?? ""} ${
e.createdBy?.lastName ?? ""
}`.trim() || "N/A"}
</span>
</div>
),
@ -166,7 +176,15 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
{
key: "amount",
label: "Amount",
getValue: (e) => <>{formatCurrency(e?.amount)}</>,
getValue: (e) => (
<>
{" "}
{formatFigure(e?.amount, {
type: "currency",
currency: e?.currency?.currencyCode,
})}
</>
),
isAlwaysVisible: true,
align: "text-end",
},
@ -176,16 +194,26 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
align: "text-center",
getValue: (e) => (
<span
className={`badge bg-label-${getColorNameFromHex(e?.status?.color) || "secondary"
}`}
className={`badge bg-label-${
getColorNameFromHex(e?.status?.color) || "secondary"
}`}
>
{e.status?.name || "Unknown"}
</span>
),
},
];
if (isInitialLoading && !data) return <ExpenseTableSkeleton />;
const headers = [
"Expense Category",
"Payment Mode",
"Submitted By",
"Submitted",
"Amount",
"Status",
"Action",
];
if (isInitialLoading && !data)
return <ExpenseTableSkeleton headers={headers} />;
if (isError) return <div>{error?.message}</div>;
const grouped = groupBy
@ -209,6 +237,7 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
expense?.status?.id === EXPENSE_DRAFT && expense?.createdBy?.id === SelfId
);
};
return (
<>
{IsDeleteModalOpen && (
@ -245,10 +274,10 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
(col.isAlwaysVisible || groupBy !== col.key) && (
<th
key={col.key}
className={`sorting d-table-cell`}
className={`sorting d-table-cell `}
aria-sort="descending"
>
<div className={`${col.align}`}>{col.label}</div>
<div className={`${col.align} `}>{col.label}</div>
</th>
)
)}
@ -263,12 +292,12 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
<React.Fragment key={key}>
<tr className="tr-group text-dark">
<td colSpan={8} className="text-start">
<div className="d-flex align-items-center">
<div className="d-flex align-items-center px-2">
{" "}
<small className="fs-6 ms-2 py-1">
<small className="fs-6 py-1">
{displayField} :{" "}
</small>{" "}
<small className="fs-6 ms-3">
<small className="fs-6 ms-3">
{IsGroupedByDate
? formatUTCToLocalTime(key)
: key}
@ -283,16 +312,37 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
(col.isAlwaysVisible || groupBy !== col.key) && (
<td
key={col.key}
className={`d-table-cell ${col.align ?? ""}`}
className={`d-table-cell ml-2 ${
col.align ?? ""
} `}
>
{col.customRender
? col.customRender(expense)
: col.getValue(expense)}
<div
className={`d-flex px-2 ${
col.key === "status"
? "justify-content-center"
: ""
}
${
col.key === "amount"
? "justify-content-end"
: ""
}
${
col.key === "submitted"
? "justify-content-center"
: ""
}
`}
>
{col.customRender
? col.customRender(expense)
: col.getValue(expense)}
</div>
</td>
)
)}
<td className="sticky-action-column bg-white">
<div className="d-flex justify-content-center gap-2">
<div className="d-flex flex-row gap-2">
<i
className="bx bx-show text-primary cursor-pointer"
onClick={() =>
@ -374,15 +424,15 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => {
)}
</tbody>
</table>
{data?.data?.length > 0 && (
<Pagination
currentPage={currentPage}
totalPages={data.totalPages}
onPageChange={paginate}
/>
)}
</div>
</div>
{data?.data?.length > 0 && (
<Pagination
currentPage={currentPage}
totalPages={data.totalPages}
onPageChange={paginate}
/>
)}
</div>
</>
);

View File

@ -1,4 +1,6 @@
import { z } from "zod";
import { localToUtc } from "../../utils/appUtils";
import { DEFAULT_CURRENCY } from "../../utils/constants";
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
const ALLOWED_TYPES = [
@ -8,24 +10,25 @@ const ALLOWED_TYPES = [
"image/jpeg",
];
export const ExpenseSchema = (expenseTypes) => {
export const ExpenseSchema = (ExpenseCategories) => {
return z
.object({
projectId: z.string().min(1, { message: "Project is required" }),
expensesTypeId: z
expenseCategoryId: z
.string()
.min(1, { message: "Expense type is required" }),
paymentModeId: z.string().min(1, { message: "Payment mode is required" }),
paidById: z.string().min(1, { message: "Employee name is required" }),
transactionDate: z
.string()
.min(1, { message: "Date is required" })
,
transactionDate: z.string().min(1, { message: "Date is required" }),
transactionId: z.string().optional(),
description: z.string().min(1, { message: "Description is required" }),
location: z.string().min(1, { message: "Location is required" }),
supplerName: z.string().min(1, { message: "Supplier name is required" }),
gstNumber :z.string().optional(),
gstNumber: z.string().optional(),
currencyId: z
.string()
.min(1, { message: "currency is required" })
.default(DEFAULT_CURRENCY),
amount: z.coerce
.number({
invalid_type_error: "Amount is required and must be a number",
@ -54,8 +57,6 @@ export const ExpenseSchema = (expenseTypes) => {
})
)
.nonempty({ message: "At least one file attachment is required" }),
})
.refine(
(data) => {
@ -68,9 +69,14 @@ export const ExpenseSchema = (expenseTypes) => {
path: ["paidById"],
}
)
.superRefine((data, ctx) => {
const expenseType = expenseTypes.find((et) => et.id === data.expensesTypeId);
if (expenseType?.noOfPersonsRequired && (!data.noOfPersons || data.noOfPersons < 1)) {
.superRefine((data, ctx) => {
const ExpenseCategory = ExpenseCategories.find(
(et) => et.id === data.expenseCategoryId
);
if (
ExpenseCategory?.noOfPersonsRequired &&
(!data.noOfPersons || data.noOfPersons < 1)
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "No. of Persons is required and must be at least 1",
@ -82,7 +88,7 @@ export const ExpenseSchema = (expenseTypes) => {
export const defaultExpense = {
projectId: "",
expensesTypeId: "",
expenseCategoryId: "",
paymentModeId: "",
paidById: "",
transactionDate: "",
@ -92,12 +98,15 @@ export const defaultExpense = {
supplerName: "",
amount: "",
noOfPersons: "",
gstNumber:"",
gstNumber: "",
currencyId: DEFAULT_CURRENCY,
billAttachments: [],
};
export const ExpenseActionScheam = (isReimbursement = false) => {
export const ExpenseActionScheam = (
isReimbursement = false,
transactionDate
) => {
return z
.object({
comment: z.string().min(1, { message: "Please leave comment" }),
@ -105,6 +114,9 @@ export const ExpenseActionScheam = (isReimbursement = false) => {
reimburseTransactionId: z.string().nullable().optional(),
reimburseDate: z.string().nullable().optional(),
reimburseById: z.string().nullable().optional(),
tdsPercentage: z.string().nullable().optional(),
baseAmount: z.string().nullable().optional(),
taxAmount: z.string().nullable().optional(),
})
.superRefine((data, ctx) => {
if (isReimbursement) {
@ -122,6 +134,7 @@ export const ExpenseActionScheam = (isReimbursement = false) => {
message: "Reimburse Date is required",
});
}
if (!data.reimburseById) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
@ -129,26 +142,42 @@ export const ExpenseActionScheam = (isReimbursement = false) => {
message: "Reimburse By is required",
});
}
if (!data.baseAmount) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["baseAmount"],
message: "Base Amount i required",
});
}
if (!data.taxAmount) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["taxAmount"],
message: "Tax is required",
});
}
}
});
};
export const defaultActionValues = {
export const defaultActionValues = {
comment: "",
statusId: "",
reimburseTransactionId: null,
reimburseDate: null,
reimburseById: null,
tdsPercentage: null,
baseAmount:null,
taxAmount: null,
};
export const SearchSchema = z.object({
projectIds: z.array(z.string()).optional(),
statusIds: z.array(z.string()).optional(),
createdByIds: z.array(z.string()).optional(),
paidById: z.array(z.string()).optional(),
ExpenseCategoryIds: z.array(z.string()).optional(),
startDate: z.string().optional(),
endDate: z.string().optional(),
isTransactionDate: z.boolean().default(true),
@ -159,8 +188,8 @@ export const defaultFilter = {
statusIds: [],
createdByIds: [],
paidById: [],
ExpenseCategoryIds: [],
isTransactionDate: true,
startDate: null,
endDate: null,
};

View File

@ -1,10 +1,11 @@
import { useState,useMemo } from "react";
import { useState, useMemo } from "react";
import Avatar from "../common/Avatar";
import { formatUTCToLocalTime } from "../../utils/dateUtils";
import Timeline from "../common/TimeLine";
import moment from "moment";
import { getColorNameFromHex } from "../../utils/appUtils";
const ExpenseStatusLogs = ({ data }) => {
const [visibleCount, setVisibleCount] = useState(4);
const sortedLogs = useMemo(() => {
if (!data?.expenseLogs) return [];
@ -13,56 +14,35 @@ const ExpenseStatusLogs = ({ data }) => {
);
}, [data?.expenseLogs]);
const logsToShow = sortedLogs.slice(0, visibleCount);
const timelineData = useMemo(() => {
return sortedLogs.map((log, index) => ({
id: index + 1,
title: log.action || "Status Updated",
description: log.comment || "",
timeAgo: log.updateAt,
color: getColorNameFromHex(log.nextStatus?.color) || "primary",
users: log.updatedBy
? [
{
firstName: log.updatedBy.firstName || "",
lastName: log?.updatedBy?.lastName || "",
role: log.updatedBy.jobRoleName || "",
avatar: log.updatedBy.photo,
},
]
: [],
}));
}, [sortedLogs]);
const handleShowMore = () => {
setVisibleCount((prev) => prev + 4);
};
return (
<>
<div className="row g-2">
{logsToShow.map((log) => (
<div key={log.id} className="col-12 d-flex align-items-start mb-1">
<Avatar
size="xs"
firstName={log.updatedBy.firstName}
lastName={log.updatedBy.lastName}
/>
<div className="flex-grow-1">
<div className="text-start">
<div className="flex">
<span>{`${log.updatedBy.firstName} ${log.updatedBy.lastName}`}</span>
<small className="text-secondary text-tiny ms-2">
<em>{log.action}</em>
</small>
<span className="text-tiny text-secondary d-block" >
{formatUTCToLocalTime(log.updateAt,true)}
</span>
</div>
<div className="d-flex align-items-center text-muted small mt-1">
<span>{log.comment}</span>
</div>
</div>
</div>
</div>
))}
</div>
{sortedLogs.length > visibleCount && (
<div className="text-center my-1">
<button
className="btn btn-xs btn-outline-primary"
onClick={handleShowMore}
>
Show More
</button>
</div>
)}
</>
<div className="page-min-h overflow-auto py-1">
<Timeline items={timelineData} />
</div>
);
};
export default ExpenseStatusLogs;

View File

@ -0,0 +1,95 @@
import React from "react";
import { formatFileSize, getIconByFileType } from "../../utils/appUtils";
import Tooltip from "../common/Tooltip";
const Filelist = ({ files, removeFile, expenseToEdit }) => {
return (
<div className="d-flex flex-wrap gap-2 my-1">
{files
.filter((file) => {
if (expenseToEdit) {
return file.isActive;
}
return true;
})
.map((file, idx) => (
<div className="col-12 col-sm-6 col-md-4 mb-2" key={idx}>
<div className="d-flex align-items-center justify-content-between bg-white border rounded p-1">
{/* File icon and info */}
<div className="d-flex align-items-center flex-grow-1 gap-2 overflow-hidden">
<i
className={`bx ${getIconByFileType(
file?.contentType
)} fs-3 text-primary`}
style={{ minWidth: "30px" }}
></i>
<div className="d-flex flex-column text-truncate">
<span className="fw-semibold small text-truncate">
{file.fileName}
</span>
<span className="text-body-secondary small">
{file.fileSize ? formatFileSize(file.fileSize) : ""}
</span>
</div>
</div>
{/* Delete icon */}
<Tooltip text="Remove file">
<i
className="bx bx-sm bx-trash text-danger fs-4 cursor-pointer ms-2"
role="button"
onClick={(e) => {
e.preventDefault();
removeFile(expenseToEdit ? file.documentId : idx);
}}
></i>
</Tooltip>
</div>
</div>
))}
</div>
);
};
export default Filelist;
export const FilelistView = ({ files, viewFile }) => {
return (
<div className="d-flex flex-wrap gap-2 mt-2">
{files?.map((file, idx) => (
<div className=" bg-white " key={idx}>
<div className="row align-items-center">
{/* File icon and info */}
<div className="col-12 d-flex align-items-center gap-2">
<i
className={`bx ${getIconByFileType(file?.fileName)} fs-3`}
></i>
<div
className="d-flex flex-column text-truncate"
onClick={(e) => {
e.preventDefault();
viewFile({
IsOpen: true,
Image: file.preSignedUrl,
});
}}
>
<span className="fw-medium small text-truncate">
{file.fileName}
</span>
<span className="text-body-secondary small">
<Tooltip text={"Click on file"}>
{" "}
{file.fileSize ? formatFileSize(file.fileSize) : ""}
</Tooltip>
</span>
</div>
</div>
</div>
</div>
))}
</div>
);
};

View File

@ -7,8 +7,8 @@ import { useProjectName } from "../../hooks/useProjects";
import { useDispatch, useSelector } from "react-redux";
import { changeMaster } from "../../slices/localVariablesSlice";
import useMaster, {
useExpenseCategory,
useExpenseStatus,
useExpenseType,
usePaymentMode,
} from "../../hooks/masterHook/useMaster";
import {
@ -39,11 +39,11 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
const [ExpenseType, setExpenseType] = useState();
const dispatch = useDispatch();
const {
ExpenseTypes,
expenseCategories,
loading: ExpenseLoading,
error: ExpenseError,
} = useExpenseType();
const schema = ExpenseSchema(ExpenseTypes);
} = useExpenseCategory();
const schema = ExpenseSchema(expenseCategories);
const {
register,
handleSubmit,
@ -146,7 +146,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
if (expenseToEdit && data) {
reset({
projectId: data.project.id || "",
expensesTypeId: data.expensesType.id || "",
expenseCategoryId: data?.expenseCategory?.id || "",
paymentModeId: data.paymentMode.id || "",
paidById: data.paidBy.id || "",
transactionDate: data.transactionDate?.slice(0, 10) || "",
@ -192,11 +192,11 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
CreateExpense(payload);
}
};
const ExpenseTypeId = watch("expensesTypeId");
const expenseCategoryId = watch("expenseCategoryId");
useEffect(() => {
setExpenseType(ExpenseTypes?.find((type) => type.id === ExpenseTypeId));
}, [ExpenseTypeId]);
setExpenseType(expenseCategories?.find((type) => type.id === expenseCategoryId));
}, [expenseCategoryId]);
const handleClose = () => {
reset();
@ -237,13 +237,13 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
</div>
<div className="col-md-6">
<Label htmlFor="expensesTypeId" className="form-label" required>
<Label htmlFor="expenseCategoryId" className="form-label" required>
Expense Type
</Label>
<select
className="form-select form-select-sm"
id="expensesTypeId"
{...register("expensesTypeId")}
id="expenseCategoryId"
{...register("expenseCategoryId")}
>
<option value="" disabled>
Select Type
@ -251,16 +251,16 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
{ExpenseLoading ? (
<option disabled>Loading...</option>
) : (
ExpenseTypes?.map((expense) => (
expenseCategories?.map((expense) => (
<option key={expense.id} value={expense.id}>
{expense.name}
</option>
))
)}
</select>
{errors.expensesTypeId && (
{errors.expenseCategoryId && (
<small className="danger-text">
{errors.expensesTypeId.message}
{errors.expenseCategoryId.message}
</small>
)}
</div>

View File

@ -1,137 +1,54 @@
import { useState, useRef ,useEffect} from "react";
import { useState } from "react";
const PreviewDocument = ({ imageUrl }) => {
const [loading, setLoading] = useState(true);
const [rotation, setRotation] = useState(0);
const [zoom, setZoom] = useState(1);
const [position, setPosition] = useState({ x: 0, y: 0 });
const [isDragging, setIsDragging] = useState(false);
const [startPos, setStartPos] = useState({ x: 0, y: 0 });
const containerRef = useRef(null);
// Zoom handlers
const handleZoomIn = () => setZoom((prev) => Math.min(prev + 0.2, 3));
const handleZoomOut = () => setZoom((prev) => Math.max(prev - 0.2, 0.5));
// Mouse wheel zoom
const handleWheel = (e) => {
e.preventDefault();
const delta = e.deltaY > 0 ? -0.1 : 0.1;
setZoom((prev) => Math.min(Math.max(prev + delta, 0.5), 3));
};
useEffect(() => {
const container = containerRef.current;
if (!container) return;
container.addEventListener("wheel", handleWheel, { passive: false });
return () => {
container.removeEventListener("wheel", handleWheel);
};
}, []);
const handleMouseDown = (e) => {
if (zoom <= 1) return;
setIsDragging(true);
setStartPos({
x: e.clientX - position.x,
y: e.clientY - position.y,
});
};
const handleMouseMove = (e) => {
if (!isDragging) return;
setPosition({
x: e.clientX - startPos.x,
y: e.clientY - startPos.y,
});
};
const handleMouseUp = () => setIsDragging(false);
const handleMouseLeave = () => setIsDragging(false);
const handleReset = () => {
setRotation(0);
setZoom(1);
setPosition({ x: 0, y: 0 });
};
return (
<>
<div className="d-flex justify-content-start align-items-center gap-3 mb-2 px-3 py-2 px-md-0 py-md-0">
<>
<div className="d-flex justify-content-start">
<i
className="bx bx-rotate-right fs-4 cursor-pointer"
title="Rotate Right"
className="bx bx-rotate-right cursor-pointer"
onClick={() => setRotation((prev) => prev + 90)}
></i>
<i
className="bx bx-zoom-in fs-4 cursor-pointer"
title="Zoom In"
onClick={handleZoomIn}
></i>
<i
className="bx bx-zoom-out fs-4 cursor-pointer"
title="Zoom Out"
onClick={handleZoomOut}
></i>
<i
className="bx bx-reset fs-4 cursor-pointer"
title="Reset"
onClick={handleReset}
></i>
</div>
<div
className="position-relative d-flex flex-column justify-content-center align-items-center"
style={{ minHeight: "80vh" }}
>
<div
ref={containerRef}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseLeave}
className="d-flex justify-content-center align-items-center overflow-hidden border rounded "
style={{
width: "100%",
height: "80vh",
background: "#f8f9fa",
cursor: zoom > 1 ? (isDragging ? "grabbing" : "grab") : "default",
userSelect: "none",
position: "relative",
}}
>
{loading && (
<div className="text-secondary text-center position-absolute">
Loading...
</div>
)}
{loading && (
<div className="text-secondary text-center mb-2">Loading...</div>
)}
<div className="mb-3 d-flex justify-content-center align-items-center">
<img
src={imageUrl}
alt="Preview"
onLoad={() => setLoading(false)}
alt="Full View"
className="img-fluid"
style={{
transform: `translate(${position.x}px, ${position.y}px) rotate(${rotation}deg) scale(${zoom})`,
transition: isDragging ? "none" : "transform 0.3s ease",
maxHeight: "80vh",
objectFit: "contain",
maxWidth: "100%",
maxHeight: "100%",
display: loading ? "none" : "block",
pointerEvents: "none",
transform: `rotate(${rotation}deg)`,
transition: "transform 0.3s ease",
}}
onLoad={() => setLoading(false)}
/>
</div>
{/* <div className="d-flex justify-content-center gap-2 mt-2">
<div className="position-absolute bottom-0 start-0 justify-content-center gap-2">
<button
className="btn btn-sm btn-outline-secondary"
onClick={handleReset}
title="Reset View"
className="btn btn-outline-secondary"
onClick={() => setRotation(0)}
title="Reset Rotation"
>
<i className="bx bx-reset"></i> Reset View
<i className="bx bx-reset"></i> Reset
</button>
</div> */}
</>
</div>
</div>
</>
);
};
export default PreviewDocument;

View File

@ -10,6 +10,8 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { defaultActionValues, ExpenseActionScheam } from "./ExpenseSchema";
import { useExpenseContext } from "../../pages/Expense/ExpensePage";
import {
formatCurrency,
formatFigure,
getColorNameFromHex,
getIconByFileType,
localToUtc,
@ -42,7 +44,7 @@ const ViewExpense = ({ ExpenseId }) => {
const IsReview = useHasUserPermission(REVIEW_EXPENSE);
const [imageLoaded, setImageLoaded] = useState({});
const { setDocumentView } = useExpenseContext();
const ActionSchema = ExpenseActionScheam(IsPaymentProcess) ?? z.object({});
const ActionSchema = ExpenseActionScheam(IsPaymentProcess,data?.createdAt) ?? z.object({});
const navigate = useNavigate();
const {
register,
@ -95,7 +97,7 @@ const ViewExpense = ({ ExpenseId }) => {
const onSubmit = (formData) => {
const Payload = {
...formData,
reimburseDate: localToUtc(formData.reimburseDate),
reimburseDate:localToUtc(formData.reimburseDate),
expenseId: ExpenseId,
comment: formData.comment,
};
@ -107,366 +109,430 @@ const ViewExpense = ({ ExpenseId }) => {
const handleImageLoad = (id) => {
setImageLoaded((prev) => ({ ...prev, [id]: true }));
};
console.log(errors)
return (
<form className="container px-3" onSubmit={handleSubmit(onSubmit)}>
<div className="row mb-3">
<div className="col-12 mb-3">
<h5 className="fw-semibold">Expense Details</h5>
<hr />
</div>
<div className="text-start mb-2">
<div className="text-muted">{data?.description}</div>
</div>
{/* Row 1 */}
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Transaction Date :
</label>
<div className="text-muted">
{formatUTCToLocalTime(data?.transactionDate)}
</div>
</div>
</div>
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Expense Type :
</label>
<div className="text-muted">{data?.expensesType?.name}</div>
</div>
</div>
<div className="col-12 mb-1">
<h5 className="fw-semibold m-0">Expense Details</h5>
</div>
{/* Row 2 */}
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Supplier :
</label>
<div className="text-muted">{data?.supplerName}</div>
</div>
</div>
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Amount :
</label>
<div className="text-muted"> {data.amount}</div>
</div>
</div>
{/* Row 3 */}
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Payment Mode :
</label>
<div className="text-muted">{data?.paymentMode?.name}</div>
</div>
</div>
{data?.gstNumber && (
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
<div className="row mb-1 ">
<div className="col-12 col-lg-7 col-xl-7 mb-3">
<div className="row">
<div className="col-12 d-flex justify-content-between text-start fw-semibold my-2">
<span>{data?.expenseUId}</span>
<span
className={`badge bg-label-${
getColorNameFromHex(data?.status?.color) || "secondary"
}`}
t
>
GST Number :
</label>
<div className="text-muted">{data?.gstNumber}</div>
</div>
</div>
)}
{/* Row 4 */}
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Status :
</label>
<span
className={`badge bg-label-${
getColorNameFromHex(data?.status?.color) || "secondary"
}`}
>
{data?.status?.name}
</span>
</div>
</div>
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Pre-Approved :
</label>
<div className="text-muted">{data.preApproved ? "Yes" : "No"}</div>
</div>
</div>
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Project :
</label>
<div className="text-muted">{data?.project?.name}</div>
</div>
</div>
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Created At :
</label>
<div className="text-muted">
{formatUTCToLocalTime(data?.createdAt, true)}
</div>
</div>
</div>
{/* Row 6 */}
{data.createdBy && (
<div className="col-md-6 text-start">
<div className="d-flex align-items-center">
<label
className="form-label me-2 mb-0 fw-semibold"
style={{ minWidth: "130px" }}
>
Created By :
</label>
<div className="d-flex align-items-center">
<Avatar
size="xs"
classAvatar="m-0"
firstName={data.createdBy?.firstName}
lastName={data.createdBy?.lastName}
/>
<span className="text-muted">
{`${data.createdBy?.firstName ?? ""} ${
data.createdBy?.lastName ?? ""
}`.trim() || "N/A"}
</span>
</div>
</div>
</div>
)}
<div className="col-md-6 text-start">
<div className="d-flex align-items-center">
<label
className="form-label me-2 mb-0 fw-semibold"
style={{ minWidth: "130px" }}
>
Paid By:
</label>
<div className="d-flex align-items-center ">
<Avatar
size="xs"
classAvatar="m-0"
firstName={data.paidBy?.firstName}
lastName={data.paidBy?.lastName}
/>
<span className="text-muted">
{`${data.paidBy?.firstName ?? ""} ${
data.paidBy?.lastName ?? ""
}`.trim() || "N/A"}
{data?.status?.name}
</span>
</div>
</div>
</div>
</div>
<div className="col-12 text-start">
<label className="form-label me-2 mb-2 fw-semibold">Attachment :</label>
<div className="d-flex flex-wrap gap-2">
{data?.documents?.map((doc) => {
const isImage = doc.contentType?.startsWith("image");
return (
<div
key={doc.documentId}
className="border rounded hover-scale p-2 d-flex flex-column align-items-center"
style={{
width: "80px",
cursor: "pointer",
}}
onClick={() => {
if (isImage) {
setDocumentView({
IsOpen: true,
Image: doc.preSignedUrl,
});
} else {
window.open(doc.preSignedUrl, "_blank");
}
}}
>
<i
className={`bx ${getIconByFileType(doc.contentType)}`}
style={{ fontSize: "30px" }}
></i>
<small
className="text-center text-tiny text-truncate w-100"
title={doc.fileName}
{/* Row 1 */}
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
{doc.fileName}
</small>
Transaction Date :
</label>
<div className="text-muted">
{formatUTCToLocalTime(data?.transactionDate)}
</div>
</div>
);
}) ?? "No Attachment"}
</div>
</div>
</div>
{data.expensesReimburse && (
<div className="row text-start mt-2">
<div className="col-md-6 mb-sm-0 mb-2">
<label className="form-label me-2 mb-0 fw-semibold">
Transaction ID :
</label>
{data.expensesReimburse.reimburseTransactionId || "N/A"}
</div>
<div className="col-md-6 ">
<label className="form-label me-2 mb-0 fw-semibold">
Reimburse Date :
</label>
{formatUTCToLocalTime(data.expensesReimburse.reimburseDate)}
</div>
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Expense Category :
</label>
<div className="text-muted">{data?.expenseCategory?.name}</div>
</div>
</div>
{data.expensesReimburse && (
<>
<div className="col-md-6 d-flex align-items-center">
<label className="form-label me-2 mb-0 fw-semibold">
Reimburse By :
{/* Row 2 */}
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Supplier :
</label>
<div className="text-muted">{data?.supplerName}</div>
</div>
</div>
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Amount :
</label>
<div className="text-muted">
{" "}
{formatFigure(data?.amount, {
type: "currency",
currency: data?.currency?.currencyCode ?? "INR",
})}
</div>
</div>
</div>
{/* Row 3 */}
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Payment Mode :
</label>
<div className="text-muted">{data?.paymentMode?.name}</div>
</div>
</div>
{data?.gstNumber && (
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
GST Number :
</label>
<div className="text-muted">{data?.gstNumber}</div>
</div>
</div>
)}
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Pre-Approved :
</label>
<div className="text-muted">
{data.preApproved ? "Yes" : "No"}
</div>
</div>
</div>
{/* Row 5 */}
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Project :
</label>
<div className="text-muted">{data?.project?.name}</div>
</div>
</div>
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Created At :
</label>
<div className="text-muted">
{formatUTCToLocalTime(data?.createdAt, true)}
</div>
</div>
</div>
{/* Created & Paid By */}
{data.createdBy && (
<div className="col-md-6 text-start mb-3">
<div className="d-flex align-items-center">
<label
className="form-label me-2 mb-0 fw-semibold"
style={{ minWidth: "130px" }}
>
Created By :
</label>
<Avatar
size="xs"
classAvatar="m-0 me-1"
firstName={data.createdBy?.firstName}
lastName={data.createdBy?.lastName}
/>
<span className="text-muted">
{`${data.createdBy?.firstName ?? ""} ${
data.createdBy?.lastName ?? ""
}`.trim() || "N/A"}
</span>
</div>
</div>
)}
<div className="col-md-6 text-start mb-3">
<div className="d-flex align-items-center">
<label
className="form-label me-2 mb-0 fw-semibold"
style={{ minWidth: "130px" }}
>
Paid By :
</label>
<Avatar
size="xs"
classAvatar="m-0 me-1"
firstName={data?.expensesReimburse?.reimburseBy?.firstName}
lastName={data?.expensesReimburse?.reimburseBy?.lastName}
firstName={data.paidBy?.firstName}
lastName={data.paidBy?.lastName}
/>
<span className="text-muted">
{`${data?.expensesReimburse?.reimburseBy?.firstName} ${data?.expensesReimburse?.reimburseBy?.lastName}`.trim()}
{`${data.paidBy?.firstName ?? ""} ${
data.paidBy?.lastName ?? ""
}`.trim() || "N/A"}
</span>
</div>
</>
)}
</div>
)}
<hr className="divider my-1 border-2 divider-primary my-2" />
</div>
{Array.isArray(data?.nextStatus) && data.nextStatus.length > 0 && (
<>
{IsPaymentProcess && nextStatusWithPermission?.length > 0 && (
<div className="row">
<div className="col-12 col-md-6 text-start">
<label className="form-label">Transaction Id </label>
<input
type="text"
className="form-control form-control-sm"
{...register("reimburseTransactionId")}
/>
{errors.reimburseTransactionId && (
<small className="danger-text">
{errors.reimburseTransactionId.message}
</small>
)}
</div>
<div className="col-12 col-md-6 text-start">
<label className="form-label">Transaction Date </label>
<DatePicker
name="reimburseDate"
control={control}
minDate={data?.transactionDate}
maxDate={new Date()}
/>
{errors.reimburseDate && (
<small className="danger-text">
{errors.reimburseDate.message}
</small>
)}
</div>
<div className="col-12 col-md-6 text-start">
<label className="form-label">Reimburse By </label>
<EmployeeSearchInput
control={control}
name="reimburseById"
projectId={null}
/>
{/* Description */}
<div className="col-12 text-start mb-2">
<label className="fw-semibold form-label">Description : </label>
<div className="text-muted">{data?.description}</div>
</div>
{/* Attachments */}
<div className="col-12 text-start mb-2">
<label className="form-label me-2 mb-2 fw-semibold">
Attachment :
</label>
<div className="d-flex flex-wrap gap-2">
{data?.documents?.map((doc) => {
const isImage = doc.contentType?.includes("image");
return (
<div
key={doc.documentId}
className="d-flex align-items-center cusor-pointer"
onClick={() => {
if (isImage) {
setDocumentView({
IsOpen: true,
Image: doc.preSignedUrl,
});
}
}}
>
<i
className={`bx ${getIconByFileType(doc.contentType)}`}
style={{ fontSize: "30px" }}
></i>
<small
className="text-center text-tiny text-truncate w-100"
title={doc.fileName}
>
{doc.fileName}
</small>
</div>
);
})}
</div>
</div>
)}
<div className="col-12 mb-3 text-start">
{((nextStatusWithPermission.length > 0 && !IsRejectedExpense) ||
(IsRejectedExpense && isCreatedBy)) && (
<>
<Label className="form-label me-2 mb-0" required>
Comment
</Label>
<textarea
className="form-control form-control-sm"
{...register("comment")}
rows="2"
/>
{errors.comment && (
<small className="danger-text">
{errors.comment.message}
</small>
{data.expensesReimburse && (
<div className="row text-start mt-2">
<div className="col-md-6 mb-sm-0 mb-2">
<label className="form-label me-2 mb-0 fw-semibold">
Transaction ID :
</label>
{data.expensesReimburse.reimburseTransactionId || "N/A"}
</div>
<div className="col-md-6 ">
<label className="form-label me-2 mb-0 fw-semibold">
Reimburse Date :
</label>
{formatUTCToLocalTime(data.expensesReimburse.reimburseDate)}
</div>
{data.expensesReimburse && (
<>
<div className="col-md-6 d-flex align-items-center">
<label className="form-label me-2 mb-0 fw-semibold">
Reimburse By :
</label>
<Avatar
size="xs"
classAvatar="m-0 me-1"
firstName={
data?.expensesReimburse?.reimburseBy?.firstName
}
lastName={
data?.expensesReimburse?.reimburseBy?.lastName
}
/>
<span className="text-muted">
{`${data?.expensesReimburse?.reimburseBy?.firstName} ${data?.expensesReimburse?.reimburseBy?.lastName}`.trim()}
</span>
</div>
</>
)}
</div>
)}
{/* <hr className="divider my-1 border-2 divider-primary my-2" /> */}
{Array.isArray(data?.nextStatus) && data.nextStatus.length > 0 && (
<>
{IsPaymentProcess && nextStatusWithPermission?.length > 0 && (
<div className="row ">
<div className="col-12 col-md-6 text-start">
<Label className="form-label" required>Transaction Id </Label>
<input
type="text"
className="form-control form-control-sm"
{...register("reimburseTransactionId")}
/>
{errors.reimburseTransactionId && (
<small className="danger-text">
{errors.reimburseTransactionId.message}
</small>
)}
</div>
<div className="col-12 col-md-6 text-start mb-1">
<Label className="form-label" required>Transaction Date </Label>
<DatePicker className="w-100"
name="reimburseDate"
control={control}
minDate={data?.transactionDate}
maxDate={new Date()}
/>
{errors.reimburseDate && (
<small className="danger-text">
{errors.reimburseDate.message}
</small>
)}
</div>
<div className="col-12 col-md-6 text-start mb-1">
<Label className="form-label" required>
Reimburse By{" "}
</Label>
<EmployeeSearchInput
control={control}
name="reimburseById"
projectId={null}
/>
</div>
<div className="col-12 col-md-6 text-start">
<Label className="form-label" >
TDS Percentage
</Label>
<input
type="text"
className="form-control form-control-sm"
{...register("tdsPercentage")}
/>
{errors.tdsPercentage && (
<small className="danger-text">
{errors.tdsPercentage.message}
</small>
)}
</div>
<div className="col-12 col-md-6 text-start">
<Label className="form-label" required>
Base Amount
</Label>
<input
type="text"
className="form-control form-control-sm"
{...register("baseAmount")}
/>
{errors.baseAmount && (
<small className="danger-text">
{errors.baseAmount.message}
</small>
)}
</div>
<div className="col-12 col-md-6 text-start">
<Label className="form-label" required>
Tax Amount
</Label>
<input
type="text"
className="form-control form-control-sm"
{...register("taxAmount")}
/>
{errors.taxAmount && (
<small className="danger-text">
{errors.taxAmount.message}
</small>
)}
</div>
</div>
)}
<div className="col-12 mb-3 text-start mt-1">
{((nextStatusWithPermission.length > 0 &&
!IsRejectedExpense) ||
(IsRejectedExpense && isCreatedBy)) && (
<>
<Label className="form-label me-2 mb-0" required>
Comment
</Label>
<textarea
className="form-control form-control-sm"
{...register("comment")}
rows="2"
/>
{errors.comment && (
<small className="danger-text">
{errors.comment.message}
</small>
)}
</>
)}
{nextStatusWithPermission?.length > 0 &&
(!IsRejectedExpense || isCreatedBy) && (
<div className="text-end flex-wrap gap-2 my-2 mt-3">
{nextStatusWithPermission.map((status, index) => (
<button
key={status.id || index}
type="button"
onClick={() => {
setClickedStatusId(status.id);
setValue("statusId", status.id);
handleSubmit(onSubmit)();
}}
disabled={isPending || isFetching}
className="btn btn-primary btn-sm cursor-pointer mx-2 border-0"
>
{isPending && clickedStatusId === status.id
? "Please Wait..."
: status.displayName || status.name}
</button>
))}
</div>
)}
</div>
</>
)}
{nextStatusWithPermission?.length > 0 &&
(!IsRejectedExpense || isCreatedBy) && (
<div className="text-end flex-wrap gap-2 my-2 mt-3">
{nextStatusWithPermission.map((status, index) => (
<button
key={status.id || index}
type="button"
onClick={() => {
setClickedStatusId(status.id);
setValue("statusId", status.id);
handleSubmit(onSubmit)();
}}
disabled={isPending || isFetching}
className="btn btn-primary btn-sm cursor-pointer mx-2 border-0"
>
{isPending && clickedStatusId === status.id
? "Please Wait..."
: status.displayName || status.name}
</button>
))}
</div>
)}
</div>
</>
)}
</div>
<ExpenseStatusLogs data={data} />
<div className="col-12 col-lg-5 col-xl-5">
<div className="d-flex align-items-center text-secondary mb-4">
<i className="bx bx-time-five me-2"></i>{" "}
<p className=" m-0">TimeLine</p>
</div>
<ExpenseStatusLogs data={data} />
</div>
</div>
</form>
);
};

View File

@ -0,0 +1,104 @@
import React from "react";
import Avatar from "./Avatar";
import Tooltip from "./Tooltip";
import { formatUTCToLocalTime } from "../../utils/dateUtils";
import moment from "moment";
const Timeline = ({ items = [], transparent = true }) => {
if(items.length === 0){
return (
<div className="d-flex justify-content-center align-item-center page-min-h">
<p>Not Action yet</p>
</div>
)
}
return (
<ul
className={`timeline ${
transparent ? "timeline-transparent text-start" : ""
}`}
>
{items.map((item) => (
<li
key={item.id}
className={`timeline-item ${
transparent ? "timeline-item-transparent" : ""
}`}
>
<span
className={`timeline-point timeline-point-${
item.color || "primary"
}`}
></span>
<div className="timeline-event">
<div className="timeline-header mb-1 d-flex justify-content-between">
<h6 className="mb-0 text-body">{item.title}</h6>
<small className="text-body-secondary"><Tooltip text={formatUTCToLocalTime(item.timeAgo,true)}>{moment.utc(item.timeAgo).local().fromNow()}</Tooltip></small>
</div>
{item.description && <p className="mb-1">{item.description}</p>}
{item.attachments && item.attachments.length > 0 && (
<div className="d-flex align-items-center mb-2">
{item.attachments.map((att, i) => (
<div
key={i}
className="badge bg-lighter rounded d-flex align-items-center gap-2 p-2"
>
{att.icon && (
<img
src={att.icon}
alt="file"
width="15"
className="me-2"
/>
)}
<span className="h6 mb-0">{att.name}</span>
</div>
))}
</div>
)}
{item.users && item.users.length > 0 && (
<div className="d-flex flex-wrap align-items-center ">
<ul className="list-unstyled users-list d-flex align-items-center avatar-group m-0">
{item.users.map((user, i) => (
<li key={i} className="avatar me-1" title={user.name}>
{user.avatar ? (
<img
src={user.avatar}
alt={user.name}
className="rounded-circle"
width="32"
height="32"
/>
) : (
<Avatar
firstName={user.firstName}
lastName={user.lastName}
/>
)}
</li>
))}
</ul>
{item.users?.length === 1 && (
<div className="m-0">
<p className="mb-0 small fw-medium">{`${item.users[0].firstName} ${item.users[0].lastName}`}</p>
<small>{item.users[0].role}</small>
</div>
)}
</div>
)}
<div className="d-flex flex-wrap ms-10">{item.userComment && <p className="mb-2 ">{item.userComment}</p>}</div>
</div>
</li>
))}
</ul>
);
};
export default Timeline;

View File

@ -0,0 +1,24 @@
import { useEffect } from "react";
const Tooltip = ({ text, placement = "top", children }) => {
useEffect(() => {
const el = document.querySelector(`[data-tooltip-id="${text}"]`);
if (el) {
new window.bootstrap.Tooltip(el);
}
}, [text]);
return (
<span
data-bs-toggle="tooltip"
data-bs-placement={placement}
title={text}
data-tooltip-id={text}
style={{ cursor: "pointer", display: "inline-flex", alignItems: "center" }}
>
{children}
</span>
);
};
export default Tooltip;

View File

@ -2,10 +2,7 @@ import React, { useEffect } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import {
useCreateExpenseType,
useUpdateExpenseType,
} from "../../hooks/masterHook/useMaster";
import { useCreateExpenseCategory, useUpdateExpenseCategory } from "../../hooks/masterHook/useMaster";
import Label from "../common/Label";
const ExpnseSchema = z.object({
@ -14,7 +11,7 @@ const ExpnseSchema = z.object({
description: z.string().min(1, { message: "Description is required" }),
});
const ManageExpenseType = ({ data = null, onClose }) => {
const ManageExpenseCategory = ({ data = null, onClose }) => {
const {
register,
handleSubmit,
@ -24,21 +21,21 @@ const ManageExpenseType = ({ data = null, onClose }) => {
resolver: zodResolver(ExpnseSchema),
defaultValues: { name: "", noOfPersonsRequired: false, description: "" },
});
const { mutate: UpdateExpenseType, isPending:isPendingUpdate } = useUpdateExpenseType(
const { mutate: UpdateExpenseCategory, isPending:isPendingUpdate } = useUpdateExpenseCategory(
() => onClose?.()
);
const { mutate: CreateExpenseType, isPending } = useCreateExpenseType(() =>
const { mutate: CreateExpenseCategory, isPending } = useCreateExpenseCategory(() =>
onClose?.()
);
const onSubmit = (payload) => {
if (data) {
UpdateExpenseType({
UpdateExpenseCategory({
id: data.id,
payload: { ...payload, id: data.id },
});
} else {
CreateExpenseType(payload);
CreateExpenseCategory(payload);
}
};
@ -112,4 +109,4 @@ const ManageExpenseType = ({ data = null, onClose }) => {
);
};
export default ManageExpenseType;
export default ManageExpenseCategory;

View File

@ -9,7 +9,6 @@ import CreateCategory from "./CreateContactCategory";
import CreateContactTag from "./CreateContactTag";
import EditContactCategory from "./EditContactCategory";
import EditContactTag from "./EditContactTag";
import ManageExpenseType from "./ManageExpenseType";
import ManagePaymentMode from "./ManagePaymentMode";
import ManageExpenseStatus from "./ManageExpenseStatus";
import ManageDocumentCategory from "./ManageDocumentCategory";
@ -17,6 +16,7 @@ import ManageDocumentType from "./ManageDocumentType";
import ManageServices from "./Services/ManageServices";
import ServiceGroups from "./Services/ServicesGroups";
import ManagePaymentHead from "./paymentAdjustmentHead/ManagePaymentHead";
import ManageExpenseCategory from "./ManageExpenseCategory";
const MasterModal = ({ modaldata, closeModal }) => {
if (!modaldata?.modalType || modaldata.modalType === "delete") {
@ -42,8 +42,8 @@ const MasterModal = ({ modaldata, closeModal }) => {
),
"Contact Tag": <CreateContactTag data={item} onClose={closeModal} />,
"Edit-Contact Tag": <EditContactTag data={item} onClose={closeModal} />,
"Expense Type": <ManageExpenseType onClose={closeModal} />,
"Edit-Expense Type": <ManageExpenseType data={item} onClose={closeModal} />,
"Expense Category": <ManageExpenseCategory onClose={closeModal} />,
"Edit-Expense Category": <ManageExpenseCategory data={item} onClose={closeModal} />,
"Payment Mode": <ManagePaymentMode onClose={closeModal} />,
"Edit-Payment Mode": <ManagePaymentMode data={item} onClose={closeModal} />,
"Expense Status": <ManageExpenseStatus onClose={closeModal} />,

View File

@ -150,15 +150,15 @@ export const useContactTags = () => {
return { contactTags, loading, error };
};
export const useExpenseType = () => {
export const useExpenseCategory = () => {
const {
data: ExpenseTypes = [],
data: expenseCategories = [],
isLoading: loading,
error,
} = useQuery({
queryKey: ["Expense Type"],
queryKey: ["Expense Category"],
queryFn: async () => {
const res = await MasterRespository.getExpenseType();
const res = await MasterRespository.getExpenseCategories();
return res.data;
},
onError: (error) => {
@ -275,8 +275,8 @@ export const useOrganizationType = () => {
queryFn: async () => await MasterRespository.getOrganizationType(),
});
};
// ===Application Masters Query=================================================
//#region ==Get Masters==
const fetchMasterData = async (masterType) => {
switch (masterType) {
case "Application Role":
@ -293,8 +293,8 @@ const fetchMasterData = async (masterType) => {
return (await MasterRespository.getContactCategory()).data;
case "Contact Tag":
return (await MasterRespository.getContactTag()).data;
case "Expense Type":
return (await MasterRespository.getExpenseType()).data;
case "Expense Category":
return (await MasterRespository.getExpenseCategories()).data;
case "Payment Mode":
return (await MasterRespository.getPaymentMode()).data;
case "Expense Status":
@ -363,10 +363,11 @@ const useMaster = () => {
};
export default useMaster;
//#endregion
// ================================Mutation====================================
// Job Role-----------------------------------
//#region Job Role
export const useUpdateJobRole = (onSuccessCallback, onErrorCallback) => {
const queryClient = useQueryClient();
@ -411,9 +412,10 @@ export const useCreateJobRole = (onSuccessCallback) => {
},
});
};
//#endregion Job Role
// Application Role-------------------------------------------
//#region Application Role
export const useCreateApplicationRole = (onSuccessCallback) => {
const queryClient = useQueryClient();
@ -456,8 +458,9 @@ export const useUpdateApplicationRole = (onSuccessCallback) => {
},
});
};
//#endregion
//-----Create work Category-------------------------------
//#region Create work Category
export const useCreateWorkCategory = (onSuccessCallback) => {
const queryClient = useQueryClient();
@ -500,8 +503,9 @@ export const useUpdateWorkCategory = (onSuccessCallback) => {
},
});
};
//#endregion
//-- Contact Category---------------------------
//#region Contact Category
export const useCreateContactCategory = (onSuccessCallback) => {
const queryClient = useQueryClient();
@ -549,7 +553,9 @@ export const useUpdateContactCategory = (onSuccessCallback) => {
});
};
// ---------Contact Tag-------------------
//#endregion
//#region Contact Tag
export const useCreateContactTag = (onSuccessCallback) => {
const queryClient = useQueryClient();
@ -593,14 +599,15 @@ export const useUpdateContactTag = (onSuccessCallback) => {
},
});
};
//#endregion
// ----------------------Expense Type------------------
export const useCreateExpenseType = (onSuccessCallback) => {
//#region Expense Category
export const useCreateExpenseCategory = (onSuccessCallback) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (payload) => {
const resp = await MasterRespository.createExpenseType(payload);
const resp = await MasterRespository.createExpenseCategory(payload);
return resp.data;
},
onSuccess: (data) => {
@ -615,12 +622,12 @@ export const useCreateExpenseType = (onSuccessCallback) => {
},
});
};
export const useUpdateExpenseType = (onSuccessCallback) => {
export const useUpdateExpenseCategory = (onSuccessCallback) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, payload }) => {
const response = await MasterRespository.updateExpenseType(id, payload);
const response = await MasterRespository.updateExpenseCategory(id, payload);
return response.data;
},
onSuccess: (data, variables) => {
@ -637,7 +644,9 @@ export const useUpdateExpenseType = (onSuccessCallback) => {
});
};
// -----------------Payment Mode -------------
//#endregion
//#region Payment Mode
export const useCreatePaymentMode = (onSuccessCallback) => {
const queryClient = useQueryClient();
@ -681,6 +690,8 @@ export const useUpdatePaymentMode = (onSuccessCallback) => {
});
};
//#endregion
// Services-------------------------------
// export const useCreateService = (onSuccessCallback) => {
@ -704,7 +715,7 @@ export const useUpdatePaymentMode = (onSuccessCallback) => {
// },
// });
// };
//#region Services
export const useCreateService = (onSuccessCallback) => {
const queryClient = useQueryClient();
@ -760,6 +771,10 @@ export const useUpdateService = (onSuccessCallback) => {
});
};
//#endregion
//#region Activity Grouph
export const useCreateActivityGroup = (onSuccessCallback) => {
const queryClient = useQueryClient();
@ -819,7 +834,10 @@ export const useUpdateActivityGroup = (onSuccessCallback) => {
},
});
};
// Activity------------------------------
//#endregion
//#region Activities
export const useCreateActivity = (onSuccessCallback) => {
const queryClient = useQueryClient();
@ -871,7 +889,9 @@ export const useUpdateActivity = (onSuccessCallback) => {
});
};
// -------------------Expense Status----------------------------------
//#endregion
//#region Expense Status
export const useCreateExpenseStatus = (onSuccessCallback) => {
const queryClient = useQueryClient();
@ -912,8 +932,9 @@ export const useUpdateExpenseStatus = (onSuccessCallback) => {
},
});
};
//#endregion
// --------------------Document-Category--------------------------------
//#region Document-Category
export const useCreateDocumentCatgory = (onSuccessCallback) => {
const queryClient = useQueryClient();
@ -957,8 +978,9 @@ export const useUpdateDocumentCategory = (onSuccessCallback) => {
},
});
};
//#endregion
// ------------------------------Document-Type-----------------------------------
//#region Document-Type
export const useCreateDocumentType = (onSuccessCallback) => {
const queryClient = useQueryClient();
@ -1000,9 +1022,9 @@ export const useUpdateDocumentType = (onSuccessCallback) => {
},
});
};
// ------------------------------x-x--------x-x------------------------------------
//#endregion
// ==============================Payment Adjustment Head =============================
//#region Payment Adjustment Head
export const useCreatePaymentAjustmentHead = (onSuccessCallback) => {
const queryClient = useQueryClient();
@ -1043,9 +1065,9 @@ export const useUpdatePaymentAjustmentHead = (onSuccessCallback) => {
},
});
};
// ====================x=x====================x=x==================================
//#endregion
// --------Delete Master --------
//#region ==Delete Master==
export const useDeleteMasterItem = () => {
const queryClient = useQueryClient();
@ -1078,6 +1100,8 @@ export const useDeleteMasterItem = () => {
});
};
//#endregion
export const useDeleteServiceGroup = () => {
const queryClient = useQueryClient();

View File

@ -50,7 +50,7 @@ export const MasterRespository = {
"Contact Category": (id) => api.delete(`/api/master/contact-category/${id}`),
"Contact Tag": (id) => api.delete(`/api/master/contact-tag/${id}`),
"Expense Type": (id, isActive) =>
api.delete(`/api/Master/expenses-type/delete/${id}`, (isActive = false)),
api.delete(`/api/Master/expenses-category/delete/${id}`, (isActive = false)),
"Payment Mode": (id, isActive) =>
api.delete(`/api/Master/payment-mode/delete/${id}`, (isActive = false)),
"Expense Status": (id, isActive) =>
@ -78,10 +78,10 @@ export const MasterRespository = {
getAuditStatus: () => api.get("/api/Master/work-status"),
getExpenseType: () => api.get("/api/Master/expenses-types"),
createExpenseType: (data) => api.post("/api/Master/expenses-type", data),
updateExpenseType: (id, data) =>
api.put(`/api/Master/expenses-type/edit/${id}`, data),
getExpenseCategories: () => api.get("/api/Master/expenses-categories"),
createExpenseCategory: (data) => api.post("/api/Master/expenses-category", data),
updateExpenseCategory: (id, data) =>
api.put(`/api/Master/expenses-category/edit/${id}`, data),
getPaymentMode: () => api.get("/api/Master/payment-modes"),
createPaymentMode: (data) => api.post(`/api/Master/payment-mode`, data),

View File

@ -1,12 +1,13 @@
export const BASE_URL = process.env.VITE_BASE_URL;
// export const BASE_URL = "https://api.marcoaiot.com";
export const THRESH_HOLD = 48; // hours
export const DURATION_TIME = 10; // minutes
export const ITEMS_PER_PAGE = 20;
export const OTP_EXPIRY_SECONDS = 300; // OTP time
export const BASE_URL = process.env.VITE_BASE_URL;
// export const BASE_URL = "https://api.marcoaiot.com";
export const MANAGE_MASTER = "588a8824-f924-4955-82d8-fc51956cf323";
export const VIEW_MASTER = "5ffbafe0-7ab0-48b1-bb50-c1bf76b65f9d";
@ -49,7 +50,7 @@ export const DIRECTORY_ADMIN = "4286a13b-bb40-4879-8c6d-18e9e393beda";
export const DIRECTORY_MANAGER = "62668630-13ce-4f52-a0f0-db38af2230c5";
export const DIRECTORY_USER = "0f919170-92d4-4337-abd3-49b66fc871bb";
// ========================Finance=========================================================
// -----------------------Expense----------------------------------------
export const VIEW_SELF_EXPENSE = "385be49f-8fde-440e-bdbc-3dffeb8dd116";
@ -63,7 +64,16 @@ export const APPROVE_EXPENSE = "eaafdd76-8aac-45f9-a530-315589c6deca";
export const PROCESS_EXPENSE = "ea5a1529-4ee8-4828-80ea-0e23c9d4dd11";
export const EXPENSE_MANAGE = "ea5a1529-4ee8-4828-80ea-0e23c9d4dd11";
export const EXPENSE_MANAGE = "bdee29a2-b73b-402d-8dd1-c4b1f81ccbc3";
export const EXPENSE_REJECTEDBY = [
"965eda62-7907-4963-b4a1-657fb0b2724b",
"d1ee5eec-24b6-4364-8673-a8f859c60729",
];
export const EXPENSE_DRAFT = "297e0d8f-f668-41b5-bfea-e03b354251c8";
// --------------------------------Collection----------------------------
@ -73,15 +83,6 @@ export const CREATE_COLLECTION = "b93141fd-dbd3-4051-8f57-bf25d18e3555";
export const EDIT_COLLECTION = "455187b4-fef1-41f9-b3d0-025d0b6302c3";
export const ADDPAYMENT_COLLECTION = "061d9ccd-85b4-4cb0-be06-2f9f32cebb72";
// ==========================================================================================
export const EXPENSE_REJECTEDBY = [
"d1ee5eec-24b6-4364-8673-a8f859c60729",
"965eda62-7907-4963-b4a1-657fb0b2724b",
];
export const EXPENSE_DRAFT = "297e0d8f-f668-41b5-bfea-e03b354251c8";
// ----------------------------Tenant-------------------------
export const SUPPER_TENANT = "d032cb1a-3f30-462c-bef0-7ace73a71c0b";
export const MANAGE_TENANTS = "00e20637-ce8d-4417-bec4-9b31b5e65092";
@ -99,7 +100,8 @@ export const VERIFY_DOCUMENT = "13a1f30f-38d1-41bf-8e7a-b75189aab8e0";
// 1 - Expense Manage
export const EXPENSE_MANAGEMENT = "a4e25142-449b-4334-a6e5-22f70e4732d7";
export const INR_CURRENCY_CODE = "78e96e4a-7ce0-4164-ae3a-c833ad45ec2c";
export const EXPENSE_PROCESSED = "61578360-3a49-4c34-8604-7b35a3787b95";
export const TENANT_STATUS = [
{ id: "62b05792-5115-4f99-8ff5-e8374859b191", name: "Active" },
{ id: "c0b5def8-087e-4235-b3a4-8e2f0ed91b94", name: "In Active" },
@ -155,12 +157,53 @@ export const PROJECT_STATUS = [
},
];
export const DEFAULT_CURRENCY = "78e96e4a-7ce0-4164-ae3a-c833ad45ec2c";
export const EXPENSE_STATUS = {
daft:"297e0d8f-f668-41b5-bfea-e03b354251c8",
review_pending:"6537018f-f4e9-4cb3-a210-6c3b2da999d7",
payment_pending:"f18c5cfd-7815-4341-8da2-2c2d65778e27",
approve_pending:"4068007f-c92f-4f37-a907-bc15fe57d4d8",
process_pending:"61578360-3a49-4c34-8604-7b35a3787b95"
daft: "297e0d8f-f668-41b5-bfea-e03b354251c8",
review_pending: "6537018f-f4e9-4cb3-a210-6c3b2da999d7",
payment_pending: "f18c5cfd-7815-4341-8da2-2c2d65778e27",
approve_pending: "4068007f-c92f-4f37-a907-bc15fe57d4d8",
}
export const UUID_REGEX =
/^\/employee\/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
export const ALLOW_PROJECTSTATUS_ID = [
"603e994b-a27f-4e5d-a251-f3d69b0498ba",
"cdad86aa-8a56-4ff4-b633-9c629057dfef",
"b74da4c2-d07e-46f2-9919-e75e49b12731",
];
export const DEFAULT_EMPTY_STATUS_ID = "00000000-0000-0000-0000-000000000000";
export const FREQUENCY_FOR_RECURRING = {
0: "Monthly",
1: "Quarterly",
2: "Half-Yearly",
3: "Yearly",
4: "Daily",
5: "Weekly"
};
export const PAYEE_RECURRING_EXPENSE = [
{
id: "da462422-13b2-45cc-a175-910a225f6fc8",
label: "Active",
},
{
id: "306856fb-5655-42eb-bf8b-808bb5e84725",
label: "Completed",
},
{
id: "3ec864d2-8bf5-42fb-ba70-5090301dd816",
label: "De-Activited",
},
{
id: "8bfc9346-e092-4a80-acbf-515ae1ef6868",
label: "Paused",
},
];