Sfoglia il codice sorgente

Add copy project function

tags/Baseline_180220205_Frontend
cyril.tsui 7 mesi fa
parent
commit
7ba7f838c9
8 ha cambiato i file con 262 aggiunte e 7 eliminazioni
  1. +17
    -0
      src/app/(main)/projects/copy/not-found.tsx
  2. +77
    -0
      src/app/(main)/projects/copy/page.tsx
  3. +17
    -0
      src/app/(main)/projects/copySub/not-found.tsx
  4. +79
    -0
      src/app/(main)/projects/copySub/page.tsx
  5. +3
    -1
      src/components/CreateProject/CreateProject.tsx
  6. +29
    -5
      src/components/CreateProject/CreateProjectWrapper.tsx
  7. +22
    -0
      src/components/ProjectSearch/ProjectSearch.tsx
  8. +18
    -1
      src/components/SearchResults/SearchResults.tsx

+ 17
- 0
src/app/(main)/projects/copy/not-found.tsx Vedi File

@@ -0,0 +1,17 @@
import { getServerI18n } from "@/i18n";
import { Stack, Typography, Link } from "@mui/material";
import NextLink from "next/link";

export default async function NotFound() {
const { t } = await getServerI18n("projects", "common");

return (
<Stack spacing={2}>
<Typography variant="h4">{t("Not Found")}</Typography>
<Typography variant="body1">{t("The project was not found!")}</Typography>
<Link href="/projects" component={NextLink} variant="body2">
{t("Return to all projects")}
</Link>
</Stack>
);
}

+ 77
- 0
src/app/(main)/projects/copy/page.tsx Vedi File

@@ -0,0 +1,77 @@
import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer";
import { fetchGrades } from "@/app/api/grades";
import {
fetchProjectBuildingTypes,
fetchProjectCategories,
fetchProjectContractTypes,
fetchProjectDetails,
fetchProjectFundingTypes,
fetchProjectLocationTypes,
fetchProjectServiceTypes,
fetchProjectWorkNatures,
} from "@/app/api/projects";
import { preloadStaff, preloadTeamLeads } from "@/app/api/staff";
import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks";
import { fetchUserAbilities } from "@/app/utils/fetchUtil";
import { ServerFetchError } from "@/app/utils/fetchUtil";
import CreateProject from "@/components/CreateProject";
import { I18nProvider, getServerI18n } from "@/i18n";
import { MAINTAIN_PROJECT } from "@/middleware";
import Typography from "@mui/material/Typography";
import { isArray } from "lodash";
import { Metadata } from "next";
import { notFound } from "next/navigation";

interface Props {
searchParams: { [key: string]: string | string[] | undefined };
}

export const metadata: Metadata = {
title: "Copy Project",
};

const Projects: React.FC<Props> = async ({ searchParams }) => {
const { t } = await getServerI18n("projects");
// Assume projectId is string here
const projectId = searchParams["id"];
const abilities = await fetchUserAbilities()

if (!projectId || isArray(projectId) || ![MAINTAIN_PROJECT].some(ability => abilities.includes(ability))) {
notFound();
}

// Preload necessary dependencies
fetchAllTasks();
fetchTaskTemplates();
fetchProjectCategories();
fetchProjectContractTypes();
fetchProjectFundingTypes();
fetchProjectLocationTypes();
fetchProjectServiceTypes();
fetchProjectBuildingTypes();
fetchProjectWorkNatures();
fetchAllCustomers();
fetchAllSubsidiaries();
fetchGrades();
preloadTeamLeads();
preloadStaff();

try {
console.log(projectId)
await fetchProjectDetails(projectId);
} catch (e) {
if (e instanceof ServerFetchError && e.response?.status === 404) {
notFound();
}
}

return (
<>
<I18nProvider namespaces={["projects"]}>
<CreateProject isCopyMode projectId={projectId} />
</I18nProvider>
</>
);
};

export default Projects;

+ 17
- 0
src/app/(main)/projects/copySub/not-found.tsx Vedi File

@@ -0,0 +1,17 @@
import { getServerI18n } from "@/i18n";
import { Stack, Typography, Link } from "@mui/material";
import NextLink from "next/link";

export default async function NotFound() {
const { t } = await getServerI18n("projects", "common");

return (
<Stack spacing={2}>
<Typography variant="h4">{t("Not Found")}</Typography>
<Typography variant="body1">{t("The sub project was not found!")}</Typography>
<Link href="/projects" component={NextLink} variant="body2">
{t("Return to all projects")}
</Link>
</Stack>
);
}

