From 5e471b7e20056b7562c8d9d2cc405f2d5ad6cb30 Mon Sep 17 00:00:00 2001 From: HasanMu Date: Thu, 13 Feb 2025 17:27:59 +0700 Subject: [PATCH] wip: impl crud devices --- src/components/IconButton.tsx | 1 + src/layouts/AuthLayout.tsx | 9 +++ src/layouts/Dashboard/Header.tsx | 10 +-- src/layouts/Dashboard/Sidebar.tsx | 2 +- src/pages/Devices.tsx | 13 ++-- src/pages/Devices/Create.tsx | 95 ++++++++++++++++++++++++++++ src/pages/Devices/Form.tsx | 92 +++++++++++++++++++++++++++ src/repositories/authRepository.ts | 20 ++++-- src/repositories/deviceRepository.ts | 29 +++++++++ src/routes/index.tsx | 5 ++ src/stores/authStore.ts | 2 +- src/stores/deviceStore.ts | 81 ++++++++++++++++++++++++ src/stores/types.ts | 15 ----- src/theme/components/Chip.ts | 2 +- src/types/auth.types.ts | 18 ++++++ src/types/device.types.ts | 44 +++++++++++++ src/types/response.types.ts | 10 +++ 17 files changed, 417 insertions(+), 31 deletions(-) create mode 100644 src/pages/Devices/Create.tsx create mode 100644 src/pages/Devices/Form.tsx create mode 100644 src/repositories/deviceRepository.ts create mode 100644 src/stores/deviceStore.ts delete mode 100644 src/stores/types.ts create mode 100644 src/types/device.types.ts create mode 100644 src/types/response.types.ts diff --git a/src/components/IconButton.tsx b/src/components/IconButton.tsx index b7ccc7e..209c139 100644 --- a/src/components/IconButton.tsx +++ b/src/components/IconButton.tsx @@ -154,6 +154,7 @@ export interface Props extends IconButtonProps { variant?: ButtonVariantProps; children: ReactNode; tooltip?: boolean | ReactNode | ReactPortal; + href?: string; } function IconButton( diff --git a/src/layouts/AuthLayout.tsx b/src/layouts/AuthLayout.tsx index 771646b..8b3b100 100644 --- a/src/layouts/AuthLayout.tsx +++ b/src/layouts/AuthLayout.tsx @@ -1,9 +1,18 @@ import { Outlet } from "react-router"; import Box from "@mui/material/Box"; import { useTheme } from "@mui/material"; +import { useAuthStore } from "../stores/authStore"; export default function AuthLayout() { const theme = useTheme(); + const { token } = useAuthStore(); + + if (token) { + window.location.href = "/dashboard"; + + return; + } + return ( void; }) { + const { user, logout } = useAuthStore(); const [anchorEl, setAnchorEl] = React.useState(null); const open = Boolean(anchorEl); - let navigate = useNavigate(); const handleClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); @@ -25,10 +25,10 @@ export default function Header({ setAnchorEl(null); }; - const onLogout = () => { + const onLogout = async () => { setAnchorEl(null); - navigate("/login") + await logout(); }; return ( @@ -61,7 +61,7 @@ export default function Header({ }} > - Hi, Admin + Hi, {user?.name || ''} diff --git a/src/pages/Devices.tsx b/src/pages/Devices.tsx index 76aa1c8..f317764 100644 --- a/src/pages/Devices.tsx +++ b/src/pages/Devices.tsx @@ -3,6 +3,7 @@ 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"; @@ -71,7 +72,7 @@ export default function Devices() { enableSorting: false, cell: () => ( <> - + @@ -86,7 +87,7 @@ export default function Devices() { Devices - @@ -100,8 +101,12 @@ export default function Devices() { - - + + + List Devices + + + { + e.preventDefault(); + + const formData = new FormData(e.target as HTMLFormElement); + + const data = Object.fromEntries(formData.entries()) as unknown as DeviceRequest; + + if (IS_EDIT_PAGE) { + await update(pid!!, data); + } else { + await create(data); + } + } + return ( + + + + + + + + {IS_EDIT_PAGE ? `Edit Device - ${pid}` : 'Create Device'} + + + + + + + + + Devices + + {IS_EDIT_PAGE ? pid : 'Create'} + + + + + {error && ( + + {error.toString()} asass + + )} + + + + Fill the form + + + + +
+ + + + ); +} diff --git a/src/pages/Devices/Form.tsx b/src/pages/Devices/Form.tsx new file mode 100644 index 0000000..782432f --- /dev/null +++ b/src/pages/Devices/Form.tsx @@ -0,0 +1,92 @@ +import Box from '@mui/material/Box'; +import Chip from '@mui/material/Chip'; +import FormControl from "@mui/material/FormControl"; +import Grid from "@mui/material/Grid2"; +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 { useState } from 'react'; +import { DeviceStatus } from '../../types/device.types'; + +export default function Form() { + const [select, setSelect] = useState('active'); + + const STATUS = ["active", "inactive", "maintenance"]; + + return ( + + {/* Device Code */} + + + + + {/* Total Port */} + + + + + {/* Status */} + + + Status + + + + + + + + + + + + + ); +} diff --git a/src/repositories/authRepository.ts b/src/repositories/authRepository.ts index 334901c..ae2edf5 100644 --- a/src/repositories/authRepository.ts +++ b/src/repositories/authRepository.ts @@ -5,11 +5,23 @@ const api = createAPI(); export const authRepository = { async login(payload: LoginPayload): Promise { - const response = await api.post('/auth/login', payload); - return response.data; + // const response = await api.post('/auth/login', payload); + + // return response.data; + return Promise.resolve({ + status: { + code: 200, + description: 'OK', + }, + data: { + token: 'sample-token', + user: { + name: 'John Doe', + } + }, + }); }, async logout(): Promise { await api.post('/auth/logout'); - }, -}; \ No newline at end of file + },}; \ No newline at end of file diff --git a/src/repositories/deviceRepository.ts b/src/repositories/deviceRepository.ts new file mode 100644 index 0000000..fc2039b --- /dev/null +++ b/src/repositories/deviceRepository.ts @@ -0,0 +1,29 @@ +import { createAPI } from "../services/api"; +import { DeviceRepositoryProps, Device } from "../types/device.types"; +import { ResponseApi } from "../types/response.types"; + +const api = createAPI(); + +export const deviceRepository: DeviceRepositoryProps = { + async getAll() { + const response = await api.get>("/api/v1/devices"); + + return response.data.data as Device[]; + }, + + async getDeviceById(id: string) { + const response = await api.get>(`/api/v1/devices/${id}`); + + return response.data.data as Device; + }, + + async createDevice(data: Device) { + const response = await api.post>("/api/v1/devices", data); + return response.data; + }, + + async updateDevice(id: string, data: Device) { + const response = await api.put>(`/api/v1/devices/${id}`, data); + return response.data; + }, +}; diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 80fea02..0fcc180 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -9,6 +9,7 @@ import Devices from "../pages/Devices"; import Fishbone from "../pages/Fishbone"; import Towers from "../pages/Towers"; import RequiredAuth from "./RequiredAuth"; +import DeviceCreate from "../pages/Devices/Create"; export default function Routes() { return ( @@ -28,6 +29,10 @@ export default function Routes() { } /> } /> } /> + + {/* Create Page */} + } /> + } /> diff --git a/src/stores/authStore.ts b/src/stores/authStore.ts index 42a7823..ed85cbd 100644 --- a/src/stores/authStore.ts +++ b/src/stores/authStore.ts @@ -1,9 +1,9 @@ import { create } from 'zustand'; import { authRepository } from '../repositories/authRepository'; -import { AuthState } from './types'; import { persist } from 'zustand/middleware'; import { createJSONStorage } from 'zustand/middleware'; import { secureStorage } from './storage'; +import { AuthState } from '../types/auth.types'; type PersistedAuthState = Pick; diff --git a/src/stores/deviceStore.ts b/src/stores/deviceStore.ts new file mode 100644 index 0000000..a7e931e --- /dev/null +++ b/src/stores/deviceStore.ts @@ -0,0 +1,81 @@ +import { create } from "zustand"; +import { deviceRepository } from "../repositories/deviceRepository"; +import { persist } from "zustand/middleware"; +import { createJSONStorage } from "zustand/middleware"; +import { secureStorage } from "./storage"; +import { Device, DeviceState } from "../types/device.types"; + +export const useDeviceStore = create()( + persist( + (set) => ({ + data: [], + device: null, + isLoading: false, + error: null, + getAll: async () => { + set({ isLoading: true }); + try { + const devices = await deviceRepository.getAll(); + set({ data: devices }); + } 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 }); + } + }, + find: async (id: string) => { + set({ isLoading: true }); + try { + const device = await deviceRepository.getDeviceById(id); + + set({ device: device }); + } 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 (device: Omit) => { + set({ isLoading: true }); + try { + await deviceRepository.createDevice(device); + + } 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, device: Omit) => { + set({ isLoading: true }); + try { + await deviceRepository.updateDevice(id, device); + + } 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 }); + } + }, + }), + { name: "devices", storage: createJSONStorage(() => secureStorage) } + ) +); diff --git a/src/stores/types.ts b/src/stores/types.ts deleted file mode 100644 index 1586263..0000000 --- a/src/stores/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { LoginPayload, UserProfile } from "../types/auth.types"; - -export interface AuthState { - token: string | null; - user: UserProfile | null; - isLoading: boolean; - error: string | null | AuthErrorInputState; - login: (payload: LoginPayload) => Promise; - logout: () => Promise; -} - -export interface AuthErrorInputState { - username?: string | null; - password?: string | null; -} diff --git a/src/theme/components/Chip.ts b/src/theme/components/Chip.ts index b8473bc..3d2a6be 100644 --- a/src/theme/components/Chip.ts +++ b/src/theme/components/Chip.ts @@ -1,5 +1,5 @@ // MATERIAL - UI -import { Theme } from '@mui/material/styles'; +import { Theme } from '@mui/material'; // PROJECT IMPORTS import getColors from '../../utils/getColors'; diff --git a/src/types/auth.types.ts b/src/types/auth.types.ts index a286e24..386caf0 100644 --- a/src/types/auth.types.ts +++ b/src/types/auth.types.ts @@ -17,3 +17,21 @@ export interface LoginResponse { export interface UserProfile { name: string; } + +/** + * Auth state + * =========== + */ +export interface AuthState { + token: string | null; + user: UserProfile | null; + isLoading: boolean; + error: string | null | AuthErrorInputState; + login: (payload: LoginPayload) => Promise; + logout: () => Promise; +} + +export interface AuthErrorInputState { + username?: string | null; + password?: string | null; +} \ No newline at end of file diff --git a/src/types/device.types.ts b/src/types/device.types.ts new file mode 100644 index 0000000..a4507ff --- /dev/null +++ b/src/types/device.types.ts @@ -0,0 +1,44 @@ +import { ResponseApi } from "./response.types"; + +export interface DeviceRepositoryProps { + getAll: () => Promise; + getDeviceById: (id: string) => Promise; + createDevice: (device: DeviceRequest) => Promise, "status">>; + updateDevice: (id: string, device: DeviceRequest) => Promise, "status">>; +} + +export interface Device { + id: string; + device_code: string; + device_type: string; + longitude: string | number; + latitude: string | number; + port_amount: number; + status: string; + created_at: string; + updated_at: string; +} + +export type DeviceStatus = "active" | "inactive" | "maintenance"; + +export interface DeviceRequest extends Omit {} + +/** + * Device State + * ============= + */ +export interface DeviceState { + data?: Device[]; + device?: Device | null; + isLoading: boolean; + error: string | null | DeviceErrorInputState; + getAll: () => Promise; + find: (id: string) => Promise; + create: (device: DeviceRequest) => Promise; + update: (id: string, device: DeviceRequest) => Promise; +} + +export interface DeviceErrorInputState { + username?: string | null; + password?: string | null; +} \ No newline at end of file diff --git a/src/types/response.types.ts b/src/types/response.types.ts new file mode 100644 index 0000000..a458cda --- /dev/null +++ b/src/types/response.types.ts @@ -0,0 +1,10 @@ +interface ResponseApiStatus { + status: string | number; + description: string; +} + +export interface ResponseApi { + status: ResponseApiStatus; + message: string; + data?: T | [] | null; +} \ No newline at end of file