Procházet zdrojové kódy

Merge branch 'main' of https://git.2fi-solutions.com/wayne.lee/tsms

# Conflicts:
#	package-lock.json
#	src/components/NavigationContent/NavigationContent.tsx
tags/Baseline_30082024_FRONTEND_UAT
leoho2fi před 1 rokem
rodič
revize
5598dcfbc7
96 změnil soubory, kde provedl 2630 přidání a 857 odebrání
  1. +0
    -13
      package-lock.json
  2. +25
    -0
      src/app/(main)/analytics/EX02ProjectCashFlowReport/page.tsx
  3. +29
    -0
      src/app/(main)/dashboard/ProjectResourceSummary/page.tsx
  4. +1
    -1
      src/app/(main)/dashboard/StaffUtilization/page.tsx
  5. +2
    -2
      src/app/(main)/settings/customer/create/page.tsx
  6. +2
    -2
      src/app/(main)/settings/customer/edit/page.tsx
  7. +48
    -0
      src/app/(main)/settings/skill/create/page.tsx
  8. +50
    -0
      src/app/(main)/settings/skill/page.tsx
  9. +54
    -0
      src/app/(main)/settings/user/page.tsx
  10. +4
    -1
      src/app/(main)/tasks/create/page.tsx
  11. +26
    -0
      src/app/(main)/tasks/edit/page.tsx
  12. +8
    -5
      src/app/(main)/tasks/page.tsx
  13. +1
    -1
      src/app/api/claims/actions.ts
  14. +3
    -3
      src/app/api/clientprojects/index.ts
  15. +14
    -0
      src/app/api/projects/index.ts
  16. +23
    -0
      src/app/api/reports/actions.ts
  17. +8
    -0
      src/app/api/reports/index.ts
  18. +53
    -0
      src/app/api/resourcesummary/index.ts
  19. +17
    -1
      src/app/api/skill/actions.ts
  20. +22
    -0
      src/app/api/skill/index.ts
  21. +3
    -3
      src/app/api/staff/actions.ts
  22. +28
    -2
      src/app/api/tasks/actions.ts
  23. +3
    -4
      src/app/api/team/actions.ts
  24. +27
    -0
      src/app/api/user/actions.ts
  25. +43
    -0
      src/app/api/user/index.ts
  26. +8
    -0
      src/app/utils/commonUtil.ts
  27. +77
    -1
      src/app/utils/fetchUtil.ts
  28. +1
    -0
      src/components/Breadcrumb/Breadcrumb.tsx
  29. +2
    -1
      src/components/ClaimDetail/ClaimDetail.tsx
  30. +5
    -4
      src/components/ClaimDetail/ClaimFormInputGrid.tsx
  31. +6
    -6
      src/components/ClaimSearch/ClaimSearch.tsx
  32. +122
    -0
      src/components/CreateSkill/CreateSkill.tsx
  33. +40
    -0
      src/components/CreateSkill/CreateSkillLoading.tsx
  34. +19
    -0
      src/components/CreateSkill/CreateSkillWrapper.tsx
  35. +90
    -0
      src/components/CreateSkill/SkillInfo.tsx
  36. +1
    -0
      src/components/CreateSkill/index.ts
  37. +1
    -1
      src/components/CreateStaff/CreateStaff.tsx
  38. +122
    -69
      src/components/CreateTaskTemplate/CreateTaskTemplate.tsx
  39. +1
    -1
      src/components/CreateTeam/CreateTeam.tsx
  40. +94
    -80
      src/components/CreateTeam/StaffAllocation.tsx
  41. +15
    -3
      src/components/CustomDatagrid/CustomDatagrid.tsx
  42. +65
    -3
      src/components/CustomInputForm/CustomInputForm.tsx
  43. +0
    -1
      src/components/CustomerDetail/index.ts
  44. +0
    -0
      src/components/CustomerSave/ContactInfo.tsx
  45. +0
    -0
      src/components/CustomerSave/CustomerInfo.tsx
  46. +2
    -2
      src/components/CustomerSave/CustomerSave.tsx
  47. +4
    -4
      src/components/CustomerSave/CustomerSaveWrapper.tsx
  48. +0
    -0
      src/components/CustomerSave/SubsidiaryAllocation.tsx
  49. +1
    -0
      src/components/CustomerSave/index.ts
  50. +22
    -7
      src/components/EditStaff/EditStaff.tsx
  51. +49
    -0
      src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx
  52. +38
    -0
      src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportLoading.tsx
  53. +18
    -0
      src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportWrapper.tsx
  54. +1
    -0
      src/components/GenerateEX02ProjectCashFlowReport/index.ts
  55. +11
    -0
      src/components/NavigationContent/NavigationContent.tsx
  56. +2
    -1
      src/components/ProgressByClient/ProgressByClient.tsx
  57. +18
    -3
      src/components/ProgressByClientSearch/ProgressByClientSearch.tsx
  58. +2
    -1
      src/components/ProgressByTeam/ProgressByTeam.tsx
  59. +548
    -0
      src/components/ProjectResourceSummary/ProjectResourceSummary.tsx
  60. +1
    -0
      src/components/ProjectResourceSummary/index.ts
  61. +75
    -0
      src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearch.tsx
  62. +40
    -0
      src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearchLoading.tsx
  63. +20
    -0
      src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearchWrapper.tsx
  64. +1
    -0
      src/components/ProjectResourceSummarySearch/index.ts
  65. +2
    -1
      src/components/SearchBox/SearchBox.tsx
  66. +96
    -0
      src/components/SkillSearch/SkillSearch.tsx
  67. +40
    -0
      src/components/SkillSearch/SkillSearchLoading.tsx
  68. +27
    -0
      src/components/SkillSearch/SkillSearchWrapper.tsx
  69. +1
    -0
      src/components/SkillSearch/index.ts
  70. +0
    -106
      src/components/StaffSearch/ConfirmDeleteModal.tsx
  71. +28
    -51
      src/components/StaffSearch/StaffSearch.tsx
  72. +2
    -2
      src/components/SubsidiaryDetail/SubsidiaryDetailWrapper.tsx
  73. +1
    -1
      src/components/SubsidiarySearch/SubsidiarySearch.tsx
  74. +27
    -1
      src/components/TaskTemplateSearch/TaskTemplateSearch.tsx
  75. +0
    -105
      src/components/TeamSearch/ConfirmDeleteModal.tsx
  76. +74
    -95
      src/components/TeamSearch/TeamSearch.tsx
  77. +4
    -1
      src/components/TimesheetModal/TimesheetModal.tsx
  78. +13
    -115
      src/components/TimesheetTable/EntryInputTable.tsx
  79. +18
    -7
      src/components/TimesheetTable/TimesheetTable.tsx
  80. +1
    -1
      src/components/TransferList/TransferList.tsx
  81. +98
    -0
      src/components/UserSearch/UserSearch.tsx
  82. +40
    -0
      src/components/UserSearch/UserSearchLoading.tsx
  83. +19
    -0
      src/components/UserSearch/UserSearchWrapper.tsx
  84. +1
    -0
      src/components/UserSearch/index.ts
  85. +10
    -43
      src/components/UserWorkspacePage/AssignedProjects.tsx
  86. +3
    -20
      src/components/UserWorkspacePage/ProjectGrid.tsx
  87. +39
    -22
      src/components/UserWorkspacePage/UserWorkspacePage.tsx
  88. +4
    -60
      src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx
  89. +1
    -0
      src/i18n/en/claim.json
  90. +2
    -0
      src/i18n/en/common.json
  91. +3
    -0
      src/i18n/en/report.json
  92. +27
    -0
      src/i18n/en/tasks.json
  93. +1
    -0
      src/i18n/zh/claim.json
  94. +2
    -0
      src/i18n/zh/common.json
  95. +3
    -0
      src/i18n/zh/report.json
  96. +27
    -0
      src/i18n/zh/tasks.json

+ 0
- 13
package-lock.json Zobrazit soubor

@@ -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",


+ 25
- 0
src/app/(main)/analytics/EX02ProjectCashFlowReport/page.tsx Zobrazit soubor

@@ -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 (
<>
<I18nProvider namespaces={["report", "common"]}>
<Suspense fallback={<GenerateEX02ProjectCashFlowReport.Loading />}>
<GenerateEX02ProjectCashFlowReport />
</Suspense>
</I18nProvider>
</>
);
};

export default ProjectCashFlowReport;

+ 29
- 0
src/app/(main)/dashboard/ProjectResourceSummary/page.tsx Zobrazit soubor

@@ -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 (
<I18nProvider namespaces={["dashboard"]}>
<Typography variant="h4" marginInlineEnd={2}>
Project Resource Summary
</Typography>
<Suspense fallback={<ProjectResourceSummarySearch.Loading />}>
<ProjectResourceSummarySearch/>
</Suspense>
</I18nProvider>
);
};
export default ProjectResourceSummary;

+ 1
- 1
src/app/(main)/dashboard/StaffUtilization/page.tsx Zobrazit soubor

@@ -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 = () => {


+ 2
- 2
src/app/(main)/settings/customer/create/page.tsx Zobrazit soubor

@@ -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 () => {
<>
<Typography variant="h4">{t("Create Customer")}</Typography>
<I18nProvider namespaces={["customer", "common"]}>
<CustomerDetail />
<CustomerSave />
</I18nProvider>
</>
);


+ 2
- 2
src/app/(main)/settings/customer/edit/page.tsx Zobrazit soubor

@@ -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 () => {
<>
<Typography variant="h4">{t("Edit Customer")}</Typography>
<I18nProvider namespaces={["customer", "common"]}>
<CustomerDetail />
<CustomerSave />
</I18nProvider>
</>
);


+ 48
- 0
src/app/(main)/settings/skill/create/page.tsx Zobrazit soubor

@@ -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 (
<>
<Typography variant="h4">{t("Create Skill")}</Typography>
<I18nProvider namespaces={["skill"]}>
<CreateSkill
/>
</I18nProvider>
</>
);
};

export default CreateStaff;

+ 50
- 0
src/app/(main)/settings/skill/page.tsx Zobrazit soubor

@@ -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 (
<>
<Stack
direction="row"
justifyContent="space-between"
flexWrap="wrap"
rowGap={2}
>
<Typography variant="h4" marginInlineEnd={2}>
{t("Skill")}
</Typography>
<Button
variant="contained"
startIcon={<Add />}
LinkComponent={Link}
href="/settings/skill/create"
>
{t("Create Skill")}
</Button>
</Stack>
<I18nProvider namespaces={["staff", "common"]}>
<Suspense fallback={<SkillSearch.Loading />}>
<SkillSearch />
</Suspense>
</I18nProvider>
</>
);
};

