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