diff --git a/src/app/(main)/projects/create/not-found.tsx b/src/app/(main)/projects/create/not-found.tsx
new file mode 100644
index 0000000..002e9f5
--- /dev/null
+++ b/src/app/(main)/projects/create/not-found.tsx
@@ -0,0 +1,17 @@
+import { getServerI18n } from "@/i18n";
+import { Stack, Typography, Link } from "@mui/material";
+import NextLink from "next/link";
+
+export default async function NotFound() {
+ const { t } = await getServerI18n("projects", "common");
+
+ return (
+
+ {t("Not Found")}
+ {t("The create project page was not found!")}
+
+ {t("Return to all projects")}
+
+
+ );
+}
diff --git a/src/app/(main)/projects/create/page.tsx b/src/app/(main)/projects/create/page.tsx
index 1671262..62f2492 100644
--- a/src/app/(main)/projects/create/page.tsx
+++ b/src/app/(main)/projects/create/page.tsx
@@ -11,10 +11,13 @@ import {
} from "@/app/api/projects";
import { preloadStaff, preloadTeamLeads } from "@/app/api/staff";
import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks";
+import { getUserAbilities } from "@/app/utils/commonUtil";
import CreateProject from "@/components/CreateProject";
import { I18nProvider, getServerI18n } from "@/i18n";
+import { MAINTAIN_PROJECT } from "@/middleware";
import Typography from "@mui/material/Typography";
import { Metadata } from "next";
+import { notFound } from "next/navigation";
export const metadata: Metadata = {
title: "Create Project",
@@ -23,6 +26,12 @@ export const metadata: Metadata = {
const Projects: React.FC = async () => {
const { t } = await getServerI18n("projects");
+ const abilities = await getUserAbilities()
+
+ if (!abilities.includes(MAINTAIN_PROJECT)) {
+ notFound();
+ }
+
// Preload necessary dependencies
fetchAllTasks();
fetchTaskTemplates();
diff --git a/src/app/(main)/projects/createSub/page.tsx b/src/app/(main)/projects/createSub/page.tsx
index 3f474de..cc5be9c 100644
--- a/src/app/(main)/projects/createSub/page.tsx
+++ b/src/app/(main)/projects/createSub/page.tsx
@@ -13,9 +13,11 @@ import {
} from "@/app/api/projects";
import { preloadStaff, preloadTeamLeads } from "@/app/api/staff";
import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks";
+import { getUserAbilities } from "@/app/utils/commonUtil";
import { ServerFetchError } from "@/app/utils/fetchUtil";
import CreateProject from "@/components/CreateProject";
import { I18nProvider, getServerI18n } from "@/i18n";
+import { MAINTAIN_PROJECT } from "@/middleware";
import Typography from "@mui/material/Typography";
import { Metadata } from "next";
import { notFound } from "next/navigation";
@@ -26,6 +28,11 @@ export const metadata: Metadata = {
const Projects: React.FC = async () => {
const { t } = await getServerI18n("projects");
+ const abilities = await getUserAbilities()
+
+ if (!abilities.includes(MAINTAIN_PROJECT)) {
+ notFound();
+ }
// Preload necessary dependencies
fetchAllTasks();
diff --git a/src/app/(main)/projects/edit/page.tsx b/src/app/(main)/projects/edit/page.tsx
index 78e0ed1..55f401a 100644
--- a/src/app/(main)/projects/edit/page.tsx
+++ b/src/app/(main)/projects/edit/page.tsx
@@ -12,9 +12,11 @@ import {
} from "@/app/api/projects";
import { preloadStaff, preloadTeamLeads } from "@/app/api/staff";
import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks";
+import { getUserAbilities } from "@/app/utils/commonUtil";
import { ServerFetchError } from "@/app/utils/fetchUtil";
import CreateProject from "@/components/CreateProject";
import { I18nProvider, getServerI18n } from "@/i18n";
+import { MAINTAIN_PROJECT } from "@/middleware";
import Typography from "@mui/material/Typography";
import { isArray } from "lodash";
import { Metadata } from "next";
@@ -32,8 +34,9 @@ const Projects: React.FC = async ({ searchParams }) => {
const { t } = await getServerI18n("projects");
// Assume projectId is string here
const projectId = searchParams["id"];
+ const abilities = await getUserAbilities()
- if (!projectId || isArray(projectId)) {
+ if (!projectId || isArray(projectId) || abilities.includes(MAINTAIN_PROJECT)) {
notFound();
}
diff --git a/src/app/(main)/projects/editSub/page.tsx b/src/app/(main)/projects/editSub/page.tsx
index eb4f5c6..37279a1 100644
--- a/src/app/(main)/projects/editSub/page.tsx
+++ b/src/app/(main)/projects/editSub/page.tsx
@@ -13,8 +13,10 @@ import {
} from "@/app/api/projects";
import { preloadStaff, preloadTeamLeads } from "@/app/api/staff";
import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks";
+import { getUserAbilities } from "@/app/utils/commonUtil";
import CreateProject from "@/components/CreateProject";
import { I18nProvider, getServerI18n } from "@/i18n";
+import { MAINTAIN_PROJECT } from "@/middleware";
import Typography from "@mui/material/Typography";
import { isArray } from "lodash";
import { Metadata } from "next";
@@ -32,7 +34,8 @@ const Projects: React.FC = async ({ searchParams }) => {
const { t } = await getServerI18n("projects");
const projectId = searchParams["id"];
- if (!projectId || isArray(projectId)) {
+ const abilities = await getUserAbilities()
+ if (!projectId || isArray(projectId) || !abilities.includes(MAINTAIN_PROJECT)) {
notFound();
}
diff --git a/src/app/(main)/projects/not-found.tsx b/src/app/(main)/projects/not-found.tsx
new file mode 100644
index 0000000..6464b10
--- /dev/null
+++ b/src/app/(main)/projects/not-found.tsx
@@ -0,0 +1,17 @@
+import { getServerI18n } from "@/i18n";
+import { Stack, Typography, Link } from "@mui/material";
+import NextLink from "next/link";
+
+export default async function NotFound() {
+ const { t } = await getServerI18n("projects", "common");
+
+ return (
+
+ {t("Not Found")}
+ {t("The project page was not found!")}
+
+ {t("Return to home")}
+
+
+ );
+}
diff --git a/src/app/(main)/projects/page.tsx b/src/app/(main)/projects/page.tsx
index 7cf44d0..837be0a 100644
--- a/src/app/(main)/projects/page.tsx
+++ b/src/app/(main)/projects/page.tsx
@@ -1,13 +1,15 @@
import { fetchProjectCategories, fetchProjects, preloadProjects } from "@/app/api/projects";
+import { getUserAbilities } from "@/app/utils/commonUtil";
import ProjectSearch from "@/components/ProjectSearch";
import { getServerI18n } from "@/i18n";
+import { MAINTAIN_PROJECT, VIEW_PROJECT } from "@/middleware";
import Add from "@mui/icons-material/Add";
-import { ButtonGroup } from "@mui/material";
import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { Metadata } from "next";
import Link from "next/link";
+import { notFound } from "next/navigation";
import { Suspense } from "react";
export const metadata: Metadata = {
@@ -19,6 +21,10 @@ const Projects: React.FC = async () => {
// preloadProjects();
fetchProjectCategories();
const projects = await fetchProjects();
+ const abilities = await getUserAbilities()
+ if (![MAINTAIN_PROJECT].some(ability => abilities.includes(ability))) {
+ notFound();
+ }
return (
<>
@@ -31,7 +37,7 @@ const Projects: React.FC = async () => {
{t("Projects")}
- {
>
{t("Create Project")}
-
+ }
}>
diff --git a/src/app/api/reports/index.ts b/src/app/api/reports/index.ts
index 7202d2e..29f1797 100644
--- a/src/app/api/reports/index.ts
+++ b/src/app/api/reports/index.ts
@@ -1,3 +1,4 @@
+import { WildCard } from "@/app/utils/commonUtil";
import { records } from "../staff/actions";
export interface FinancialStatusReportFilter {
@@ -36,17 +37,17 @@ export interface ProjectCashFlowReportRequest {
// - Project Potential Delay Report
export interface ProjectPotentialDelayReportFilter {
- team: AutocompleteOptions[];
+ team: string[];
client: AutocompleteOptions[];
- numberOfDays: number;
- projectCompletion: number;
+ daysUntilCurrentStageEnd: number;
+ resourceUtilizationPercentage: number;
}
export interface ProjectPotentialDelayReportRequest {
teamId: number | "All";
clientId: number | "All";
- numberOfDays: number;
- projectCompletion: number;
+ daysUntilCurrentStageEnd: number;
+ resourceUtilizationPercentage: number;
type: string;
}
@@ -68,9 +69,10 @@ export interface ProjectResourceOverconsumptionReportFilter {
lowerLimit: number;
}
-export interface ProjectResourceOverconsumptionReportRequest {
+export interface ProjectResourceOverconsumptionReportRequest extends WildCard {
teamId?: number
custId?: number
+ subsidiaryId?: number
status: "All" | "Within Budget" | "Potential Overconsumption" | "Overconsumption"
lowerLimit: number
}
@@ -99,6 +101,7 @@ export interface ProjectCompletionReportRequest {
startDate: String;
endDate: String;
outstanding: Boolean;
+ teamId?: number
}
export interface CostAndExpenseReportFilter {
team: string[];
diff --git a/src/app/api/resourcesummary/index.ts b/src/app/api/resourcesummary/index.ts
index e2e062e..666849d 100644
--- a/src/app/api/resourcesummary/index.ts
+++ b/src/app/api/resourcesummary/index.ts
@@ -10,6 +10,7 @@ export interface ResourceSummaryResult {
customerCode: string;
customerName: string;
customerCodeAndName: string;
+ subsidiaryCodeAndName: string;
}
export const preloadProjects = () => {
diff --git a/src/app/api/timesheets/actions.ts b/src/app/api/timesheets/actions.ts
index 5d4ecb0..d362264 100644
--- a/src/app/api/timesheets/actions.ts
+++ b/src/app/api/timesheets/actions.ts
@@ -94,6 +94,34 @@ export const saveMemberLeave = async (data: {
);
};
+export const deleteMemberEntry = async (data: {
+ staffId: number;
+ entryId: number;
+}) => {
+ return serverFetchJson(
+ `${BASE_API_URL}/timesheets/deleteMemberEntry`,
+ {
+ method: "POST",
+ body: JSON.stringify(data),
+ headers: { "Content-Type": "application/json" },
+ },
+ );
+};
+
+export const deleteMemberLeave = async (data: {
+ staffId: number;
+ entryId: number;
+}) => {
+ return serverFetchJson(
+ `${BASE_API_URL}/timesheets/deleteMemberLeave`,
+ {
+ method: "POST",
+ body: JSON.stringify(data),
+ headers: { "Content-Type": "application/json" },
+ },
+ );
+};
+
export const revalidateCacheAfterAmendment = () => {
revalidatePath("/(main)/home");
};
diff --git a/src/app/api/timesheets/utils.ts b/src/app/api/timesheets/utils.ts
index 673a03d..bbb13c2 100644
--- a/src/app/api/timesheets/utils.ts
+++ b/src/app/api/timesheets/utils.ts
@@ -28,7 +28,7 @@ export const validateTimeEntry = (
if (!entry.inputHours && !entry.otHours) {
error[isHoliday ? "otHours" : "inputHours"] = "Required";
} else if (entry.inputHours && isHoliday) {
- error.inputHours = "Cannot input normal hours for holidays";
+ error.inputHours = "Cannot input normal hours on holidays";
} else if (entry.inputHours && entry.inputHours <= 0) {
error.inputHours =
"Input hours should be between 0 and {{DAILY_NORMAL_MAX_HOURS}}";
@@ -55,16 +55,31 @@ export const validateTimeEntry = (
return Object.keys(error).length > 0 ? error : undefined;
};
-export const isValidLeaveEntry = (entry: Partial): string => {
+export type LeaveEntryError = {
+ [field in keyof LeaveEntry]?: string;
+};
+
+export const validateLeaveEntry = (
+ entry: Partial,
+ isHoliday: boolean,
+): LeaveEntryError | undefined => {
// Test for errrors
- let error: keyof LeaveEntry | "" = "";
+ const error: LeaveEntryError = {};
if (!entry.leaveTypeId) {
- error = "leaveTypeId";
- } else if (!entry.inputHours || !(entry.inputHours >= 0)) {
- error = "inputHours";
+ error.leaveTypeId = "Required";
+ } else if (entry.inputHours && isHoliday) {
+ error.inputHours = "Cannot input normal hours on holidays";
+ } else if (!entry.inputHours) {
+ error.inputHours = "Required";
+ } else if (
+ entry.inputHours &&
+ (entry.inputHours <= 0 || entry.inputHours > DAILY_NORMAL_MAX_HOURS)
+ ) {
+ error.inputHours =
+ "Input hours should be between 0 and {{DAILY_NORMAL_MAX_HOURS}}";
}
- return error;
+ return Object.keys(error).length > 0 ? error : undefined;
};
export const validateTimesheet = (
@@ -95,27 +110,10 @@ export const validateTimesheet = (
}
// Check total hours
- const leaves = leaveRecords[date];
- const leaveHours =
- leaves?.reduce((acc, entry) => acc + entry.inputHours, 0) || 0;
-
- const totalInputHours = timeEntries.reduce((acc, entry) => {
- return acc + (entry.inputHours || 0);
- }, 0);
-
- const totalOtHours = timeEntries.reduce((acc, entry) => {
- return acc + (entry.otHours || 0);
- }, 0);
-
- if (totalInputHours + leaveHours > DAILY_NORMAL_MAX_HOURS) {
- errors[date] =
- "The daily normal hours (timesheet hours + leave hours) cannot be more than {{DAILY_NORMAL_MAX_HOURS}}. Please use other hours for exceeding hours or decrease the leave hours.";
- } else if (
- totalInputHours + totalOtHours + leaveHours >
- TIMESHEET_DAILY_MAX_HOURS
- ) {
- errors[date] =
- "The daily total hours cannot be more than {{TIMESHEET_DAILY_MAX_HOURS}}";
+ const leaves = leaveRecords[date] || [];
+ const totalHourError = checkTotalHours(timeEntries, leaves);
+ if (totalHourError) {
+ errors[date] = totalHourError;
}
});
@@ -125,48 +123,64 @@ export const validateTimesheet = (
export const validateLeaveRecord = (
leaveRecords: RecordLeaveInput,
timesheet: RecordTimesheetInput,
+ companyHolidays: HolidaysResult[],
): { [date: string]: string } | undefined => {
const errors: { [date: string]: string } = {};
+ const holidays = new Set(
+ compact([
+ ...getPublicHolidaysForNYears(2).map((h) => h.date),
+ ...companyHolidays.map((h) => convertDateArrayToString(h.date)),
+ ]),
+ );
+
Object.keys(leaveRecords).forEach((date) => {
const leaves = leaveRecords[date];
// Check each leave entry
for (const entry of leaves) {
- const entryError = isValidLeaveEntry(entry);
+ const entryError = validateLeaveEntry(entry, holidays.has(date));
if (entryError) {
errors[date] = "There are errors in the entries";
+ return;
}
}
// Check total hours
const timeEntries = timesheet[date] || [];
- const leaveHours = leaves.reduce((acc, entry) => acc + entry.inputHours, 0);
-
- const totalInputHours = timeEntries.reduce((acc, entry) => {
- return acc + (entry.inputHours || 0);
- }, 0);
-
- const totalOtHours = timeEntries.reduce((acc, entry) => {
- return acc + (entry.otHours || 0);
- }, 0);
-
- if (totalInputHours + leaveHours > DAILY_NORMAL_MAX_HOURS) {
- errors[date] =
- "The daily normal hours (timesheet hours + leave hours) cannot be more than {{DAILY_NORMAL_MAX_HOURS}}. Please use other hours for exceeding hours or decrease the leave hours.";
- } else if (
- totalInputHours + totalOtHours + leaveHours >
- TIMESHEET_DAILY_MAX_HOURS
- ) {
- errors[date] =
- "The daily total hours cannot be more than {{TIMESHEET_DAILY_MAX_HOURS}}";
+ const totalHourError = checkTotalHours(timeEntries, leaves);
+ if (totalHourError) {
+ errors[date] = totalHourError;
}
});
return Object.keys(errors).length > 0 ? errors : undefined;
};
+export const checkTotalHours = (
+ timeEntries: TimeEntry[],
+ leaves: LeaveEntry[],
+): string | undefined => {
+ const leaveHours = leaves.reduce((acc, entry) => acc + entry.inputHours, 0);
+
+ const totalInputHours = timeEntries.reduce((acc, entry) => {
+ return acc + (entry.inputHours || 0);
+ }, 0);
+
+ const totalOtHours = timeEntries.reduce((acc, entry) => {
+ return acc + (entry.otHours || 0);
+ }, 0);
+
+ if (totalInputHours + leaveHours > DAILY_NORMAL_MAX_HOURS) {
+ return "The daily normal hours (timesheet hours + leave hours) cannot be more than {{DAILY_NORMAL_MAX_HOURS}}. Please use other hours for exceeding hours or decrease the leave hours.";
+ } else if (
+ totalInputHours + totalOtHours + leaveHours >
+ TIMESHEET_DAILY_MAX_HOURS
+ ) {
+ return "The daily total hours cannot be more than {{TIMESHEET_DAILY_MAX_HOURS}}";
+ }
+};
+
export const DAILY_NORMAL_MAX_HOURS = 8;
-export const LEAVE_DAILY_MAX_HOURS = 8;
export const TIMESHEET_DAILY_MAX_HOURS = 20;
diff --git a/src/app/utils/commonUtil.ts b/src/app/utils/commonUtil.ts
index 72d4a56..fa68bda 100644
--- a/src/app/utils/commonUtil.ts
+++ b/src/app/utils/commonUtil.ts
@@ -1,3 +1,9 @@
+import { SessionWithTokens, authOptions } from "@/config/authConfig"
+import { getServerSession } from "next-auth"
+export interface WildCard {
+ [key: string]: any;
+}
+
export const dateInRange = (currentDate: string, startDate: string, endDate: string) => {
if (currentDate === undefined) {
@@ -28,4 +34,24 @@ export const downloadFile = (blobData: Uint8Array, filename: string) => {
link.href = url;
link.setAttribute("download", filename);
link.click();
+}
+
+export function readIntFromString(input: string): [string, number | null] | string {
+ // Split the input string by the "-" character
+ if (!input.includes("-")) {
+ return [input, null]
+ }
+ const parts = input.split("-");
+
+ // Extract the string part and the integer part (if available)
+ const stringPart = parts.slice(0, parts.length - 1).join("-");
+ const intPartStr = parts[parts.length - 1];
+ const intPart = intPartStr ? parseInt(intPartStr, 10) : null;
+
+ return [stringPart, intPart];
+}
+
+export const getUserAbilities = async () => {
+ const session = await getServerSession(authOptions) as SessionWithTokens;
+ return session?.abilities ?? [] as string[]
}
\ No newline at end of file
diff --git a/src/app/utils/fetchUtil.ts b/src/app/utils/fetchUtil.ts
index a97a09c..9d9a63d 100644
--- a/src/app/utils/fetchUtil.ts
+++ b/src/app/utils/fetchUtil.ts
@@ -22,7 +22,7 @@ export const serverFetch: typeof fetch = async (input, init) => {
const session = await getServerSession(authOptions);
const accessToken = session?.accessToken;
- console.log(accessToken);
+ // console.log(accessToken);
return fetch(input, {
...init,
headers: {
diff --git a/src/components/AppBar/AppBar.tsx b/src/components/AppBar/AppBar.tsx
index 57c190a..10cf3fe 100644
--- a/src/components/AppBar/AppBar.tsx
+++ b/src/components/AppBar/AppBar.tsx
@@ -16,7 +16,7 @@ export interface AppBarProps {
const AppBar: React.FC = async ({ avatarImageSrc, profileName }) => {
const session = await getServerSession(authOptions) as any;
const abilities: string[] = session.abilities
- console.log(abilities)
+ // console.log(abilities)
return (
diff --git a/src/components/CostAndExpenseReport/CostAndExpenseReport.tsx b/src/components/CostAndExpenseReport/CostAndExpenseReport.tsx
index da15baf..1b837ee 100644
--- a/src/components/CostAndExpenseReport/CostAndExpenseReport.tsx
+++ b/src/components/CostAndExpenseReport/CostAndExpenseReport.tsx
@@ -11,12 +11,13 @@ import { downloadFile } from "@/app/utils/commonUtil";
interface Props {
team: TeamResult[];
customer: Customer[];
+ needAll: boolean | undefined;
}
type SearchQuery = Partial>;
type SearchParamNames = keyof SearchQuery;
-const CostAndExpenseReport: React.FC = ({ team, customer }) => {
+const CostAndExpenseReport: React.FC = ({ team, customer, needAll }) => {
const { t } = useTranslation("report");
const teamCombo = team.map((t) => `${t.name} - ${t.code}`);
const custCombo = customer.map(c => ({label: `${c.name} - ${c.code}`, value: c.id}))
@@ -28,7 +29,7 @@ const CostAndExpenseReport: React.FC = ({ team, customer }) => {
paramName: "team",
type: "select",
options: teamCombo,
- needAll: true,
+ needAll: needAll,
},
{
label: t("Client"),
diff --git a/src/components/CostAndExpenseReport/CostAndExpenseReportWrapper.tsx b/src/components/CostAndExpenseReport/CostAndExpenseReportWrapper.tsx
index 2b32c99..3c2a5d9 100644
--- a/src/components/CostAndExpenseReport/CostAndExpenseReportWrapper.tsx
+++ b/src/components/CostAndExpenseReport/CostAndExpenseReportWrapper.tsx
@@ -1,18 +1,30 @@
import React from "react";
import { fetchAllCustomers } from "@/app/api/customer";
-import { fetchTeam } from "@/app/api/team";
+import { fetchIndivTeam, fetchTeam } from "@/app/api/team";
import CostAndExpenseReport from "./CostAndExpenseReport";
import CostAndExpenseReportLoading from "./CostAndExpenseReportLoading";
-
+import { headers, cookies } from 'next/headers';
+import { getServerSession } from "next-auth";
+import { authOptions } from "@/config/authConfig";
+import { TEAM_LEAD } from "@/middleware";
interface SubComponents {
Loading: typeof CostAndExpenseReportLoading;
}
const CostAndExpenseReportWrapper: React.FC & SubComponents = async () => {
- const customers = await fetchAllCustomers()
- const teams = await fetchTeam ()
+ const session: any = await getServerSession(authOptions)
+ const teamId = session.staff?.team.id
+ const role = session.role
+ let customers = await fetchAllCustomers()
+ let teams = await fetchTeam()
+ let needAll = true
+
+ if (role === TEAM_LEAD) {
+ needAll = false
+ teams = teams.filter((team) => team.id === teamId);
+ }
- return
+ return
};
CostAndExpenseReportWrapper.Loading = CostAndExpenseReportLoading;
diff --git a/src/components/CreateProject/CreateProject.tsx b/src/components/CreateProject/CreateProject.tsx
index 0161188..31ea689 100644
--- a/src/components/CreateProject/CreateProject.tsx
+++ b/src/components/CreateProject/CreateProject.tsx
@@ -50,6 +50,7 @@ import {
successDialog,
} from "../Swal/CustomAlerts";
import dayjs from "dayjs";
+import { DELETE_PROJECT } from "@/middleware";
export interface Props {
isEditMode: boolean;
@@ -70,6 +71,7 @@ export interface Props {
workNatures: WorkNature[];
allStaffs: StaffResult[];
grades: Grade[];
+ abilities: string[];
}
const hasErrorsInTab = (
@@ -113,6 +115,7 @@ const CreateProject: React.FC = ({
buildingTypes,
workNatures,
allStaffs,
+ abilities,
}) => {
const [serverError, setServerError] = useState("");
const [tabIndex, setTabIndex] = useState(0);
@@ -302,7 +305,7 @@ const CreateProject: React.FC = ({
{isEditMode && !(formProps.getValues("projectDeleted") === true) && (
{/* {!formProps.getValues("projectActualStart") && ( */}
- {formProps.getValues("projectStatus").toLowerCase() === "pending to start" && (
+ {formProps.getValues("projectStatus")?.toLowerCase() === "pending to start" && (