From 34716400e9a0b9a8f6514e430ff3070cdf67533c Mon Sep 17 00:00:00 2001 From: elizielx Date: Wed, 28 Feb 2024 18:06:52 +0800 Subject: [PATCH 1/7] feat(bot): add user create page --- .gitignore | 5 +- apps/app/src/locales/en.json | 8 +- apps/app/src/locales/id.json | 3 + .../pages/operator/user/create-user.page.tsx | 22 +- .../pages/operator/user/get-users.query.tsx | 14 + .../src/pages/operator/user/users.page.tsx | 154 ++++++++++- apps/app/src/queries/get-users-query.ts | 2 +- .../forms/operator/user/create-user.form.tsx | 247 +++++++++++++++++- libs/utils/.gitignore | 1 + 9 files changed, 443 insertions(+), 13 deletions(-) create mode 100644 apps/app/src/pages/operator/user/get-users.query.tsx create mode 100644 libs/utils/.gitignore diff --git a/.gitignore b/.gitignore index 3c188ab..67d7330 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,7 @@ note.txt .DS_Store Thumbs.db -.nx/cache \ No newline at end of file +.nx/cache + +# to be removed later +libs/utils/src/app-constants.ts \ No newline at end of file diff --git a/apps/app/src/locales/en.json b/apps/app/src/locales/en.json index 6b394c8..1ef558b 100644 --- a/apps/app/src/locales/en.json +++ b/apps/app/src/locales/en.json @@ -122,12 +122,16 @@ "username": "Username", "phoneNumber": "Phone Number", "description": "Description", + "active": "Active", + "active_select": "Select Active", "role": "Role", + "role_select": "Select Role", "submit": "Create User", "back": "Back", "validation": { - "name": "Name must be at least 8 characters long.", - "username": "Username must be at least 8 characters long.", + "name": "Name must be at least 4 characters long.", + "username": "Username must be at least 4 characters long.", + "password": "Password must be at least 8 characters long.", "phoneNumber": "Phone number must be at least 8 characters long.", "description": "Description must be at least 8 characters long." } diff --git a/apps/app/src/locales/id.json b/apps/app/src/locales/id.json index e085f7c..7de72cd 100644 --- a/apps/app/src/locales/id.json +++ b/apps/app/src/locales/id.json @@ -122,7 +122,10 @@ "username": "Username", "phoneNumber": "Nomor Telepon", "description": "Deskripsi", + "active": "Status Aktif", + "select_active": "Pilih Status Aktif", "role": "Role", + "role_select": "Pilih Role", "submit": "Buat Pengguna", "back": "Kembali", "validation": { diff --git a/apps/app/src/pages/operator/user/create-user.page.tsx b/apps/app/src/pages/operator/user/create-user.page.tsx index 3f7fae0..791b176 100644 --- a/apps/app/src/pages/operator/user/create-user.page.tsx +++ b/apps/app/src/pages/operator/user/create-user.page.tsx @@ -1,7 +1,23 @@ +import { IonContent, IonPage } from "@ionic/react"; +import { CreateTrashBinForm, CreateUserForm } from "@trashtrack/ui"; +import { useTranslation } from "react-i18next"; + export function CreateUserPage() { + const { t } = useTranslation(); + return ( -
-
-
+ + +
+

TrashTrack

+

{t("operator.user.create_user.subtitle")}

