Ver a proveniência

update tasks (can edit now), staff, subsidiary, customer, user, rename customer,

tags/Baseline_30082024_FRONTEND_UAT
cyril.tsui há 1 ano
ascendente
cometimento
6fdbe94610
25 ficheiros alterados com 291 adições e 96 eliminações
  1. +2
    -2
      src/app/(main)/settings/customer/create/page.tsx
  2. +2
    -2
      src/app/(main)/settings/customer/edit/page.tsx
  3. +4
    -1
      src/app/(main)/tasks/create/page.tsx
  4. +26
    -0
      src/app/(main)/tasks/edit/page.tsx
  5. +8
    -5
      src/app/(main)/tasks/page.tsx
  6. +28
    -2
      src/app/api/tasks/actions.ts
  7. +122
    -69
      src/components/CreateTaskTemplate/CreateTaskTemplate.tsx
  8. +0
    -1
      src/components/CustomerDetail/index.ts
  9. +0
    -0
      src/components/CustomerSave/ContactInfo.tsx
  10. +0
    -0
      src/components/CustomerSave/CustomerInfo.tsx
  11. +2
    -2
      src/components/CustomerSave/CustomerSave.tsx
  12. +4
    -4
      src/components/CustomerSave/CustomerSaveWrapper.tsx
  13. +0
    -0
      src/components/CustomerSave/SubsidiaryAllocation.tsx
  14. +1
    -0
      src/components/CustomerSave/index.ts
  15. +1
    -1
      src/components/StaffSearch/StaffSearch.tsx
  16. +2
    -2
      src/components/SubsidiaryDetail/SubsidiaryDetailWrapper.tsx
  17. +1
    -1
      src/components/SubsidiarySearch/SubsidiarySearch.tsx
  18. +27
    -1
      src/components/TaskTemplateSearch/TaskTemplateSearch.tsx
  19. +1
    -1
      src/components/TeamSearch/TeamSearch.tsx
  20. +1
    -1
      src/components/TransferList/TransferList.tsx
  21. +1
    -1
      src/components/UserSearch/UserSearch.tsx
  22. +2
    -0
      src/i18n/en/common.json
  23. +27
    -0
      src/i18n/en/tasks.json
  24. +2
    -0
      src/i18n/zh/common.json
  25. +27
    -0
      src/i18n/zh/tasks.json

+ 2
- 2
src/app/(main)/settings/customer/create/page.tsx Ver ficheiro

@@ -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 Ver ficheiro

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


+ 4
- 1
src/app/(main)/tasks/create/page.tsx Ver ficheiro

@@ -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 Ver ficheiro

@@ -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 Ver ficheiro

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


+ 28
- 2
src/app/api/tasks/actions.ts Ver ficheiro

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

+ 122
- 69
src/components/CreateTaskTemplate/CreateTaskTemplate.tsx Ver ficheiro

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



+ 0
- 1
src/components/CustomerDetail/index.ts Ver ficheiro

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

src/components/CustomerDetail/ContactInfo.tsx → src/components/CustomerSave/ContactInfo.tsx Ver ficheiro


src/components/CustomerDetail/CustomerInfo.tsx → src/components/CustomerSave/CustomerInfo.tsx Ver ficheiro


src/components/CustomerDetail/CustomerDetail.tsx → src/components/CustomerSave/CustomerSave.tsx Ver ficheiro

@@ -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 Ver ficheiro

@@ -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 Ver ficheiro


+ 1
- 0
src/components/CustomerSave/index.ts Ver ficheiro

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

+ 1
- 1
src/components/StaffSearch/StaffSearch.tsx Ver ficheiro

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


+ 2
- 2
src/components/SubsidiaryDetail/SubsidiaryDetailWrapper.tsx Ver ficheiro

@@ -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 Ver ficheiro

@@ -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 Ver ficheiro

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


+ 1
- 1
src/components/TeamSearch/TeamSearch.tsx Ver ficheiro

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


+ 1
- 1
src/components/TransferList/TransferList.tsx Ver ficheiro

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


+ 1
- 1
src/components/UserSearch/UserSearch.tsx Ver ficheiro

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


+ 2
- 0
src/i18n/en/common.json Ver ficheiro

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


+ 27
- 0
src/i18n/en/tasks.json Ver ficheiro

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

+ 2
- 0
src/i18n/zh/common.json Ver ficheiro

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


+ 27
- 0
src/i18n/zh/tasks.json Ver ficheiro

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

Carregando…
Cancelar
Guardar