feat: imp mock api for user management

This commit is contained in:
HasanMu 2025-02-19 09:51:47 +07:00
parent ed88b1ed27
commit ce874def91
13 changed files with 586 additions and 11 deletions

View File

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

View File

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

View File

@ -66,7 +66,7 @@ const MENU = [
menus: [
{
title: "User",
link: "/user",
link: "/users",
icon: Users,
},
],

View File

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

97
src/pages/Users.tsx Normal file
View File

@ -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<UserDataTable>[] = useMemo(() => [
{
header: "Name",
accessorKey: "name",
},
{
header: "Username",
accessorKey: "username",
},
{
header: "Role",
accessorKey: "role",
},
{
header: '',
accessorKey: "action",
enableSorting: false,
cell: ({ row }) => (
<>
<IconButton variant="text" color="secondary" href={`/users/${row.original.id}`}>
<Edit variant="Bulk" color="currentColor" />
</IconButton>
</>
)
}
], []);
useEffect(() => {
if (cacheExpired()) {
(async () => {
await getAll();
})();
}
}, []);
return (
<Box component="main">
<Grid container spacing={2} alignItems={"center"} mb={4}>
<Grid size="grow">
<Typography variant="h3">Users</Typography>
</Grid>
<Grid size="auto">
<Button variant="contained" href="/users/create" startIcon={<AddIcon color="inherit" />}>
Add New
</Button>
</Grid>
<Grid size={12}>
<Breadcrumbs aria-label="breadcrumb">
<Link underline="hover" color="inherit" href="/dashboard">
<Home />
</Link>
<Typography sx={{ color: "text.primary" }}>Users</Typography>
</Breadcrumbs>
</Grid>
</Grid>
<FlashMessage store={useUserStore} />
<Card>
<Box sx={{ p: 2 }}>
<Typography variant="h5">List Users</Typography>
</Box>
<Divider />
<CardContent sx={{ flex: "1 0 auto", pt: 2, px: 0 }}>
<ReactTable data={data || []} columns={columns} />
</CardContent>
</Card>
</Box>
);
}

View File

@ -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 (
<Box component="main">
<Grid container spacing={2} alignItems={"center"} mb={4}>
<Grid size="auto">
<Box display={"flex"} alignItems={"center"} gap={1}>
<IconButton variant="dashed" color="primary" href="/users">
<Back variant="Outline" color="currentColor" />
</IconButton>
<Typography variant="h3">{IS_EDIT_PAGE ? `Edit User - ${pid}` : 'Create User'}</Typography>
</Box>
</Grid>
<Grid size={12}>
<Breadcrumbs aria-label="breadcrumb">
<Link underline="hover" color="inherit" href="/dashboard">
<Home />
</Link>
<Link underline="hover" color="inherit" href="/users">
<Typography sx={{ color: "text.primary" }}>Users</Typography>
</Link>
<Typography sx={{ color: "text.primary" }}>{IS_EDIT_PAGE ? pid : 'Create'}</Typography>
</Breadcrumbs>
</Grid>
</Grid>
<FlashMessage store={useUserStore} />
<Card component="form" onSubmit={onSubmit}>
<Box
sx={{ px: 3, py: 1 }}
display={"flex"}
justifyContent={"space-between"}
alignItems={"center"}
>
<Typography variant="h5">Fill the form</Typography>
<Button
disabled={isLoading}
type="submit"
variant="contained"
size="small"
startIcon={<Save color="inherit" />}
>
{IS_EDIT_PAGE ? "Save Changes" : "Create"}
</Button>
</Box>
<Divider />
<CardContent>
<Form isEdit={IS_EDIT_PAGE} />
</CardContent>
</Card>
</Box>
);
}

115
src/pages/Users/Form.tsx Normal file
View File