+
+
+ +
+
+
); } + +export default CreateUserPage; diff --git a/apps/app/src/pages/operator/user/get-users.query.tsx b/apps/app/src/pages/operator/user/get-users.query.tsx new file mode 100644 index 0000000..6ac0993 --- /dev/null +++ b/apps/app/src/pages/operator/user/get-users.query.tsx @@ -0,0 +1,14 @@ +import { CapacitorHttp } from "@capacitor/core"; +import { useQuery } from "@tanstack/react-query"; +import { API_URL } from "@trashtrack/utils"; + +export const useGetUsers = () => { + return useQuery({ + queryKey: ["getUsers"], + queryFn: () => + CapacitorHttp.request({ + url: API_URL + `/user`, + method: "GET", + }).then((res) => res.data), + }); +}; diff --git a/apps/app/src/pages/operator/user/users.page.tsx b/apps/app/src/pages/operator/user/users.page.tsx index 16a5cee..a668c17 100644 --- a/apps/app/src/pages/operator/user/users.page.tsx +++ b/apps/app/src/pages/operator/user/users.page.tsx @@ -1,7 +1,155 @@ +import { + IonContent, + IonPage, + IonRefresher, + IonRefresherContent, + RefresherEventDetail, + useIonViewDidEnter, +} from "@ionic/react"; +import { Button, Card, CardContent, CardHeader, CardTitle, Input, Label, Separator, Skeleton } from "@trashtrack/ui"; +import { useQueryClient } from "@tanstack/react-query"; +import { useHistory } from "react-router-dom"; +import { useState } from "react"; +import Fuse from "fuse.js"; +import { useTranslation } from "react-i18next"; +import { useGetUsers } from "./get-users.query"; + +export enum EnumUserRole { + operator = "operator", + admin = "admin", +} +export interface InterfaceUser { + id: number; + name: string; + username: string; + phoneNumber: string; + role: EnumUserRole; + active: boolean; + description?: string; +} + export function UsersPage() { + const history = useHistory(); + const queryClient = useQueryClient(); + const [searchTerm, setSearchTerm] = useState(""); + const { t } = useTranslation(); + + const { data: usersData, isLoading, isFetching, isError, error, refetch } = useGetUsers(); + + useIonViewDidEnter(() => { + queryClient.invalidateQueries({ + queryKey: ["getUsers"], + }); + + refetch(); + }); + + const fuseOptions = { + keys: ["username"], + threshold: 0.4, + }; + const fuse = new Fuse(!isLoading ? (usersData.data as InterfaceUser[]) : [], fuseOptions); + + const filteredData: InterfaceUser[] = + !isLoading && searchTerm === "" ? usersData.data : fuse.search(searchTerm).map((result) => result.item); + + function handleRefresh(event: CustomEvent) { + queryClient.invalidateQueries({ + queryKey: ["getUsers"], + }); + + refetch(); + event.detail.complete(); + } + return ( -
-
-
+ + + + +

Refreshing...

+
+
+
+

TrashTrack

+

{t("operator.user.subtitle")}

+
+
+
+ +
+ +
+ + setSearchTerm(e.target.value)} + /> +
+ {isLoading || isFetching ? ( + Array.from({ length: 5 }).map((_, index) => ( + + + + + + + + + + + + + )) + ) : filteredData.length === 0 && searchTerm !== "" ? ( + + +

{t("operator.trashbin.noResults")}

+
+
+ ) : ( + filteredData.map((user: InterfaceUser) => ( + + +
+

{user.username}

+

+ {user.active ? t("operator.user.is_active") : t("operator.user.not_active")} +

+
+ +
+
+
+
+ )) + )} + {isError && ( + + +

{JSON.stringify(error)}

+
+
+ )} +
+
+
); } + +export default UsersPage; diff --git a/apps/app/src/queries/get-users-query.ts b/apps/app/src/queries/get-users-query.ts index af0ac4c..48470a7 100644 --- a/apps/app/src/queries/get-users-query.ts +++ b/apps/app/src/queries/get-users-query.ts @@ -3,7 +3,7 @@ import { API_URL } from "@trashtrack/utils"; export const useGetUsersQuery = () => { return useQuery({ - queryKey: ["getUsers"], + queryKey: ["getUsers1"], queryFn: () => fetch(API_URL + `/user`).then((res) => res.json()), }); }; diff --git a/libs/components/src/forms/operator/user/create-user.form.tsx b/libs/components/src/forms/operator/user/create-user.form.tsx index 97c8bb0..20c1976 100644 --- a/libs/components/src/forms/operator/user/create-user.form.tsx +++ b/libs/components/src/forms/operator/user/create-user.form.tsx @@ -1,7 +1,248 @@ +import { CapacitorHttp } from "@capacitor/core"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation } from "@tanstack/react-query"; +import { API_URL } from "@trashtrack/utils"; +import { useForm } from "react-hook-form"; +import { useHistory } from "react-router-dom"; +import { z } from "zod"; + +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "../../../ui/form"; +import { Button } from "../../../ui/button"; +import { Input } from "../../../ui/input"; +import { Textarea } from "../../../ui/textarea"; +import { useTranslation } from "react-i18next"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../../ui/select"; + export function CreateUserForm() { + const history = useHistory(); + const { t } = useTranslation(); + + const formSchema = z.object({ + name: z.string().min(4, { + message: t("operator.user.create_user.validation.name"), + }), + username: z.string().min(4, { + message: t("operator.user.create_user.validation.username"), + }), + password: z.string().min(8, { + message: t("operator.user.create_user.validation.password"), + }), + phoneNumber: z.string().min(8, { + message: t("operator.user.create_user.validation.phoneNumber"), + }), + role: z.enum(["admin", "operator"]), + active: z.enum(["true", "false"]), + description: z.string().min(8, { + message: t("operator.user.create_user.validation.description"), + }), + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: "", + username: "", + password: "", + phoneNumber: "", + role: "operator", + active: "false", + description: "", + }, + }); + + const { mutateAsync, isPending, isError } = useMutation({ + mutationKey: ["createUser"], + mutationFn: (values: { + name: string; + username: string; + password: string; + phoneNumber: string; + role: "admin" | "operator"; + active: boolean; + description: string; + }) => { + return CapacitorHttp.post({ + url: API_URL + `/user`, + data: JSON.stringify(values), + headers: { + "Content-Type": "application/json", + }, + }).then((res) => res.data); + }, + onSuccess: () => { + history.replace(`/operator/tabs/user`); + }, + }); + + async function onSubmit(values: z.infer) { + console.log(values.active); + await mutateAsync({ + name: values.name, + username: values.username, + password: values.password, + phoneNumber: values.phoneNumber, + role: values.role, + active: Boolean(values.active === "true"), + description: values.description, + }); + } + return ( -
-
-
+
+ + ( + + {t("operator.user.create_user.name")} + + + + + + )} + /> + ( + + {t("operator.user.create_user.username")} + + + + + + )} + /> + ( + + {t("operator.user.create_user.password")} + + + + + + )} + /> + ( + + {t("operator.user.create_user.phoneNumber")} + + + + + + )} + /> + ( + + {t("operator.user.create_user.description")} + +