diff --git a/package-lock.json b/package-lock.json index 4e65451..ed756cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5749,19 +5749,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/frac": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", diff --git a/src/app/(main)/analytics/EX02ProjectCashFlowReport/page.tsx b/src/app/(main)/analytics/EX02ProjectCashFlowReport/page.tsx new file mode 100644 index 0000000..d1d54d8 --- /dev/null +++ b/src/app/(main)/analytics/EX02ProjectCashFlowReport/page.tsx @@ -0,0 +1,25 @@ +import { Metadata } from "next"; +import { Suspense } from "react"; +import { I18nProvider } from "@/i18n"; +import { fetchProjects } from "@/app/api/projects"; +import GenerateEX02ProjectCashFlowReport from "@/components/GenerateEX02ProjectCashFlowReport"; + +export const metadata: Metadata = { + title: "EX02 - Project Cash Flow Report", +}; + +const ProjectCashFlowReport: React.FC = async () => { + fetchProjects(); + + return ( + <> + + }> + + + + + ); +}; + +export default ProjectCashFlowReport; diff --git a/src/app/(main)/dashboard/ProjectResourceSummary/page.tsx b/src/app/(main)/dashboard/ProjectResourceSummary/page.tsx new file mode 100644 index 0000000..5dc2f77 --- /dev/null +++ b/src/app/(main)/dashboard/ProjectResourceSummary/page.tsx @@ -0,0 +1,29 @@ +import { Metadata } from "next"; +import { I18nProvider } from "@/i18n"; +import DashboardPage from "@/components/DashboardPage/DashboardPage"; +import DashboardPageButton from "@/components/DashboardPage/DashboardTabButton"; +import { Suspense } from "react"; +import Tabs, { TabsProps } from "@mui/material/Tabs"; +import Tab from "@mui/material/Tab"; +import Typography from "@mui/material/Typography"; +import StaffUtilizationComponent from "@/components/StaffUtilization"; +import ProjectResourceSummarySearch from "@/components/ProjectResourceSummarySearch"; +import { ResourceSummaryResult } from "@/app/api/resourcesummary"; + +export const metadata: Metadata = { + title: "Project Resource Summary", +}; + +const ProjectResourceSummary: React.FC = () => { + return ( + + + Project Resource Summary + + }> + + + + ); +}; +export default ProjectResourceSummary; diff --git a/src/app/(main)/dashboard/StaffUtilization/page.tsx b/src/app/(main)/dashboard/StaffUtilization/page.tsx index 2ddea02..87bb6c0 100644 --- a/src/app/(main)/dashboard/StaffUtilization/page.tsx +++ b/src/app/(main)/dashboard/StaffUtilization/page.tsx @@ -10,7 +10,7 @@ import Typography from "@mui/material/Typography"; import StaffUtilizationComponent from "@/components/StaffUtilization"; export const metadata: Metadata = { - title: "Project Status by Client", + title: "Staff Utilization", }; const StaffUtilization: React.FC = () => { diff --git a/src/app/(main)/settings/customer/create/page.tsx b/src/app/(main)/settings/customer/create/page.tsx index e0dc0e0..13b38eb 100644 --- a/src/app/(main)/settings/customer/create/page.tsx +++ b/src/app/(main)/settings/customer/create/page.tsx @@ -1,4 +1,4 @@ -import CustomerDetail from "@/components/CustomerDetail"; +import CustomerSave from "@/components/CustomerSave"; // import { preloadAllTasks } from "@/app/api/tasks"; import CreateTaskTemplate from "@/components/CreateTaskTemplate"; import { I18nProvider, getServerI18n } from "@/i18n"; @@ -16,7 +16,7 @@ const CreateCustomer: React.FC = async () => { <> {t("Create Customer")} - + ); diff --git a/src/app/(main)/settings/customer/edit/page.tsx b/src/app/(main)/settings/customer/edit/page.tsx index 004781b..7755403 100644 --- a/src/app/(main)/settings/customer/edit/page.tsx +++ b/src/app/(main)/settings/customer/edit/page.tsx @@ -1,5 +1,5 @@ import { fetchAllSubsidiaries, preloadAllCustomers } from "@/app/api/customer"; -import CustomerDetail from "@/components/CustomerDetail"; +import CustomerSave from "@/components/CustomerSave"; // import { preloadAllTasks } from "@/app/api/tasks"; import CreateTaskTemplate from "@/components/CreateTaskTemplate"; import { I18nProvider, getServerI18n } from "@/i18n"; @@ -18,7 +18,7 @@ const EditCustomer: React.FC = async () => { <> {t("Edit Customer")} - + ); diff --git a/src/app/(main)/settings/skill/create/page.tsx b/src/app/(main)/settings/skill/create/page.tsx new file mode 100644 index 0000000..c98f993 --- /dev/null +++ b/src/app/(main)/settings/skill/create/page.tsx @@ -0,0 +1,48 @@ +// 'use client'; +import { I18nProvider, getServerI18n } from "@/i18n"; +import CustomInputForm from "@/components/CustomInputForm"; +import Check from "@mui/icons-material/Check"; +import Close from "@mui/icons-material/Close"; +import Button from "@mui/material/Button"; +import Stack from "@mui/material/Stack"; +import Tab from "@mui/material/Tab"; +import Tabs, { TabsProps } from "@mui/material/Tabs"; +import { useRouter } from "next/navigation"; +import React, { useCallback, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Task, TaskTemplate } from "@/app/api/tasks"; +import { + FieldErrors, + FormProvider, + SubmitErrorHandler, + SubmitHandler, + useForm, +} from "react-hook-form"; +import { CreateProjectInputs, saveProject } from "@/app/api/projects/actions"; +import { Error } from "@mui/icons-material"; +import { ProjectCategory } from "@/app/api/projects"; +import { Grid, Typography } from "@mui/material"; +import CreateStaffForm from "@/components/CreateStaff/CreateStaff"; +import CreateSkill from "@/components/CreateSkill"; + +// const Title = ["title1", "title2"]; + +const CreateStaff: React.FC = async () => { + const { t } = await getServerI18n("staff"); + + const title = ['', t('Additional Info')] + // const regex = new RegExp("^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$") + // console.log(regex) + + return ( + <> + {t("Create Skill")} + + + + + ); +}; + +export default CreateStaff; diff --git a/src/app/(main)/settings/skill/page.tsx b/src/app/(main)/settings/skill/page.tsx new file mode 100644 index 0000000..f263c87 --- /dev/null +++ b/src/app/(main)/settings/skill/page.tsx @@ -0,0 +1,50 @@ +import { preloadClaims } from "@/app/api/claims"; +// import { preloadSkill, preloadTeamLeads } from "@/app/api/staff"; +import SkillSearch from "@/components/SkillSearch"; +import { I18nProvider, getServerI18n } from "@/i18n"; +import Add from "@mui/icons-material/Add"; +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 { Suspense } from "react"; + +export const metadata: Metadata = { + title: "Skill", +}; + +const Skill: React.FC = async () => { + const { t } = await getServerI18n("skill"); +// preloadTeamLeads(); +// preloadSkill(); + return ( + <> + + + {t("Skill")} + + + + + }> + + + + + ); +}; + +export default Skill; diff --git a/src/app/(main)/settings/user/page.tsx b/src/app/(main)/settings/user/page.tsx new file mode 100644 index 0000000..95973ab --- /dev/null +++ b/src/app/(main)/settings/user/page.tsx @@ -0,0 +1,54 @@ +import { preloadClaims } from "@/app/api/claims"; +import { preloadStaff, preloadTeamLeads } from "@/app/api/staff"; +import StaffSearch from "@/components/StaffSearch"; +import TeamSearch from "@/components/TeamSearch"; +import UserSearch from "@/components/UserSearch"; +import { I18nProvider, getServerI18n } from "@/i18n"; +import Add from "@mui/icons-material/Add"; +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 { Suspense } from "react"; + + +export const metadata: Metadata = { + title: "User", + }; + + + const User: React.FC = async () => { + const { t } = await getServerI18n("User"); + // preloadTeamLeads(); + // preloadStaff(); + return ( + <> + + + {t("User")} + + + + + }> + + + + + ); + }; + + export default User; \ No newline at end of file diff --git a/src/app/(main)/tasks/create/page.tsx b/src/app/(main)/tasks/create/page.tsx index 656139f..262f624 100644 --- a/src/app/(main)/tasks/create/page.tsx +++ b/src/app/(main)/tasks/create/page.tsx @@ -3,6 +3,7 @@ import CreateTaskTemplate from "@/components/CreateTaskTemplate"; import { getServerI18n } from "@/i18n"; import Typography from "@mui/material/Typography"; import { Metadata } from "next"; +import { I18nProvider } from "@/i18n"; export const metadata: Metadata = { title: "Create Task Template", @@ -15,7 +16,9 @@ const Projects: React.FC = async () => { return ( <> {t("Create Task Template")} - + + + ); }; diff --git a/src/app/(main)/tasks/edit/page.tsx b/src/app/(main)/tasks/edit/page.tsx new file mode 100644 index 0000000..2b2c02c --- /dev/null +++ b/src/app/(main)/tasks/edit/page.tsx @@ -0,0 +1,26 @@ +import { preloadAllTasks } from "@/app/api/tasks"; +import CreateTaskTemplate from "@/components/CreateTaskTemplate"; +import { getServerI18n } from "@/i18n"; +import Typography from "@mui/material/Typography"; +import { Metadata } from "next"; +import { I18nProvider } from "@/i18n"; + +export const metadata: Metadata = { + title: "Edit Task Template", +}; + +const TaskTemplates: React.FC = async () => { + const { t } = await getServerI18n("tasks"); + preloadAllTasks(); + + return ( + <> + {t("Edit Task Template")} + + + + + ); +}; + +export default TaskTemplates; diff --git a/src/app/(main)/tasks/page.tsx b/src/app/(main)/tasks/page.tsx index b9e9bf8..bf06dc2 100644 --- a/src/app/(main)/tasks/page.tsx +++ b/src/app/(main)/tasks/page.tsx @@ -8,13 +8,14 @@ import Typography from "@mui/material/Typography"; import { Metadata } from "next"; import Link from "next/link"; import { Suspense } from "react"; +import { I18nProvider } from "@/i18n"; export const metadata: Metadata = { title: "Tasks", }; const TaskTemplates: React.FC = async () => { - const { t } = await getServerI18n("projects"); + const { t } = await getServerI18n("tasks"); preloadTaskTemplates(); return ( @@ -34,12 +35,14 @@ const TaskTemplates: React.FC = async () => { LinkComponent={Link} href="/tasks/create" > - {t("Create Template")} + {t("Create Task Template")} - }> - - + + }> + + + ); }; diff --git a/src/app/api/claims/actions.ts b/src/app/api/claims/actions.ts index d607c48..542effe 100644 --- a/src/app/api/claims/actions.ts +++ b/src/app/api/claims/actions.ts @@ -21,7 +21,7 @@ export interface ClaimDetailTable { id: number; invoiceDate: Date; description: string; - project: ProjectCombo; + project: number; amount: number; supportingDocumentName: string; oldSupportingDocument: SupportingDocument; diff --git a/src/app/api/clientprojects/index.ts b/src/app/api/clientprojects/index.ts index 5c65810..3eed422 100644 --- a/src/app/api/clientprojects/index.ts +++ b/src/app/api/clientprojects/index.ts @@ -27,7 +27,7 @@ const mockProjects: ClientProjectResult[] = [ NoOfProjects: 5, }, { - id: 1, + id: 2, clientCode: "CUST-001", clientName: "Client A", SubsidiaryClientCode: "SUBS-001", @@ -35,7 +35,7 @@ const mockProjects: ClientProjectResult[] = [ NoOfProjects: 5, }, { - id: 1, + id: 3, clientCode: "CUST-001", clientName: "Client A", SubsidiaryClientCode: "SUBS-002", @@ -43,7 +43,7 @@ const mockProjects: ClientProjectResult[] = [ NoOfProjects: 3, }, { - id: 1, + id: 4, clientCode: "CUST-001", clientName: "Client A", SubsidiaryClientCode: "SUBS-003", diff --git a/src/app/api/projects/index.ts b/src/app/api/projects/index.ts index 0c7209c..7bfb067 100644 --- a/src/app/api/projects/index.ts +++ b/src/app/api/projects/index.ts @@ -59,6 +59,11 @@ export interface AssignedProject { endDate: string; }; }; + // Manhour info + hoursSpent: number; + hoursSpentOther: number; + hoursAllocated: number; + hoursAllocatedOther: number; } export const preloadProjects = () => { @@ -131,3 +136,12 @@ export const fetchProjectWorkNatures = cache(async () => { next: { tags: ["projectWorkNatures"] }, }); }); + +export const fetchAssignedProjects = cache(async () => { + return serverFetchJson( + `${BASE_API_URL}/projects/assignedProjects`, + { + next: { tags: ["assignedProjects"] }, + }, + ); +}); diff --git a/src/app/api/reports/actions.ts b/src/app/api/reports/actions.ts new file mode 100644 index 0000000..be74c6f --- /dev/null +++ b/src/app/api/reports/actions.ts @@ -0,0 +1,23 @@ +"use server"; + +import { serverFetchBlob, serverFetchJson } from "@/app/utils/fetchUtil"; +import { EX02ProjectCashFlowReportRequest } from "."; +import { BASE_API_URL } from "@/config/api"; + +export interface FileResponse { + filename: string; + blobValue: Uint8Array; +} + +export const fetchEX02ProjectCashFlowReport = async (data: EX02ProjectCashFlowReportRequest) => { + const reportBlob = await serverFetchBlob( + `${BASE_API_URL}/reports/EX02-ProjectCashFlowReport`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); + + return reportBlob +}; \ No newline at end of file diff --git a/src/app/api/reports/index.ts b/src/app/api/reports/index.ts new file mode 100644 index 0000000..6baa7aa --- /dev/null +++ b/src/app/api/reports/index.ts @@ -0,0 +1,8 @@ +// EX02 - Project Cash Flow Report +export interface EX02ProjectCashFlowReportFilter { + project: string[]; +} + +export interface EX02ProjectCashFlowReportRequest { + projectId: number; +} \ No newline at end of file diff --git a/src/app/api/resourcesummary/index.ts b/src/app/api/resourcesummary/index.ts new file mode 100644 index 0000000..ffaba69 --- /dev/null +++ b/src/app/api/resourcesummary/index.ts @@ -0,0 +1,53 @@ +import { cache } from "react"; + +export interface ResourceSummaryResult { + id: number; + projectCode: string; + projectName: string; + clientCode: string; + clientName: string; + clientCodeAndName: string; +} + +export const preloadProjects = () => { + fetchResourceSummary(); +}; + +export const fetchResourceSummary = cache(async () => { + return mockProjects; +}); + +const mockProjects: ResourceSummaryResult[] = [ + { + id: 1, + projectCode: 'C-1001-001', + projectName: 'Consultancy Project A', + clientCode: 'Client-001', + clientName: 'AAA Construction', + clientCodeAndName: 'Client-001 - AAA Construction', + }, + { + id: 2, + projectCode: 'C-1002-001', + projectName: 'Consultancy Project B', + clientCode: 'Client-001', + clientName: 'AAA Construction', + clientCodeAndName: 'Client-001 - AAA Construction', + }, + { + id: 3, + projectCode: 'C-1003-001', + projectName: 'Consultancy Project C', + clientCode: 'Client-002', + clientName: 'BBB Construction', + clientCodeAndName: 'Client-002 - BBB Construction', + }, + { + id: 4, + projectCode: 'C-1004-001', + projectName: 'Consultancy Project D', + clientCode: 'Client-002', + clientName: 'BBB Construction', + clientCodeAndName: 'Client-002 - BBB Construction', + }, +]; diff --git a/src/app/api/skill/actions.ts b/src/app/api/skill/actions.ts index eda7f39..6a0deca 100644 --- a/src/app/api/skill/actions.ts +++ b/src/app/api/skill/actions.ts @@ -5,6 +5,13 @@ import { serverFetchJson } from "@/app/utils/fetchUtil"; import { BASE_API_URL } from "@/config/api"; import { cache } from "react"; +export interface CreateSkillInputs { + id?: number; + name: String; + code: String; + description: String; +} + export interface comboProp { id: any; label: string; @@ -18,4 +25,13 @@ export const fetchSkillCombo = cache(async () => { return serverFetchJson(`${BASE_API_URL}/skill/combo`, { next: { tags: ["skill"] }, }); - }); \ No newline at end of file + }); + + +export const saveSkill = async (data: CreateSkillInputs) => { + return serverFetchJson(`${BASE_API_URL}/skill/save`, { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }); + }; \ No newline at end of file diff --git a/src/app/api/skill/index.ts b/src/app/api/skill/index.ts new file mode 100644 index 0000000..cf6ebec --- /dev/null +++ b/src/app/api/skill/index.ts @@ -0,0 +1,22 @@ +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; +import { cache } from "react"; +import "server-only"; + +export interface SkillResult { + action: any; + id: number; + name: string; + description: string; + code: string; + } + + export const preloadSkill = () => { + fetchSkill(); + }; + + export const fetchSkill = cache(async () => { + return serverFetchJson(`${BASE_API_URL}/skill`, { + next: { tags: ["sill"] }, + }); + }); \ No newline at end of file diff --git a/src/app/api/staff/actions.ts b/src/app/api/staff/actions.ts index 9416d2d..a2235d6 100644 --- a/src/app/api/staff/actions.ts +++ b/src/app/api/staff/actions.ts @@ -1,5 +1,5 @@ "use server"; -import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; import { BASE_API_URL } from "@/config/api"; import { StaffResult, data } from "."; import { cache } from "react"; @@ -59,8 +59,8 @@ export const testing = async (data: CreateStaffInputs) => { }); }; -export const deleteStaff = async (data: StaffResult) => { - return serverFetchJson(`${BASE_API_URL}/staffs/delete/${data.id}`, { +export const deleteStaff = async (id: number) => { + return serverFetchWithNoContent(`${BASE_API_URL}/staffs/delete/${id}`, { method: "DELETE", // body: JSON.stringify(data), headers: { "Content-Type": "application/json" }, diff --git a/src/app/api/tasks/actions.ts b/src/app/api/tasks/actions.ts index 862cc62..2c043be 100644 --- a/src/app/api/tasks/actions.ts +++ b/src/app/api/tasks/actions.ts @@ -1,6 +1,6 @@ "use server"; -import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; import { BASE_API_URL } from "@/config/api"; import { TaskTemplate } from "."; import { revalidateTag } from "next/cache"; @@ -9,11 +9,13 @@ export interface NewTaskTemplateFormInputs { code: string; name: string; taskIds: number[]; + + id: number | null; } export const saveTaskTemplate = async (data: NewTaskTemplateFormInputs) => { const newTaskTemplate = await serverFetchJson( - `${BASE_API_URL}/tasks/templates/new`, + `${BASE_API_URL}/tasks/templates/save`, { method: "POST", body: JSON.stringify(data), @@ -25,3 +27,27 @@ export const saveTaskTemplate = async (data: NewTaskTemplateFormInputs) => { return newTaskTemplate; }; + +export const fetchTaskTemplate = async (id: number) => { + const taskTemplate = await serverFetchJson( + `${BASE_API_URL}/tasks/templates/${id}`, + { + method: "GET", + headers: { "Content-Type": "application/json" }, + }, + ); + + return taskTemplate; +}; + +export const deleteTaskTemplate = async (id: number) => { + const taskTemplate = await serverFetchWithNoContent( + `${BASE_API_URL}/tasks/templates/${id}`, + { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + }, + ); + + return taskTemplate +}; diff --git a/src/app/api/team/actions.ts b/src/app/api/team/actions.ts index 28496d0..47e1a82 100644 --- a/src/app/api/team/actions.ts +++ b/src/app/api/team/actions.ts @@ -1,5 +1,5 @@ "use server"; -import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; import { BASE_API_URL } from "@/config/api"; import { cache } from "react"; import { TeamResult } from "."; @@ -53,10 +53,9 @@ export const saveTeam = async (data: CreateTeamInputs) => { }; -export const deleteTeam = async (data: TeamResult) => { - return serverFetchJson(`${BASE_API_URL}/team/delete/${data.id}`, { +export const deleteTeam = async (id: number) => { + return serverFetchWithNoContent(`${BASE_API_URL}/team/delete/${id}`, { method: "DELETE", - // body: JSON.stringify(data), headers: { "Content-Type": "application/json" }, }); }; diff --git a/src/app/api/user/actions.ts b/src/app/api/user/actions.ts new file mode 100644 index 0000000..5df734a --- /dev/null +++ b/src/app/api/user/actions.ts @@ -0,0 +1,27 @@ +"use server"; + +import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; +import { revalidateTag } from "next/cache"; +import { UserDetail, UserResult } from "."; +import { cache } from "react"; + +export interface UserInputs { + username: string; + firstname: string; + lastname: string; +} + + +export const fetchUserDetails = cache(async (id: number) => { + return serverFetchJson(`${BASE_API_URL}/user/${id}`, { + next: { tags: ["user"] }, + }); + }); + +export const deleteUser = async (id: number) => { + return serverFetchWithNoContent(`${BASE_API_URL}/user/${id}`, { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + }); + }; \ No newline at end of file diff --git a/src/app/api/user/index.ts b/src/app/api/user/index.ts new file mode 100644 index 0000000..9a6065b --- /dev/null +++ b/src/app/api/user/index.ts @@ -0,0 +1,43 @@ +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; +import { cache } from "react"; +import "server-only"; + + +export interface UserResult { + action: any; + id: number; + name: string; + locale: string; + username: string; + fullName: string; + firstname: string; + lastname: string; + title: string; + department: string; + email: string; + phone1: string; + phone2: string; + remarks: string; + } + +// export interface DetailedUser extends UserResult { +// username: string; +// password: string +// } + +export interface UserDetail { + authIds: number[]; + data: UserResult; + groupIds: number[]; + } + + export const preloadUser = () => { + fetchUser(); + }; + + export const fetchUser = cache(async () => { + return serverFetchJson(`${BASE_API_URL}/user`, { + next: { tags: ["user"] }, + }); + }); \ No newline at end of file diff --git a/src/app/utils/commonUtil.ts b/src/app/utils/commonUtil.ts index d4c71b6..72d4a56 100644 --- a/src/app/utils/commonUtil.ts +++ b/src/app/utils/commonUtil.ts @@ -20,4 +20,12 @@ export const dateInRange = (currentDate: string, startDate: string, endDate: str return true } } +} + +export const downloadFile = (blobData: Uint8Array, filename: string) => { + const url = URL.createObjectURL(new Blob([blobData])); + const link = document.createElement("a"); + link.href = url; + link.setAttribute("download", filename); + link.click(); } \ No newline at end of file diff --git a/src/app/utils/fetchUtil.ts b/src/app/utils/fetchUtil.ts index 0aaa798..5060991 100644 --- a/src/app/utils/fetchUtil.ts +++ b/src/app/utils/fetchUtil.ts @@ -8,6 +8,7 @@ export const serverFetch: typeof fetch = async (input, init) => { const session = await getServerSession(authOptions); const accessToken = session?.accessToken; + console.log(accessToken); return fetch(input, { ...init, headers: { @@ -15,7 +16,8 @@ export const serverFetch: typeof fetch = async (input, init) => { ...(accessToken ? { Authorization: `Bearer ${accessToken}`, - Accept: "application/json" + Accept: + "application/json, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", } : {}), }, @@ -56,6 +58,80 @@ export async function serverFetchWithNoContent(...args: FetchParams) { } } +export async function serverFetchBlob(...args: FetchParams) { + const response = await serverFetch(...args); + + if (response.ok) { + const body = response.body; + // console.log(body) + // console.log(body?.tee()[0].getReader()) + + const reader = body?.getReader(); + let finalUInt8Array = new Uint8Array(); + let done = false; + + while (!done) { + const read = await reader?.read(); + + // version 1 + if (read?.done) { + done = true; + } else { + const tempUInt8Array = new Uint8Array( + finalUInt8Array.length + read?.value.length!, + ); + tempUInt8Array.set(finalUInt8Array); + tempUInt8Array.set(read?.value!, finalUInt8Array.length); + finalUInt8Array = new Uint8Array(tempUInt8Array.length!); + finalUInt8Array.set(tempUInt8Array); + + // console.log("1", finalUInt8Array) + } + } + + // version 2 & return bodyRead + // const bodyRead = reader?.read().then(function processText({ done, value }): any { + // // Result objects contain two properties: + // // done - true if the stream has already given you all its data. + // // value - some data. Always undefined when done is true. + // if (done) { + // console.log("Stream complete"); + // return { filename: response.headers.get("filename"), blobValue: finalUInt8Array } as T;; + // } + + // // value for fetch streams is a Uint8Array + // finalUInt8Array = new Uint8Array(value.length) + // finalUInt8Array.set(value) + + // console.log(finalUInt8Array) + // // Read some more, and call this function again + // return reader.read().then(processText); + // }) + // const bodyValue = bodyRead?.value + + // const blob = await response.blob() + // const blobText = await blob.text(); + // const blobType = await blob.type; + + // console.log(bodyReader) + // console.log("2", finalUInt8Array) + // console.log(bodyValue) + + return { + filename: response.headers.get("filename"), + blobValue: finalUInt8Array, + } as T; + } else { + switch (response.status) { + case 401: + signOutUser(); + default: + console.error(await response.text()); + throw Error("Something went wrong fetching data in server."); + } + } +} + export const signOutUser = () => { const headersList = headers(); const referer = headersList.get("referer"); diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx index 314ea63..3d6123a 100644 --- a/src/components/Breadcrumb/Breadcrumb.tsx +++ b/src/components/Breadcrumb/Breadcrumb.tsx @@ -28,6 +28,7 @@ const pathToLabelMap: { [path: string]: string } = { "/settings/position": "Position", "/settings/position/new": "Create Position", "/settings/salarys": "Salary", + "/analytics/EX02ProjectCashFlowReport": "EX02 - Project Cash Flow Report", }; const Breadcrumb = () => { diff --git a/src/components/ClaimDetail/ClaimDetail.tsx b/src/components/ClaimDetail/ClaimDetail.tsx index db74447..54a82a5 100644 --- a/src/components/ClaimDetail/ClaimDetail.tsx +++ b/src/components/ClaimDetail/ClaimDetail.tsx @@ -75,9 +75,10 @@ const ClaimDetail: React.FC = ({ projectCombo }) => { const formData = new FormData() formData.append("expenseType", data.expenseType) data.addClaimDetails.forEach((claimDetail) => { + console.log(claimDetail) formData.append("addClaimDetailIds", JSON.stringify(claimDetail.id)) formData.append("addClaimDetailInvoiceDates", convertDateToString(claimDetail.invoiceDate, "YYYY-MM-DD")) - formData.append("addClaimDetailProjectIds", JSON.stringify(claimDetail.project.id)) + formData.append("addClaimDetailProjectIds", JSON.stringify(claimDetail.project)) formData.append("addClaimDetailDescriptions", claimDetail.description) formData.append("addClaimDetailAmounts", JSON.stringify(claimDetail.amount)) formData.append("addClaimDetailNewSupportingDocuments", claimDetail.newSupportingDocument) diff --git a/src/components/ClaimDetail/ClaimFormInputGrid.tsx b/src/components/ClaimDetail/ClaimFormInputGrid.tsx index 24807bd..6aac620 100644 --- a/src/components/ClaimDetail/ClaimFormInputGrid.tsx +++ b/src/components/ClaimDetail/ClaimFormInputGrid.tsx @@ -371,20 +371,21 @@ const ClaimFormInputGrid: React.FC = ({ flex: 1, editable: true, type: "singleSelect", - getOptionLabel: (value: any) => { + getOptionLabel: (value: ProjectCombo) => { return !value?.code || value?.code.length === 0 ? `${value?.name}` : `${value?.code} - ${value?.name}`; }, - getOptionValue: (value: any) => value, + getOptionValue: (value: ProjectCombo) => value.id, valueOptions: () => { const options = projectCombo ?? [] if (options.length === 0) { options.push({ id: -1, code: "", name: "No Projects" }) } - return options; + + return options as ProjectCombo[]; }, valueGetter: (params) => { - return params.value ?? projectCombo[0].id ?? -1 + return params.value ?? projectCombo[0] ?? { id: -1, code: "", name: "No Projects" } as ProjectCombo }, }, { diff --git a/src/components/ClaimSearch/ClaimSearch.tsx b/src/components/ClaimSearch/ClaimSearch.tsx index c0ab01f..304993a 100644 --- a/src/components/ClaimSearch/ClaimSearch.tsx +++ b/src/components/ClaimSearch/ClaimSearch.tsx @@ -50,12 +50,12 @@ const ClaimSearch: React.FC = ({ claims }) => { const columns = useMemo[]>( () => [ - // { - // name: "action", - // label: t("Actions"), - // onClick: onClaimClick, - // buttonIcon: , - // }, + { + name: "id", + label: t("Details"), + onClick: onClaimClick, + buttonIcon: , + }, { name: "created", label: t("Creation Date"), type: "date" }, { name: "code", label: t("Claim Code") }, // { name: "project", label: t("Related Project Name") }, diff --git a/src/components/CreateSkill/CreateSkill.tsx b/src/components/CreateSkill/CreateSkill.tsx new file mode 100644 index 0000000..d264b34 --- /dev/null +++ b/src/components/CreateSkill/CreateSkill.tsx @@ -0,0 +1,122 @@ +"use client"; + +import { + FieldErrors, + FormProvider, + SubmitErrorHandler, + SubmitHandler, + useForm, +} from "react-hook-form"; +import { Button, Stack, Tab, Tabs, TabsProps, Typography } from "@mui/material"; +import { Check, Close, RestartAlt } from "@mui/icons-material"; +import { useCallback, useState } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useTranslation } from "react-i18next"; +import { CreateSkillInputs, saveSkill } from "@/app/api/skill/actions"; +import { Error } from "@mui/icons-material"; +import SkillInfo from "./SkillInfo"; + +interface Props {} + +const CreateSkill: React.FC = () => { + const formProps = useForm(); + const [serverError, setServerError] = useState(""); + const router = useRouter(); + const { t } = useTranslation(); + const [tabIndex, setTabIndex] = useState(0); + const errors = formProps.formState.errors; + + const onSubmit = useCallback>( + async (data) => { + try { + console.log(data); + await saveSkill(data) + router.replace(`/settings/skill`) + } catch (e) { + console.log(e); + setServerError(t("An error has occurred. Please try again later.")); + } + }, + [router] + ); + + const handleCancel = () => { + router.back(); + }; + +// const handleReset = useCallback(() => { +// console.log(defaultValues) +// }, [defaultValues]) + + const handleTabChange = useCallback>( + (_e, newValue) => { + setTabIndex(newValue); + }, + [] + ); + + const hasErrorsInTab = ( + tabIndex: number, + errors: FieldErrors + ) => { + switch (tabIndex) { + case 0: + return Object.keys(errors).length > 0; + default: + false; + } + }; + return ( + <> + + + + + ) : undefined + } + iconPosition="end" + /> + {/* */} + + {serverError && ( + + {serverError} + + )} + {tabIndex === 0 && } + + + + + + + + ); +}; + +export default CreateSkill; diff --git a/src/components/CreateSkill/CreateSkillLoading.tsx b/src/components/CreateSkill/CreateSkillLoading.tsx new file mode 100644 index 0000000..f7d17bf --- /dev/null +++ b/src/components/CreateSkill/CreateSkillLoading.tsx @@ -0,0 +1,40 @@ +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Skeleton from "@mui/material/Skeleton"; +import Stack from "@mui/material/Stack"; +import React from "react"; + +// Can make this nicer +export const CreateSkillLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + CreateSkill + + + + + + + + + + + ); +}; + +export default CreateSkillLoading; diff --git a/src/components/CreateSkill/CreateSkillWrapper.tsx b/src/components/CreateSkill/CreateSkillWrapper.tsx new file mode 100644 index 0000000..f2f667a --- /dev/null +++ b/src/components/CreateSkill/CreateSkillWrapper.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import CreateSkill from "./CreateSkill"; +import CreateSkillLoading from "./CreateSkillLoading"; +import { fetchStaff, fetchTeamLeads } from "@/app/api/staff"; +import { useSearchParams } from "next/navigation"; + +interface SubComponents { + Loading: typeof CreateSkillLoading; +} + +const CreateSkillWrapper: React.FC & SubComponents = async () => { + + + return ; +}; + +CreateSkillWrapper.Loading = CreateSkillLoading; + +export default CreateSkillWrapper; diff --git a/src/components/CreateSkill/SkillInfo.tsx b/src/components/CreateSkill/SkillInfo.tsx new file mode 100644 index 0000000..be9724d --- /dev/null +++ b/src/components/CreateSkill/SkillInfo.tsx @@ -0,0 +1,90 @@ +"use client"; +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Grid from "@mui/material/Grid"; +import TextField from "@mui/material/TextField"; +import Typography from "@mui/material/Typography"; +import { useTranslation } from "react-i18next"; +import CardActions from "@mui/material/CardActions"; +import RestartAlt from "@mui/icons-material/RestartAlt"; +import Button from "@mui/material/Button"; +import { Controller, useFormContext } from "react-hook-form"; +import { FormControl, InputLabel, MenuItem, Select } from "@mui/material"; +import { useCallback } from "react"; +import { CreateSkillInputs } from "@/app/api/skill/actions"; + +const SkillInfo: React.FC = ( +) => { + const { t } = useTranslation(); + const { + register, + formState: { errors, defaultValues }, + control, + reset, + resetField, + setValue, + } = useFormContext(); + + const resetSkill = useCallback(() => { + console.log(defaultValues); + if (defaultValues !== undefined) { + resetField("name"); + } + }, [defaultValues]); + + return ( + <> + + + + + {t("Skill Info")} + + + + + + + + + + + + + + + + + ); +}; +export default SkillInfo; diff --git a/src/components/CreateSkill/index.ts b/src/components/CreateSkill/index.ts new file mode 100644 index 0000000..044c4cf --- /dev/null +++ b/src/components/CreateSkill/index.ts @@ -0,0 +1 @@ +export { default } from "./CreateSkillWrapper"; diff --git a/src/components/CreateStaff/CreateStaff.tsx b/src/components/CreateStaff/CreateStaff.tsx index 312f18e..94e7f71 100644 --- a/src/components/CreateStaff/CreateStaff.tsx +++ b/src/components/CreateStaff/CreateStaff.tsx @@ -190,7 +190,7 @@ const CreateStaff: React.FC = ({ Title }) => { { id: "skillSetId", label: t("Skillset"), - type: "combo-Obj", + type: "multiSelect-Obj", options: skillCombo || [], required: false, }, diff --git a/src/components/CreateTaskTemplate/CreateTaskTemplate.tsx b/src/components/CreateTaskTemplate/CreateTaskTemplate.tsx index 066c994..f7d5912 100644 --- a/src/components/CreateTaskTemplate/CreateTaskTemplate.tsx +++ b/src/components/CreateTaskTemplate/CreateTaskTemplate.tsx @@ -10,15 +10,17 @@ import TransferList from "../TransferList"; import Button from "@mui/material/Button"; import Check from "@mui/icons-material/Check"; import Close from "@mui/icons-material/Close"; -import { useRouter } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; import React from "react"; import Stack from "@mui/material/Stack"; import { Task } from "@/app/api/tasks"; import { NewTaskTemplateFormInputs, + fetchTaskTemplate, saveTaskTemplate, } from "@/app/api/tasks/actions"; -import { SubmitHandler, useForm } from "react-hook-form"; +import { SubmitHandler, useFieldArray, useForm } from "react-hook-form"; +import { errorDialog, submitDialog, successDialog } from "../Swal/CustomAlerts"; interface Props { tasks: Task[]; @@ -27,6 +29,7 @@ interface Props { const CreateTaskTemplate: React.FC = ({ tasks }) => { const { t } = useTranslation(); + const searchParams = useSearchParams() const router = useRouter(); const handleCancel = () => { router.back(); @@ -49,6 +52,7 @@ const CreateTaskTemplate: React.FC = ({ tasks }) => { handleSubmit, setValue, watch, + resetField, formState: { errors, isSubmitting }, } = useForm({ defaultValues: { taskIds: [] } }); @@ -57,12 +61,56 @@ const CreateTaskTemplate: React.FC = ({ tasks }) => { return items.filter((item) => currentTaskIds.includes(item.id)); }, [currentTaskIds, items]); + const [refTaskTemplate, setRefTaskTemplate] = React.useState() + const id = searchParams.get('id') + + const fetchCurrentTaskTemplate = async () => { + try { + const taskTemplate = await fetchTaskTemplate(parseInt(id!!)) + + const defaultValues = { + id: parseInt(id!!), + code: taskTemplate.code ?? null, + name: taskTemplate.name ?? null, + taskIds: taskTemplate.tasks.map(task => task.id) ?? [], + } + + setRefTaskTemplate(defaultValues) + } catch (e) { + console.log(e) + } + } + + React.useLayoutEffect(() => { + if (id !== null && parseInt(id) > 0) fetchCurrentTaskTemplate() + }, [id]) + + React.useEffect(() => { + if (refTaskTemplate) { + setValue("taskIds", refTaskTemplate.taskIds) + resetField("code", { defaultValue: refTaskTemplate.code }) + resetField("name", { defaultValue: refTaskTemplate.name }) + setValue("id", refTaskTemplate.id) + } + }, [refTaskTemplate]) + const onSubmit: SubmitHandler = React.useCallback( async (data) => { try { setServerError(""); - await saveTaskTemplate(data); - router.replace("/tasks"); + submitDialog(async () => { + const response = await saveTaskTemplate(data); + + if (response?.id !== null && response?.id !== undefined && response?.id > 0) { + successDialog(t("Submit Success"), t).then(() => { + router.replace("/tasks"); + }) + } else { + errorDialog(t("Submit Fail"), t).then(() => { + return false + }) + } + }, t) } catch (e) { setServerError(t("An error has occurred. Please try again later.")); } @@ -71,72 +119,77 @@ const CreateTaskTemplate: React.FC = ({ tasks }) => { ); return ( - - - - {t("Task List Setup")} - - - - - - + { + (id === null || refTaskTemplate !== undefined) && + + + {t("Task List Setup")} + + + + + + + + + { + setValue( + "taskIds", + selectedTasks.map((item) => item.id), + ); + }} + allItemsLabel={t("Task Pool")} + selectedItemsLabel={t("Task List Template")} /> - - - { - setValue( - "taskIds", - selectedTasks.map((item) => item.id), - ); - }} - allItemsLabel={t("Task Pool")} - selectedItemsLabel={t("Task List Template")} - /> - - - {serverError && ( - - {serverError} - - )} - - - - - + + + { + serverError && ( + + {serverError} + + ) + } + + + + + } + ); }; diff --git a/src/components/CreateTeam/CreateTeam.tsx b/src/components/CreateTeam/CreateTeam.tsx index 93b585e..64159c0 100644 --- a/src/components/CreateTeam/CreateTeam.tsx +++ b/src/components/CreateTeam/CreateTeam.tsx @@ -89,7 +89,7 @@ const hasErrorsInTab = ( } iconPosition="end" /> - + {serverError && ( diff --git a/src/components/CreateTeam/StaffAllocation.tsx b/src/components/CreateTeam/StaffAllocation.tsx index c51b839..bbd768c 100644 --- a/src/components/CreateTeam/StaffAllocation.tsx +++ b/src/components/CreateTeam/StaffAllocation.tsx @@ -18,9 +18,21 @@ import { StaffResult } from "@/app/api/staff"; import SearchResults, { Column } from "../SearchResults"; import { Clear, PersonAdd, PersonRemove, Search } from "@mui/icons-material"; import { Card } from "reactstrap"; -import { Box, CardContent, Grid, IconButton, InputAdornment, Stack, Tab, Tabs, TabsProps, TextField, Typography } from "@mui/material"; +import { + Box, + CardContent, + Grid, + IconButton, + InputAdornment, + Stack, + Tab, + Tabs, + TabsProps, + TextField, + Typography, +} from "@mui/material"; import { differenceBy } from "lodash"; -import StarsIcon from '@mui/icons-material/Stars'; +import StarsIcon from "@mui/icons-material/Stars"; export interface Props { allStaffs: StaffResult[]; @@ -35,16 +47,15 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { reset, resetField, } = useFormContext(); - + const initialStaffs = staff.map((s) => ({ ...s })); -// console.log(initialStaffs) + // console.log(initialStaffs) const [filteredStaff, setFilteredStaff] = useState(initialStaffs); const [selectedStaff, setSelectedStaff] = useState( initialStaffs.filter((s) => getValues("addStaffIds")?.includes(s.id)) ); - const [seletedTeamLead, setSeletedTeamLead] = useState() - // Adding / Removing staff + // Adding / Removing staff const addStaff = useCallback((staff: StaffResult) => { setSelectedStaff((s) => [...s, staff]); }, []); @@ -53,27 +64,31 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { setSelectedStaff((s) => s.filter((s) => s.id !== staff.id)); }, []); - const setTeamLead = useCallback((staff: StaffResult) => { - - setSeletedTeamLead(staff.id) - const rearrangedList = getValues("addStaffIds").reduce((acc, num, index) => { - if (num === staff.id && index !== 0) { + const setTeamLead = useCallback( + (staff: StaffResult) => { + const rearrangedList = getValues("addStaffIds").reduce( + (acc, num, index) => { + if (num === staff.id && index !== 0) { acc.splice(index, 1); - acc.unshift(num) - } - return acc; - }, getValues("addStaffIds")); - console.log(rearrangedList) - console.log(selectedStaff) - - const rearrangedStaff = rearrangedList.map((id) => { + acc.unshift(num); + } + return acc; + }, + getValues("addStaffIds") + ); + console.log(rearrangedList); + console.log(selectedStaff); + + const rearrangedStaff = rearrangedList.map((id) => { return selectedStaff.find((staff) => staff.id === id); }); - console.log(rearrangedStaff) - setSelectedStaff(rearrangedStaff as StaffResult[]); + console.log(rearrangedStaff); + setSelectedStaff(rearrangedStaff as StaffResult[]); - setValue("addStaffIds", rearrangedList) - }, [addStaff, selectedStaff]); + setValue("addStaffIds", rearrangedList); + }, + [addStaff, selectedStaff] + ); const clearSubsidiary = useCallback(() => { if (defaultValues !== undefined) { @@ -86,7 +101,7 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { // Sync with form useEffect(() => { - console.log(selectedStaff) + console.log(selectedStaff); setValue( "addStaffIds", selectedStaff.map((s) => s.id) @@ -94,7 +109,7 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { }, [selectedStaff, setValue]); useEffect(() => { - console.log(selectedStaff) + console.log(selectedStaff); }, [selectedStaff]); const StaffPoolColumns = useMemo[]>( @@ -107,7 +122,7 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { }, { label: t("Staff Id"), name: "staffId" }, { label: t("Staff Name"), name: "name" }, - { label: t("Current Position"), name: "currentPosition" }, + { label: t("Position"), name: "currentPosition" }, ], [addStaff, t] ); @@ -122,7 +137,7 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { }, { label: t("Staff Id"), name: "staffId" }, { label: t("Staff Name"), name: "name" }, - { label: t("Current Position"), name: "currentPosition" }, + { label: t("Position"), name: "currentPosition" }, { label: t("Team Lead"), name: "action", @@ -144,16 +159,16 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { }, []); React.useEffect(() => { - // setFilteredStaff( - // initialStaffs.filter((s) => { - // const q = query.toLowerCase(); - // // s.staffId.toLowerCase().includes(q) - // // const q = query.toLowerCase(); - // // return s.name.toLowerCase().includes(q); - // // s.code.toString().includes(q) || - // // (s.brNo != null && s.brNo.toLowerCase().includes(q)) - // }) - // ); + setFilteredStaff( + initialStaffs.filter((i) => { + const q = query.toLowerCase(); + return ( + i.staffId.toLowerCase().includes(q) || + i.name.toLowerCase().includes(q) || + i.currentPosition.toLowerCase().includes(q) + ); + }) + ); }, [staff, query]); const resetStaff = React.useCallback(() => { @@ -161,8 +176,7 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { clearSubsidiary(); }, [clearQueryInput, clearSubsidiary]); - const formProps = useForm({ - }); + const formProps = useForm({}); // Tab related const [tabIndex, setTabIndex] = React.useState(0); @@ -170,7 +184,7 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { (_e, newValue) => { setTabIndex(newValue); }, - [], + [] ); return ( @@ -185,48 +199,48 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { {t("staff")} - - - - - - - - ), - }} - /> + + + + + + + + ), + }} + /> + - - - - - - - {tabIndex === 0 && ( - - )} - {tabIndex === 1 && ( - + + - )} - + + + {tabIndex === 0 && ( + + )} + {tabIndex === 1 && ( + + )} + diff --git a/src/components/CustomDatagrid/CustomDatagrid.tsx b/src/components/CustomDatagrid/CustomDatagrid.tsx index 314ba6c..4867623 100644 --- a/src/components/CustomDatagrid/CustomDatagrid.tsx +++ b/src/components/CustomDatagrid/CustomDatagrid.tsx @@ -1,7 +1,7 @@ "use client"; import * as React from "react"; import { Card, CardHeader, CardContent, SxProps, Theme } from "@mui/material"; -import { DataGrid, GridColDef, GridRowSelectionModel } from "@mui/x-data-grid"; +import { DataGrid, GridColDef, GridRowSelectionModel, GridColumnGroupingModel} from "@mui/x-data-grid"; import { darken, lighten, styled } from "@mui/material/styles"; import { useState } from "react"; @@ -19,6 +19,8 @@ interface CustomDatagridProps { newSelectionModel: GridRowSelectionModel, ) => void; selectionModel?: any; + columnGroupingModel?: any; + pageSize?:any; } const CustomDatagrid: React.FC = ({ @@ -32,6 +34,8 @@ const CustomDatagrid: React.FC = ({ checkboxSelection, // Destructure the new prop onRowSelectionModelChange, // Destructure the new prop selectionModel, + columnGroupingModel, + pageSize, ...props }) => { const modifiedColumns = columns.map((column) => { @@ -193,6 +197,8 @@ const CustomDatagrid: React.FC = ({ editMode="row" checkboxSelection={checkboxSelection} onRowSelectionModelChange={onRowSelectionModelChange} + experimentalFeatures={{ columnGrouping: true }} + columnGroupingModel={columnGroupingModel} initialState={{ pagination: { paginationModel: { pageSize: 10 } }, }} @@ -222,6 +228,8 @@ const CustomDatagrid: React.FC = ({ editMode="row" checkboxSelection={checkboxSelection} onRowSelectionModelChange={onRowSelectionModelChange} + experimentalFeatures={{ columnGrouping: true }} + columnGroupingModel={columnGroupingModel} initialState={{ pagination: { paginationModel: { pageSize: 10 } }, }} @@ -251,6 +259,8 @@ const CustomDatagrid: React.FC = ({ editMode="row" checkboxSelection={checkboxSelection} onRowSelectionModelChange={onRowSelectionModelChange} + experimentalFeatures={{ columnGrouping: true }} + columnGroupingModel={columnGroupingModel} style={{ marginRight: 20 }} initialState={{ pagination: { paginationModel: { pageSize: 10 } }, @@ -282,8 +292,10 @@ const CustomDatagrid: React.FC = ({ style={{ marginRight: 0 }} checkboxSelection={checkboxSelection} onRowSelectionModelChange={onRowSelectionModelChange} + experimentalFeatures={{ columnGrouping: true }} + columnGroupingModel={columnGroupingModel} initialState={{ - pagination: { paginationModel: { pageSize: 10 } }, + pagination: { paginationModel: { pageSize: pageSize ?? 10 } }, }} className="customDataGrid" sx={{ @@ -293,7 +305,7 @@ const CustomDatagrid: React.FC = ({ "& .MuiDataGrid-cell:hover": { color: "primary.main", }, - height: 300, + height: dataGridHeight ?? 300, "& .MuiDataGrid-root": { overflow: "auto", }, diff --git a/src/components/CustomInputForm/CustomInputForm.tsx b/src/components/CustomInputForm/CustomInputForm.tsx index 9497208..417c6f1 100644 --- a/src/components/CustomInputForm/CustomInputForm.tsx +++ b/src/components/CustomInputForm/CustomInputForm.tsx @@ -15,6 +15,7 @@ import { Checkbox, FormControlLabel, Button, + Chip, } from "@mui/material"; import { DataGrid, GridColDef, GridRowSelectionModel } from "@mui/x-data-grid"; import { darken, lighten, styled } from "@mui/material/styles"; @@ -31,6 +32,7 @@ import { useCallback, useEffect, useState } from "react"; import { Check, Close, RestartAlt } from "@mui/icons-material"; import { NumericFormat, NumericFormatProps } from "react-number-format"; import * as React from "react"; +import CancelIcon from "@mui/icons-material/Cancel"; interface Options { id: any; @@ -286,7 +288,7 @@ const CustomInputForm: React.FC = ({ ); } else if (field.type === "multiDate") { - console.log(dayjs(field.value)) + // console.log(dayjs(field.value)) return ( @@ -343,8 +345,6 @@ const CustomInputForm: React.FC = ({ id={field.id} value={value} onChange={(event) => { - console.log(event); - console.log(event.target); onChange(event.target.value); const newValue = event.target.value; const selectedOption = field.options?.find( @@ -379,6 +379,68 @@ const CustomInputForm: React.FC = ({ ); + } else if (field.type === "multiSelect-Obj") { + return ( + + + {field.label} + ( + + )} + /> + + + ); } else if (field.type === "numeric") { return ( diff --git a/src/components/CustomerDetail/index.ts b/src/components/CustomerDetail/index.ts deleted file mode 100644 index a8811e6..0000000 --- a/src/components/CustomerDetail/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./CustomerDetailWrapper"; \ No newline at end of file diff --git a/src/components/CustomerDetail/ContactInfo.tsx b/src/components/CustomerSave/ContactInfo.tsx similarity index 100% rename from src/components/CustomerDetail/ContactInfo.tsx rename to src/components/CustomerSave/ContactInfo.tsx diff --git a/src/components/CustomerDetail/CustomerInfo.tsx b/src/components/CustomerSave/CustomerInfo.tsx similarity index 100% rename from src/components/CustomerDetail/CustomerInfo.tsx rename to src/components/CustomerSave/CustomerInfo.tsx diff --git a/src/components/CustomerDetail/CustomerDetail.tsx b/src/components/CustomerSave/CustomerSave.tsx similarity index 99% rename from src/components/CustomerDetail/CustomerDetail.tsx rename to src/components/CustomerSave/CustomerSave.tsx index 88a99ad..fc2469e 100644 --- a/src/components/CustomerDetail/CustomerDetail.tsx +++ b/src/components/CustomerSave/CustomerSave.tsx @@ -42,7 +42,7 @@ const hasErrorsInTab = ( } }; -const CustomerDetail: React.FC = ({ +const CustomerSave: React.FC = ({ subsidiaries, customerTypes, }) => { @@ -277,4 +277,4 @@ const CustomerDetail: React.FC = ({ ); }; -export default CustomerDetail; \ No newline at end of file +export default CustomerSave; \ No newline at end of file diff --git a/src/components/CustomerDetail/CustomerDetailWrapper.tsx b/src/components/CustomerSave/CustomerSaveWrapper.tsx similarity index 73% rename from src/components/CustomerDetail/CustomerDetailWrapper.tsx rename to src/components/CustomerSave/CustomerSaveWrapper.tsx index 0206940..078f50a 100644 --- a/src/components/CustomerDetail/CustomerDetailWrapper.tsx +++ b/src/components/CustomerSave/CustomerSaveWrapper.tsx @@ -3,7 +3,7 @@ // import { fetchProjectCategories } from "@/app/api/projects"; // import { fetchTeamLeads } from "@/app/api/staff"; import { fetchCustomerTypes, fetchAllSubsidiaries } from "@/app/api/customer"; -import CustomerDetail from "./CustomerDetail"; +import CustomerSave from "./CustomerSave"; // type Props = { // params: { @@ -11,7 +11,7 @@ import CustomerDetail from "./CustomerDetail"; // }; // }; -const CustomerDetailWrapper: React.FC = async () => { +const CustomerSaveWrapper: React.FC = async () => { // const { params } = props // console.log(params) const [subsidiaries, customerTypes] = @@ -21,8 +21,8 @@ const CustomerDetailWrapper: React.FC = async () => { ]); return ( - + ); }; -export default CustomerDetailWrapper; +export default CustomerSaveWrapper; diff --git a/src/components/CustomerDetail/SubsidiaryAllocation.tsx b/src/components/CustomerSave/SubsidiaryAllocation.tsx similarity index 100% rename from src/components/CustomerDetail/SubsidiaryAllocation.tsx rename to src/components/CustomerSave/SubsidiaryAllocation.tsx diff --git a/src/components/CustomerSave/index.ts b/src/components/CustomerSave/index.ts new file mode 100644 index 0000000..ea74d25 --- /dev/null +++ b/src/components/CustomerSave/index.ts @@ -0,0 +1 @@ +export { default } from "./CustomerSaveWrapper"; \ No newline at end of file diff --git a/src/components/EditStaff/EditStaff.tsx b/src/components/EditStaff/EditStaff.tsx index bcc7346..0d62e28 100644 --- a/src/components/EditStaff/EditStaff.tsx +++ b/src/components/EditStaff/EditStaff.tsx @@ -15,8 +15,15 @@ import { fetchSkillCombo } from "@/app/api/skill/actions"; import { fetchSalaryCombo } from "@/app/api/salarys/actions"; // import { Field } from "react-hook-form"; -interface dataType { - [key: string]: any; + +interface skill { + id: number; + name: string; + code: string; +} +interface skillObj { + id: number; + skill: skill; } interface Options { @@ -113,7 +120,9 @@ const EditStaff: React.FC = async () => { if (data) setGradeCombo(data.records); }); fetchSkillCombo().then((data) => { - if (data) setSkillCombo(data.records); + if (data) { + }setSkillCombo(data.records); + console.log(data.records) }); fetchSalaryCombo().then((data) => { if (data) setSalaryCombo(data.records); @@ -127,6 +136,10 @@ const EditStaff: React.FC = async () => { console.log(id) fetchStaffEdit(id).then((staff) => { console.log(staff.data); + const skillset = staff.data.skillset + console.log(skillset); + const skillIds = skillset.map((item: skillObj) => item.skill.id); + console.log(skillIds) const data = staff.data; ///////////////////// list 1 ///////////////////// const list1 = keyOrder1 @@ -181,15 +194,17 @@ const EditStaff: React.FC = async () => { label: t(`Grade`), type: "combo-Obj", options: gradeCombo, - value: data[key] !== null ? data[key].id ?? "" : "", + value: data[key]?.id ?? "", }; case "skill": + console.log(skillIds) return { id: `${key}SetId`, label: t(`Skillset`), - type: "combo-Obj", + type: "multiSelect-Obj", options: skillCombo, - value: data[key] !== null ? data[key].id ?? "" : "", + value: skillIds ?? [], + //array problem }; case "currentPosition": return { @@ -206,7 +221,7 @@ const EditStaff: React.FC = async () => { label: t(`Salary Point`), type: "combo-Obj", options: salaryCombo, - value: data[key] !== null ? data[key].id ?? "" : "", + value: data[key]?.id ?? "", required: true, }; // case "hourlyRate": diff --git a/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx b/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx new file mode 100644 index 0000000..7aec1c2 --- /dev/null +++ b/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx @@ -0,0 +1,49 @@ +"use client"; + +import React, { useMemo } from "react"; +import SearchBox, { Criterion } from "../SearchBox"; +import { useTranslation } from "react-i18next"; +import { ProjectResult } from "@/app/api/projects"; +import { EX02ProjectCashFlowReportFilter } from "@/app/api/reports"; +import { fetchEX02ProjectCashFlowReport } from "@/app/api/reports/actions"; +import { downloadFile } from "@/app/utils/commonUtil"; +import { BASE_API_URL } from "@/config/api"; + +interface Props { + projects: ProjectResult[]; +} + +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const GenerateEX02ProjectCashFlowReport: React.FC = ({ projects }) => { + const { t } = useTranslation(); + const projectCombo = projects.map(project => `${project.code} - ${project.name}`) + + const searchCriteria: Criterion[] = useMemo( + () => [ + { label: t("Project"), paramName: "project", type: "select", options: projectCombo, needAll: false}, + ], + [t], + ); + + return ( + <> + { + + if (query.project.length > 0 && query.project.toLocaleLowerCase() !== "all") { + const projectIndex = projectCombo.findIndex(project => project === query.project) + const response = await fetchEX02ProjectCashFlowReport({ projectId: projects[projectIndex].id }) + if (response) { + downloadFile(new Uint8Array(response.blobValue), response.filename!!) + } + } + }} + /> + + ); +}; + +export default GenerateEX02ProjectCashFlowReport; \ No newline at end of file diff --git a/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportLoading.tsx b/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportLoading.tsx new file mode 100644 index 0000000..1792221 --- /dev/null +++ b/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportLoading.tsx @@ -0,0 +1,38 @@ +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Skeleton from "@mui/material/Skeleton"; +import Stack from "@mui/material/Stack"; +import React from "react"; + +// Can make this nicer +export const GenerateEX02ProjectCashFlowReportLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + ); +}; + +export default GenerateEX02ProjectCashFlowReportLoading; \ No newline at end of file diff --git a/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportWrapper.tsx b/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportWrapper.tsx new file mode 100644 index 0000000..5bf1089 --- /dev/null +++ b/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportWrapper.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import GenerateEX02ProjectCashFlowReportLoading from "./GenerateEX02ProjectCashFlowReportLoading"; +import { fetchProjects } from "@/app/api/projects"; +import GenerateEX02ProjectCashFlowReport from "./GenerateEX02ProjectCashFlowReport"; + +interface SubComponents { + Loading: typeof GenerateEX02ProjectCashFlowReportLoading; +} + +const GenerateEX02ProjectCashFlowReportWrapper: React.FC & SubComponents = async () => { + const projects = await fetchProjects(); + + return ; +}; + +GenerateEX02ProjectCashFlowReportWrapper.Loading = GenerateEX02ProjectCashFlowReportLoading; + +export default GenerateEX02ProjectCashFlowReportWrapper; \ No newline at end of file diff --git a/src/components/GenerateEX02ProjectCashFlowReport/index.ts b/src/components/GenerateEX02ProjectCashFlowReport/index.ts new file mode 100644 index 0000000..b547e33 --- /dev/null +++ b/src/components/GenerateEX02ProjectCashFlowReport/index.ts @@ -0,0 +1 @@ +export { default } from "./GenerateEX02ProjectCashFlowReportWrapper"; \ No newline at end of file diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index 45bfda9..cb171fa 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -31,6 +31,9 @@ import { NAVIGATION_CONTENT_WIDTH } from "@/config/uiConfig"; import Logo from "../Logo"; import GroupIcon from '@mui/icons-material/Group'; import BusinessIcon from '@mui/icons-material/Business'; +import ViewWeekIcon from '@mui/icons-material/ViewWeek'; +import ManageAccountsIcon from '@mui/icons-material/ManageAccounts'; +import EmojiEventsIcon from '@mui/icons-material/EmojiEvents'; interface NavigationItem { icon: React.ReactNode; @@ -76,6 +79,11 @@ const navigationItems: NavigationItem[] = [ label: "Staff Utilization", path: "/dashboard/StaffUtilization", }, + { + icon: , + label: "Project Resource Summary", + path: "/dashboard/ProjectResourceSummary", + } ], }, { @@ -109,6 +117,7 @@ const navigationItems: NavigationItem[] = [ {icon: , label:"Project Claims Report", path: "/analytics/ProjectClaimsReport"}, {icon: , label:"Project P&L Report", path: "/analytics/ProjectPLReport"}, {icon: , label:"Financial Status Report", path: "/analytics/FinancialStatusReport"}, + {icon: , label:"EX02 - Project Cash Flow Report", path: "/analytics/EX02ProjectCashFlowReport"}, ], }, { @@ -118,10 +127,12 @@ const navigationItems: NavigationItem[] = [ { icon: , label: "Subsidiary", path: "/settings/subsidiary" }, { icon: , label: "Staff", path: "/settings/staff" }, { icon: , label: "Company", path: "/settings/company" }, + { icon: , label: "Skill", path: "/settings/skill" }, { icon: , label: "Department", path: "/settings/department" }, { icon: , label: "Position", path: "/settings/position" }, { icon: , label: "Salary", path: "/settings/salary" }, { icon: , label: "Team", path: "/settings/team" }, + { icon: , label: "User", path: "/settings/user" }, ], }, ]; diff --git a/src/components/ProgressByClient/ProgressByClient.tsx b/src/components/ProgressByClient/ProgressByClient.tsx index 6815f73..6750392 100644 --- a/src/components/ProgressByClient/ProgressByClient.tsx +++ b/src/components/ProgressByClient/ProgressByClient.tsx @@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next"; import { Card, CardHeader } from "@mui/material"; import CustomSearchForm from "../CustomSearchForm/CustomSearchForm"; import CustomDatagrid from "../CustomDatagrid/CustomDatagrid"; -import ReactApexChart from "react-apexcharts"; +// import ReactApexChart from "react-apexcharts"; import { ApexOptions } from "apexcharts"; import { GridColDef, GridRowSelectionModel } from "@mui/x-data-grid"; import ReportProblemIcon from "@mui/icons-material/ReportProblem"; @@ -18,6 +18,7 @@ import { AnyARecord, AnyCnameRecord } from "dns"; import SearchBox, { Criterion } from "../SearchBox"; import ProgressByClientSearch from "@/components/ProgressByClientSearch"; import { Suspense } from "react"; +const ReactApexChart = dynamic(() => import('react-apexcharts'), { ssr: false }); const ProgressByClient: React.FC = () => { const [activeTab, setActiveTab] = useState("financialSummary"); diff --git a/src/components/ProgressByClientSearch/ProgressByClientSearch.tsx b/src/components/ProgressByClientSearch/ProgressByClientSearch.tsx index 553a306..2062919 100644 --- a/src/components/ProgressByClientSearch/ProgressByClientSearch.tsx +++ b/src/components/ProgressByClientSearch/ProgressByClientSearch.tsx @@ -1,11 +1,13 @@ "use client"; import { ProjectResult } from "@/app/api/projects"; -import React, { useMemo, useState } from "react"; +import React, { useMemo, useState, useCallback } from "react"; import SearchBox, { Criterion } from "../SearchBox"; import { useTranslation } from "react-i18next"; import SearchResults, { Column } from "../SearchResults"; import { ClientProjectResult } from "@/app/api/clientprojects"; +import EditNote from "@mui/icons-material/EditNote"; +import { useRouter, useSearchParams } from "next/navigation"; interface Props { projects: ClientProjectResult[]; @@ -15,7 +17,7 @@ type SearchParamNames = keyof SearchQuery; const ProgressByClientSearch: React.FC = ({ projects }) => { const { t } = useTranslation("projects"); - + const searchParams = useSearchParams() // If project searching is done on the server-side, then no need for this. const [filteredProjects, setFilteredProjects] = useState(projects); @@ -27,15 +29,28 @@ const ProgressByClientSearch: React.FC = ({ projects }) => { [t], ); + const onTaskClick = useCallback((clientProjectResult: ClientProjectResult) => { + const params = new URLSearchParams(searchParams.toString()) + params.set("id", clientProjectResult.id.toString()) + console.log(clientProjectResult) +}, []); + const columns = useMemo[]>( () => [ + { + name: "id", + label: t("Details"), + onClick: onTaskClick, + buttonIcon: , + }, { name: "clientCode", label: t("Client Code") }, { name: "clientName", label: t("Client Name") }, { name: "SubsidiaryClientCode", label: t("Subsidiary Code") }, { name: "SubsidiaryClientName", label: t("Subisdiary") }, { name: "NoOfProjects", label: t("No. of Projects") }, ], - [t], + [onTaskClick, t], + // [t], ); return ( diff --git a/src/components/ProgressByTeam/ProgressByTeam.tsx b/src/components/ProgressByTeam/ProgressByTeam.tsx index bee6f73..164a00d 100644 --- a/src/components/ProgressByTeam/ProgressByTeam.tsx +++ b/src/components/ProgressByTeam/ProgressByTeam.tsx @@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next"; import { Card, CardHeader } from "@mui/material"; import CustomSearchForm from "../CustomSearchForm/CustomSearchForm"; import CustomDatagrid from "../CustomDatagrid/CustomDatagrid"; -import ReactApexChart from "react-apexcharts"; +// import ReactApexChart from "react-apexcharts"; import { ApexOptions } from "apexcharts"; import { GridColDef, GridRowSelectionModel } from "@mui/x-data-grid"; import ReportProblemIcon from "@mui/icons-material/ReportProblem"; @@ -18,6 +18,7 @@ import { AnyARecord, AnyCnameRecord } from "dns"; import SearchBox, { Criterion } from "../SearchBox"; import ProgressByTeamSearch from "@/components/ProgressByTeamSearch"; import { Suspense } from "react"; +const ReactApexChart = dynamic(() => import('react-apexcharts'), { ssr: false }); const ProgressByTeam: React.FC = () => { const [activeTab, setActiveTab] = useState("financialSummary"); diff --git a/src/components/ProjectResourceSummary/ProjectResourceSummary.tsx b/src/components/ProjectResourceSummary/ProjectResourceSummary.tsx new file mode 100644 index 0000000..57d25d2 --- /dev/null +++ b/src/components/ProjectResourceSummary/ProjectResourceSummary.tsx @@ -0,0 +1,548 @@ +"use client"; +import * as React from "react"; +import Grid from "@mui/material/Grid"; +import { useState, useEffect, useMemo } from "react"; +import Paper from "@mui/material/Paper"; +import { TFunction } from "i18next"; +import { useTranslation } from "react-i18next"; +import { Card, CardHeader } from "@mui/material"; +import CustomSearchForm from "../CustomSearchForm/CustomSearchForm"; +import CustomDatagrid from "../CustomDatagrid/CustomDatagrid"; +import ReactApexChart from "react-apexcharts"; +import { ApexOptions } from "apexcharts"; +import { DataGrid, GridColDef, GridRowSelectionModel} from "@mui/x-data-grid"; +import ReportProblemIcon from "@mui/icons-material/ReportProblem"; +import dynamic from "next/dynamic"; +import "../../app/global.css"; +import { AnyARecord, AnyCnameRecord } from "dns"; +import SearchBox, { Criterion } from "../SearchBox"; +import ProgressByClientSearch from "@/components/ProgressByClientSearch"; +import { Suspense } from "react"; +import { getPossibleInstrumentationHookFilenames } from "next/dist/build/utils"; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import Collapse from '@mui/material/Collapse'; +import IconButton from '@mui/material/IconButton'; +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; +import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; + + +const ProjectResourceSummary: React.FC = () => { + const [SearchCriteria, setSearchCriteria] = React.useState({}); + const { t } = useTranslation("dashboard"); + const [selectionModel, setSelectionModel]: any[] = React.useState([]); + const [projectName, setProjectName]:any = React.useState("NA"); + const [projectFee, setProjectFee]:any = React.useState(0); + const [status, setStatus]:any = React.useState("NA"); + const [plannedResources, setPlannedResources]:any = React.useState(0); + const [actualResourcesSpent, setActualResourcesSpent]:any = React.useState(0); + const [remainingResources, setRemainingResources]:any = React.useState(0); + + function createData(stage:any, taskCount:any, g1Planned:any, g1Actual:any, g2Planned:any, g2Actual:any, g3Planned:any, g3Actual:any, g4Planned:any, g4Actual:any, g5Planned:any, g5Actual:any, totalPlanned:any, totalActual:any, task:any) { + return { + stage, + taskCount, + g1Planned, + g1Actual, + g2Planned, + g2Actual, + g3Planned, + g3Actual, + g4Planned, + g4Actual, + g5Planned, + g5Actual, + totalPlanned, + totalActual, + task:task + } + } + + function createTaskData(stage:any, taskCount:any, g1Planned:any, g1Actual:any, g2Planned:any, g2Actual:any, g3Planned:any, g3Actual:any, g4Planned:any, g4Actual:any, g5Planned:any, g5Actual:any, totalPlanned:any, totalActual:any) { + return { + stage, + taskCount, + g1Planned, + g1Actual, + g2Planned, + g2Actual, + g3Planned, + g3Actual, + g4Planned, + g4Actual, + g5Planned, + g5Actual, + totalPlanned, + totalActual, + } + } + + const task1Rows:any = [ + {stage:"1.1 Preparation of preliminary...",taskCount:"-",g1Planned:"-",g1Actual:"172.00",g2Planned:"-", g2Actual:"54.00", g3Planned:"-", g3Actual:"42.00", g4Planned: "-", g4Actual:"12.00", g5Planned:"-", g5Actual:"3.00", totalPlanned:"-", totalActual:"283.00"}, + {stage:"1.2 Cash flow forecast",taskCount:"-",g1Planned:"-",g1Actual:"172.00",g2Planned:"-", g2Actual:"54.00", g3Planned:"-", g3Actual:"42.00", g4Planned: "-", g4Actual:"12.00", g5Planned:"-", g5Actual:"3.00", totalPlanned:"-", totalActual:"283.00"}, + {stage:"1.3 Cost studies for alterative design solutions",taskCount:"-",g1Planned:"-",g1Actual:"115.00",g2Planned:"-", g2Actual:"36.00", g3Planned:"-", g3Actual:"28.00", g4Planned: "-", g4Actual:"7.00", g5Planned:"-", g5Actual:"1.75", totalPlanned:"-", totalActual:"188.00"}, + {stage:"1.4 Attend design co-ordiantion / project",taskCount:"-",g1Planned:"-",g1Actual:"29.00",g2Planned:"-", g2Actual:"9.00", g3Planned:"-", g3Actual:"7.00", g4Planned: "-", g4Actual:"2.00", g5Planned:"-", g5Actual:"1.00", totalPlanned:"-", totalActual:"48.00"}, + {stage:"1.5 Prepare / Review RIC",taskCount:"-",g1Planned:"-",g1Actual:"88.00",g2Planned:"-", g2Actual:"27.00", g3Planned:"-", g3Actual:"21.00", g4Planned: "-", g4Actual:"5.00", g5Planned:"-", g5Actual:"1.00", totalPlanned:"-", totalActual:"141.75"} + ] + + const task2Rows:any = [ + ] + + const task3Rows:any = [ + ] + + const task4Rows:any = [ + ] + + const task5Rows:any = [ + ] + + const task6Rows:any = [ + ] + + const rows = [ + createData("Stage 1 - Design & Cost Planning / Estimating","5","576.00","576.00","192.00", "180.00", "144.00", "140.00", "38.40", "38.00", "9.60", "9.75", "960.00", "943.75",task1Rows), + createData("Stage 2 - Tender Documentation","11", "384.00", "382.00", "128.00", "130.00", "96.00", "79.00", "25.60", "25.00", "6.40", "4.00", "640.00", "620.00",task2Rows), + createData("Stage 3 - Tender Analysis & Report & Contract Documentation","7", "384.00", "300.00", "128.00", "130.00", "96.00", "79.00", "25.60", "25.00", "6.40", "4.00", "640.00", "538.00",task3Rows), + createData("Stage 4 - Construction", "13", "480.00", "400.00", "160.00", "160.00", "120.00", "128.00", "32.00", "25.00", "8.00", "3.00", "800.00", "716.00",task4Rows), + createData("Stage 5 - Miscellaneous", "4", "96.00", "-", "32.00", "-", "24.00", "-0", "6.40", "-", "1.600", "-", "160.00", "-",task5Rows), + createData("","Total", "1920.00", "1658.00", "640.00", "600.00", "480.00", "426.00", "128.00", "113.00", "32.00", "20.75", "3,200.00", "2817.75",task6Rows), + ]; + + // const taskRows = [ + // createTaskData("1.1 Preparation of preliminary...","-","-","172.00","-","54.00","-","42.00","-","12.00","-","3.00","-","283.00"), + // ]; + + function Row(props:any) { + const { row } = props; + const [open, setOpen] = React.useState(false); + + return ( + + *': { borderBottom: 'unset' } }}> + + {row.task.length > 0 && ( + setOpen(!open)} + > + {open ? : } + + )} + + {row.stage} + {row.taskCount} + {row.g1Planned} + {row.g1Actual} + {row.g2Planned} + {row.g2Actual} + {row.g3Planned} + {row.g3Actual} + {row.g4Planned} + {row.g4Actual} + {row.g5Planned} + {row.g5Actual} + {row.totalPlanned} + {row.totalActual} + + {row.task.map((taskRow:any) => ( + <> + + + + + + + + + {taskRow.stage} + + + + + {taskRow.taskCount} + + + + + {taskRow.g1Planned} + + + + + {taskRow.g1Actual} + + + + + {taskRow.g2Planned} + + + + + {taskRow.g2Actual} + + + + + {taskRow.g3Planned} + + + + + {taskRow.g3Actual} + + + + + {taskRow.g4Planned} + + + + + {taskRow.g4Actual} + + + + + {taskRow.g5Planned} + + + + + {taskRow.g5Actual} + + + + + {taskRow.totalPlanned} + + + + + {taskRow.totalActual} + + + + + ))} + {/* + + + + {row.task.map((taskRow:any) => ( + + + {taskRow.stage} + {taskRow.taskCount} + {taskRow.g1Planned} + {taskRow.g1Actual} + {taskRow.g2Planned} + {taskRow.g2Actual} + {taskRow.g3Planned} + {taskRow.g3Actual} + {taskRow.g4Planned} + {taskRow.g4Actual} + {taskRow.g5Planned} + {taskRow.g5Actual} + {taskRow.totalPlanned} + {taskRow.totalActual} + + ))} + + + + */} + + ); + } + + useEffect(() => { + setProjectName("C-1001-001 - Consultancy Project A") + const fee = 2000000 + setProjectFee(fee.toLocaleString()) + setStatus("Within Budget / Overconsumption") + const plannedResourcesInt = 3200 + setPlannedResources(plannedResourcesInt.toLocaleString()) + const actualResourcesSpentInt = 2817.75 + setActualResourcesSpent(actualResourcesSpentInt.toLocaleString()) + const remainingResourcesInt = 382.25 + setRemainingResources(remainingResourcesInt.toLocaleString()) + }, []) + + const projectResourcesRows = [ + {id: 1,stage:"Stage 1 - Design & Cost Planning / Estimating",taskCount:"5",g1Planned:"576.00",g1Actual:"576.00",g2Planned:"192.00", g2Actual:"180.00", g3Planned:"144.00", g3Actual:"140.00", g4Planned: "38.40", g4Actual:"38S.00", g5Planned:"9.60", g5Actual:"9.75", totalPlanned:"960.00", totalActual:"943.75"}, + {id: 2,stage:"1.1 Preparation of preliminary...",taskCount:"-",g1Planned:"-",g1Actual:"172.00",g2Planned:"-", g2Actual:"54.00", g3Planned:"-", g3Actual:"42.00", g4Planned: "-", g4Actual:"12.00", g5Planned:"-", g5Actual:"3.00", totalPlanned:"-", totalActual:"283.00"}, + {id: 3,stage:"1.2 Cash flow forecast",taskCount:"-",g1Planned:"-",g1Actual:"172.00",g2Planned:"-", g2Actual:"54.00", g3Planned:"-", g3Actual:"42.00", g4Planned: "-", g4Actual:"12.00", g5Planned:"-", g5Actual:"3.00", totalPlanned:"-", totalActual:"283.00"}, + {id: 4,stage:"1.3 Cost studies for alterative design solutions",taskCount:"-",g1Planned:"-",g1Actual:"115.00",g2Planned:"-", g2Actual:"36.00", g3Planned:"-", g3Actual:"28.00", g4Planned: "-", g4Actual:"7.00", g5Planned:"-", g5Actual:"1.75", totalPlanned:"-", totalActual:"188.00"}, + {id: 5,stage:"1.4 Attend design co-ordiantion / project",taskCount:"-",g1Planned:"-",g1Actual:"29.00",g2Planned:"-", g2Actual:"9.00", g3Planned:"-", g3Actual:"7.00", g4Planned: "-", g4Actual:"2.00", g5Planned:"-", g5Actual:"1.00", totalPlanned:"-", totalActual:"48.00"}, + {id: 6,stage:"1.5 Prepare / Review RIC",taskCount:"-",g1Planned:"-",g1Actual:"88.00",g2Planned:"-", g2Actual:"27.00", g3Planned:"-", g3Actual:"21.00", g4Planned: "-", g4Actual:"5.00", g5Planned:"-", g5Actual:"1.00", totalPlanned:"-", totalActual:"141.75"}, + {id: 7,stage:"Stage 2 - Tender Documentation",taskCount:"11",g1Planned:"384.00",g1Actual:"382.00",g2Planned:"128.00", g2Actual:"130.00", g3Planned:"96.00", g3Actual:"79.00", g4Planned: "25.60", g4Actual:"25.00", g5Planned:"6.40", g5Actual:"4.00", totalPlanned:"640.00", totalActual:"620.00"}, + {id: 8,stage:"Stage 3 - Tender Analysis & Report & Contract Documentation",taskCount:"7",g1Planned:"384.00",g1Actual:"300.00",g2Planned:"128.00", g2Actual:"130.00", g3Planned:"96.00", g3Actual:"79.00", g4Planned: "25.60", g4Actual:"25.00", g5Planned:"6.40", g5Actual:"4.00", totalPlanned:"640.00", totalActual:"538.00"}, + {id: 9,stage:"Stage 4 - Construction",taskCount:"13",g1Planned:"480.00",g1Actual:"400.00",g2Planned:"160.00", g2Actual:"160.00", g3Planned:"120.00", g3Actual:"128.00", g4Planned: "32.00", g4Actual:"25.00", g5Planned:"8.00", g5Actual:"3.00", totalPlanned:"800.00", totalActual:"716.00"}, + {id: 10,stage:"Stage 5 - Miscellaneous",taskCount:"4",g1Planned:"96.00",g1Actual:"-",g2Planned:"32.00", g2Actual:"-", g3Planned:"24.00", g3Actual:"-0", g4Planned: "6.40", g4Actual:"-", g5Planned:"1.600", g5Actual:"-", totalPlanned:"160.00", totalActual:"-"}, + {id: 11,stage:"",taskCount:"Total",g1Planned:"1920.00",g1Actual:"1658.00",g2Planned:"640.00", g2Actual:"600.00", g3Planned:"480.00", g3Actual:"426.00", g4Planned: "128.00", g4Actual:"113.00", g5Planned:"32.00", g5Actual:"20.75", totalPlanned:"3,200.00", totalActual:"2817.75"}, + ] + +const columns2 = [ + { + id: 'stage', + field: 'stage', + headerName: "Stage", + flex: 2, + }, + { + id: 'taskCount', + field: 'taskCount', + headerName: "Task Count", + flex: 0.5, + }, + { + id: 'g1Planned', + field: 'g1Planned', + headerName: "Planned", + flex: 0.7, + }, + { + id: 'g1Actual', + field: 'g1Actual', + headerName: "Actual", + flex: 0.7, + }, + { + id: 'g2Planned', + field: 'g2Planned', + headerName: "Planned", + flex: 0.7, + }, + { + id: 'g2Actual', + field: 'g2Actual', + headerName: "Actual", + flex: 0.7, + }, + { + id: 'g3Planned', + field: 'g3Planned', + headerName: "Planned", + flex: 0.7, + }, + { + id: 'g3Actual', + field: 'g3Actual', + headerName: "Actual", + flex: 0.7, + }, + { + id: 'g4Planned', + field: 'g4Planned', + headerName: "Planned", + flex: 0.7, + }, + { + id: 'g4Actual', + field: 'g4Actual', + headerName: "Actual", + flex: 0.7, + }, + { + id: 'g5Planned', + field: 'g5Planned', + headerName: "Planned", + flex: 0.7, + }, + { + id: 'g5Actual', + field: 'g5Actual', + headerName: "Actual", + flex: 0.7, + }, + { + id: 'totalPlanned', + field: 'totalPlanned', + headerName: "Planned", + flex: 0.7, + }, + { + id: 'totalActual', + field: 'totalActual', + headerName: "Actual", + flex: 0.7, + }, +]; + + const columnGroupingModel = [ + { + groupId: 'G1', + children: [{ field: 'g1Planned' },{ field: 'g1Actual' }], + headerClassName: 'groupColor', + }, + { + groupId: 'G2', + children: [{ field: 'g2Planned' },{ field: 'g2Actual' }], + headerClassName: 'groupColor', + }, + { + groupId: 'G3', + children: [{ field: 'g3Planned' },{ field: 'g3Actual' }], + headerClassName: 'groupColor', + }, + { + groupId: 'G4', + children: [{ field: 'g4Planned' },{ field: 'g4Actual' }], + headerClassName: 'groupColor', + }, + { + groupId: 'G5', + children: [{ field: 'g5Planned' },{ field: 'g5Actual' }], + headerClassName: 'groupColor', + }, + { + groupId: 'Total', + children: [{ field: 'totalPlanned' },{ field: 'totalActual' }], + headerClassName: 'totalGroupColor', + }, + ]; + + return ( + + + +
+
+
+ + Project + +
+
+ {projectName} +
+
+
+
+ + Project Fee + +
+
+ HKD {projectFee} +
+
+
+
+ + Status + +
+
+ {status} +
+
+
+
+ + Planned Resources + +
+
+ {plannedResources} Manhours +
+
+
+
+ + Actual Resources Spent + +
+
+ {actualResourcesSpent} Manhours +
+
+
+
+ + Remaining Resources + +
+
+ {remainingResources} Manhours +
+
+
+ {/*
+ +
*/} +
+ + + + + + + + + G1 + + + G2 + + + G3 + + + G4 + + + G5 + + + Total + + + + + Stage + Task Count + Planned + Actual + Planned + Actual + Planned + Actual + Planned + Actual + Planned + Actual + Planned + Actual + + + + {rows.map((row) => ( + + ))} + +
+
+
+
+
+ ); +}; + +export default ProjectResourceSummary; diff --git a/src/components/ProjectResourceSummary/index.ts b/src/components/ProjectResourceSummary/index.ts new file mode 100644 index 0000000..056f6f9 --- /dev/null +++ b/src/components/ProjectResourceSummary/index.ts @@ -0,0 +1 @@ +export { default } from "./ProjectResourceSummary"; diff --git a/src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearch.tsx b/src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearch.tsx new file mode 100644 index 0000000..d8efb61 --- /dev/null +++ b/src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearch.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { ProjectResult } from "@/app/api/projects"; +import React, { useMemo, useState, useCallback } from "react"; +import SearchBox, { Criterion } from "../SearchBox"; +import { useTranslation } from "react-i18next"; +import SearchResults, { Column } from "../SearchResults"; +import { ResourceSummaryResult } from "@/app/api/resourcesummary"; +import EditNote from "@mui/icons-material/EditNote"; +import { useRouter, useSearchParams } from "next/navigation"; +import ProjectResourceSummary from "@/components/ProjectResourceSummary"; +import ArticleIcon from '@mui/icons-material/Article'; + +interface Props { + projects: ResourceSummaryResult[]; +} +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + + +const ProjectResourceSummarySearch: React.FC = ({ projects }) => { + const { t } = useTranslation("projects"); + const searchParams = useSearchParams() + // If project searching is done on the server-side, then no need for this. + const [filteredProjects, setFilteredProjects] = useState(projects); + + const searchCriteria: Criterion[] = useMemo( + () => [ + { label: "Project Code", paramName: "projectCode", type: "text" }, + { label: "Project Name", paramName: "projectName", type: "text" }, + { label: "Client Code", paramName: "clientCode", type: "text" }, + { label: "Client Name", paramName: "clientName", type: "text" }, + ], + [t], + ); + + const onTaskClick = useCallback((resourceSummaryResult: ResourceSummaryResult) => { + console.log(resourceSummaryResult) + }, []); + + + const columns = useMemo[]>( + () => [ + { + name: "id", + label: t("View"), + onClick: onTaskClick, + buttonIcon: , + }, + { name: "projectCode", label: t("Project Code") }, + { name: "projectName", label: t("Project Name") }, + { name: "clientCodeAndName", label: t("Client Code And Name") }, + ], + [onTaskClick, t], + // [t], + ); + + return ( + <> + { + console.log(query); + }} + /> + + items={filteredProjects} + columns={columns} + /> + + + ); +}; + +export default ProjectResourceSummarySearch; diff --git a/src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearchLoading.tsx b/src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearchLoading.tsx new file mode 100644 index 0000000..b6d4bc1 --- /dev/null +++ b/src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearchLoading.tsx @@ -0,0 +1,40 @@ +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Skeleton from "@mui/material/Skeleton"; +import Stack from "@mui/material/Stack"; +import React from "react"; + +// Can make this nicer +export const ProjectResourceSummarySearchLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default ProjectResourceSummarySearchLoading; diff --git a/src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearchWrapper.tsx b/src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearchWrapper.tsx new file mode 100644 index 0000000..068debc --- /dev/null +++ b/src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearchWrapper.tsx @@ -0,0 +1,20 @@ +import { fetchResourceSummary } from "@/app/api/resourcesummary"; +import React from "react"; +import ProjectResourceSummarySearch from "./ProjectResourceSummarySearch"; +import ProjectResourceSummarySearchLoading from "./ProjectResourceSummarySearchLoading"; + +interface SubComponents { + Loading: typeof ProjectResourceSummarySearchLoading; +} + +const ProjectResourceSummarySearchWrapper: React.FC & SubComponents = async () => { + const clentprojects = await fetchResourceSummary(); + + return ; +}; + +ProjectResourceSummarySearchWrapper.Loading = ProjectResourceSummarySearchLoading; + +export default ProjectResourceSummarySearchWrapper; + + diff --git a/src/components/ProjectResourceSummarySearch/index.ts b/src/components/ProjectResourceSummarySearch/index.ts new file mode 100644 index 0000000..98ec034 --- /dev/null +++ b/src/components/ProjectResourceSummarySearch/index.ts @@ -0,0 +1 @@ +export { default } from "./ProjectResourceSummarySearchWrapper"; diff --git a/src/components/SearchBox/SearchBox.tsx b/src/components/SearchBox/SearchBox.tsx index 26914fe..5ea1690 100644 --- a/src/components/SearchBox/SearchBox.tsx +++ b/src/components/SearchBox/SearchBox.tsx @@ -36,6 +36,7 @@ interface TextCriterion extends BaseCriterion { interface SelectCriterion extends BaseCriterion { type: "select"; options: string[]; + needAll?: boolean; } interface DateRangeCriterion extends BaseCriterion { @@ -134,7 +135,7 @@ function SearchBox({ onChange={makeSelectChangeHandler(c.paramName)} value={inputs[c.paramName]} > - {t("All")} + {!(c.needAll === false) && {t("All")}} {c.options.map((option, index) => ( {t(option)} diff --git a/src/components/SkillSearch/SkillSearch.tsx b/src/components/SkillSearch/SkillSearch.tsx new file mode 100644 index 0000000..01db336 --- /dev/null +++ b/src/components/SkillSearch/SkillSearch.tsx @@ -0,0 +1,96 @@ +"use client"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import SearchBox, { Criterion } from "../SearchBox/index"; +import { useTranslation } from "react-i18next"; +import SearchResults, { Column } from "../SearchResults/index"; +import EditNote from "@mui/icons-material/EditNote"; +import DeleteIcon from "@mui/icons-material/Delete"; +import { useRouter } from "next/navigation"; +import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; +import { SkillResult } from "@/app/api/skill"; + +interface Props { + skill: SkillResult[]; +} + +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const SkillSearch: React.FC = ({ skill }) => { + const { t } = useTranslation(); + const [filteredStaff, setFilteredStaff] = useState(skill); + const router = useRouter(); + + const searchCriteria: Criterion[] = useMemo( + () => [ + { + label: t("Staff Name"), + paramName: "name", + type: "text", + }, + ], + [t] + ); + + const onSkillClick = useCallback( + (skill: SkillResult) => { + console.log(skill); + const id = skill.id; + // router.push(`/settings/skill/edit?id=${id}`); + }, + [router, t] + ); + + const deleteClick = useCallback((skill: SkillResult) => { + // deleteDialog(async () => { + // await deleteStaff(skill.id); + // successDialog("Delete Success", t); + // setFilteredStaff((prev) => prev.filter((obj) => obj.id !== skill.id)); + // }, t); + }, []); + + const columns = useMemo[]>( + () => [ + { + name: "action", + label: t("Actions"), + onClick: onSkillClick, + buttonIcon: , + }, + { name: "name", label: t("Name") }, + { name: "code", label: t("Code") }, + { name: "description", label: t("Description") }, + { + name: "action", + label: t("Actions"), + onClick: deleteClick, + buttonIcon: , + color: "error", + }, + ], + [t, onSkillClick, deleteClick] + ); + + return ( + <> + { + // setFilteredStaff( + // skill.filter( + // (s) => + // s.skillId.toLowerCase().includes(query.skillId.toLowerCase()) && + // s.name.toLowerCase().includes(query.name.toLowerCase()) + // // (query.team === "All" || s.team === query.team) && + // // (query.category === "All" || s.category === query.category) && + // // (query.team === "All" || s.team === query.team), + // ) + // ); + }} + /> + items={filteredStaff} columns={columns} /> + + ); +}; + +export default SkillSearch; diff --git a/src/components/SkillSearch/SkillSearchLoading.tsx b/src/components/SkillSearch/SkillSearchLoading.tsx new file mode 100644 index 0000000..a5959e9 --- /dev/null +++ b/src/components/SkillSearch/SkillSearchLoading.tsx @@ -0,0 +1,40 @@ +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Skeleton from "@mui/material/Skeleton"; +import Stack from "@mui/material/Stack"; +import React from "react"; + +// Can make this nicer +export const SkillSearchLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default SkillSearchLoading; diff --git a/src/components/SkillSearch/SkillSearchWrapper.tsx b/src/components/SkillSearch/SkillSearchWrapper.tsx new file mode 100644 index 0000000..33d0547 --- /dev/null +++ b/src/components/SkillSearch/SkillSearchWrapper.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import SkillSearch from "./SkillSearch"; +import SkillSearchLoading from "./SkillSearchLoading"; +import { comboProp, fetchCompanyCombo } from "@/app/api/companys/actions"; +import { fetchTeamCombo } from "@/app/api/team/actions"; +import { fetchDepartmentCombo } from "@/app/api/departments/actions"; +import { fetchPositionCombo } from "@/app/api/positions/actions"; +import { fetchGradeCombo } from "@/app/api/grades/actions"; +import { fetchSkillCombo } from "@/app/api/skill/actions"; +import { fetchSalaryCombo } from "@/app/api/salarys/actions"; +import { SkillResult, fetchSkill } from "@/app/api/skill"; +// import { preloadStaff } from "@/app/api/staff"; + +interface SubComponents { + Loading: typeof SkillSearchLoading; +} + +const SkillSearchWrapper: React.FC & SubComponents = async () => { + const skill = await fetchSkill() + console.log(skill); + + return ; +}; + +SkillSearchWrapper.Loading = SkillSearchLoading; + +export default SkillSearchWrapper; diff --git a/src/components/SkillSearch/index.ts b/src/components/SkillSearch/index.ts new file mode 100644 index 0000000..5833a58 --- /dev/null +++ b/src/components/SkillSearch/index.ts @@ -0,0 +1 @@ +export { default } from "./SkillSearchWrapper"; diff --git a/src/components/StaffSearch/ConfirmDeleteModal.tsx b/src/components/StaffSearch/ConfirmDeleteModal.tsx deleted file mode 100644 index abeb962..0000000 --- a/src/components/StaffSearch/ConfirmDeleteModal.tsx +++ /dev/null @@ -1,106 +0,0 @@ -"use client"; -import React, { useCallback, useMemo, useState } from "react"; -import Button from "@mui/material/Button"; -import { Card, Modal, Stack, Typography } from "@mui/material"; -import { useTranslation } from "react-i18next"; -import { Add } from "@mui/icons-material"; -import Check from "@mui/icons-material/Check"; -import Close from "@mui/icons-material/Close"; -import { TSMS_BUTTON_THEME } from "@/theme/colorConst"; -import { ThemeProvider } from "@emotion/react"; - -interface Props { - isOpen: boolean; - onConfirm: (data: any) => void; - onCancel: (data: any | null) => void; - // staff: StaffResult[]; -} - -const ConfirmModal: React.FC = ({ ...props }) => { - const { t } = useTranslation(); - return ( - <> - - - <> - - {t("Confirm")} - - <> - - {t("Are You Sure")} - - - {/* */} - - - - - {/* */} - - - - - ); -}; - -export default ConfirmModal; diff --git a/src/components/StaffSearch/StaffSearch.tsx b/src/components/StaffSearch/StaffSearch.tsx index e65cfe7..4111d14 100644 --- a/src/components/StaffSearch/StaffSearch.tsx +++ b/src/components/StaffSearch/StaffSearch.tsx @@ -5,15 +5,11 @@ import SearchBox, { Criterion } from "../SearchBox/index"; import { useTranslation } from "react-i18next"; import SearchResults, { Column } from "../SearchResults/index"; import EditNote from "@mui/icons-material/EditNote"; -import DeleteIcon from '@mui/icons-material/Delete'; -import ConfirmModal from "./ConfirmDeleteModal"; +import DeleteIcon from "@mui/icons-material/Delete"; import { deleteStaff } from "@/app/api/staff/actions"; import { useRouter } from "next/navigation"; +import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; -interface combo { - id: any; - label: string; -} interface Props { staff: StaffResult[]; } @@ -24,8 +20,6 @@ type SearchParamNames = keyof SearchQuery; const StaffSearch: React.FC = ({ staff }) => { const { t } = useTranslation(); const [filteredStaff, setFilteredStaff] = useState(staff); - const [data, setData] = useState(); - const [isOpen, setIsOpen] = useState(false); const router = useRouter(); const searchCriteria: Criterion[] = useMemo( @@ -41,10 +35,10 @@ const StaffSearch: React.FC = ({ staff }) => { paramName: "name", type: "text", }, - { - label: t("Staff ID"), - paramName: "staffId", - type: "text" + { + label: t("Staff ID"), + paramName: "staffId", + type: "text", }, { label: t("Grade"), @@ -59,39 +53,26 @@ const StaffSearch: React.FC = ({ staff }) => { options: ["pos1", "CEO"], }, ], - [t], + [t] ); - const onStaffClick = useCallback((staff: StaffResult) => { - console.log(staff); - const id = staff.id - router.push(`/settings/staff/edit?id=${id}`); - }, [router, t]); - - const deleteClick = (staff: StaffResult) => { - console.log(staff); - setData(staff) - setIsOpen(!isOpen) - }; - - const onConfirm = useCallback(async (staff: StaffResult) => { - console.log(staff); - if (data) - await deleteStaff(data) - setIsOpen(false) - window.location.reload; - }, [deleteStaff, data]); + const onStaffClick = useCallback( + (staff: StaffResult) => { + console.log(staff); + const id = staff.id; + router.push(`/settings/staff/edit?id=${id}`); + }, + [router, t] + ); - const onCancel = useCallback((staff: StaffResult) => { - console.log(staff); - setIsOpen(false) + const deleteClick = useCallback((staff: StaffResult) => { + deleteDialog(async () => { + await deleteStaff(staff.id); + successDialog(t("Delete Success"), t); + setFilteredStaff((prev) => prev.filter((obj) => obj.id !== staff.id)); + }, t); }, []); - // useEffect(() => { - // console.log("id"); - // console.log(id); - // }, [id]); - const columns = useMemo[]>( () => [ { @@ -110,34 +91,30 @@ const StaffSearch: React.FC = ({ staff }) => { label: t("Actions"), onClick: deleteClick, buttonIcon: , + color: "error", }, ], - [t, onStaffClick, deleteClick], + [t, onStaffClick, deleteClick] ); return ( <> { + onSearch={(query) => { setFilteredStaff( staff.filter( - (s) => - s.staffId.toLowerCase().includes(query.staffId.toLowerCase()) && - s.name.toLowerCase().includes(query.name.toLowerCase()) + (s) => + s.staffId.toLowerCase().includes(query.staffId.toLowerCase()) && + s.name.toLowerCase().includes(query.name.toLowerCase()) // (query.team === "All" || s.team === query.team) && // (query.category === "All" || s.category === query.category) && // (query.team === "All" || s.team === query.team), ) - ) + ); }} /> items={filteredStaff} columns={columns} /> - ); }; diff --git a/src/components/SubsidiaryDetail/SubsidiaryDetailWrapper.tsx b/src/components/SubsidiaryDetail/SubsidiaryDetailWrapper.tsx index 1a9ced0..c335042 100644 --- a/src/components/SubsidiaryDetail/SubsidiaryDetailWrapper.tsx +++ b/src/components/SubsidiaryDetail/SubsidiaryDetailWrapper.tsx @@ -1,7 +1,7 @@ import { fetchAllCustomers, fetchSubsidiaryTypes } from "@/app/api/subsidiary"; import SubsidiaryDetail from "./SubsidiaryDetail"; -const CustomerDetailWrapper: React.FC = async () => { +const CustomerSaveWrapper: React.FC = async () => { const [customers, subsidiaryTypes] = await Promise.all([ fetchAllCustomers(), @@ -13,4 +13,4 @@ const CustomerDetailWrapper: React.FC = async () => { ); }; -export default CustomerDetailWrapper; +export default CustomerSaveWrapper; diff --git a/src/components/SubsidiarySearch/SubsidiarySearch.tsx b/src/components/SubsidiarySearch/SubsidiarySearch.tsx index 95c901f..c4e1db5 100644 --- a/src/components/SubsidiarySearch/SubsidiarySearch.tsx +++ b/src/components/SubsidiarySearch/SubsidiarySearch.tsx @@ -46,7 +46,7 @@ const SubsidiarySearch: React.FC = ({ subsidiaries }) => { deleteDialog(async() => { await deleteSubsidiary(subsidiary.id) - successDialog("Delete Success", t) + successDialog(t("Delete Success"), t) setFilteredSubsidiaries((prev) => prev.filter((obj) => obj.id !== subsidiary.id)) }, t) diff --git a/src/components/TaskTemplateSearch/TaskTemplateSearch.tsx b/src/components/TaskTemplateSearch/TaskTemplateSearch.tsx index 72be3c1..7563f53 100644 --- a/src/components/TaskTemplateSearch/TaskTemplateSearch.tsx +++ b/src/components/TaskTemplateSearch/TaskTemplateSearch.tsx @@ -6,6 +6,10 @@ import SearchBox, { Criterion } from "../SearchBox"; import { useTranslation } from "react-i18next"; import SearchResults, { Column } from "../SearchResults"; import EditNote from "@mui/icons-material/EditNote"; +import { useRouter, useSearchParams } from "next/navigation"; +import DeleteIcon from '@mui/icons-material/Delete'; +import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; +import { deleteTaskTemplate } from "@/app/api/tasks/actions"; interface Props { taskTemplates: TaskTemplate[]; @@ -16,6 +20,8 @@ type SearchParamNames = keyof SearchQuery; const TaskTemplateSearch: React.FC = ({ taskTemplates }) => { const { t } = useTranslation("tasks"); + const searchParams = useSearchParams() + const router = useRouter() const [filteredTemplates, setFilteredTemplates] = useState(taskTemplates); const searchCriteria: Criterion[] = useMemo( @@ -30,7 +36,20 @@ const TaskTemplateSearch: React.FC = ({ taskTemplates }) => { }, [taskTemplates]); const onTaskClick = useCallback((taskTemplate: TaskTemplate) => { - console.log(taskTemplate); + const params = new URLSearchParams(searchParams.toString()) + params.set("id", taskTemplate.id.toString()) + router.replace(`/tasks/edit?${params.toString()}`); + }, []); + + const onDeleteClick = useCallback((taskTemplate: TaskTemplate) => { + + deleteDialog(async () => { + await deleteTaskTemplate(taskTemplate.id) + + successDialog(t("Delete Success"), t) + + setFilteredTemplates((prev) => prev.filter((obj) => obj.id !== taskTemplate.id)) + }, t) }, []); const columns = useMemo[]>( @@ -43,6 +62,13 @@ const TaskTemplateSearch: React.FC = ({ taskTemplates }) => { }, { name: "code", label: t("Task Template Code") }, { name: "name", label: t("Task Template Name") }, + { + name: "id", + label: t("Delete"), + onClick: onDeleteClick, + buttonIcon: , + color: "error" + }, ], [onTaskClick, t], ); diff --git a/src/components/TeamSearch/ConfirmDeleteModal.tsx b/src/components/TeamSearch/ConfirmDeleteModal.tsx deleted file mode 100644 index a5e7ed0..0000000 --- a/src/components/TeamSearch/ConfirmDeleteModal.tsx +++ /dev/null @@ -1,105 +0,0 @@ -"use client"; -import React, { useCallback, useMemo, useState } from "react"; -import Button from "@mui/material/Button"; -import { Card, Modal, Stack, Typography } from "@mui/material"; -import { useTranslation } from "react-i18next"; -import { Add } from "@mui/icons-material"; -import Check from "@mui/icons-material/Check"; -import Close from "@mui/icons-material/Close"; -import { TSMS_BUTTON_THEME } from "@/theme/colorConst"; -import { ThemeProvider } from "@emotion/react"; - -interface Props { - isOpen: boolean; - onConfirm: (data: any) => void; - onCancel: (data: any | null) => void; -} - -const ConfirmModal: React.FC = ({ ...props }) => { - const { t } = useTranslation(); - return ( - <> - - - <> - - {t("Confirm")} - - <> - - {t("Are You Sure")} - - - {/* */} - - - - - {/* */} - - - - - ); -}; - -export default ConfirmModal; diff --git a/src/components/TeamSearch/TeamSearch.tsx b/src/components/TeamSearch/TeamSearch.tsx index b2cc9e8..71ecb79 100644 --- a/src/components/TeamSearch/TeamSearch.tsx +++ b/src/components/TeamSearch/TeamSearch.tsx @@ -6,12 +6,10 @@ import { useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import SearchResults, { Column } from "../SearchResults/index"; import EditNote from "@mui/icons-material/EditNote"; -import DeleteIcon from '@mui/icons-material/Delete'; -import { deleteStaff } from "@/app/api/staff/actions"; +import DeleteIcon from "@mui/icons-material/Delete"; import { useRouter } from "next/navigation"; -import ConfirmModal from "./ConfirmDeleteModal"; import { deleteTeam } from "@/app/api/team/actions"; - +import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; interface Props { team: TeamResult[]; @@ -20,109 +18,90 @@ type SearchQuery = Partial>; type SearchParamNames = keyof SearchQuery; const TeamSearch: React.FC = ({ team }) => { - const { t } = useTranslation(); - const [filteredTeam, setFilteredTeam] = useState(team); - const [data, setData] = useState(); - const [isOpen, setIsOpen] = useState(false); - const router = useRouter(); - - const searchCriteria: Criterion[] = useMemo( - () => [ - { - label: t("Team Name"), - paramName: "name", - type: "text", - }, - { - label: t("Team Code"), - paramName: "code", - type: "text", - }, - { - label: t("Team Description"), - paramName: "description", - type: "text", - }, - ], - [t], - ); + const { t } = useTranslation(); + const [filteredTeam, setFilteredTeam] = useState(team); + const router = useRouter(); - const onTeamClick = useCallback((team: TeamResult) => { - console.log(team); - const id = team.id - router.push(`/settings/team/edit?id=${id}`); - }, [router, t]); - - // const onDeleteClick = useCallback((team: TeamResult) => { - // console.log(team); - // deleteTeam + const searchCriteria: Criterion[] = useMemo( + () => [ + { + label: t("Team Name"), + paramName: "name", + type: "text", + }, + { + label: t("Team Code"), + paramName: "code", + type: "text", + }, + { + label: t("Team Description"), + paramName: "description", + type: "text", + }, + ], + [t] + ); - // }, [router, t]); + const onTeamClick = useCallback( + (team: TeamResult) => { + console.log(team); + const id = team.id; + router.push(`/settings/team/edit?id=${id}`); + }, + [router, t] + ); - const onDeleteClick = (team: TeamResult) => { - console.log(team); - setData(team) - setIsOpen(!isOpen) - }; + const onDeleteClick = useCallback((team: TeamResult) => { + deleteDialog(async () => { + await deleteTeam(team.id); - const onConfirm = useCallback(async (team: TeamResult) => { - console.log(team); - if (data) - await deleteTeam(data) - setIsOpen(false) - window.location.reload; - }, [deleteTeam, data]); + successDialog(t("Delete Success"), t); - const onCancel = useCallback(() => { - setIsOpen(false) - }, []); + setFilteredTeam((prev) => prev.filter((obj) => obj.id !== team.id)); + }, t); + }, []); - const columns = useMemo[]>( - () => [ - { - name: "action", - label: t("Edit"), - onClick: onTeamClick, - buttonIcon: , - }, - { name: "name", label: t("Name") }, - { name: "code", label: t("Code") }, - { name: "description", label: t("description") }, - { - name: "action", - label: t("Delete"), - onClick: onDeleteClick, - buttonIcon: , - }, - ], - [t], - ); + const columns = useMemo[]>( + () => [ + { + name: "action", + label: t("Edit"), + onClick: onTeamClick, + buttonIcon: , + }, + { name: "name", label: t("Name") }, + { name: "code", label: t("Code") }, + { name: "description", label: t("description") }, + { name: "staffName", label: t("TeamLead") }, + { + name: "action", + label: t("Delete"), + onClick: onDeleteClick, + buttonIcon: , + color: "error" + }, + ], + [t] + ); return ( - <> - + { - // setFilteredStaff( - // staff.filter( - // (s) => - // s.staffId.toLowerCase().includes(query.staffId.toLowerCase()) && - // s.name.toLowerCase().includes(query.name.toLowerCase()) - // // (query.team === "All" || s.team === query.team) && - // // (query.category === "All" || s.category === query.category) && - // // (query.team === "All" || s.team === query.team), - // ) - // ) + onSearch={(query) => { + setFilteredTeam( + team.filter( + (t) => + t.name.toLowerCase().includes(query.name.toLowerCase()) && + t.code.toLowerCase().includes(query.code.toLowerCase()) && + t.description.toLowerCase().includes(query.description.toLowerCase()) + ) + ) }} /> items={filteredTeam} columns={columns} /> - - - + ); }; export default TeamSearch; diff --git a/src/components/TimesheetModal/TimesheetModal.tsx b/src/components/TimesheetModal/TimesheetModal.tsx index 055e0d9..d6146b0 100644 --- a/src/components/TimesheetModal/TimesheetModal.tsx +++ b/src/components/TimesheetModal/TimesheetModal.tsx @@ -16,11 +16,13 @@ import { FormProvider, useForm } from "react-hook-form"; import { RecordTimesheetInput } from "@/app/api/timesheets/actions"; import dayjs from "dayjs"; import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; +import { AssignedProject } from "@/app/api/projects"; interface Props { isOpen: boolean; onClose: () => void; timesheetType: "time" | "leave"; + assignedProjects: AssignedProject[]; } const modalSx: SxProps = { @@ -37,6 +39,7 @@ const TimesheetModal: React.FC = ({ isOpen, onClose, timesheetType, + assignedProjects, }) => { const { t } = useTranslation("home"); @@ -73,7 +76,7 @@ const TimesheetModal: React.FC = ({ marginBlock: 4, }} > - + - - + {Boolean(assignedProjects.length) && ( + + + + + )} - - - + {assignedProjects.length > 0 ? ( + <> + + + + + ) : ( + <> + + {t("You have no assigned projects!")} + + + )} ); }; diff --git a/src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx b/src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx index 42b3756..c311488 100644 --- a/src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx +++ b/src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx @@ -1,65 +1,9 @@ +import { fetchAssignedProjects } from "@/app/api/projects"; import UserWorkspacePage from "./UserWorkspacePage"; -export interface ProjectHours { - code: string; - name: string; - hoursSpent: number; - hoursSpentOther: number; - hoursAllocated: number; - hoursAllocatedOther: number; - projectStatus: "On Track" | "Potential Delay"; -} - -const mockProjectCards: ProjectHours[] = [ - { - code: "M1001 (C)", - name: "Consultancy Project A", - hoursSpent: 12.75, - hoursSpentOther: 0.0, - hoursAllocated: 150.0, - hoursAllocatedOther: 30.0, - projectStatus: "On Track", - }, - { - code: "M1301 (C)", - name: "Consultancy Project AAA", - hoursSpent: 4.25, - hoursSpentOther: 0.25, - hoursAllocated: 30.0, - hoursAllocatedOther: 0.0, - projectStatus: "On Track", - }, - { - code: "M1354 (C)", - name: "Consultancy Project BBB", - hoursSpent: 57.0, - hoursSpentOther: 6.5, - hoursAllocated: 100.0, - hoursAllocatedOther: 20.0, - projectStatus: "On Track", - }, - { - code: "M1973 (C)", - name: "Construction Project CCC", - hoursSpent: 12.75, - hoursSpentOther: 0.0, - hoursAllocated: 150.0, - hoursAllocatedOther: 30.0, - projectStatus: "Potential Delay", - }, - { - code: "M2014 (T)", - name: "Consultancy Project DDD", - hoursSpent: 1.0, - hoursSpentOther: 0.0, - hoursAllocated: 10.0, - hoursAllocatedOther: 0.0, - projectStatus: "Potential Delay", - }, -]; - -const UserWorkspaceWrapper: React.FC = () => { - return ; +const UserWorkspaceWrapper: React.FC = async () => { + const assignedProjects = await fetchAssignedProjects(); + return ; }; export default UserWorkspaceWrapper; diff --git a/src/i18n/en/claim.json b/src/i18n/en/claim.json index 51f657c..b5b5e42 100644 --- a/src/i18n/en/claim.json +++ b/src/i18n/en/claim.json @@ -31,6 +31,7 @@ "Please ensure the projects are selected": "Please ensure the projects are selected", "Please ensure the amount are correct": "Please ensure the amount are correct", + "Details": "Details", "Description": "Description", "Actions": "Actions" } \ No newline at end of file diff --git a/src/i18n/en/common.json b/src/i18n/en/common.json index 5f1d289..a7d019a 100644 --- a/src/i18n/en/common.json +++ b/src/i18n/en/common.json @@ -17,6 +17,8 @@ "Do you want to delete?": "Do you want to delete", "Delete Success": "Delete Success", + "Details": "Details", + "Delete": "Delete", "Search": "Search", "Search Criteria": "Search Criteria", "Cancel": "Cancel", diff --git a/src/i18n/en/report.json b/src/i18n/en/report.json new file mode 100644 index 0000000..e7e61fb --- /dev/null +++ b/src/i18n/en/report.json @@ -0,0 +1,3 @@ +{ + "Project": "Project" +} \ No newline at end of file diff --git a/src/i18n/en/tasks.json b/src/i18n/en/tasks.json new file mode 100644 index 0000000..d70d00a --- /dev/null +++ b/src/i18n/en/tasks.json @@ -0,0 +1,27 @@ +{ + "Task Template": "Task Template", + "Create Task Template": "Create Task Template", + "Edit Task Template": "Edit Task Template", + + "Task Template Code": "Task Template Code", + "Task Template Name": "Task Template Name", + "Task List Setup": "Task List Setup", + "Task Pool": "Task Pool", + "Task List Template": "Task List Template", + + "Task template code is required": "Task template code is required", + "Task template name is required": "Task template name is required", + + "Do you want to submit?": "Do you want to submit?", + "Submit Success": "Submit Success", + "Submit Fail": "Submit Fail", + "Do you want to delete?": "Do you want to delete?", + "Delete Success": "Delete Success", + + "selected": "selected", + "Details": "Details", + "Delete": "Delete", + "Cancel": "Cancel", + "Submit": "Submit", + "Confirm": "Confirm" +} \ No newline at end of file diff --git a/src/i18n/zh/claim.json b/src/i18n/zh/claim.json index 92e5b7e..9e2de7e 100644 --- a/src/i18n/zh/claim.json +++ b/src/i18n/zh/claim.json @@ -31,6 +31,7 @@ "Please ensure the projects are selected": "請確保所有項目欄位已選擇", "Please ensure the amount are correct": "請確保所有金額輸入正確", + "Details": "詳請", "Description": "描述", "Actions": "行動" } \ No newline at end of file diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json index e4642ea..4ff2fcf 100644 --- a/src/i18n/zh/common.json +++ b/src/i18n/zh/common.json @@ -15,6 +15,8 @@ "Do you want to delete?": "你是否確認要刪除?", "Delete Success": "刪除成功", + "Details": "詳情", + "Delete": "刪除", "Search": "搜尋", "Search Criteria": "搜尋條件", "Cancel": "取消", diff --git a/src/i18n/zh/report.json b/src/i18n/zh/report.json new file mode 100644 index 0000000..a6257cf --- /dev/null +++ b/src/i18n/zh/report.json @@ -0,0 +1,3 @@ +{ + "Project": "項目" +} \ No newline at end of file diff --git a/src/i18n/zh/tasks.json b/src/i18n/zh/tasks.json new file mode 100644 index 0000000..16ba727 --- /dev/null +++ b/src/i18n/zh/tasks.json @@ -0,0 +1,27 @@ +{ + "Task Template": "工作範本", + "Create Task Template": "建立工作範本", + "Edit Task Template": "編輯工作範本", + + "Task Template Code": "工作範本編號", + "Task Template Name": "工作範本名稱", + "Task List Setup": "工作名單設置", + "Task Pool": "所有工作", + "Task List Template": "工作名單範本", + + "Task template code is required": "需要工作範本編號", + "Task template name is required": "需要工作範本名稱", + + "Do you want to submit?": "你是否確認要提交?", + "Submit Success": "提交成功", + "Submit Fail": "提交失敗", + "Do you want to delete?": "你是否確認要刪除?", + "Delete Success": "刪除成功", + + "selected": "已選擇", + "Details": "詳情", + "Delete": "刪除", + "Cancel": "取消", + "Submit": "提交", + "Confirm": "確認" +} \ No newline at end of file