| @@ -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("tasks", "common"); | |||
| return ( | |||
| <Stack spacing={2}> | |||
| <Typography variant="h4">{t("Not Found")}</Typography> | |||
| <Typography variant="body1">{t("The task template was not found!")}</Typography> | |||
| <Link href="/projects" component={NextLink} variant="body2"> | |||
| {t("Return to all task templates")} | |||
| </Link> | |||
| </Stack> | |||
| ); | |||
| } | |||
| @@ -1,23 +1,44 @@ | |||
| import { preloadAllTasks } from "@/app/api/tasks"; | |||
| import { fetchTaskTemplate, 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"; | |||
| import { ServerFetchError } from "@/app/utils/fetchUtil"; | |||
| import { isArray } from "lodash"; | |||
| import { notFound } from "next/navigation"; | |||
| export const metadata: Metadata = { | |||
| title: "Edit Task Template", | |||
| }; | |||
| const TaskTemplates: React.FC = async () => { | |||
| interface Props { | |||
| searchParams: { [key: string]: string | string[] | undefined }; | |||
| } | |||
| const TaskTemplates: React.FC<Props> = async ({ searchParams }) => { | |||
| const { t } = await getServerI18n("tasks"); | |||
| const taskTemplateId = searchParams["id"]; | |||
| if (!taskTemplateId || isArray(taskTemplateId)) { | |||
| notFound(); | |||
| } | |||
| preloadAllTasks(); | |||
| try { | |||
| await fetchTaskTemplate(taskTemplateId); | |||
| } catch (e) { | |||
| if (e instanceof ServerFetchError && e.response?.status === 404) { | |||
| notFound(); | |||
| } | |||
| } | |||
| return ( | |||
| <> | |||
| <Typography variant="h4">{t("Edit Task Template")}</Typography> | |||
| <I18nProvider namespaces={["tasks", "common"]}> | |||
| <CreateTaskTemplate /> | |||
| <CreateTaskTemplate taskTemplateId={taskTemplateId}/> | |||
| </I18nProvider> | |||
| </> | |||
| ); | |||
| @@ -4,13 +4,26 @@ import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil | |||
| import { BASE_API_URL } from "@/config/api"; | |||
| import { TaskTemplate } from "."; | |||
| import { revalidateTag } from "next/cache"; | |||
| import { ManhourAllocation } from "@/app/api/projects/actions"; | |||
| import { Task, TaskGroup } from '@/app/api/tasks'; | |||
| export interface NewTaskTemplateFormInputs { | |||
| // task template | |||
| code: string; | |||
| name: string; | |||
| taskIds: number[]; | |||
| id: number | null; | |||
| // resource allocation template | |||
| manhourPercentageByGrade: ManhourAllocation; | |||
| taskGroups: { | |||
| [taskGroup: TaskGroup["id"]]: { | |||
| taskIds: Task["id"][]; | |||
| percentAllocation: number; | |||
| }; | |||
| }; | |||
| } | |||
| export const saveTaskTemplate = async (data: NewTaskTemplateFormInputs) => { | |||
| @@ -39,3 +39,15 @@ export const preloadAllTasks = () => { | |||
| export const fetchAllTasks = cache(async () => { | |||
| return serverFetchJson<Task[]>(`${BASE_API_URL}/tasks`); | |||
| }); | |||
| export const fetchTaskTemplate = cache(async (id: string) => { | |||
| const taskTemplate = await serverFetchJson<TaskTemplate>( | |||
| `${BASE_API_URL}/tasks/templatesDetails/${id}`, | |||
| { | |||
| method: "GET", | |||
| headers: { "Content-Type": "application/json" }, | |||
| }, | |||
| ); | |||
| return taskTemplate; | |||
| }); | |||
| @@ -13,7 +13,7 @@ import Close from "@mui/icons-material/Close"; | |||
| import { useRouter, useSearchParams } from "next/navigation"; | |||
| import React from "react"; | |||
| import Stack from "@mui/material/Stack"; | |||
| import { Task } from "@/app/api/tasks"; | |||
| import { Task, TaskTemplate } from "@/app/api/tasks"; | |||
| import { | |||
| NewTaskTemplateFormInputs, | |||
| fetchTaskTemplate, | |||
| @@ -24,9 +24,10 @@ import { errorDialog, submitDialog, successDialog } from "../Swal/CustomAlerts"; | |||
| interface Props { | |||
| tasks: Task[]; | |||
| defaultInputs?: TaskTemplate; | |||
| } | |||
| const CreateTaskTemplate: React.FC<Props> = ({ tasks }) => { | |||
| const CreateTaskTemplate: React.FC<Props> = ({ tasks, defaultInputs }) => { | |||
| const { t } = useTranslation(); | |||
| const searchParams = useSearchParams() | |||
| @@ -54,45 +55,45 @@ const CreateTaskTemplate: React.FC<Props> = ({ tasks }) => { | |||
| watch, | |||
| resetField, | |||
| formState: { errors, isSubmitting }, | |||
| } = useForm<NewTaskTemplateFormInputs>({ defaultValues: { taskIds: [] } }); | |||
| } = useForm<NewTaskTemplateFormInputs>({ defaultValues: defaultInputs }); | |||
| const currentTaskIds = watch("taskIds"); | |||
| const selectedItems = React.useMemo(() => { | |||
| 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 [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) => { | |||
| @@ -120,9 +121,9 @@ const CreateTaskTemplate: React.FC<Props> = ({ tasks }) => { | |||
| return ( | |||
| <> | |||
| { | |||
| (id === null || refTaskTemplate !== undefined) && <Stack component="form" onSubmit={handleSubmit(onSubmit)} gap={2}> | |||
| <Stack component="form" onSubmit={handleSubmit(onSubmit)} gap={2}> | |||
| <Card> | |||
| {/* Task List Setup */} | |||
| <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | |||
| <Typography variant="overline">{t("Task List Setup")}</Typography> | |||
| <Grid | |||
| @@ -166,6 +167,9 @@ const CreateTaskTemplate: React.FC<Props> = ({ tasks }) => { | |||
| allItemsLabel={t("Task Pool")} | |||
| selectedItemsLabel={t("Task List Template")} | |||
| /> | |||
| {/* Task List Setup */} | |||
| {/* Task List Setup */} | |||
| </CardContent> | |||
| </Card> | |||
| { | |||
| @@ -188,7 +192,7 @@ const CreateTaskTemplate: React.FC<Props> = ({ tasks }) => { | |||
| {t("Confirm")} | |||
| </Button> | |||
| </Stack> | |||
| </Stack >} | |||
| </Stack > | |||
| </> | |||
| ); | |||
| }; | |||
| @@ -1,11 +1,18 @@ | |||
| import React from "react"; | |||
| import CreateTaskTemplate from "./CreateTaskTemplate"; | |||
| import { fetchAllTasks } from "@/app/api/tasks"; | |||
| import { fetchAllTasks, fetchTaskTemplate } from "@/app/api/tasks"; | |||
| const CreateTaskTemplateWrapper: React.FC = async () => { | |||
| const tasks = await fetchAllTasks(); | |||
| interface Props { | |||
| taskTemplateId?: string; | |||
| } | |||
| return <CreateTaskTemplate tasks={tasks} />; | |||
| const CreateTaskTemplateWrapper: React.FC<Props> = async (props) => { | |||
| const [tasks] = await Promise.all([ | |||
| fetchAllTasks(), | |||
| ]); | |||
| const taskTemplateInfo = props.taskTemplateId ? await fetchTaskTemplate(props.taskTemplateId) : undefined | |||
| return <CreateTaskTemplate tasks={tasks} defaultInputs={taskTemplateInfo}/>; | |||
| }; | |||
| export default CreateTaskTemplateWrapper; | |||
| @@ -51,13 +51,17 @@ export const warningDialog = (text, t) => { | |||
| }) | |||
| } | |||
| export const submitDialog = async (confirmAction, t, {...props}) => { | |||
| export const submitDialog = async (confirmAction, t, {...props} = { | |||
| title: null, | |||
| confirmButtonText: null | |||
| }) => { | |||
| // console.log(props) | |||
| // const { t } = useTranslation("common") | |||
| const result = await Swal.fire({ | |||
| icon: "question", | |||
| title: props.title ?? t("Do you want to submit?"), | |||
| title: props?.title ?? t("Do you want to submit?"), | |||
| cancelButtonText: t("Cancel"), | |||
| confirmButtonText: props.confirmButtonText ?? t("Submit"), | |||
| confirmButtonText: props?.confirmButtonText ?? t("Submit"), | |||
| showCancelButton: true, | |||
| showConfirmButton: true, | |||
| customClass: { | |||