+ 79
- 0
src/app/(main)/projects/copySub/page.tsx Vedi File

@@ -0,0 +1,79 @@
import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer";
import { fetchGrades } from "@/app/api/grades";
import {
fetchMainProjects,
fetchProjectBuildingTypes,
fetchProjectCategories,
fetchProjectContractTypes,
fetchProjectDetails,
fetchProjectFundingTypes,
fetchProjectLocationTypes,
fetchProjectServiceTypes,
fetchProjectWorkNatures,
} from "@/app/api/projects";
import { preloadStaff, preloadTeamLeads } from "@/app/api/staff";
import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks";
import { fetchUserAbilities } from "@/app/utils/fetchUtil";
import CreateProject from "@/components/CreateProject";
import { I18nProvider, getServerI18n } from "@/i18n";
import { MAINTAIN_PROJECT } from "@/middleware";
import Typography from "@mui/material/Typography";
import { isArray } from "lodash";
import { Metadata } from "next";
import { notFound } from "next/navigation";

interface Props {
searchParams: { [key: string]: string | string[] | undefined };
}

export const metadata: Metadata = {
title: "Edit Sub Project",
};

const Projects: React.FC<Props> = async ({ searchParams }) => {
const { t } = await getServerI18n("projects");
const projectId = searchParams["id"];

const abilities = await fetchUserAbilities()
if (!projectId || isArray(projectId) || !abilities.includes(MAINTAIN_PROJECT)) {
notFound();
}

// Preload necessary dependencies
fetchAllTasks();
fetchTaskTemplates();
fetchProjectCategories();
fetchProjectContractTypes();
fetchProjectFundingTypes();
fetchProjectLocationTypes();
fetchProjectServiceTypes();
fetchProjectBuildingTypes();
fetchProjectWorkNatures();
fetchAllCustomers();
fetchAllSubsidiaries();
fetchGrades();
preloadTeamLeads();
preloadStaff();

try {
await fetchProjectDetails(projectId);
const data = await fetchMainProjects();

if (!Boolean(data) || data.length === 0) {
notFound();
}
} catch (e) {
notFound();
}

return (
<>
<Typography variant="h4">{t("Edit Sub Project")}</Typography>
<I18nProvider namespaces={["projects"]}>
<CreateProject isCopyMode isSubProject projectId={projectId}/>
</I18nProvider>
</>
);
};

export default Projects;

+ 3
- 1
src/components/CreateProject/CreateProject.tsx Vedi File

@@ -64,6 +64,7 @@ import { deleteDraft, loadDraft, saveToLocalStorage } from "@/app/utils/draftUti