export default Skill;

+ 54
- 0
src/app/(main)/settings/user/page.tsx Zobrazit soubor

@@ -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 (
<>
<Stack
direction="row"
justifyContent="space-between"
flexWrap="wrap"
rowGap={2}
>
<Typography variant="h4" marginInlineEnd={2}>
{t("User")}
</Typography>
<Button
variant="contained"
startIcon={<Add />}
LinkComponent={Link}
href="/settings/team/create"
>
{t("Create User")}
</Button>
</Stack>
<I18nProvider namespaces={["User", "common"]}>
<Suspense fallback={<UserSearch.Loading />}>
<UserSearch />
</Suspense>
</I18nProvider>
</>
);
};
export default User;

+ 4
- 1
src/app/(main)/tasks/create/page.tsx Zobrazit soubor

@@ -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 (
<>
<Typography variant="h4">{t("Create Task Template")}</Typography>
<CreateTaskTemplate />
<I18nProvider namespaces={["tasks", "common"]}>
<CreateTaskTemplate />
</I18nProvider>
</>
);
};


+ 26
- 0
src/app/(main)/tasks/edit/page.tsx Zobrazit soubor

@@ -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 (
<>
<Typography variant="h4">{t("Edit Task Template")}</Typography>
<I18nProvider namespaces={["tasks", "common"]}>
<CreateTaskTemplate />
</I18nProvider>
</>
);
};

export default TaskTemplates;

+ 8
- 5
src/app/(main)/tasks/page.tsx Zobrazit soubor

@@ -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")}
</Button>
</Stack>
<Suspense fallback={<TaskTemplateSearch.Loading />}>
<TaskTemplateSearch />
</Suspense>
<I18nProvider namespaces={["tasks", "common"]}>
<Suspense fallback={<TaskTemplateSearch.Loading />}>
<TaskTemplateSearch />
</Suspense>
</I18nProvider>
</>
);
};


+ 1
- 1
src/app/api/claims/actions.ts Zobrazit soubor

@@ -21,7 +21,7 @@ export interface ClaimDetailTable {
id: number;
invoiceDate: Date;
description: string;
project: ProjectCombo;
project: number;
amount: number;
supportingDocumentName: string;
oldSupportingDocument: SupportingDocument;


+ 3
- 3
src/app/api/clientprojects/index.ts Zobrazit soubor

@@ -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",


+ 14
- 0
src/app/api/projects/index.ts Zobrazit soubor

@@ -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<AssignedProject[]>(
`${BASE_API_URL}/projects/assignedProjects`,
{
next: { tags: ["assignedProjects"] },
},
);
});

+ 23
- 0
src/app/api/reports/actions.ts Zobrazit soubor

@@ -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<FileResponse>(
`${BASE_API_URL}/reports/EX02-ProjectCashFlowReport`,
{
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
},
);

return reportBlob
};

+ 8
- 0
src/app/api/reports/index.ts Zobrazit soubor

@@ -0,0 +1,8 @@
// EX02 - Project Cash Flow Report
export interface EX02ProjectCashFlowReportFilter {
project: string[];
}

export interface EX02ProjectCashFlowReportRequest {
projectId: number;
}

+ 53
- 0
src/app/api/resourcesummary/index.ts Zobrazit soubor

@@ -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',
},
];

+ 17
- 1
src/app/api/skill/actions.ts Zobrazit soubor

@@ -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<combo>(`${BASE_API_URL}/skill/combo`, {
next: { tags: ["skill"] },
});
});
});

export const saveSkill = async (data: CreateSkillInputs) => {
return serverFetchJson(`${BASE_API_URL}/skill/save`, {
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
};

+ 22
- 0
src/app/api/skill/index.ts Zobrazit soubor

@@ -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<SkillResult[]>(`${BASE_API_URL}/skill`, {
next: { tags: ["sill"] },
});
});

+ 3
- 3
src/app/api/staff/actions.ts Zobrazit soubor

@@ -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" },


+ 28
- 2
src/app/api/tasks/actions.ts Zobrazit soubor

@@ -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<TaskTemplate>(
`${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<TaskTemplate>(
`${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
};

+ 3
- 4
src/app/api/team/actions.ts Zobrazit soubor

@@ -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" },
});
};

+ 27
- 0
src/app/api/user/actions.ts Zobrazit soubor

