diff --git a/src/components/FlashMessage.tsx b/src/components/FlashMessage.tsx index 19b221c..cf4b756 100644 --- a/src/components/FlashMessage.tsx +++ b/src/components/FlashMessage.tsx @@ -6,9 +6,15 @@ import { AuthState } from "../types/auth.types"; import { FishboneState } from "../types/fishbone.types"; import { TowerState } from "../types/tower.types"; import { BackboneState } from "../types/backbone.types"; +import { UserState } from "../types/users.types"; interface FlashMessageProps { - store: () => DeviceState | AuthState | FishboneState | TowerState | BackboneState; + store: () => DeviceState + | AuthState + | FishboneState + | TowerState + | BackboneState + | UserState; } export default function FlashMessage(props: FlashMessageProps) { diff --git a/src/components/IconButton.tsx b/src/components/IconButton.tsx index 209c139..3aa827a 100644 --- a/src/components/IconButton.tsx +++ b/src/components/IconButton.tsx @@ -78,7 +78,7 @@ function getColorStyle({ variant, theme, color }: IconButtonStyleProps) { }; case 'dashed': return { - backgroundColor: lighter, + backgroundColor: theme.palette.mode === 'dark' ? alpha(lighter, 0.85) : lighter, '&:hover': { color: dark, borderColor: dark diff --git a/src/layouts/Dashboard/Sidebar.tsx b/src/layouts/Dashboard/Sidebar.tsx index 7beede1..d53198f 100644 --- a/src/layouts/Dashboard/Sidebar.tsx +++ b/src/layouts/Dashboard/Sidebar.tsx @@ -66,7 +66,7 @@ const MENU = [ menus: [ { title: "User", - link: "/user", + link: "/users", icon: Users, }, ], diff --git a/src/pages/Towers.tsx b/src/pages/Towers.tsx index 43c7826..d9d4bc4 100644 --- a/src/pages/Towers.tsx +++ b/src/pages/Towers.tsx @@ -19,12 +19,6 @@ import { useEffect, useMemo } from "react"; import FlashMessage from "../components/FlashMessage"; import { TowerDataTable } from "../types/tower.types"; -interface UserData { - tower_code: string; - device: string; - address: string; -} - export default function Towers() { useTitle("Towers"); diff --git a/src/pages/Users.tsx b/src/pages/Users.tsx new file mode 100644 index 0000000..10e4f40 --- /dev/null +++ b/src/pages/Users.tsx @@ -0,0 +1,97 @@ +import Box from "@mui/material/Box"; +import Breadcrumbs from "@mui/material/Breadcrumbs"; +import Button from "@mui/material/Button"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Divider from "@mui/material/Divider"; +import Link from "@mui/material/Link"; +import Grid from "@mui/material/Grid2"; +import Typography from "@mui/material/Typography"; +import AddIcon from "@mui/icons-material/Add"; +import { Home } from "@mui/icons-material"; +import ReactTable from "../components/ReactTable"; +import { ColumnDef } from "@tanstack/react-table"; +import IconButton from "../components/IconButton"; +import { Edit } from "iconsax-react"; +import useTitle from "../hooks/useTitle"; +import { useUserStore } from "../stores/userStore"; +import { useEffect, useMemo } from "react"; +import FlashMessage from "../components/FlashMessage"; +import { UserDataTable } from "../types/users.types"; + +export default function Users() { + useTitle("Users"); + + const { data, cacheExpired, getAll } = useUserStore(); + + // Define columns + const columns: ColumnDef[] = useMemo(() => [ + { + header: "Name", + accessorKey: "name", + }, + { + header: "Username", + accessorKey: "username", + }, + { + header: "Role", + accessorKey: "role", + }, + { + header: '', + accessorKey: "action", + enableSorting: false, + cell: ({ row }) => ( + <> + + + + + ) + } + ], []); + + useEffect(() => { + if (cacheExpired()) { + (async () => { + await getAll(); + })(); + } + }, []); + + return ( + + + + Users + + + + + + + + + + Users + + + + + + + + + List Users + + + + + + + + ); +} diff --git a/src/pages/Users/CreateOrEdit.tsx b/src/pages/Users/CreateOrEdit.tsx new file mode 100644 index 0000000..efb2e40 --- /dev/null +++ b/src/pages/Users/CreateOrEdit.tsx @@ -0,0 +1,105 @@ +import Box from "@mui/material/Box"; +import Breadcrumbs from "@mui/material/Breadcrumbs"; +import Button from "@mui/material/Button"; +import Card from "@mui/material/Card"; +import Divider from "@mui/material/Divider"; +import CardContent from "@mui/material/CardContent"; +import Grid from "@mui/material/Grid2"; +import Link from "@mui/material/Link"; +import Typography from "@mui/material/Typography"; +import { Home, Save } from "@mui/icons-material"; +import useTitle from "../../hooks/useTitle"; +import IconButton from "../../components/IconButton"; +import { Back } from "iconsax-react"; +import { useParams } from "react-router"; +import Form from "./Form"; +import { FormEvent, useEffect } from "react"; +import { useUserStore } from "../../stores/userStore"; +import { UserRequest } from "../../types/users.types"; +import FlashMessage from "../../components/FlashMessage"; + +export default function UserCreateOrEdit() { + useTitle("Users"); + const { pid } = useParams(); + const { update, create, isLoading, find, clearUser } = useUserStore(); + + const IS_EDIT_PAGE: boolean = !!pid; + + const onSubmit = async (e: FormEvent) => { + e.preventDefault(); + + const formData = new FormData(e.target as HTMLFormElement); + + const data = Object.fromEntries(formData.entries()) as unknown as UserRequest; + + if (IS_EDIT_PAGE) { + await update(pid!, data); + } else { + await create(data); + } + } + + useEffect(() => { + if (pid) { + (async () => { + await find(pid); + })() + } else { + (async () => { + await clearUser() + })() + } + }, []) + + return ( + + + + + + + + {IS_EDIT_PAGE ? `Edit User - ${pid}` : 'Create User'} + + + + + + + + + Users + + {IS_EDIT_PAGE ? pid : 'Create'} + + + + + + + + + Fill the form + + + + +
+ + + + ); +} diff --git a/src/pages/Users/Form.tsx b/src/pages/Users/Form.tsx new file mode 100644 index 0000000..19e6d83 --- /dev/null +++ b/src/pages/Users/Form.tsx @@ -0,0 +1,115 @@ +import FormControl from "@mui/material/FormControl"; +import Grid from "@mui/material/Grid2"; +import IconButton from "@mui/material/IconButton"; +import InputAdornment from "@mui/material/InputAdornment"; +import InputLabel from "@mui/material/InputLabel"; +import MenuItem from "@mui/material/MenuItem"; +import Select from "@mui/material/Select"; +import TextField from "@mui/material/TextField"; +import { useEffect, useState } from "react"; +import { useUserStore } from "../../stores/userStore"; +import { Visibility, VisibilityOff } from "@mui/icons-material"; + +export default function Form({ isEdit }: { isEdit: boolean }) { + const { user, clearUser, getRoleSelect, roleSelect } = useUserStore(); + const [role, setRole] = useState(""); + const [showPassword, setShowPassword] = useState(false); + + const handleClickShowPassword = () => setShowPassword((show) => !show); + + useEffect(() => { + (async () => { + await getRoleSelect(); + })(); + + if (user) { + setRole(user.role_id as string); + } + + const clear = async () => { + await clearUser(); + }; + + return () => { + clear(); + }; + }, [user]); + + return ( + + + + + + + + + + + + Role + + + + + {!isEdit ? ( + + + + {showPassword ? : } + + + ), + }, + }} + /> + + ) : ( + "" + )} + + ); +} diff --git a/src/repositories/userRepository.ts b/src/repositories/userRepository.ts new file mode 100644 index 0000000..0962aeb --- /dev/null +++ b/src/repositories/userRepository.ts @@ -0,0 +1,78 @@ +import { createAPI } from "../services/api"; +import { UserRepositoryProps, User, IGetRoleSelect } from "../types/users.types"; +import { ResponseApi } from "../types/response.types"; +import { SelectState } from "../types/store.types"; + +const api = createAPI(); + +export const userRepository: UserRepositoryProps = { + async getAll() { + // const response = await api.get>("/api/v1/users"); + + // return response.data.data as User[]; + + return Promise.resolve([ + { + id: "1", + name: "John Doe", + username: "johndoe", + role: "Admin", + created_at: "2024-01-01", + updated_at: "2024-01-01", + }, + { + id: "2", + name: "Jane Doe", + username: "janedoe", + role: "Admin", + created_at: "2024-01-01", + updated_at: "2024-01-01", + } + ]); + }, + + async getUserById(id: string) { + // const response = await api.get>(`/api/v1/users/${id}`); + + // return response.data.data as User; + return Promise.resolve({ + id: "1", + name: "John Doe", + username: "johndoe", + role_id: "1", + created_at: "2024-01-01", + updated_at: "2024-01-01", + }); + }, + + async createUser(user) { + const response = await api.post("/api/v1/users", user); + + return response.data.status as unknown as Pick; + }, + async updateUser(id, user) { + const response = await api.put(`/api/v1/users/${id}`, user); + + return response.data.status as unknown as Pick; + }, + + async getRoleSelect() { + // const response = await api.get>("/api/v1/users/role-select"); + + // return (response.data.data as IGetRoleSelect[])?.map((i: IGetRoleSelect) => ({label: i.role_name, value: i.id})) as SelectState[]; + return Promise.resolve([ + { + label: "Admin", + value: "1", + }, + { + label: "Teknisi", + value: "2", + }, + { + label: "Head", + value: "3", + }, + ]); + } +}; diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 944edbd..bcbefce 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -12,6 +12,8 @@ import DeviceCreateOrEdit from "../pages/Devices/CreateOrEdit"; import FishboneCreateOrEdit from "../pages/Fishbone/CreateOrEdit"; import TowerCreateOrEdit from "../pages/Towers/CreateOrEdit"; import BackboneCreateOrEdit from "../pages/Backbone/CreateOrEdit"; +import Users from "../pages/Users"; +import UserCreateOrEdit from "../pages/Users/CreateOrEdit"; export default function Routes() { return ( @@ -31,6 +33,7 @@ export default function Routes() { } /> } /> } /> + } /> {/* Create Page */} } /> @@ -44,6 +47,9 @@ export default function Routes() { } /> } /> + + } /> + } /> diff --git a/src/stores/userStore.ts b/src/stores/userStore.ts new file mode 100644 index 0000000..575e336 --- /dev/null +++ b/src/stores/userStore.ts @@ -0,0 +1,125 @@ +import { create } from "zustand"; +import { userRepository } from "../repositories/userRepository"; +import { persist } from "zustand/middleware"; +import { createJSONStorage } from "zustand/middleware"; +import { immer } from "zustand/middleware/immer"; +import { secureStorage } from "./storage"; +import { UserRequest, UserState } from "../types/users.types"; +import dayjs from "dayjs"; + +export const useUserStore = create()( + persist( + immer((set, get) => ({ + data: [], + user: null, + isLoading: false, + error: null, + lastUpdate: null, + roleSelect: [], + getAll: async () => { + set({ isLoading: true }); + try { + const users = await userRepository.getAll(); + set({ data: users, lastUpdate: dayjs().valueOf() }); + } catch (error: any) { + if (error.status === 401) { + set({ error: error.response?.status?.description }); + } else { + set({ + error: error.response?.status?.description || error.message, + }); + } + } finally { + set({ isLoading: false, lastUpdate: dayjs().valueOf() }); + } + }, + find: async (id: string) => { + set({ isLoading: true }); + try { + const user = await userRepository.getUserById(id); + + set({ user: user }); + } catch (error: any) { + if (error.status === 401) { + set({ error: error.response.status.description }); + } else { + set({ + error: error.response?.status?.description || error.message, + }); + } + } finally { + set({ isLoading: false }); + } + }, + create: async (user: UserRequest) => { + console.log(user); + + set({ isLoading: true }); + try { + await userRepository.createUser(user); + } catch (error: any) { + console.log(error); + + if (error.status === 401) { + set({ error: error.response.status.description }); + } else { + set({ + error: error.response?.status?.description || error.message, + }); + } + } finally { + set({ isLoading: false }); + } + }, + update: async (id: string, user: UserRequest) => { + set({ isLoading: true }); + try { + await userRepository.updateUser(id, user); + } catch (error: any) { + if (error.status === 401) { + set({ error: error.response.status.description }); + } else { + set({ + error: error.response?.status?.description || error.message, + }); + } + } finally { + set({ isLoading: false }); + } + }, + getRoleSelect: async () => { + set({ isLoading: true }); + try { + const roles = await userRepository.getRoleSelect(); + set({ roleSelect: roles }); + } catch (error: any) { + if (error.status === 401) { + set({ error: error.response?.status?.description }); + } else { + set({ + error: error.response?.status?.description || error.message, + }); + } + } finally { + set({ isLoading: false }); + } + }, + clearError: () => set({ error: null }), + clearUser: () => { + set({ user: null }); + }, + cacheExpired: () => { + return ( + get().lastUpdate === null || + dayjs(get().lastUpdate).isBefore( + dayjs().subtract( + import.meta.env.VITE_APP_CACHE_TIME_VALUE, + import.meta.env.VITE_APP_CACHE_TIME_UNIT + ) + ) + ); + }, + })), + { name: "users", storage: createJSONStorage(() => secureStorage) } + ) +); diff --git a/src/theme/components/Alert.ts b/src/theme/components/Alert.ts index 7e382b6..6b8e6d3 100644 --- a/src/theme/components/Alert.ts +++ b/src/theme/components/Alert.ts @@ -32,7 +32,7 @@ export default function Alert(theme: Theme) { MuiAlert: { styleOverrides: { root: { - color: theme.palette.text.primary, + color: theme.palette.mode === 'dark' ? theme.palette.primary.main : theme.palette.text.primary, fontSize: '0.875rem' }, icon: { diff --git a/src/types/response.types.ts b/src/types/response.types.ts index a458cda..af6d7b3 100644 --- a/src/types/response.types.ts +++ b/src/types/response.types.ts @@ -3,7 +3,7 @@ interface ResponseApiStatus { description: string; } -export interface ResponseApi { +export interface ResponseApi { status: ResponseApiStatus; message: string; data?: T | [] | null; diff --git a/src/types/users.types.ts b/src/types/users.types.ts new file mode 100644 index 0000000..1b3fd9c --- /dev/null +++ b/src/types/users.types.ts @@ -0,0 +1,49 @@ +import { ResponseApi } from "./response.types"; +import { CacheState, ErrorState, SelectState } from "./store.types"; + +export interface UserRepositoryProps { + getAll: () => Promise; + getUserById: (id: string) => Promise; + createUser: (user: UserRequest) => Promise>; + updateUser: (id: string, user: UserRequest) => Promise>; + getRoleSelect: () => Promise; +} + +export interface User { + id: string; + role_id: string | number; + name: string; + username: string; + password?: string; + created_at: string; + updated_at: string; +} + +export interface UserRequest extends Pick {} + +/** + * User State + * ============= + */ +export interface UserState extends ErrorState, CacheState { + data?: UserDataTable[]; + user?: User | null; + isLoading: boolean; + lastUpdate: number | null; + roleSelect: SelectState[] | null; + getRoleSelect: () => Promise; + getAll: () => Promise; + find: (id: string) => Promise; + create: (user: UserRequest) => Promise; + update: (id: string, user: UserRequest) => Promise; + clearUser: () => void; +} + +export interface UserDataTable extends Pick { + role: string; +} + +export interface IGetRoleSelect { + id: string; + role_name: string; +} \ No newline at end of file