export interface Props {
isEditMode: boolean;
isCopyMode: boolean;
draftId?: number;
isSubProject: boolean;
mainProjects?: MainProject[];
@@ -116,6 +117,7 @@ const hasErrorsInTab = (

const CreateProject: React.FC<Props> = ({
isEditMode,
isCopyMode,
draftId,
isSubProject,
mainProjects,
@@ -546,7 +548,7 @@ const CreateProject: React.FC<Props> = ({
}
}, [totalManhour]);

const loading = isEditMode ? !Boolean(projectName) : false;
const loading = isEditMode || isCopyMode ? !Boolean(projectName) : false;

const submitDisabled =
loading ||


+ 29
- 5
src/components/CreateProject/CreateProjectWrapper.tsx Vedi File

@@ -21,17 +21,26 @@ import { fetchGrades } from "@/app/api/grades";
import { fetchUserAbilities, fetchUserStaff } from "@/app/utils/fetchUtil";

type CreateProjectProps = {
isEditMode: false;
isEditMode?: false;
isCopyMode?: false;
isSubProject?: boolean;
draftId?: number;
};
interface EditProjectProps {
isEditMode: true;
isCopyMode?: false;
projectId?: string;
isSubProject?: boolean;
}

type Props = CreateProjectProps | EditProjectProps;
interface CopyProjectProps {
isEditMode?: false;
isCopyMode: true;
projectId?: string;
isSubProject?: boolean;
}

type Props = CreateProjectProps | EditProjectProps | CopyProjectProps;

const CreateProjectWrapper: React.FC<Props> = async (props) => {
const [
@@ -79,7 +88,7 @@ const CreateProjectWrapper: React.FC<Props> = async (props) => {
(teamLead) => teamLead.teamId === teamId || teamLead.team == "ST",
)
}
const projectInfo = props.isEditMode
const projectInfo = props.isEditMode || props.isCopyMode
? await fetchProjectDetails(props.projectId!)
: undefined;

@@ -87,10 +96,25 @@ const CreateProjectWrapper: React.FC<Props> = async (props) => {
? await fetchMainProjects()
: undefined;

if (props.isCopyMode && projectInfo) {
projectInfo.projectId = null
projectInfo.projectCode = projectInfo.projectCode + "-copy"
projectInfo.projectName = projectInfo.projectName + "-copy"
projectInfo.projectStatus = ""
Object.entries(projectInfo.milestones).forEach(([key, value]) => {
projectInfo.milestones[Number(key)].payments.forEach(({ ...rest}, idx, orig) => {
orig[idx] = { ...rest, id: rest.id * -1 }
})

// console.log(projectInfo.milestones[Number(key)].payments)
})
}

return (
<CreateProject
isEditMode={props.isEditMode}
draftId={props.isEditMode ? undefined : props.draftId}
isEditMode={Boolean(props.isEditMode)}
isCopyMode={Boolean(props.isCopyMode)}
draftId={props.isEditMode || props.isCopyMode ? undefined : props.draftId}
isSubProject={Boolean(props.isSubProject)}
defaultInputs={projectInfo}
allTasks={tasks}


+ 22
- 0
src/components/ProjectSearch/ProjectSearch.tsx Vedi File

@@ -13,6 +13,7 @@ import { reverse, uniqBy } from "lodash";
import { loadDrafts } from "@/app/utils/draftUtils";
import { TeamResult } from "@/app/api/team";
import { Customer } from "@/app/api/customer";
import ContentCopyIcon from '@mui/icons-material/ContentCopy';

type ProjectResultOrDraft = ProjectResult & { isDraft?: boolean };

@@ -129,6 +130,17 @@ const ProjectSearch: React.FC<Props> = ({
[router],
);

const onProjectCopyClick = useCallback(
(project: ProjectResultOrDraft) => {
if (!project.isDraft) {
if (Boolean(project.mainProject)) {
router.push(`/projects/copySub?id=${project.id}`);
} else router.push(`/projects/copy?id=${project.id}`);
}
},
[router],
);

const columns = useMemo<Column<ProjectResult>[]>(
() => [
{
@@ -138,6 +150,16 @@ const ProjectSearch: React.FC<Props> = ({
buttonIcon: <EditNote />,
disabled: !abilities.includes(MAINTAIN_PROJECT),
},
{
name: "id",
label: t("Copy"),
onClick: onProjectCopyClick,
buttonIcon: <ContentCopyIcon />,
disabled: !abilities.includes(MAINTAIN_PROJECT),
disabledRows: {
status: ["Draft"]
}
},
{ name: "code", label: t("Project Code") },
{ name: "name", label: t("Project Name") },
{ name: "category", label: t("Project Category") },


+ 18
- 1
src/components/SearchResults/SearchResults.tsx Vedi File

@@ -35,6 +35,7 @@ interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> {
onClick: (item: T) => void;
buttonIcon: React.ReactNode;
disabled?: boolean;
disabledRows?: { [columnName in keyof T]: string[] }; // Filter the row which is going to be disabled
}

export type Column<T extends ResultWithId> =
@@ -84,6 +85,22 @@ function SearchResults<T extends ResultWithId>({
setPage(0);
};

const disabledRows = <T extends ResultWithId> (
column: ColumnWithAction<T>,
item: T
): Boolean => {
if (column.disabledRows) {
for (const [key, value] of Object.entries(column.disabledRows)) {
if (value
.map(v => v.toLowerCase())
.includes(String(item[key as keyof T]).toLowerCase())
) return true;
}
}

return false;
};

const table = (
<>
<TableContainer sx={{ maxHeight: 440 }}>
@@ -112,7 +129,7 @@ function SearchResults<T extends ResultWithId>({
<IconButton
color={column.color ?? "primary"}
onClick={() => column.onClick(item)}
disabled={Boolean(column.disabled)}
disabled={Boolean(column.disabled) || Boolean(disabledRows(column, item))}
>
{column.buttonIcon}
</IconButton>


Caricamento…
Annulla
Salva