@@ -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<UserDetail>(`${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" },
});
};

+ 43
- 0
src/app/api/user/index.ts Zobrazit soubor

@@ -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<UserResult[]>(`${BASE_API_URL}/user`, {
next: { tags: ["user"] },
});
});

+ 8
- 0
src/app/utils/commonUtil.ts Zobrazit soubor

@@ -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();
}

+ 77
- 1
src/app/utils/fetchUtil.ts Zobrazit soubor

@@ -8,6 +8,7 @@ export const serverFetch: typeof fetch = async (input, init) => {
const session = await getServerSession<any, SessionWithTokens>(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<T>(...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");


+ 1
- 0
src/components/Breadcrumb/Breadcrumb.tsx Zobrazit soubor

@@ -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 = () => {


+ 2
- 1
src/components/ClaimDetail/ClaimDetail.tsx Zobrazit soubor

@@ -75,9 +75,10 @@ const ClaimDetail: React.FC<Props> = ({ 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)


+ 5
- 4
src/components/ClaimDetail/ClaimFormInputGrid.tsx Zobrazit soubor

@@ -371,20 +371,21 @@ const ClaimFormInputGrid: React.FC<ClaimFormInputGridProps> = ({
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
},
},
{


+ 6
- 6
src/components/ClaimSearch/ClaimSearch.tsx Zobrazit soubor

@@ -50,12 +50,12 @@ const ClaimSearch: React.FC<Props> = ({ claims }) => {

const columns = useMemo<Column<Claim>[]>(
() => [
// {
// name: "action",
// label: t("Actions"),
// onClick: onClaimClick,
// buttonIcon: <EditNote />,
// },
{
name: "id",
label: t("Details"),
onClick: onClaimClick,
buttonIcon: <EditNote />,
},
{ name: "created", label: t("Creation Date"), type: "date" },
{ name: "code", label: t("Claim Code") },
// { name: "project", label: t("Related Project Name") },


+ 122
- 0
src/components/CreateSkill/CreateSkill.tsx Zobrazit soubor

@@ -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<Props> = () => {
const formProps = useForm<CreateSkillInputs>();
const [serverError, setServerError] = useState("");
const router = useRouter();
const { t } = useTranslation();
const [tabIndex, setTabIndex] = useState(0);
const errors = formProps.formState.errors;

const onSubmit = useCallback<SubmitHandler<CreateSkillInputs>>(
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<NonNullable<TabsProps["onChange"]>>(
(_e, newValue) => {
setTabIndex(newValue);
},
[]
);

const hasErrorsInTab = (
tabIndex: number,
errors: FieldErrors<CreateSkillInputs>
) => {
switch (tabIndex) {
case 0:
return Object.keys(errors).length > 0;
default:
false;
}
};
return (
<>
<FormProvider {...formProps}>
<Stack
spacing={2}
component="form"
onSubmit={formProps.handleSubmit(onSubmit)}
>
<Tabs
value={tabIndex}
onChange={handleTabChange}
variant="scrollable"
>
<Tab
label={t("Team Info")}
icon={
hasErrorsInTab(0, errors) ? (
<Error sx={{ marginInlineEnd: 1 }} color="error" />
) : undefined
}
iconPosition="end"
/>
{/* <Tab label={t("Certification")} iconPosition="end" /> */}
</Tabs>
{serverError && (
<Typography variant="body2" color="error" alignSelf="flex-end">
{serverError}
</Typography>
)}
{tabIndex === 0 && <SkillInfo />}
<Stack direction="row" justifyContent="flex-end" gap={1}>
<Button
variant="outlined"
startIcon={<Close />}
onClick={handleCancel}
>
{t("Cancel")}
</Button>
<Button
variant="contained"
startIcon={<Check />}
type="submit"
// disabled={Boolean(formProps.watch("isGridEditing"))}
>
{t("Confirm")}
</Button>
</Stack>
</Stack>
</FormProvider>
</>
);
};

export default CreateSkill;

+ 40
- 0
src/components/CreateSkill/CreateSkillLoading.tsx Zobrazit soubor

@@ -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 (
<>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton
variant="rounded"
height={50}
width={100}
sx={{ alignSelf: "flex-end" }}
/>
</Stack>
</CardContent>
</Card>
<Card>CreateSkill
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
</Stack>
</CardContent>
</Card>
</>
);
};

export default CreateSkillLoading;

+ 19
- 0
src/components/CreateSkill/CreateSkillWrapper.tsx Zobrazit soubor

@@ -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 <CreateSkill/>;
};

CreateSkillWrapper.Loading = CreateSkillLoading;

export default CreateSkillWrapper;

+ 90
- 0
src/components/CreateSkill/SkillInfo.tsx Zobrazit soubor

@@ -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<CreateSkillInputs>();

const resetSkill = useCallback(() => {
console.log(defaultValues);
if (defaultValues !== undefined) {
resetField("name");
}
}, [defaultValues]);
return (
<>
<Card sx={{ display: "block" }}>
<CardContent component={Stack} spacing={4}>
<Box>
<Typography variant="overline" display="block" marginBlockEnd={1}>
{t("Skill Info")}
</Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6}>
<TextField
label={t("Skill Name")}
fullWidth
rows={4}
{...register("name", {
required: true,
})}
error={Boolean(errors.name)}
helperText={Boolean(errors.name) && (errors.name?.message ? t(errors.name.message) : t("Please input correct name"))}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Skill Code")}
fullWidth
rows={4}
{...register("code", {
required: true,
})}
error={Boolean(errors.code)}
helperText={Boolean(errors.code) && (errors.code?.message ? t(errors.code.message) : t("Please input correct name"))}
/>
</Grid>
<Grid item xs={12}>
<TextField
label={t("Skill Description")}
fullWidth
multiline
rows={4}
{...register("description", {
required: true,
})}
error={Boolean(errors.description)}
helperText={Boolean(errors.description) && (errors.description?.message ? t(errors.description.message) : t("Please input correct name"))}
/>
</Grid>
</Grid>
</Box>
</CardContent>
</Card>
</>
);
};
export default SkillInfo;

+ 1
- 0
src/components/CreateSkill/index.ts Zobrazit soubor

@@ -0,0 +1 @@
export { default } from "./CreateSkillWrapper";

+ 1
- 1
src/components/CreateStaff/CreateStaff.tsx Zobrazit soubor

@@ -190,7 +190,7 @@ const CreateStaff: React.FC<formProps> = ({ Title }) => {
{
id: "skillSetId",
label: t("Skillset"),
type: "combo-Obj",
type: "multiSelect-Obj",
options: skillCombo || [],
required: false,
},


+ 122
- 69
src/components/CreateTaskTemplate/CreateTaskTemplate.tsx Zobrazit soubor

@@ -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<Props> = ({ tasks }) => {
const { t } = useTranslation();

const searchParams = useSearchParams()
const router = useRouter();
const handleCancel = () => {
router.back();
@@ -49,6 +52,7 @@ const CreateTaskTemplate: React.FC<Props> = ({ tasks }) => {
handleSubmit,
setValue,
watch,
resetField,
formState: { errors, isSubmitting },
} = useForm<NewTaskTemplateFormInputs>({ defaultValues: { taskIds: [] } });

@@ -57,12 +61,56 @@ const CreateTaskTemplate: React.FC<Props> = ({ tasks }) => {
return items.filter((item) => currentTaskIds.includes(item.id));
}, [currentTaskIds, items]);

const [refTaskTemplate, setRefTaskTemplate] = React.useState<NewTaskTemplateFormInputs>()
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<NewTaskTemplateFormInputs> = 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<Props> = ({ tasks }) => {
);

return (
<Stack component="form" onSubmit={handleSubmit(onSubmit)} gap={2}>
<Card>
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
<Typography variant="overline">{t("Task List Setup")}</Typography>
<Grid
container
spacing={2}
columns={{ xs: 6, sm: 12 }}
marginBlockEnd={1}
>
<Grid item xs={6}>
<TextField
label={t("Task Template Code")}
fullWidth
{...register("code", {
required: t("Task template code is required"),
})}
error={Boolean(errors.code?.message)}
helperText={errors.code?.message}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Task Template Name")}
fullWidth
{...register("name", {
required: t("Task template name is required"),
})}
error={Boolean(errors.name?.message)}
helperText={errors.name?.message}
<>
{
(id === null || refTaskTemplate !== undefined) && <Stack component="form" onSubmit={handleSubmit(onSubmit)} gap={2}>
<Card>
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
<Typography variant="overline">{t("Task List Setup")}</Typography>
<Grid
container
spacing={2}
columns={{ xs: 6, sm: 12 }}
marginBlockEnd={1}
>
<Grid item xs={6}>
<TextField
label={t("Task Template Code")}
fullWidth
{...register("code", {
required: t("Task template code is required"),
})}
error={Boolean(errors.code?.message)}
helperText={errors.code?.message}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Task Template Name")}
fullWidth
{...register("name", {
required: t("Task template name is required"),
})}
error={Boolean(errors.name?.message)}
helperText={errors.name?.message}
/>
</Grid>
</Grid>
<TransferList
allItems={items}
selectedItems={selectedItems}
onChange={(selectedTasks) => {
setValue(
"taskIds",
selectedTasks.map((item) => item.id),
);
}}
allItemsLabel={t("Task Pool")}
selectedItemsLabel={t("Task List Template")}
/>
</Grid>
</Grid>
<TransferList
allItems={items}
selectedItems={selectedItems}
onChange={(selectedTasks) => {
setValue(
"taskIds",
selectedTasks.map((item) => item.id),
);
}}
allItemsLabel={t("Task Pool")}
selectedItemsLabel={t("Task List Template")}
/>
</CardContent>
</Card>
{serverError && (
<Typography variant="body2" color="error" alignSelf="flex-end">
{serverError}
</Typography>
)}
<Stack direction="row" justifyContent="flex-end" gap={1}>
<Button variant="outlined" startIcon={<Close />} onClick={handleCancel}>
{t("Cancel")}
</Button>
<Button
variant="contained"
startIcon={<Check />}
type="submit"
disabled={isSubmitting}
>
{t("Confirm")}
</Button>
</Stack>
</Stack>
</CardContent>
</Card>
{
serverError && (
<Typography variant="body2" color="error" alignSelf="flex-end">
{serverError}
</Typography>
)
}
<Stack direction="row" justifyContent="flex-end" gap={1}>
<Button variant="outlined" startIcon={<Close />} onClick={handleCancel}>
{t("Cancel")}
</Button>
<Button
variant="contained"
startIcon={<Check />}
type="submit"
disabled={isSubmitting}
>
{t("Confirm")}
</Button>
</Stack>
</Stack >}
</>
);
};



+ 1
- 1
src/components/CreateTeam/CreateTeam.tsx Zobrazit soubor

@@ -89,7 +89,7 @@ const hasErrorsInTab = (
}
iconPosition="end"
/>
<Tab label={t("Subsidiary Allocation")} iconPosition="end" />
<Tab label={t("Staff Allocation")} iconPosition="end" />
</Tabs>
{serverError && (
<Typography variant="body2" color="error" alignSelf="flex-end">


+ 94
- 80
src/components/CreateTeam/StaffAllocation.tsx Zobrazit soubor

@@ -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<Props> = ({ allStaffs: staff }) => {
reset,
resetField,
} = useFormContext<CreateTeamInputs>();
const initialStaffs = staff.map((s) => ({ ...s }));
// console.log(initialStaffs)
// console.log(initialStaffs)
const [filteredStaff, setFilteredStaff] = useState(initialStaffs);
const [selectedStaff, setSelectedStaff] = useState<typeof filteredStaff>(
initialStaffs.filter((s) => getValues("addStaffIds")?.includes(s.id))
);
const [seletedTeamLead, setSeletedTeamLead] = useState<number>()
// Adding / Removing staff

// Adding / Removing staff
const addStaff = useCallback((staff: StaffResult) => {
setSelectedStaff((s) => [...s, staff]);
}, []);
@@ -53,27 +64,31 @@ const StaffAllocation: React.FC<Props> = ({ allStaffs: staff }) => {
setSelectedStaff((s) => s.filter((s) => s.id !== staff.id));
}, []);

const setTeamLead = useCallback((staff: StaffResult) => {
setSeletedTeamLead(staff.id)
const rearrangedList = getValues("addStaffIds").reduce<number[]>((acc, num, index) => {
if (num === staff.id && index !== 0) {
const setTeamLead = useCallback(
(staff: StaffResult) => {
const rearrangedList = getValues("addStaffIds").reduce<number[]>(
(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<Props> = ({ 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<Props> = ({ allStaffs: staff }) => {
}, [selectedStaff, setValue]);

useEffect(() => {
console.log(selectedStaff)
console.log(selectedStaff);
}, [selectedStaff]);

const StaffPoolColumns = useMemo<Column<StaffResult>[]>(
@@ -107,7 +122,7 @@ const StaffAllocation: React.FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ allStaffs: staff }) => {
(_e, newValue) => {
setTabIndex(newValue);
},
[],
[]
);

return (
@@ -185,48 +199,48 @@ const StaffAllocation: React.FC<Props> = ({ allStaffs: staff }) => {
{t("staff")}
</Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6} display="flex" alignItems="center">
<Search sx={{ marginInlineEnd: 1 }} />
<TextField
variant="standard"
fullWidth
onChange={onQueryInputChange}
value={query}
placeholder={t("Search by subsidiary code, name or br no.")}
InputProps={{
endAdornment: query && (
<InputAdornment position="end">
<IconButton onClick={clearQueryInput}>
<Clear />
</IconButton>
</InputAdornment>
),
}}
/>
<Grid item xs={6} display="flex" alignItems="center">
<Search sx={{ marginInlineEnd: 1 }} />
<TextField
variant="standard"
fullWidth
onChange={onQueryInputChange}
value={query}
placeholder={t("Search by Staff Id, Name or Position.")}
InputProps={{
endAdornment: query && (
<InputAdornment position="end">
<IconButton onClick={clearQueryInput}>
<Clear />
</IconButton>
</InputAdornment>
),
}}
/>
</Grid>
</Grid>
</Grid>
<Tabs value={tabIndex} onChange={handleTabChange}>
<Tab label={t("Staff Pool")} />
<Tab
label={`${t("Allocated Staff")} (${selectedStaff.length})`}
/>
</Tabs>
<Box sx={{ marginInline: -3 }}>
{tabIndex === 0 && (
<SearchResults
noWrapper
items={differenceBy(filteredStaff, selectedStaff, "id")}
columns={StaffPoolColumns}
/>
)}
{tabIndex === 1 && (
<SearchResults
noWrapper
items={selectedStaff}
columns={allocatedStaffColumns}
<Tabs value={tabIndex} onChange={handleTabChange}>
<Tab label={t("Staff Pool")} />
<Tab
label={`${t("Allocated Staff")} (${selectedStaff.length})`}
/>
)}
</Box>
</Tabs>
<Box sx={{ marginInline: -3 }}>
{tabIndex === 0 && (
<SearchResults
noWrapper
items={differenceBy(filteredStaff, selectedStaff, "id")}
columns={StaffPoolColumns}
/>
)}
{tabIndex === 1 && (
<SearchResults
noWrapper
items={selectedStaff}
columns={allocatedStaffColumns}
/>
)}
</Box>
</Stack>
</CardContent>
</Card>


+ 15
- 3
src/components/CustomDatagrid/CustomDatagrid.tsx Zobrazit soubor

@@ -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<CustomDatagridProps> = ({
@@ -32,6 +34,8 @@ const CustomDatagrid: React.FC<CustomDatagridProps> = ({
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<CustomDatagridProps> = ({
editMode="row"
checkboxSelection={checkboxSelection}
onRowSelectionModelChange={onRowSelectionModelChange}
experimentalFeatures={{ columnGrouping: true }}
columnGroupingModel={columnGroupingModel}
initialState={{
pagination: { paginationModel: { pageSize: 10 } },
}}
@@ -222,6 +228,8 @@ const CustomDatagrid: React.FC<CustomDatagridProps> = ({
editMode="row"
checkboxSelection={checkboxSelection}
onRowSelectionModelChange={onRowSelectionModelChange}
experimentalFeatures={{ columnGrouping: true }}
columnGroupingModel={columnGroupingModel}
initialState={{
pagination: { paginationModel: { pageSize: 10 } },
}}
@@ -251,6 +259,8 @@ const CustomDatagrid: React.FC<CustomDatagridProps> = ({
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<CustomDatagridProps> = ({
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<CustomDatagridProps> = ({
"& .MuiDataGrid-cell:hover": {
color: "primary.main",
},
height: 300,
height: dataGridHeight ?? 300,
"& .MuiDataGrid-root": {
overflow: "auto",
},


+ 65
- 3
src/components/CustomInputForm/CustomInputForm.tsx Zobrazit soubor

@@ -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<CustomInputFormProps> = ({
</Grid>
);
} else if (field.type === "multiDate") {
console.log(dayjs(field.value))
// console.log(dayjs(field.value))
return (
<Grid item xs={field.size ?? 6} key={field.id}>
<LocalizationProvider dateAdapter={AdapterDayjs}>
@@ -343,8 +345,6 @@ const CustomInputForm: React.FC<CustomInputFormProps> = ({
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<CustomInputFormProps> = ({
</FormControl>
</Grid>
);
} else if (field.type === "multiSelect-Obj") {
return (
<Grid item xs={field.size ?? 6} key={field.id}>
<FormControl fullWidth>
<InputLabel id={`${field.id}-label`}>{field.label}</InputLabel>
<Controller
name={field.id}
control={control}
defaultValue={
field.value !== undefined ? field.value : []
}
render={({ field: { onChange, value } }) => (
<Select
labelId={`${field.id}-label`}
id={field.id}
value={value}
multiple
onChange={(event) => {
onChange(event.target.value);
const newValue = event.target.value;
const selectedOption = field.options?.find(
(option) => option.id === newValue
);
handleAutocompleteChange(
field.id,
selectedOption
);
}}
renderValue={(selected) => {
const selectedOption = field.options?.filter((option) => selected.includes(option.id));
return (
<Stack gap={1} direction="row" flexWrap="wrap">
{selectedOption ? selectedOption.map(({id, label}) => {
return (
<Chip key={id} label={label} />
)}) : null}
</Stack>
)}}
required={field.required}
>
{field.options?.map((option) => (
<MenuItem
value={
option.id !== undefined
? option.id
: option
}
key={
option.id !== undefined
? option.id
: option
}
>
{option.id ? option.label : ""}
</MenuItem>
))}
</Select>
)}
/>
</FormControl>
</Grid>
);
} else if (field.type === "numeric") {
return (
<Grid item xs={field.size ?? 6} key={field.id}>


+ 0
- 1
src/components/CustomerDetail/index.ts Zobrazit soubor

@@ -1 +0,0 @@
export { default } from "./CustomerDetailWrapper";

src/components/CustomerDetail/ContactInfo.tsx → src/components/CustomerSave/ContactInfo.tsx Zobrazit soubor


src/components/CustomerDetail/CustomerInfo.tsx → src/components/CustomerSave/CustomerInfo.tsx Zobrazit soubor


src/components/CustomerDetail/CustomerDetail.tsx → src/components/CustomerSave/CustomerSave.tsx Zobrazit soubor

@@ -42,7 +42,7 @@ const hasErrorsInTab = (
}
};

const CustomerDetail: React.FC<Props> = ({
const CustomerSave: React.FC<Props> = ({
subsidiaries,
customerTypes,
}) => {
@@ -277,4 +277,4 @@ const CustomerDetail: React.FC<Props> = ({
);
};

export default CustomerDetail;
export default CustomerSave;

src/components/CustomerDetail/CustomerDetailWrapper.tsx → src/components/CustomerSave/CustomerSaveWrapper.tsx Zobrazit soubor

@@ -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 (
<CustomerDetail subsidiaries={subsidiaries} customerTypes={customerTypes} />
<CustomerSave subsidiaries={subsidiaries} customerTypes={customerTypes} />
);
};

export default CustomerDetailWrapper;
export default CustomerSaveWrapper;

src/components/CustomerDetail/SubsidiaryAllocation.tsx → src/components/CustomerSave/SubsidiaryAllocation.tsx Zobrazit soubor


+ 1
- 0
src/components/CustomerSave/index.ts Zobrazit soubor

@@ -0,0 +1 @@
export { default } from "./CustomerSaveWrapper";

+ 22
- 7
src/components/EditStaff/EditStaff.tsx Zobrazit soubor

@@ -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":


+ 49
- 0
src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx Zobrazit soubor

@@ -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<Omit<EX02ProjectCashFlowReportFilter, "id">>;
type SearchParamNames = keyof SearchQuery;

const GenerateEX02ProjectCashFlowReport: React.FC<Props> = ({ projects }) => {
const { t } = useTranslation();
const projectCombo = projects.map(project => `${project.code} - ${project.name}`)

const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
() => [
{ label: t("Project"), paramName: "project", type: "select", options: projectCombo, needAll: false},
],
[t],
);

return (
<>
<SearchBox
criteria={searchCriteria}
onSearch={async (query) => {

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;

+ 38
- 0
src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportLoading.tsx Zobrazit soubor

@@ -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 (
<>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={60} />
<Skeleton
variant="rounded"
height={50}
width={100}
sx={{ alignSelf: "flex-end" }}
/>
</Stack>
</CardContent>
</Card>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
</Stack>
</CardContent>
</Card>
</>
);
};

export default GenerateEX02ProjectCashFlowReportLoading;

+ 18
- 0
src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportWrapper.tsx Zobrazit soubor

@@ -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 <GenerateEX02ProjectCashFlowReport projects={projects} />;
};

GenerateEX02ProjectCashFlowReportWrapper.Loading = GenerateEX02ProjectCashFlowReportLoading;

export default GenerateEX02ProjectCashFlowReportWrapper;

+ 1
- 0
src/components/GenerateEX02ProjectCashFlowReport/index.ts Zobrazit soubor

@@ -0,0 +1 @@
export { default } from "./GenerateEX02ProjectCashFlowReportWrapper";

+ 11
- 0
src/components/NavigationContent/NavigationContent.tsx Zobrazit soubor

@@ -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: <ViewWeekIcon />,
label: "Project Resource Summary",
path: "/dashboard/ProjectResourceSummary",
}
],
},
{
@@ -109,6 +117,7 @@ const navigationItems: NavigationItem[] = [
{icon: <Analytics />, label:"Project Claims Report", path: "/analytics/ProjectClaimsReport"},
{icon: <Analytics />, label:"Project P&L Report", path: "/analytics/ProjectPLReport"},
{icon: <Analytics />, label:"Financial Status Report", path: "/analytics/FinancialStatusReport"},
{icon: <Analytics />, label:"EX02 - Project Cash Flow Report", path: "/analytics/EX02ProjectCashFlowReport"},
],
},
{
@@ -118,10 +127,12 @@ const navigationItems: NavigationItem[] = [
{ icon: <BusinessIcon />, label: "Subsidiary", path: "/settings/subsidiary" },
{ icon: <Staff />, label: "Staff", path: "/settings/staff" },
{ icon: <Company />, label: "Company", path: "/settings/company" },
{ icon: <EmojiEventsIcon />, label: "Skill", path: "/settings/skill" },
{ icon: <Department />, label: "Department", path: "/settings/department" },
{ icon: <Position />, label: "Position", path: "/settings/position" },
{ icon: <Salary />, label: "Salary", path: "/settings/salary" },
{ icon: <Team />, label: "Team", path: "/settings/team" },
{ icon: <ManageAccountsIcon />, label: "User", path: "/settings/user" },
],
},
];


+ 2
- 1
src/components/ProgressByClient/ProgressByClient.tsx Zobrazit soubor

@@ -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");


+ 18
- 3
src/components/ProgressByClientSearch/ProgressByClientSearch.tsx Zobrazit soubor

@@ -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<Props> = ({ 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<Props> = ({ 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<Column<ClientProjectResult>[]>(
() => [
{
name: "id",
label: t("Details"),
onClick: onTaskClick,
buttonIcon: <EditNote />,
},
{ 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 (


+ 2
- 1
src/components/ProgressByTeam/ProgressByTeam.tsx Zobrazit soubor

@@ -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");


+ 548
- 0
src/components/ProjectResourceSummary/ProjectResourceSummary.tsx Zobrazit soubor

@@ -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 (
<React.Fragment>
<TableRow sx={{ '& > *': { borderBottom: 'unset' } }}>
<TableCell>
{row.task.length > 0 && (
<IconButton
aria-label="expand row"
size="small"
onClick={() => setOpen(!open)}
>
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton>
)}
</TableCell>
<TableCell style={{fontSize:10}}>{row.stage}</TableCell>
<TableCell style={{fontSize:10}}>{row.taskCount}</TableCell>
<TableCell style={{fontSize:10}}>{row.g1Planned}</TableCell>
<TableCell style={{fontSize:10}}>{row.g1Actual}</TableCell>
<TableCell style={{fontSize:10}}>{row.g2Planned}</TableCell>
<TableCell style={{fontSize:10}}>{row.g2Actual}</TableCell>
<TableCell style={{fontSize:10}}>{row.g3Planned}</TableCell>
<TableCell style={{fontSize:10}}>{row.g3Actual}</TableCell>
<TableCell style={{fontSize:10}}>{row.g4Planned}</TableCell>
<TableCell style={{fontSize:10}}>{row.g4Actual}</TableCell>
<TableCell style={{fontSize:10}}>{row.g5Planned}</TableCell>
<TableCell style={{fontSize:10}}>{row.g5Actual}</TableCell>
<TableCell style={{fontSize:10}}>{row.totalPlanned}</TableCell>
<TableCell style={{fontSize:10}}>{row.totalActual}</TableCell>
</TableRow>
{row.task.map((taskRow:any) => (
<>
<TableRow style={{backgroundColor:"#f0f3f7"}}>
<TableCell style={{ paddingBottom: 0, paddingTop: 0}} colSpan={1}>
<Collapse in={open} timeout="auto" unmountOnExit style={{marginLeft:-17, marginRight:-17}}>
<TableCell colSpan={1}/>
</Collapse>
</TableCell>
<TableCell style={{ paddingBottom: 0, paddingTop: 0}} colSpan={1}>
<Collapse in={open} timeout="auto" unmountOnExit style={{marginLeft:-17, marginRight:-17}}>
<TableCell style={{fontSize:10}} colSpan={1}>{taskRow.stage}</TableCell>
</Collapse>
</TableCell>
<TableCell style={{ paddingBottom: 0, paddingTop: 0}} colSpan={1}>
<Collapse in={open} timeout="auto" unmountOnExit style={{marginLeft:-17, marginRight:-17}}>
<TableCell style={{fontSize:10}} colSpan={1}>{taskRow.taskCount}</TableCell>
</Collapse>
</TableCell>
<TableCell style={{ paddingBottom: 0, paddingTop: 0}} colSpan={1}>
<Collapse in={open} timeout="auto" unmountOnExit style={{marginLeft:-17, marginRight:-17}}>
<TableCell style={{fontSize:10}} colSpan={1}>{taskRow.g1Planned}</TableCell>
</Collapse>
</TableCell>
<TableCell style={{ paddingBottom: 0, paddingTop: 0}} colSpan={1}>
<Collapse in={open} timeout="auto" unmountOnExit style={{marginLeft:-17, marginRight:-17}}>
<TableCell style={{fontSize:10}} colSpan={1}>{taskRow.g1Actual}</TableCell>
</Collapse>
</TableCell>
<TableCell style={{ paddingBottom: 0, paddingTop: 0}} colSpan={1}>
<Collapse in={open} timeout="auto" unmountOnExit style={{marginLeft:-17, marginRight:-17}}>
<TableCell style={{fontSize:10}} colSpan={1}>{taskRow.g2Planned}</TableCell>
</Collapse>
</TableCell>
<TableCell style={{ paddingBottom: 0, paddingTop: 0}} colSpan={1}>
<Collapse in={open} timeout="auto" unmountOnExit style={{marginLeft:-17, marginRight:-17}}>
<TableCell style={{fontSize:10}} colSpan={1}>{taskRow.g2Actual}</TableCell>
</Collapse>
</TableCell>
<TableCell style={{ paddingBottom: 0, paddingTop: 0}} colSpan={1}>
<Collapse in={open} timeout="auto" unmountOnExit style={{marginLeft:-17, marginRight:-17}}>
<TableCell style={{fontSize:10}} colSpan={1}>{taskRow.g3Planned}</TableCell>
</Collapse>
</TableCell>
<TableCell style={{ paddingBottom: 0, paddingTop: 0}} colSpan={1}>
<Collapse in={open} timeout="auto" unmountOnExit style={{marginLeft:-17, marginRight:-17}}>
<TableCell style={{fontSize:10}} colSpan={1}>{taskRow.g3Actual}</TableCell>
</Collapse>
</TableCell>
<TableCell style={{ paddingBottom: 0, paddingTop: 0}} colSpan={1}>
<Collapse in={open} timeout="auto" unmountOnExit style={{marginLeft:-17, marginRight:-17}}>
<TableCell style={{fontSize:10}} colSpan={1}>{taskRow.g4Planned}</TableCell>
</Collapse>
</TableCell>
<TableCell style={{ paddingBottom: 0, paddingTop: 0}} colSpan={1}>
<Collapse in={open} timeout="auto" unmountOnExit style={{marginLeft:-17, marginRight:-17}}>
<TableCell style={{fontSize:10}} colSpan={1}>{taskRow.g4Actual}</TableCell>
</Collapse>
</TableCell>
<TableCell style={{ paddingBottom: 0, paddingTop: 0}} colSpan={1}>
<Collapse in={open} timeout="auto" unmountOnExit style={{marginLeft:-17, marginRight:-17}}>
<TableCell style={{fontSize:10}} colSpan={1}>{taskRow.g5Planned}</TableCell>
</Collapse>
</TableCell>
<TableCell style={{ paddingBottom: 0, paddingTop: 0}} colSpan={1}>
<Collapse in={open} timeout="auto" unmountOnExit style={{marginLeft:-17, marginRight:-17}}>
<TableCell style={{fontSize:10}} colSpan={1}>{taskRow.g5Actual}</TableCell>
</Collapse>
</TableCell>
<TableCell style={{ paddingBottom: 0, paddingTop: 0}} colSpan={1}>
<Collapse in={open} timeout="auto" unmountOnExit style={{marginLeft:-17, marginRight:-17}}>
<TableCell style={{fontSize:10}} colSpan={1}>{taskRow.totalPlanned}</TableCell>
</Collapse>
</TableCell>
<TableCell style={{ paddingBottom: 0, paddingTop: 0}} colSpan={1}>
<Collapse in={open} timeout="auto" unmountOnExit style={{marginLeft:-17, marginRight:-17}}>
<TableCell style={{fontSize:10}} colSpan={1}>{taskRow.totalActual}</TableCell>
</Collapse>
</TableCell>
</TableRow>
</>
))}
{/* <TableRow>
<TableCell style={{ paddingBottom: 0, paddingTop: 0, borderStyle:"dotted"}} colSpan={15}>
<Collapse in={open} timeout="auto" unmountOnExit style={{borderStyle:"dotted", marginLeft:-17, marginRight:-17}}>
<Box sx={{ margin: 1 }}>
{row.task.map((taskRow:any) => (
<TableRow key={taskRow.stage}>
<TableCell style={{borderStyle:"dotted"}}/>
<TableCell style={{fontSize:10, borderStyle:"dotted"}}>{taskRow.stage}</TableCell>
<TableCell style={{fontSize:10, borderStyle:"dotted"}}>{taskRow.taskCount}</TableCell>
<TableCell style={{fontSize:10}}>{taskRow.g1Planned}</TableCell>
<TableCell style={{fontSize:10}}>{taskRow.g1Actual}</TableCell>
<TableCell style={{fontSize:10}}>{taskRow.g2Planned}</TableCell>
<TableCell style={{fontSize:10}}>{taskRow.g2Actual}</TableCell>
<TableCell style={{fontSize:10}}>{taskRow.g3Planned}</TableCell>
<TableCell style={{fontSize:10}}>{taskRow.g3Actual}</TableCell>
<TableCell style={{fontSize:10}}>{taskRow.g4Planned}</TableCell>
<TableCell style={{fontSize:10}}>{taskRow.g4Actual}</TableCell>
<TableCell style={{fontSize:10}}>{taskRow.g5Planned}</TableCell>
<TableCell style={{fontSize:10}}>{taskRow.g5Actual}</TableCell>
<TableCell style={{fontSize:10}}>{taskRow.totalPlanned}</TableCell>
<TableCell style={{fontSize:10}}>{taskRow.totalActual}</TableCell>
</TableRow>
))}
</Box>
</Collapse>
</TableCell>
</TableRow> */}
</React.Fragment>
);
}

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 (
<Grid item sm sx={{
'& .groupColor': {
backgroundColor: 'rgba(240, 240, 240, 0.55)',
},
'& .totalGroupColor': {
backgroundColor: 'rgba(218, 218, 245, 0.55)',
},
}}>
<Card className="mt-5">
<CardHeader className="text-slate-500" title="Project Information"/>
<div className="ml-6 mr-6">
<div style={{ display: "inline-block", width: "33%"}}>
<div style={{fontSize:"1em", fontWeight:"bold"}}>
<u>
Project
</u>
</div>
<div style={{fontSize:"1em"}}>
{projectName}
</div>
</div>
<div style={{ display: "inline-block", width: "33%"}}>
<div style={{ fontSize:"1em", fontWeight:"bold"}}>
<u>
Project Fee
</u>
</div>
<div style={{fontSize:"1em"}}>
HKD {projectFee}
</div>
</div>
<div style={{ display: "inline-block", width: "33%"}}>
<div style={{ fontSize:"1em", fontWeight:"bold"}}>
<u>
Status
</u>
</div>
<div style={{fontSize:"1em"}}>
{status}
</div>
</div>
<div style={{ display: "inline-block", width: "33%"}}>
<div style={{ fontSize:"1em", fontWeight:"bold"}}>
<u>
Planned Resources
</u>
</div>
<div style={{fontSize:"1em"}}>
{plannedResources} Manhours
</div>
</div>
<div style={{ display: "inline-block", width: "33%"}}>
<div style={{ fontSize:"1em", fontWeight:"bold"}}>
<u>
Actual Resources Spent
</u>
</div>
<div style={{fontSize:"1em"}}>
{actualResourcesSpent} Manhours
</div>
</div>
<div style={{ display: "inline-block", width: "33%"}}>
<div style={{ fontSize:"1em", fontWeight:"bold"}}>
<u>
Remaining Resources
</u>
</div>
<div style={{fontSize:"1em"}}>
{remainingResources} Manhours
</div>
</div>
</div>
{/* <div style={{display:"inline-block",width:"99%",marginLeft:10}}>
<CustomDatagrid rows={projectResourcesRows} columns={columns2} columnWidth={200} dataGridHeight={480} pageSize={100} columnGroupingModel={columnGroupingModel} sx={{fontSize:10}}/>
</div> */}
<div style={{display:"inline-block",width:"99%",marginLeft:10, marginRight:10, marginTop:10}}>
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell align="center" colSpan={3}>
</TableCell>
<TableCell align="center" colSpan={2}>
G1
</TableCell>
<TableCell align="center" colSpan={2}>
G2
</TableCell>
<TableCell align="center" colSpan={2}>
G3
</TableCell>
<TableCell align="center" colSpan={2}>
G4
</TableCell>
<TableCell align="center" colSpan={2}>
G5
</TableCell>
<TableCell align="center" colSpan={2} style={{backgroundColor:"rgba(218, 218, 245, 0.55)"}}>
Total
</TableCell>
</TableRow>
<TableRow>
<TableCell style={{width:"5%"}}/>
<TableCell style={{fontSize:10, width:"20%"}}>Stage</TableCell>
<TableCell style={{fontSize:10, width:"5%"}}>Task Count</TableCell>
<TableCell style={{fontSize:10, width:"5%"}}>Planned</TableCell>
<TableCell style={{fontSize:10, width:"5%"}}>Actual</TableCell>
<TableCell style={{fontSize:10, width:"5%"}}>Planned</TableCell>
<TableCell style={{fontSize:10, width:"5%"}}>Actual</TableCell>
<TableCell style={{fontSize:10, width:"5%"}}>Planned</TableCell>
<TableCell style={{fontSize:10, width:"5%"}}>Actual</TableCell>
<TableCell style={{fontSize:10, width:"5%"}}>Planned</TableCell>
<TableCell style={{fontSize:10, width:"5%"}}>Actual</TableCell>
<TableCell style={{fontSize:10, width:"5%"}}>Planned</TableCell>
<TableCell style={{fontSize:10, width:"5%"}}>Actual</TableCell>
<TableCell style={{fontSize:10, width:"5%"}}>Planned</TableCell>
<TableCell style={{fontSize:10, width:"5%"}}>Actual</TableCell>
</TableRow>
</TableHead>
<TableBody>
{rows.map((row) => (
<Row key={row.stage} row={row} />
))}
</TableBody>
</Table>
</TableContainer>
</div>
</Card>
</Grid>
);
};

export default ProjectResourceSummary;

+ 1
- 0
src/components/ProjectResourceSummary/index.ts Zobrazit soubor

@@ -0,0 +1 @@
export { default } from "./ProjectResourceSummary";

+ 75
- 0
src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearch.tsx Zobrazit soubor

@@ -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<Omit<ResourceSummaryResult, "id">>;
type SearchParamNames = keyof SearchQuery;


const ProjectResourceSummarySearch: React.FC<Props> = ({ 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<SearchParamNames>[] = 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<Column<ResourceSummaryResult>[]>(
() => [
{
name: "id",
label: t("View"),
onClick: onTaskClick,
buttonIcon: <ArticleIcon />,
},
{ name: "projectCode", label: t("Project Code") },
{ name: "projectName", label: t("Project Name") },
{ name: "clientCodeAndName", label: t("Client Code And Name") },
],
[onTaskClick, t],
// [t],
);

return (
<>
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
console.log(query);
}}
/>
<SearchResults<ResourceSummaryResult>
items={filteredProjects}
columns={columns}
/>
<ProjectResourceSummary/>
</>
);
};

export default ProjectResourceSummarySearch;

+ 40
- 0
src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearchLoading.tsx Zobrazit soubor

@@ -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 (
<>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton
variant="rounded"
height={50}
width={100}
sx={{ alignSelf: "flex-end" }}
/>
</Stack>
</CardContent>
</Card>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
</Stack>
</CardContent>
</Card>
</>
);
};

export default ProjectResourceSummarySearchLoading;

+ 20
- 0
src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearchWrapper.tsx Zobrazit soubor

@@ -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 <ProjectResourceSummarySearch projects={clentprojects} />;
};

ProjectResourceSummarySearchWrapper.Loading = ProjectResourceSummarySearchLoading;

export default ProjectResourceSummarySearchWrapper;



+ 1
- 0
src/components/ProjectResourceSummarySearch/index.ts Zobrazit soubor

@@ -0,0 +1 @@
export { default } from "./ProjectResourceSummarySearchWrapper";

+ 2
- 1
src/components/SearchBox/SearchBox.tsx Zobrazit soubor

@@ -36,6 +36,7 @@ interface TextCriterion<T extends string> extends BaseCriterion<T> {
interface SelectCriterion<T extends string> extends BaseCriterion<T> {
type: "select";
options: string[];
needAll?: boolean;
}

interface DateRangeCriterion<T extends string> extends BaseCriterion<T> {
@@ -134,7 +135,7 @@ function SearchBox<T extends string>({
onChange={makeSelectChangeHandler(c.paramName)}
value={inputs[c.paramName]}
>
<MenuItem value={"All"}>{t("All")}</MenuItem>
{!(c.needAll === false) && <MenuItem value={"All"}>{t("All")}</MenuItem>}
{c.options.map((option, index) => (
<MenuItem key={`${option}-${index}`} value={option}>
{t(option)}


+ 96
- 0
src/components/SkillSearch/SkillSearch.tsx Zobrazit soubor

@@ -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<Omit<SkillResult, "id">>;
type SearchParamNames = keyof SearchQuery;

const SkillSearch: React.FC<Props> = ({ skill }) => {
const { t } = useTranslation();
const [filteredStaff, setFilteredStaff] = useState(skill);
const router = useRouter();

const searchCriteria: Criterion<SearchParamNames>[] = 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<Column<SkillResult>[]>(
() => [
{
name: "action",
label: t("Actions"),
onClick: onSkillClick,
buttonIcon: <EditNote />,
},
{ name: "name", label: t("Name") },
{ name: "code", label: t("Code") },
{ name: "description", label: t("Description") },
{
name: "action",
label: t("Actions"),
onClick: deleteClick,
buttonIcon: <DeleteIcon />,
color: "error",
},
],
[t, onSkillClick, deleteClick]
);

return (
<>
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
// 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),
// )
// );
}}
/>
<SearchResults<SkillResult> items={filteredStaff} columns={columns} />
</>
);
};

export default SkillSearch;

+ 40
- 0
src/components/SkillSearch/SkillSearchLoading.tsx Zobrazit soubor

@@ -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 (
<>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton
variant="rounded"
height={50}
width={100}
sx={{ alignSelf: "flex-end" }}
/>
</Stack>
</CardContent>
</Card>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
</Stack>
</CardContent>
</Card>
</>
);
};

export default SkillSearchLoading;

+ 27
- 0
src/components/SkillSearch/SkillSearchWrapper.tsx Zobrazit soubor

@@ -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 <SkillSearch skill={skill} />;
};

SkillSearchWrapper.Loading = SkillSearchLoading;

export default SkillSearchWrapper;

+ 1
- 0
src/components/SkillSearch/index.ts Zobrazit soubor

@@ -0,0 +1 @@
export { default } from "./SkillSearchWrapper";

+ 0
- 106
src/components/StaffSearch/ConfirmDeleteModal.tsx Zobrazit soubor

@@ -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> = ({ ...props }) => {
const { t } = useTranslation();
return (
<>
<Modal open={props.isOpen} onClose={props.onCancel}>
<Card
style={{
flex: 10,
marginBottom: "20px",
width: "auto",
minWidth: "400px",
minHeight: "200px",
position: "fixed",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
}}
>
<>
<Typography
variant="h5"
id="modal-title"
sx={{
flex: 1,
ml: 4,
mt: 2,
}}
>
{t("Confirm")}
</Typography>
<>
<Typography
variant="h6"
id="modal-title"
sx={{
flex: 1,
mt: 4,
justifyContent: "center",
textAlign: "center",
}}
>
{t("Are You Sure")}
</Typography>
</>
{/* <ThemeProvider theme={TSMS_BUTTON_THEME}> */}
<Stack direction="row">
<Button
variant="contained"
endIcon={<Check />}
sx={{
flex: 1,
ml: 5,
mr: 2,
mt: 4,
justifyContent: "space-between",
}}
onClick={props.onConfirm}
// LinkComponent={Link}
// href="/settings/department/new"
>
Proceed
</Button>
<Button
variant="contained"
startIcon={<Close />}
sx={{
flex: 1,
mr: 5,
mt: 4,
justifyContent: "space-between",
}}
color="warning"
onClick={props.onCancel}
// LinkComponent={Link}
// href="/settings/department/new"
>
Cancel
</Button>
</Stack>
{/* </ThemeProvider> */}
</>
</Card>
</Modal>
</>
);
};

export default ConfirmModal;

+ 28
- 51
src/components/StaffSearch/StaffSearch.tsx Zobrazit soubor

@@ -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<Props> = ({ staff }) => {
const { t } = useTranslation();
const [filteredStaff, setFilteredStaff] = useState(staff);
const [data, setData] = useState<StaffResult>();
const [isOpen, setIsOpen] = useState(false);
const router = useRouter();

const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
@@ -41,10 +35,10 @@ const StaffSearch: React.FC<Props> = ({ 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<Props> = ({ 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<Column<StaffResult>[]>(
() => [
{
@@ -110,34 +91,30 @@ const StaffSearch: React.FC<Props> = ({ staff }) => {
label: t("Actions"),
onClick: deleteClick,
buttonIcon: <DeleteIcon />,
color: "error",
},
],
[t, onStaffClick, deleteClick],
[t, onStaffClick, deleteClick]
);

return (
<>
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
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),
)
)
);
}}
/>
<SearchResults<StaffResult> items={filteredStaff} columns={columns} />
<ConfirmModal
isOpen={isOpen}
onConfirm={onConfirm}
onCancel={onCancel}
/>
</>
);
};


+ 2
- 2
src/components/SubsidiaryDetail/SubsidiaryDetailWrapper.tsx Zobrazit soubor

@@ -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;

+ 1
- 1
src/components/SubsidiarySearch/SubsidiarySearch.tsx Zobrazit soubor

@@ -46,7 +46,7 @@ const SubsidiarySearch: React.FC<Props> = ({ 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)


+ 27
- 1
src/components/TaskTemplateSearch/TaskTemplateSearch.tsx Zobrazit soubor

@@ -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<Props> = ({ taskTemplates }) => {
const { t } = useTranslation("tasks");
const searchParams = useSearchParams()
const router = useRouter()

const [filteredTemplates, setFilteredTemplates] = useState(taskTemplates);
const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
@@ -30,7 +36,20 @@ const TaskTemplateSearch: React.FC<Props> = ({ 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<Column<TaskTemplate>[]>(
@@ -43,6 +62,13 @@ const TaskTemplateSearch: React.FC<Props> = ({ taskTemplates }) => {
},
{ name: "code", label: t("Task Template Code") },
{ name: "name", label: t("Task Template Name") },
{
name: "id",
label: t("Delete"),
onClick: onDeleteClick,
buttonIcon: <DeleteIcon />,
color: "error"
},
],
[onTaskClick, t],
);


+ 0
- 105
src/components/TeamSearch/ConfirmDeleteModal.tsx Zobrazit soubor

@@ -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> = ({ ...props }) => {
const { t } = useTranslation();
return (
<>
<Modal open={props.isOpen} onClose={props.onCancel}>
<Card
style={{
flex: 10,
marginBottom: "20px",
width: "auto",
minWidth: "400px",
minHeight: "200px",
position: "fixed",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
}}
>
<>
<Typography
variant="h5"
id="modal-title"
sx={{
flex: 1,
ml: 4,
mt: 2,
}}
>
{t("Confirm")}
</Typography>
<>
<Typography
variant="h6"
id="modal-title"
sx={{
flex: 1,
mt: 4,
justifyContent: "center",
textAlign: "center",
}}
>
{t("Are You Sure")}
</Typography>
</>
{/* <ThemeProvider theme={TSMS_BUTTON_THEME}> */}
<Stack direction="row">
<Button
variant="contained"
endIcon={<Check />}
sx={{
flex: 1,
ml: 5,
mr: 2,
mt: 4,
justifyContent: "space-between",
}}
onClick={props.onConfirm}
// LinkComponent={Link}
// href="/settings/department/new"
>
Proceed
</Button>
<Button
variant="contained"
startIcon={<Close />}
sx={{
flex: 1,
mr: 5,
mt: 4,
justifyContent: "space-between",
}}
color="warning"
onClick={props.onCancel}
// LinkComponent={Link}
// href="/settings/department/new"
>
Cancel
</Button>
</Stack>
{/* </ThemeProvider> */}
</>
</Card>
</Modal>
</>
);
};

export default ConfirmModal;

+ 74
- 95
src/components/TeamSearch/TeamSearch.tsx Zobrazit soubor

@@ -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<Omit<TeamResult, "id">>;
type SearchParamNames = keyof SearchQuery;

const TeamSearch: React.FC<Props> = ({ team }) => {
const { t } = useTranslation();
const [filteredTeam, setFilteredTeam] = useState(team);
const [data, setData] = useState<TeamResult>();
const [isOpen, setIsOpen] = useState(false);
const router = useRouter();

const searchCriteria: Criterion<SearchParamNames>[] = 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<SearchParamNames>[] = 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<Column<TeamResult>[]>(
() => [
{
name: "action",
label: t("Edit"),
onClick: onTeamClick,
buttonIcon: <EditNote />,
},
{ name: "name", label: t("Name") },
{ name: "code", label: t("Code") },
{ name: "description", label: t("description") },
{
name: "action",
label: t("Delete"),
onClick: onDeleteClick,
buttonIcon: <DeleteIcon />,
},
],
[t],
);
const columns = useMemo<Column<TeamResult>[]>(
() => [
{
name: "action",
label: t("Edit"),
onClick: onTeamClick,
buttonIcon: <EditNote />,
},
{ 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: <DeleteIcon />,
color: "error"
},
],
[t]
);

return (
<>
<SearchBox
<>
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
// 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())
)
)
}}
/>
<SearchResults<TeamResult> items={filteredTeam} columns={columns} />
<ConfirmModal
isOpen={isOpen}
onConfirm={onConfirm}
onCancel={onCancel}
/>

</>
</>
);
};
export default TeamSearch;

+ 4
- 1
src/components/TimesheetModal/TimesheetModal.tsx Zobrazit soubor

@@ -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<Props> = ({
isOpen,
onClose,
timesheetType,
assignedProjects,
}) => {
const { t } = useTranslation("home");

@@ -73,7 +76,7 @@ const TimesheetModal: React.FC<Props> = ({
marginBlock: 4,
}}
>
<TimesheetTable />
<TimesheetTable assignedProjects={assignedProjects} />
</Box>
<CardActions sx={{ justifyContent: "flex-end" }}>
<Button


+ 13
- 115
src/components/TimesheetTable/EntryInputTable.tsx Zobrazit soubor

@@ -26,113 +26,10 @@ import isBetween from "dayjs/plugin/isBetween";

dayjs.extend(isBetween);

const mockProjects: AssignedProject[] = [
{
id: 1,
name: "Consultancy Project A",
code: "M1001 (C)",
tasks: [
{
id: 1,
name: "1.1 Preparation of preliminary Cost Estimate / Cost Plan including Revised & Refined",
description: null,
taskGroup: {
id: 1,
name: "1. Design & Cost Planning / Estimating",
},
},
{
id: 6,
name: "2.1 Advise on tendering & contractual arrangement",
description: null,
taskGroup: {
id: 2,
name: "2. Tender Documentation",
},
},
],
milestones: {
1: {
startDate: "2000-01-01",
endDate: "2100-01-01",
},
2: {
startDate: "2100-01-01",
endDate: "2100-01-02",
},
},
},
{
id: 2,
name: "Consultancy Project B",
code: "M1354 (C)",
tasks: [
{
id: 1,
name: "1.1 Preparation of preliminary Cost Estimate / Cost Plan including Revised & Refined",
description: null,
taskGroup: {
id: 1,
name: "1. Design & Cost Planning / Estimating",
},
},
{
id: 10,
name: "3.5 Attend tender interviews",
description: null,
taskGroup: {
id: 3,
name: "3. Tender Analysis & Report & Contract Documentation",
},
},
],
milestones: {
1: {
startDate: "2000-01-01",
endDate: "2100-01-01",
},
3: {
startDate: "2100-01-01",
endDate: "2100-01-02",
},
},
},
{
id: 3,
name: "Consultancy Project C",
code: "M1973 (C)",
tasks: [
{
id: 1,
name: "1.1 Preparation of preliminary Cost Estimate / Cost Plan including Revised & Refined",
description: null,
taskGroup: {
id: 1,
name: "1. Design & Cost Planning / Estimating",
},
},
{
id: 20,
name: "4.10 Preparation of Statement of Final Account",
description: null,
taskGroup: {
id: 4,
name: "4. Construction / Post Construction",
},
},
],
milestones: {
1: {
startDate: "2000-01-01",
endDate: "2100-01-01",
},
4: {
startDate: "2100-01-01",
endDate: "2100-01-02",
},
},
},
];
interface Props {
day: string;
assignedProjects: AssignedProject[];
}

type TimeEntryRow = Partial<
TimeEntry & {
@@ -144,10 +41,10 @@ type TimeEntryRow = Partial<
}
>;

const EntryInputTable: React.FC<{ day: string }> = ({ day }) => {
const EntryInputTable: React.FC<Props> = ({ day, assignedProjects }) => {
const { t } = useTranslation("home");
const taskGroupsByProject = useMemo(() => {
return mockProjects.reduce<{
return assignedProjects.reduce<{
[projectId: AssignedProject["id"]]: {
value: TaskGroup["id"];
label: string;
@@ -164,16 +61,16 @@ const EntryInputTable: React.FC<{ day: string }> = ({ day }) => {
),
};
}, {});
}, []);
}, [assignedProjects]);

// To check for start / end planned dates
const milestonesByProject = useMemo(() => {
return mockProjects.reduce<{
return assignedProjects.reduce<{
[projectId: AssignedProject["id"]]: AssignedProject["milestones"];
}>((acc, project) => {
return { ...acc, [project.id]: { ...project.milestones } };
}, {});
}, []);
}, [assignedProjects]);

const { getValues, setValue } = useFormContext<RecordTimesheetInput>();
const currentEntries = getValues(day);
@@ -322,7 +219,7 @@ const EntryInputTable: React.FC<{ day: string }> = ({ day }) => {
editable: true,
type: "singleSelect",
valueOptions() {
return mockProjects.map((p) => ({ value: p.id, label: p.name }));
return assignedProjects.map((p) => ({ value: p.id, label: p.name }));
},
},
{
@@ -339,7 +236,7 @@ const EntryInputTable: React.FC<{ day: string }> = ({ day }) => {
return [];
}

const projectInfo = mockProjects.find(
const projectInfo = assignedProjects.find(
(p) => p.id === updatedRow.projectId,
);

@@ -364,7 +261,7 @@ const EntryInputTable: React.FC<{ day: string }> = ({ day }) => {
return [];
}

const projectInfo = mockProjects.find(
const projectInfo = assignedProjects.find(
(p) => p.id === updatedRow.projectId,
);

@@ -399,6 +296,7 @@ const EntryInputTable: React.FC<{ day: string }> = ({ day }) => {
handleCancel,
apiRef,
taskGroupsByProject,
assignedProjects,
],
);



+ 18
- 7
src/components/TimesheetTable/TimesheetTable.tsx Zobrazit soubor

@@ -18,8 +18,13 @@ import React, { useState } from "react";
import { useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import EntryInputTable from "./EntryInputTable";
import { AssignedProject } from "@/app/api/projects";

const TimesheetTable: React.FC = () => {
interface Props {
assignedProjects: AssignedProject[];
}

const TimesheetTable: React.FC<Props> = ({ assignedProjects }) => {
const { t } = useTranslation("home");

const { watch } = useFormContext<RecordTimesheetInput>();
@@ -40,7 +45,12 @@ const TimesheetTable: React.FC = () => {
{days.map((day, index) => {
const entries = currentInput[day];
return (
<DayRow key={`${day}${index}`} day={day} entries={entries} />
<DayRow
key={`${day}${index}`}
day={day}
entries={entries}
assignedProjects={assignedProjects}
/>
);
})}
</TableBody>
@@ -49,10 +59,11 @@ const TimesheetTable: React.FC = () => {
);
};

const DayRow: React.FC<{ day: string; entries: TimeEntry[] }> = ({
day,
entries,
}) => {
const DayRow: React.FC<{
day: string;
entries: TimeEntry[];
assignedProjects: AssignedProject[];
}> = ({ day, entries, assignedProjects }) => {
const {
t,
i18n: { language },
@@ -106,7 +117,7 @@ const DayRow: React.FC<{ day: string; entries: TimeEntry[] }> = ({
>
<Collapse in={open} timeout="auto" unmountOnExit>
<Box>
<EntryInputTable day={day} />
<EntryInputTable day={day} assignedProjects={assignedProjects} />
</Box>
</Collapse>
</TableCell>


+ 1
- 1
src/components/TransferList/TransferList.tsx Zobrazit soubor

@@ -109,7 +109,7 @@ const ItemList: React.FC<ItemListProps> = ({
</ListItemIcon>
<Stack>
<Typography variant="subtitle2">{label}</Typography>
<Typography variant="caption">{`${checkedItems.length}/${items.length} selected`}</Typography>
<Typography variant="caption">{`${checkedItems.length}/${items.length} ${t("selected")}`}</Typography>
</Stack>
</Stack>
<Divider />


+ 98
- 0
src/components/UserSearch/UserSearch.tsx Zobrazit soubor

@@ -0,0 +1,98 @@
"use client";

import SearchBox, { Criterion } from "../SearchBox";
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 { useRouter } from "next/navigation";
import { deleteDialog, successDialog } from "../Swal/CustomAlerts";
import { UserResult } from "@/app/api/user";
import { deleteUser } from "@/app/api/user/actions";

interface Props {
users: UserResult[];
}
type SearchQuery = Partial<Omit<UserResult, "id">>;
type SearchParamNames = keyof SearchQuery;

const UserSearch: React.FC<Props> = ({ users }) => {
const { t } = useTranslation();
const [filteredUser, setFilteredUser] = useState(users);
const router = useRouter();

const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
() => [
{
label: t("User Name"),
paramName: "title",
type: "text",
},
],
[t]
);

const onUserClick = useCallback(
(users: UserResult) => {
console.log(users);
router.push(`/settings/user/edit?id=${users.id}`)
},
[router, t]
);

const onDeleteClick = useCallback((users: UserResult) => {
deleteDialog(async () => {
await deleteUser(users.id);

successDialog(t("Delete Success"), t);

setFilteredUser((prev) => prev.filter((obj) => obj.id !== users.id));
}, t);
}, []);

const columns = useMemo<Column<UserResult>[]>(
() => [
{
name: "action",
label: t("Edit"),
onClick: onUserClick,
buttonIcon: <EditNote />,
},
{ name: "name", label: t("UserName") },
{ name: "fullName", label: t("FullName") },
{ name: "title", label: t("Title") },
{ name: "department", label: t("Department") },
{ name: "email", label: t("Email") },
{ name: "phone1", label: t("Phone") },
{
name: "action",
label: t("Delete"),
onClick: onDeleteClick,
buttonIcon: <DeleteIcon />,
color: "error"
},
],
[t]
);

return (
<>
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
// setFilteredUser(
// users.filter(
// (t) =>
// t.name.toLowerCase().includes(query.name.toLowerCase()) &&
// t.code.toLowerCase().includes(query.code.toLowerCase()) &&
// t.description.toLowerCase().includes(query.description.toLowerCase())
// )
// )
}}
/>
<SearchResults<UserResult> items={filteredUser} columns={columns} />
</>
);
};
export default UserSearch;

+ 40
- 0
src/components/UserSearch/UserSearchLoading.tsx Zobrazit soubor

@@ -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 UserSearchLoading: React.FC = () => {
return (
<>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton
variant="rounded"
height={50}
width={100}
sx={{ alignSelf: "flex-end" }}
/>
</Stack>
</CardContent>
</Card>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
</Stack>
</CardContent>
</Card>
</>
);
};

export default UserSearchLoading;

+ 19
- 0
src/components/UserSearch/UserSearchWrapper.tsx Zobrazit soubor

@@ -0,0 +1,19 @@
import React from "react";
import UserSearch from "./UserSearch";
import UserSearchLoading from "./UserSearchLoading";
import { UserResult, fetchUser } from "@/app/api/user";

interface SubComponents {
Loading: typeof UserSearchLoading;
}

const UserSearchWrapper: React.FC & SubComponents = async () => {
const users = await fetchUser()
console.log(users);

return <UserSearch users={users} />;
};

UserSearchWrapper.Loading = UserSearchLoading;

export default UserSearchWrapper;

+ 1
- 0
src/components/UserSearch/index.ts Zobrazit soubor

@@ -0,0 +1 @@
export { default } from "./UserSearchWrapper";

+ 10
- 43
src/components/UserWorkspacePage/AssignedProjects.tsx Zobrazit soubor

@@ -1,15 +1,10 @@
import React, { useEffect, useMemo } from "react";
import React, { useEffect } from "react";
import {
Card,
CardContent,
FormControl,
Grid,
IconButton,
InputAdornment,
InputLabel,
MenuItem,
Select,
SelectChangeEvent,
Stack,
TextField,
Typography,
@@ -18,13 +13,15 @@ import { useTranslation } from "react-i18next";
import { Clear, Search } from "@mui/icons-material";
import ProjectGrid from "./ProjectGrid";
import { Props as UserWorkspaceProps } from "./UserWorkspacePage";
import uniq from "lodash/uniq";

const AssignedProjects: React.FC<UserWorkspaceProps> = ({ allProjects }) => {
const AssignedProjects: React.FC<UserWorkspaceProps> = ({
assignedProjects,
}) => {
const { t } = useTranslation("home");

// Projects
const [filteredProjects, setFilterProjects] = React.useState(allProjects);
const [filteredProjects, setFilterProjects] =
React.useState(assignedProjects);

// Query related
const [query, setQuery] = React.useState("");
@@ -37,28 +34,15 @@ const AssignedProjects: React.FC<UserWorkspaceProps> = ({ allProjects }) => {
setQuery("");
}, []);

// Filter
const allStatuses = useMemo(() => {
return uniq([
"All",
...allProjects.map((project) => project.projectStatus),
]);
}, [allProjects]);
const [statusFilter, setStatusFilter] = React.useState("All");
const onStatusChange = React.useCallback((e: SelectChangeEvent) => {
setStatusFilter(e.target.value);
}, []);

useEffect(() => {
setFilterProjects(
allProjects.filter(
assignedProjects.filter(
(p) =>
(p.code.toLowerCase().includes(query.toLowerCase()) ||
p.name.toLowerCase().includes(query.toLowerCase())) &&
(p.projectStatus === statusFilter || statusFilter === "All"),
p.code.toLowerCase().includes(query.toLowerCase()) ||
p.name.toLowerCase().includes(query.toLowerCase()),
),
);
}, [allProjects, query, statusFilter]);
}, [assignedProjects, query]);

return (
<>
@@ -88,23 +72,6 @@ const AssignedProjects: React.FC<UserWorkspaceProps> = ({ allProjects }) => {
}}
/>
</Grid>
<Grid item xs={3}>
<FormControl fullWidth>
<InputLabel size="small">{t("Project Status")}</InputLabel>
<Select
label={t("Project Status")}
size="small"
value={statusFilter}
onChange={onStatusChange}
>
{allStatuses.map((option, index) => (
<MenuItem key={`${option}-${index}`} value={option}>
{option}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
</Grid>
</Stack>
</CardContent>


+ 3
- 20
src/components/UserWorkspacePage/ProjectGrid.tsx Zobrazit soubor

@@ -1,11 +1,11 @@
import React from "react";
import { ProjectHours } from "./UserWorkspaceWrapper";
import { Box, Card, CardContent, Chip, Grid, Typography } from "@mui/material";
import { Box, Card, CardContent, Grid, Typography } from "@mui/material";
import { useTranslation } from "react-i18next";
import { manhourFormatter } from "@/app/utils/formatUtil";
import { AssignedProject } from "@/app/api/projects";

interface Props {
projects: ProjectHours[];
projects: AssignedProject[];
}

const ProjectGrid: React.FC<Props> = ({ projects }) => {
@@ -17,23 +17,6 @@ const ProjectGrid: React.FC<Props> = ({ projects }) => {
<Grid key={`${project.code}${idx}`} item xs={4}>
<Card>
<CardContent>
<Box
sx={{
display: "flex",
justifyContent: "flex-end",
marginBlockEnd: 1,
}}
>
<Chip
size="small"
label={project.projectStatus}
color={
project.projectStatus === "On Track"
? "success"
: "warning"
}
/>
</Box>
<Typography variant="overline">{project.code}</Typography>
<Typography
variant="h6"


+ 39
- 22
src/components/UserWorkspacePage/UserWorkspacePage.tsx Zobrazit soubor

@@ -8,14 +8,14 @@ import { Add } from "@mui/icons-material";
import { Typography } from "@mui/material";
import ButtonGroup from "@mui/material/ButtonGroup";
import AssignedProjects from "./AssignedProjects";
import { ProjectHours } from "./UserWorkspaceWrapper";
import TimesheetModal from "../TimesheetModal";
import { AssignedProject } from "@/app/api/projects";

export interface Props {
allProjects: ProjectHours[];
assignedProjects: AssignedProject[];
}

const UserWorkspacePage: React.FC<Props> = ({ allProjects }) => {
const UserWorkspacePage: React.FC<Props> = ({ assignedProjects }) => {
const [isTimeheetModalVisible, setTimeheetModalVisible] = useState(false);
const [isLeaveModalVisible, setLeaveModalVisible] = useState(false);
const { t } = useTranslation("home");
@@ -53,27 +53,44 @@ const UserWorkspacePage: React.FC<Props> = ({ allProjects }) => {
flexWrap="wrap"
spacing={2}
>
<ButtonGroup variant="contained">
<Button startIcon={<Add />} onClick={handleAddTimesheetButtonClick}>
{t("Enter Time")}
</Button>
<Button startIcon={<Add />} onClick={handleAddLeaveButtonClick}>
{t("Record Leave")}
</Button>
</ButtonGroup>
{Boolean(assignedProjects.length) && (
<ButtonGroup variant="contained">
<Button
startIcon={<Add />}
onClick={handleAddTimesheetButtonClick}
>
{t("Enter Time")}
</Button>
<Button startIcon={<Add />} onClick={handleAddLeaveButtonClick}>
{t("Record Leave")}
</Button>
</ButtonGroup>
)}
</Stack>
</Stack>
<TimesheetModal
timesheetType="time"
isOpen={isTimeheetModalVisible}
onClose={handleCloseTimesheetModal}
/>
<TimesheetModal
timesheetType="leave"
isOpen={isLeaveModalVisible}
onClose={handleCloseLeaveModal}
/>
<AssignedProjects allProjects={allProjects} />
{assignedProjects.length > 0 ? (
<>
<TimesheetModal
timesheetType="time"
isOpen={isTimeheetModalVisible}
onClose={handleCloseTimesheetModal}
assignedProjects={assignedProjects}
/>
<TimesheetModal
timesheetType="leave"
isOpen={isLeaveModalVisible}
onClose={handleCloseLeaveModal}
assignedProjects={assignedProjects}
/>
<AssignedProjects assignedProjects={assignedProjects} />
</>
) : (
<>
<Typography variant="subtitle1">
{t("You have no assigned projects!")}
</Typography>
</>
)}
</>
);
};


+ 4
- 60
src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx Zobrazit soubor

@@ -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 <UserWorkspacePage allProjects={mockProjectCards} />;
const UserWorkspaceWrapper: React.FC = async () => {
const assignedProjects = await fetchAssignedProjects();
return <UserWorkspacePage assignedProjects={assignedProjects} />;
};

export default UserWorkspaceWrapper;

+ 1
- 0
src/i18n/en/claim.json Zobrazit soubor

@@ -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"
}

+ 2
- 0
src/i18n/en/common.json Zobrazit soubor

@@ -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",


+ 3
- 0
src/i18n/en/report.json Zobrazit soubor

@@ -0,0 +1,3 @@
{
"Project": "Project"
}

+ 27
- 0
src/i18n/en/tasks.json Zobrazit soubor

@@ -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"
}

+ 1
- 0
src/i18n/zh/claim.json Zobrazit soubor

@@ -31,6 +31,7 @@
"Please ensure the projects are selected": "請確保所有項目欄位已選擇",
"Please ensure the amount are correct": "請確保所有金額輸入正確",

"Details": "詳請",
"Description": "描述",
"Actions": "行動"
}

+ 2
- 0
src/i18n/zh/common.json Zobrazit soubor

@@ -15,6 +15,8 @@
"Do you want to delete?": "你是否確認要刪除?",
"Delete Success": "刪除成功",
"Details": "詳情",
"Delete": "刪除",
"Search": "搜尋",
"Search Criteria": "搜尋條件",
"Cancel": "取消",


+ 3
- 0
src/i18n/zh/report.json Zobrazit soubor

@@ -0,0 +1,3 @@
{
"Project": "項目"
}

+ 27
- 0
src/i18n/zh/tasks.json Zobrazit soubor

@@ -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": "確認"
}

Načítá se…
Zrušit
Uložit