@ -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<string>("");
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 (
<Grid container spacing={2}>
<Grid size={{ md: isEdit ? 6 : 4, xs: 12 }}>
<TextField
fullWidth
name="name"
label="Name"
variant="outlined"
value={user?.name}
/>
</Grid>
<Grid size={{ md: isEdit ? 6 : 4, xs: 12 }}>
<TextField
fullWidth
name="username"
label="Username"
variant="outlined"
type="text"
value={user?.username}
/>
</Grid>
<Grid size={{ md: isEdit ? 12 : 4, xs: 12 }}>
<FormControl fullWidth>
<InputLabel id="device-label">Role</InputLabel>
<Select
name="device"
labelId="device-label"
label="Device"
value={role}
onChange={(e) => setRole(e.target.value)}
>
{roleSelect?.map((role, key) => (
<MenuItem key={key} value={role.value}>
{role.label.toUpperCase()}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
{!isEdit ? (
<Grid size={{ md: 12, xs: 12 }}>
<TextField
fullWidth
name="password"
label="Password"
variant="outlined"
type={showPassword ? 'text' : 'password'}
value={user?.password}
slotProps={{
input: {
endAdornment: (
<InputAdornment position="start">
<IconButton
aria-label={
showPassword
? "hide the password"
: "display the password"
}
onClick={handleClickShowPassword}
edge="end"
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
},
}}
/>
</Grid>
) : (
""
)}
</Grid>
);
}

View File

@ -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<ResponseApi<User[]>>("/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<ResponseApi<User>>(`/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<ResponseApi>("/api/v1/users", user);
return response.data.status as unknown as Pick<ResponseApi, "status">;
},
async updateUser(id, user) {
const response = await api.put<ResponseApi>(`/api/v1/users/${id}`, user);
return response.data.status as unknown as Pick<ResponseApi, "status">;
},
async getRoleSelect() {
// const response = await api.get<ResponseApi<SelectState[]>>("/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",
},
]);
}
};

View File

@ -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() {
<Route path="/fishbone" element={<Fishbone />} />
<Route path="/devices" element={<Devices />} />
<Route path="/towers" element={<Towers />} />
<Route path="/users" element={<Users />} />
{/* Create Page */}
<Route path="/devices/:pid" element={<DeviceCreateOrEdit />} />
@ -44,6 +47,9 @@ export default function Routes() {
<Route path="/backbone/:pid" element={<BackboneCreateOrEdit />} />
<Route path="/backbone/create" element={<BackboneCreateOrEdit />} />
<Route path="/users/:pid" element={<UserCreateOrEdit />} />
<Route path="/users/create" element={<UserCreateOrEdit />} />
</Route>
</Route>

125
src/stores/userStore.ts Normal file
View File

@ -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<UserState>()(
persist(
immer<UserState>((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) }
)
);

View File

@ -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: {

View File

@ -3,7 +3,7 @@ interface ResponseApiStatus {
description: string;
}
export interface ResponseApi<T> {
export interface ResponseApi<T = null> {
status: ResponseApiStatus;
message: string;
data?: T | [] | null;

49
src/types/users.types.ts Normal file
View File

@ -0,0 +1,49 @@
import { ResponseApi } from "./response.types";
import { CacheState, ErrorState, SelectState } from "./store.types";
export interface UserRepositoryProps {
getAll: () => Promise<UserDataTable[] | []>;
getUserById: (id: string) => Promise<User>;
createUser: (user: UserRequest) => Promise<Pick<ResponseApi, "status">>;
updateUser: (id: string, user: UserRequest) => Promise<Pick<ResponseApi, "status">>;
getRoleSelect: () => Promise<SelectState[] | null>;
}
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, "name" & "username" & "password"> {}
/**
* User State
* =============
*/
export interface UserState extends ErrorState, CacheState {
data?: UserDataTable[];
user?: User | null;
isLoading: boolean;
lastUpdate: number | null;
roleSelect: SelectState[] | null;
getRoleSelect: () => Promise<void>;
getAll: () => Promise<void>;
find: (id: string) => Promise<void>;
create: (user: UserRequest) => Promise<void>;
update: (id: string, user: UserRequest) => Promise<void>;
clearUser: () => void;
}
export interface UserDataTable extends Pick<User, "id" | "name" | "username"> {
role: string;
}
export interface IGetRoleSelect {
id: string;
role_name: string;
}