@@ -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> | |||
</> | |||
); | |||
@@ -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> | |||
</> | |||
); | |||
@@ -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> | |||
</> | |||
); | |||
}; | |||
@@ -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,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,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 | |||
}; |
@@ -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 +0,0 @@ | |||
export { default } from "./CustomerDetailWrapper"; |
@@ -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; |
@@ -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; |
@@ -0,0 +1 @@ | |||
export { default } from "./CustomerSaveWrapper"; |
@@ -68,7 +68,7 @@ const StaffSearch: React.FC<Props> = ({ staff }) => { | |||
const deleteClick = useCallback((staff: StaffResult) => { | |||
deleteDialog(async () => { | |||
await deleteStaff(staff.id); | |||
successDialog("Delete Success", t); | |||
successDialog(t("Delete Success"), t); | |||
setFilteredStaff((prev) => prev.filter((obj) => obj.id !== staff.id)); | |||
}, t); | |||
}, []); | |||
@@ -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; |
@@ -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) | |||
@@ -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], | |||
); | |||
@@ -56,7 +56,7 @@ const TeamSearch: React.FC<Props> = ({ team }) => { | |||
deleteDialog(async () => { | |||
await deleteTeam(team.id); | |||
successDialog("Delete Success", t); | |||
successDialog(t("Delete Success"), t); | |||
setFilteredTeam((prev) => prev.filter((obj) => obj.id !== team.id)); | |||
}, t); | |||
@@ -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 /> | |||
@@ -45,7 +45,7 @@ const UserSearch: React.FC<Props> = ({ users }) => { | |||
deleteDialog(async () => { | |||
await deleteUser(users.id); | |||
successDialog("Delete Success", t); | |||
successDialog(t("Delete Success"), t); | |||
setFilteredUser((prev) => prev.filter((obj) => obj.id !== users.id)); | |||
}, t); | |||
@@ -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", | |||
@@ -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" | |||
} |
@@ -15,6 +15,8 @@ | |||
"Do you want to delete?": "你是否確認要刪除?", | |||
"Delete Success": "刪除成功", | |||
"Details": "詳情", | |||
"Delete": "刪除", | |||
"Search": "搜尋", | |||
"Search Criteria": "搜尋條件", | |||
"Cancel": "取消", | |||
@@ -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": "確認" | |||
} |