feat: impl mock api for tower, backbone and change icon sidebar menu

This commit is contained in:
HasanMu 2025-02-18 14:45:03 +07:00
parent f8aa6ad785
commit ed88b1ed27
22 changed files with 1021 additions and 54 deletions

6
src/assets/backbone.svg Normal file
View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="m5 20l14-8L5 4z"/>
<path d="M19 20L5 12l14-8z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 255 B

1
src/assets/device.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M9 21q-1.925 0-3.463-1.1T3.35 17h17.3q-.65 1.8-2.187 2.9T15 21zm-6-6V9q0-2.5 1.75-4.25T9 3h6q2.5 0 4.25 1.75T21 9v6z"/></svg>

After

Width:  |  Height:  |  Size: 237 B

1
src/assets/fishbone.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512"><path fill="currentColor" d="M190.406 51.97c-4.104 29.047-7.13 44.833-6.72 58.53c-8.64 1.623-17.247 4.38-25.75 8.375c-25.746-38.28-86.43-44.672-139.06-13.5c27.12 33.838 44.53 50.06 88.468 49.094c-23.64 40.44-16.737 66.468-3.094 106.405c50.213-29.74 75.816-83.433 61.906-125.22c6.763-3.156 13.588-5.373 20.47-6.75c3.344 11.76 9.795 25.62 20.75 48.032c5.394-22.616 8.44-37.168 9.03-48.906c10.954 1.452 21.88 4.78 32.688 9.657c-5.85 16.68-9.962 35.86-13.25 60.688c17.36-16.772 29.677-31.288 38.844-46.188c5.488 3.756 10.908 7.874 16.25 12.313c4.39 3.648 8.69 7.515 12.906 11.563c-17.963 19.995-33.863 41.93-46.688 65.218c24.165-10.115 46.634-25.15 67.156-43.28a318 318 0 0 1 23.344 31.5c-26.33 17.278-50.652 37.486-70.375 59.844c28.77-5.146 58.07-17.09 85.908-33.156c5.354 10.33 9.97 20.708 13.75 30.906c-30.085 15.35-57.335 37.104-81.75 65.28c47.72 50.605 103.617 99.44 170 110.25c2.28-5.74 4.45-11.502 6.5-17.312c-60.357-19.627-109.66-52.267-145.563-89.156l13.406-13.03c33.782 34.707 80.56 65.736 137.97 84.405c16.732-55.238 20.843-112.648-1.844-167.217c-29.13 2.03-56.363 8.342-81.72 18.906c-4.096-10.92-9.036-21.94-14.717-32.845c30.638-19.792 58.958-44.156 82.092-69.313c-31.87 8.903-65.846 23.524-97.718 42.47c-7.725-11.933-16.287-23.498-25.53-34.376c26.697-26.486 49.666-57.987 68.186-90.03c-31.647 16.955-62.495 40.003-89.563 67.343c-4.5-4.3-9.1-8.428-13.812-12.345a225 225 0 0 0-19.375-14.438c10.292-22.438 15.372-47.604 20.875-83.437c-22.557 26.39-37.655 45.848-48.28 68.344c-13.42-5.92-27.255-9.89-41.22-11.313c-3.22-14.055-11.237-28.646-24.47-57.31zM426.25 302.187c11.947-.273 23.58 6.173 29.375 17.53c8.063 15.802 1.77 35.125-14.03 43.188c-15.803 8.063-35.126 1.802-43.19-14c-8.062-15.8-1.8-35.156 14-43.22a32 32 0 0 1 13.845-3.498"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

1
src/assets/tower.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 2v20M2 5h20M3 3v2m4-2v2m10-2v2m4-2v2m-2 0l-7 7l-7-7"/></svg>

After

Width:  |  Height:  |  Size: 254 B

1
src/assets/users.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 640 512"><path fill="currentColor" d="M96 224c35.3 0 64-28.7 64-64s-28.7-64-64-64s-64 28.7-64 64s28.7 64 64 64m448 0c35.3 0 64-28.7 64-64s-28.7-64-64-64s-64 28.7-64 64s28.7 64 64 64m32 32h-64c-17.6 0-33.5 7.1-45.1 18.6c40.3 22.1 68.9 62 75.1 109.4h66c17.7 0 32-14.3 32-32v-32c0-35.3-28.7-64-64-64m-256 0c61.9 0 112-50.1 112-112S381.9 32 320 32S208 82.1 208 144s50.1 112 112 112m76.8 32h-8.3c-20.8 10-43.9 16-68.5 16s-47.6-6-68.5-16h-8.3C179.6 288 128 339.6 128 403.2V432c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48v-28.8c0-63.6-51.6-115.2-115.2-115.2m-223.7-13.4C161.5 263.1 145.6 256 128 256H64c-35.3 0-64 28.7-64 64v32c0 17.7 14.3 32 32 32h65.9c6.3-47.4 34.9-87.3 75.2-109.4"/></svg>

After

Width:  |  Height:  |  Size: 763 B

View File

@ -4,9 +4,11 @@ import Alert from "@mui/material/Alert";
import { DeviceState } from "../types/device.types"; import { DeviceState } from "../types/device.types";
import { AuthState } from "../types/auth.types"; import { AuthState } from "../types/auth.types";
import { FishboneState } from "../types/fishbone.types"; import { FishboneState } from "../types/fishbone.types";
import { TowerState } from "../types/tower.types";
import { BackboneState } from "../types/backbone.types";
interface FlashMessageProps { interface FlashMessageProps {
store: () => DeviceState | AuthState | FishboneState; store: () => DeviceState | AuthState | FishboneState | TowerState | BackboneState;
} }
export default function FlashMessage(props: FlashMessageProps) { export default function FlashMessage(props: FlashMessageProps) {

View File

@ -12,6 +12,11 @@ import { Link, Typography } from "@mui/material";
import { useLocation } from "react-router"; import { useLocation } from "react-router";
import { ReactSVG } from "react-svg"; import { ReactSVG } from "react-svg";
import Speedometer from "../../assets/speedometer.svg"; import Speedometer from "../../assets/speedometer.svg";
import Backbone from "../../assets/backbone.svg";
import Fishbone from "../../assets/fishbone.svg";
import Device from "../../assets/device.svg";
import Tower from "../../assets/tower.svg";
import Users from "../../assets/users.svg";
interface Props { interface Props {
/** /**
@ -37,22 +42,22 @@ const MENU = [
{ {
title: "Backbone", title: "Backbone",
link: "/backbone", link: "/backbone",
icon: Speedometer, icon: Backbone,
}, },
{ {
title: "Fishbone", title: "Fishbone",
link: "/fishbone", link: "/fishbone",
icon: Speedometer, icon: Fishbone,
}, },
{ {
title: "Devices", title: "Devices",
link: "/devices", link: "/devices",
icon: Speedometer, icon: Device,
}, },
{ {
title: "Tower", title: "Tower",
link: "/towers", link: "/towers",
icon: Speedometer, icon: Tower,
}, },
], ],
}, },
@ -62,7 +67,7 @@ const MENU = [
{ {
title: "User", title: "User",
link: "/user", link: "/user",
icon: Speedometer, icon: Users,
}, },
], ],
}, },

View File

@ -3,6 +3,7 @@ import Breadcrumbs from "@mui/material/Breadcrumbs";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent"; import CardContent from "@mui/material/CardContent";
import Divider from "@mui/material/Divider";
import Link from "@mui/material/Link"; import Link from "@mui/material/Link";
import Grid from "@mui/material/Grid2"; import Grid from "@mui/material/Grid2";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
@ -13,6 +14,10 @@ import IconButton from "../components/IconButton";
import { Edit } from "iconsax-react"; import { Edit } from "iconsax-react";
import { ColumnDef } from "@tanstack/react-table"; import { ColumnDef } from "@tanstack/react-table";
import useTitle from "../hooks/useTitle"; import useTitle from "../hooks/useTitle";
import FlashMessage from "../components/FlashMessage";
import { useBackboneStore } from "../stores/backboneStore";
import { BackboneDataTable } from "../types/backbone.types";
import { useEffect, useMemo } from "react";
interface UserData { interface UserData {
backbone: string; backbone: string;
@ -23,18 +28,15 @@ interface UserData {
} }
export default function Backbone() { export default function Backbone() {
useTitle("Backbone"); useTitle("Backbone");
const data: UserData[] = [ const { data, cacheExpired, getAll } = useBackboneStore();
{ backbone: "BB-BDG-SMD", device_start: "DV-CODE-1", device_end: "DV-CODE-2", total_core: "24 Core", total_fishbone: "3" },
{ backbone: "BB-BDG-SBG", device_start: "DV-CODE-3", device_end: "DV-CODE-4", total_core: "24 Core", total_fishbone: "5" },
];
// Define columns // Define columns
const columns: ColumnDef<UserData>[] = [ const columns: ColumnDef<BackboneDataTable>[] = useMemo(() => [
{ {
header: "Backbone", header: "Backbone",
accessorKey: "backbone", accessorKey: "backbone_code",
}, },
{ {
header: "Device Start", header: "Device Start",
@ -46,7 +48,7 @@ export default function Backbone() {
}, },
{ {
header: "Total Core", header: "Total Core",
accessorKey: "total_core", accessorKey: "core_amount",
}, },
{ {
header: "Total Fishbone", header: "Total Fishbone",
@ -56,15 +58,23 @@ export default function Backbone() {
header: "", header: "",
accessorKey: "action", accessorKey: "action",
enableSorting: false, enableSorting: false,
cell: () => ( cell: ({ row }) => (
<> <>
<IconButton variant="light" color="info"> <IconButton variant="text" color="secondary" href={`/backbone/${row.original.id}`}>
<Edit variant="Bulk" color="currentColor" /> <Edit variant="Bulk" color="currentColor" />
</IconButton> </IconButton>
</> </>
), ),
}, },
]; ], []);
useEffect(() => {
if (cacheExpired()) {
(async () => {
await getAll();
})();
}
}, []);
return ( return (
<Box component="main"> <Box component="main">
@ -73,7 +83,7 @@ export default function Backbone() {
<Typography variant="h3">Backbone</Typography> <Typography variant="h3">Backbone</Typography>
</Grid> </Grid>
<Grid size="auto"> <Grid size="auto">
<Button variant="contained" startIcon={<AddIcon color="inherit" />}> <Button variant="contained" startIcon={<AddIcon color="inherit" />} href="/backbone/create">
Add New Add New
</Button> </Button>
</Grid> </Grid>
@ -87,12 +97,15 @@ export default function Backbone() {
</Grid> </Grid>
</Grid> </Grid>
<Card sx={{ display: "flex" }}> {/* <FlashMessage store={useBackboneStore} /> */}
<CardContent sx={{ flex: "1 0 auto" }}>
<ReactTable <Card>
data={data} <Box sx={{ p: 2 }}>
columns={columns} <Typography variant="h5">List Backbone</Typography>
/> </Box>
<Divider />
<CardContent sx={{ flex: "1 0 auto", pt: 2, px: 0 }}>
<ReactTable data={data || []} columns={columns} />
</CardContent> </CardContent>
</Card> </Card>
</Box> </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 FlashMessage from "../../components/FlashMessage";
import { useBackboneStore } from "../../stores/backboneStore";
import { BackboneRequest } from "../../types/backbone.types";
export default function BackboneCreateOrEdit() {
useTitle("Backbone");
const { pid } = useParams();
const { update, create, isLoading, find, clearBackbone } = useBackboneStore();
const IS_EDIT_PAGE = 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 BackboneRequest;
if (IS_EDIT_PAGE) {
await update(pid!!, data);
} else {
await create(data);
}
}
useEffect(() => {
if (pid!!) {
(async () => {
await find(pid!!);
})()
} else {
(async () => {
await clearBackbone()
})()
}
}, [])
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="/backbone">
<Back variant="Outline" color="currentColor" />
</IconButton>
<Typography variant="h3">{IS_EDIT_PAGE ? `Edit Backbone - ${pid}` : 'Create Backbone'}</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="/backbone">
<Typography sx={{ color: "text.primary" }}>Backbone</Typography>
</Link>
<Typography sx={{ color: "text.primary" }}>{IS_EDIT_PAGE ? pid : 'Create'}</Typography>
</Breadcrumbs>
</Grid>
</Grid>
<FlashMessage store={useBackboneStore} />
<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 />
</CardContent>
</Card>
</Box>
);
}

121
src/pages/Backbone/Form.tsx Normal file
View File

@ -0,0 +1,121 @@
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 { useEffect, useState } from "react";
import { useBackboneStore } from "../../stores/backboneStore";
export default function Form() {
const {
backbone,
clearBackbone,
deviceSelect,
getDeviceSelect,
} = useBackboneStore();
const [deviceStart, setDeviceStart] = useState<string>("");
const [deviceEnd, setDeviceEnd] = useState<string>("");
useEffect(() => {
(async () => {
await getDeviceSelect();
})();
if (backbone) {
setDeviceStart(backbone.dev_start_id as string);
setDeviceEnd(backbone.dev_end_id as string);
}
const clear = async () => {
await clearBackbone();
};
return () => {
clear();
};
}, [backbone]);
return (
<Grid container spacing={2}>
<Grid size={{ md: 6, xs: 12 }}>
<TextField
fullWidth
name="backbone_code"
label="Backbone Code"
variant="outlined"
value={backbone?.backbone_code}
/>
</Grid>
<Grid size={{ md: 6, xs: 12 }}>
<TextField
fullWidth
name="core_amount"
label="Core used"
variant="outlined"
type="number"
value={backbone?.core_amount}
/>
</Grid>
<Grid size={{ md: 6, xs: 12 }}>
<FormControl fullWidth>
<InputLabel id="device-start-label">Device Start</InputLabel>
<Select
name="device-start"
labelId="device-start-label"
label="Device Start"
value={deviceStart}
onChange={(e) => setDeviceStart(e.target.value)}
>
{deviceSelect?.map((device, key) => (
<MenuItem key={key} value={device.value}>
{device.label.toUpperCase()}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid size={{ md: 6, xs: 12 }}>
<FormControl fullWidth>
<InputLabel id="device-end-label">Device End</InputLabel>
<Select
name="device-end"
labelId="device-end-label"
label="Device End"
value={deviceEnd}
onChange={(e) => setDeviceEnd(e.target.value)}
>
{deviceSelect
?.filter((b) => b.value != deviceStart)
.map((device, key) => (
<MenuItem key={key} value={device.value}>
{device.label.toUpperCase()}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid size={{ md: 6, xs: 12 }}>
<TextField
fullWidth
name="longitude"
label="Longitude"
variant="outlined"
type="number"
value={backbone?.longitude}
/>
</Grid>
<Grid size={{ md: 6, xs: 12 }}>
<TextField
fullWidth
name="latitude"
label="Latitude"
variant="outlined"
type="number"
value={backbone?.latitude}
/>
</Grid>
</Grid>
);
}

View File

@ -3,6 +3,7 @@ import Breadcrumbs from "@mui/material/Breadcrumbs";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent"; import CardContent from "@mui/material/CardContent";
import Divider from "@mui/material/Divider";
import Link from "@mui/material/Link"; import Link from "@mui/material/Link";
import Grid from "@mui/material/Grid2"; import Grid from "@mui/material/Grid2";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
@ -13,6 +14,10 @@ import { ColumnDef } from "@tanstack/react-table";
import IconButton from "../components/IconButton"; import IconButton from "../components/IconButton";
import { Edit } from "iconsax-react"; import { Edit } from "iconsax-react";
import useTitle from "../hooks/useTitle"; import useTitle from "../hooks/useTitle";
import { useTowerStore } from "../stores/towerStore";
import { useEffect, useMemo } from "react";
import FlashMessage from "../components/FlashMessage";
import { TowerDataTable } from "../types/tower.types";
interface UserData { interface UserData {
tower_code: string; tower_code: string;
@ -23,28 +28,17 @@ interface UserData {
export default function Towers() { export default function Towers() {
useTitle("Towers"); useTitle("Towers");
const data: UserData[] = [ const { data, cacheExpired, getAll } = useTowerStore();
{ tower_code: 'TW-CODE-1', device: "DV-CODE-1", address: "Jl. Contoh 1" },
{ tower_code: 'TW-CODE-2', device: "DV-CODE-2", address: "Jl. Contoh 2" },
{ tower_code: 'TW-CODE-3', device: "", address: "Jl. Contoh 3" },
{ tower_code: 'TW-CODE-4', device: "", address: "Jl. Contoh 4" },
{ tower_code: 'TW-CODE-5', device: "DV-CODE-5", address: "Jl. Contoh 5" },
{ tower_code: 'TW-CODE-6', device: "", address: "Jl. Contoh 6" },
{ tower_code: 'TW-CODE-7', device: "", address: "Jl. Contoh 7" },
{ tower_code: 'TW-CODE-8', device: "DV-CODE-8", address: "Jl. Contoh 8" },
{ tower_code: 'TW-CODE-9', device: "", address: "Jl. Contoh 9" },
{ tower_code: 'TW-CODE-10', device: "DV-CODE-10", address: "Jl. Contoh 10" },
];
// Define columns // Define columns
const columns: ColumnDef<UserData>[] = [ const columns: ColumnDef<TowerDataTable>[] = useMemo(() => [
{ {
header: "Tower Code", header: "Tower Code",
accessorKey: "tower_code", accessorKey: "tower_code",
}, },
{ {
header: "Device", header: "Device",
accessorKey: "device", accessorKey: "device_code",
}, },
{ {
header: "Address", header: "Address",
@ -54,15 +48,23 @@ export default function Towers() {
header: '', header: '',
accessorKey: "action", accessorKey: "action",
enableSorting: false, enableSorting: false,
cell: () => ( cell: ({ row }) => (
<> <>
<IconButton variant="light" color="info" > <IconButton variant="text" color="secondary" href={`/towers/${row.original.id}`}>
<Edit variant="Bulk" color="currentColor" /> <Edit variant="Bulk" color="currentColor" />
</IconButton> </IconButton>
</> </>
) )
} }
]; ], []);
useEffect(() => {
if (cacheExpired()) {
(async () => {
await getAll();
})();
}
}, []);
return ( return (
<Box component="main"> <Box component="main">
@ -71,7 +73,7 @@ export default function Towers() {
<Typography variant="h3">Towers</Typography> <Typography variant="h3">Towers</Typography>
</Grid> </Grid>
<Grid size="auto"> <Grid size="auto">
<Button variant="contained" startIcon={<AddIcon color="inherit" />}> <Button variant="contained" href="/towers/create" startIcon={<AddIcon color="inherit" />}>
Add New Add New
</Button> </Button>
</Grid> </Grid>
@ -85,12 +87,15 @@ export default function Towers() {
</Grid> </Grid>
</Grid> </Grid>
<Card sx={{ display: "flex" }}> <FlashMessage store={useTowerStore} />
<CardContent sx={{ flex: "1 0 auto" }}>
<ReactTable <Card>
data={data} <Box sx={{ p: 2 }}>
columns={columns} <Typography variant="h5">List Towers</Typography>
/> </Box>
<Divider />
<CardContent sx={{ flex: "1 0 auto", pt: 2, px: 0 }}>
<ReactTable data={data || []} columns={columns} />
</CardContent> </CardContent>
</Card> </Card>
</Box> </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 { useTowerStore } from "../../stores/towerStore";
import { TowerRequest } from "../../types/tower.types";
import FlashMessage from "../../components/FlashMessage";
export default function TowerCreateOrEdit() {
useTitle("Towers");
const { pid } = useParams();
const { update, create, isLoading, find, clearTower, tower } = useTowerStore();
const IS_EDIT_PAGE = 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 TowerRequest;
if (IS_EDIT_PAGE) {
await update(pid!!, data);
} else {
await create(data);
}
}
useEffect(() => {
if (pid!!) {
(async () => {
await find(pid!!);
})()
} else {
(async () => {
await clearTower()
})()
}
}, [])
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="/towers">
<Back variant="Outline" color="currentColor" />
</IconButton>
<Typography variant="h3">{IS_EDIT_PAGE ? `Edit Tower - ${pid}` : 'Create Tower'}</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="/towers">
<Typography sx={{ color: "text.primary" }}>Towers</Typography>
</Link>
<Typography sx={{ color: "text.primary" }}>{IS_EDIT_PAGE ? pid : 'Create'}</Typography>
</Breadcrumbs>
</Grid>
</Grid>
<FlashMessage store={useTowerStore} />
<Card component="form" onSubmit={onSubmit}>
<Box
sx={{ px: 3, py: 1 }}
display={"flex"}
justifyContent={"space-between"}
alignItems={"center"}
>
<Typography variant="h5">Fill the form {tower?.tower_code}</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 />
</CardContent>
</Card>
</Box>
);
}

89
src/pages/Towers/Form.tsx Normal file
View File

@ -0,0 +1,89 @@
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 { useEffect, useState } from "react";
import { DeviceStatus } from "../../types/device.types";
import { useTowerStore } from "../../stores/towerStore";
export default function Form() {
const { tower, clearTower, deviceSelect, getDeviceSelect } = useTowerStore();
const [device, setDevice] = useState<string>("");
useEffect(() => {
(async () => {
await getDeviceSelect();
})();
if (tower) {
setDevice(tower.device_id as string);
}
const clear = async () => {
await clearTower();
};
return () => {
clear();
};
}, [tower]);
return (
<Grid container spacing={2}>
<Grid size={{ md: 6, xs: 12 }}>
<TextField
fullWidth
name="tower_code"
label="Tower Code"
variant="outlined"
value={tower?.tower_code}
/>
</Grid>
<Grid size={{ md: 6, xs: 12 }}>
<FormControl fullWidth>
<InputLabel id="device-label">Device</InputLabel>
<Select
name="device"
labelId="device-label"
label="Device"
value={device}
onChange={(e) => setDevice(e.target.value)}
>
{deviceSelect?.map((device, key) => (
<MenuItem key={key} value={device.value}>
{device.label.toUpperCase()}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid size={{ md: 6, xs: 12 }}>
<TextField
fullWidth
name="longitude"
label="Longitude"
variant="outlined"
type="number"
value={tower?.longitude}
/>
</Grid>
<Grid size={{ md: 6, xs: 12 }}>
<TextField
fullWidth
name="latitude"
label="Latitude"
variant="outlined"
type="number"
value={tower?.latitude}
/>
</Grid>
</Grid>
);
}

View File

@ -0,0 +1,70 @@
import { createAPI } from "../services/api";
import { BackboneRepositoryProps, Backbone, IGetDeviceSelect } from "../types/backbone.types";
import { ResponseApi } from "../types/response.types";
import { SelectState } from "../types/store.types";
const api = createAPI();
export const backboneRepository: BackboneRepositoryProps = {
async getAll() {
// const response = await api.get<ResponseApi<Backbone[]>>("/api/v1/backbones");
// return response.data.data as Backbone[];
return Promise.resolve([
{
id: "1",
backbone_code: "BB-01",
device_start: "DEV-S1",
device_end: "DEV-E1",
address: "Jl. Jend Sudirman No. XX",
core_amount: 10,
longitude: "1",
latitude: "1",
total_fishbone: 2,
}
])
},
async getBackboneById(id: string) {
// const response = await api.get<ResponseApi<Backbone>>(`/api/v1/backbones/${id}`);
// return response.data.data as Backbone;
return Promise.resolve({
id: "1",
backbone_code: "BB-01",
dev_start_id: "1",
dev_end_id: "2",
core_amount: 10,
longitude: "1",
latitude: "1",
created_at: "2024-01-01",
updated_at: "2024-01-01",
})
},
async createBackbone(data: Backbone) {
const response = await api.post<ResponseApi<null>>("/api/v1/backbones", data);
return response.data;
},
async updateBackbone(id: string, data: Backbone) {
const response = await api.put<ResponseApi<null>>(`/api/v1/backbones/${id}`, data);
return response.data;
},
async getDeviceSelect() {
// const response = await api.get<ResponseApi<SelectState[]>>("/api/v1/backbones/backbone");
// return (response.data.data as IGetBackboneSelect[])?.map((i: IGetBackboneSelect) => ({label: i.backbone_code, value: i.id})) as SelectState[];
return Promise.resolve([
{
label: "DV-EX-001",
value: "1",
},
{
label: "DV-EX-002",
value: "2",
}
]);
},
};

View File

@ -23,6 +23,18 @@ export const deviceRepository: DeviceRepositoryProps = {
total_used_port: 5, total_used_port: 5,
status: "active", status: "active",
created_at: "2024-01-01", created_at: "2024-01-01",
},
{
id: "2",
device_code: "DV-EX-002",
device_type: "OTB",
longitude: "1",
latitude: "1",
address: "Jl. Jend Sudirman No. XX",
port_amount: 10,
total_used_port: 5,
status: "active",
created_at: "2024-01-01",
} }
]) ])
}, },

View File

@ -0,0 +1,63 @@
import { createAPI } from "../services/api";
import { TowerRepositoryProps, Tower, IGetDeviceSelect } from "../types/tower.types";
import { ResponseApi } from "../types/response.types";
import { SelectState } from "../types/store.types";
const api = createAPI();
export const towerRepository: TowerRepositoryProps = {
async getAll() {
// const response = await api.get<ResponseApi<Tower[]>>("/api/v1/towers");
// return response.data.data as Tower[];
return Promise.resolve( [
{
id: "1",
tower_code: "TW-001",
longitude: "1",
latitude: "1",
device_code: "DV-EX-001",
address: "Jl. Jend Sudirman No. XX",
created_at: "2024-01-01",
}
])
},
async getTowerById(id: string) {
// const response = await api.get<ResponseApi<Tower>>(`/api/v1/towers/${id}`);
// return response.data.data as Tower;
return Promise.resolve({
id: "1",
tower_code: "TW-001",
longitude: "1",
latitude: "1",
device_id: "1",
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
})
},
async createTower(data: Tower) {
const response = await api.post<ResponseApi<null>>("/api/v1/towers", data);
return response.data;
},
async updateTower(id: string, data: Tower) {
const response = await api.put<ResponseApi<null>>(`/api/v1/towers/${id}`, data);
return response.data;
},
async getDeviceSelect() {
// const response = await api.get<ResponseApi<SelectState[]>>("/api/v1/towers/tower-select");
// return (response.data.data as IGetDeviceSelect[])?.map((i: IGetDeviceSelect) => ({label: i.tower_code, value: i.id})) as SelectState[];
return Promise.resolve([
{
label: "DV-EX-001",
value: "1",
}
]);
}
};

View File

@ -1,5 +1,4 @@
import { Routes as RRRoutes, Route } from "react-router"; import { Navigate, Routes as RRRoutes, Route } from "react-router";
import App from "../App";
import AuthLayout from "../layouts/AuthLayout"; import AuthLayout from "../layouts/AuthLayout";
import Login from "../pages/auth/Login"; import Login from "../pages/auth/Login";
import Dashboard from "../pages/Dashboard"; import Dashboard from "../pages/Dashboard";
@ -11,6 +10,8 @@ import Towers from "../pages/Towers";
import RequiredAuth from "./RequiredAuth"; import RequiredAuth from "./RequiredAuth";
import DeviceCreateOrEdit from "../pages/Devices/CreateOrEdit"; import DeviceCreateOrEdit from "../pages/Devices/CreateOrEdit";
import FishboneCreateOrEdit from "../pages/Fishbone/CreateOrEdit"; import FishboneCreateOrEdit from "../pages/Fishbone/CreateOrEdit";
import TowerCreateOrEdit from "../pages/Towers/CreateOrEdit";
import BackboneCreateOrEdit from "../pages/Backbone/CreateOrEdit";
export default function Routes() { export default function Routes() {
return ( return (
@ -22,7 +23,7 @@ export default function Routes() {
</Route> </Route>
<Route element={<RequiredAuth />}> <Route element={<RequiredAuth />}>
<Route path="/" element={<App />} /> <Route path="/" element={<Navigate to={"/dashboard"} />} />
<Route element={<DashboardLayout />}> <Route element={<DashboardLayout />}>
<Route path="/dashboard" element={<Dashboard />} /> <Route path="/dashboard" element={<Dashboard />} />
@ -37,6 +38,12 @@ export default function Routes() {
<Route path="/fishbone/:pid" element={<FishboneCreateOrEdit />} /> <Route path="/fishbone/:pid" element={<FishboneCreateOrEdit />} />
<Route path="/fishbone/create" element={<FishboneCreateOrEdit />} /> <Route path="/fishbone/create" element={<FishboneCreateOrEdit />} />
<Route path="/towers/:pid" element={<TowerCreateOrEdit />} />
<Route path="/towers/create" element={<TowerCreateOrEdit />} />
<Route path="/backbone/:pid" element={<BackboneCreateOrEdit />} />
<Route path="/backbone/create" element={<BackboneCreateOrEdit />} />
</Route> </Route>
</Route> </Route>

129
src/stores/backboneStore.ts Normal file
View File

@ -0,0 +1,129 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { backboneRepository } from "../repositories/backboneRepository";
import { createJSONStorage } from "zustand/middleware";
import { immer } from 'zustand/middleware/immer'
import { secureStorage } from "./storage";
import { Backbone, BackboneState } from "../types/backbone.types";
import dayjs from "dayjs";
export const useBackboneStore = create<BackboneState>()(
persist(
immer((set, get) => ({
data: [],
backbone: null,
isLoading: false,
error: null,
lastUpdate: null,
backboneSelect: [],
deviceSelect: [],
getAll: async () => {
set({ isLoading: true });
try {
const backbones = await backboneRepository.getAll();
set({ data: backbones, 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 backbone = await backboneRepository.getBackboneById(id);
set({ backbone: backbone });
} 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 (
backbone: Omit<Backbone, "id" & "created_at" & "updated_at">
) => {
set({ isLoading: true });
try {
await backboneRepository.createBackbone(backbone);
} 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,
backbone: Omit<Backbone, "id" & "created_at" & "updated_at">
) => {
set({ isLoading: true });
try {
await backboneRepository.updateBackbone(id, backbone);
} 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 });
}
},
getDeviceSelect: async () => {
set({ isLoading: true });
try {
const deviceSelect = await backboneRepository.getDeviceSelect();
set({ deviceSelect: deviceSelect });
} 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 }),
clearBackbone: () => {
set({ backbone: 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: "backbones", storage: createJSONStorage(() => secureStorage) }
)
);

128
src/stores/towerStore.ts Normal file
View File

@ -0,0 +1,128 @@
import { create } from "zustand";
import { towerRepository } from "../repositories/towerRepository";
import { persist } from "zustand/middleware";
import { createJSONStorage } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";
import { secureStorage } from "./storage";
import { Tower, TowerState } from "../types/tower.types";
import dayjs from "dayjs";
export const useTowerStore = create<TowerState>()(
persist(
immer((set, get) => ({
data: [],
tower: null,
isLoading: false,
error: null,
lastUpdate: null,
deviceSelect: [],
getAll: async () => {
set({ isLoading: true });
try {
const towers = await towerRepository.getAll();
set({ data: towers, 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 tower = await towerRepository.getTowerById(id);
set({ tower: tower });
} 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 (
tower: Omit<Tower, "id" & "created_at" & "updated_at">
) => {
set({ isLoading: true });
try {
await towerRepository.createTower(tower);
} 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,
tower: Omit<Tower, "id" & "created_at" & "updated_at">
) => {
set({ isLoading: true });
try {
await towerRepository.updateTower(id, tower);
} 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 });
}
},
getDeviceSelect: async () => {
set({ isLoading: true });
try {
const towers = await towerRepository.getDeviceSelect();
set({ deviceSelect: towers });
} 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 }),
clearTower: () => {
set({ tower: 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: "towers", storage: createJSONStorage(() => secureStorage) }
)
);

View File

@ -46,7 +46,7 @@ export default function ThemeCustomization({
} }
} }
const theme = Palette(ThemeMode.LIGHT); const theme = Palette(themeMode);
const themeTypography: TypographyVariantsOptions = const themeTypography: TypographyVariantsOptions =
useMemo<TypographyVariantsOptions>( useMemo<TypographyVariantsOptions>(

View File

@ -0,0 +1,53 @@
import { ResponseApi } from "./response.types";
import { CacheState, ErrorState, SelectState } from "./store.types";
export interface BackboneRepositoryProps {
getAll: () => Promise<BackboneDataTable[] | []>;
getBackboneById: (id: string) => Promise<Backbone>;
createBackbone: (backbone: BackboneRequest) => Promise<Pick<ResponseApi<any>, "status">>;
updateBackbone: (id: string, backbone: BackboneRequest) => Promise<Pick<ResponseApi<any>, "status">>;
getDeviceSelect: () => Promise<SelectState[] | null>;
}
export interface Backbone {
id: string;
backbone_code: string;
longitude: string | number;
latitude: string | number;
dev_start_id?: string | number | null;
dev_end_id?: string | number | null;
core_amount: string | number;
created_at: string;
updated_at: string;
}
export interface BackboneRequest extends Omit<Backbone, "id" & "created_at" & "updated_at"> {}
/**
* Backbone State
* =============
*/
export interface BackboneState extends ErrorState, CacheState {
data?: BackboneDataTable[];
backbone?: Backbone | null;
isLoading: boolean;
lastUpdate: number | null;
deviceSelect: SelectState[] | null;
getDeviceSelect: () => Promise<void>;
getAll: () => Promise<void>;
find: (id: string) => Promise<void>;
create: (backbone: BackboneRequest) => Promise<void>;
update: (id: string, backbone: BackboneRequest) => Promise<void>;
clearBackbone: () => void;
}
export interface BackboneDataTable extends Pick<Backbone, "id" | "backbone_code" | "longitude" | "latitude" | "core_amount"> {
device_start: string;
device_end: string;
total_fishbone: string | number;
}
export interface IGetDeviceSelect {
id: string;
device_code: string;
}

50
src/types/tower.types.ts Normal file
View File

@ -0,0 +1,50 @@
import { ResponseApi } from "./response.types";
import { CacheState, ErrorState, SelectState } from "./store.types";
export interface TowerRepositoryProps {
getAll: () => Promise<TowerDataTable[] | []>;
getTowerById: (id: string) => Promise<Tower>;
createTower: (tower: TowerRequest) => Promise<Pick<ResponseApi<any>, "status">>;
updateTower: (id: string, tower: TowerRequest) => Promise<Pick<ResponseApi<any>, "status">>;
getDeviceSelect: () => Promise<SelectState[] | null>;
}
export interface Tower {
id: string;
tower_code: string;
longitude: string | number;
latitude: string | number;
device_id?: string | number | null
created_at: string;
updated_at: string;
}
export interface TowerRequest extends Omit<Tower, "id" & "created_at" & "updated_at"> {}
/**
* Tower State
* =============
*/
export interface TowerState extends ErrorState, CacheState {
data?: TowerDataTable[];
tower?: Tower | null;
isLoading: boolean;
lastUpdate: number | null;
deviceSelect: SelectState[] | null;
getDeviceSelect: () => Promise<void>;
getAll: () => Promise<void>;
find: (id: string) => Promise<void>;
create: (tower: TowerRequest) => Promise<void>;
update: (id: string, tower: TowerRequest) => Promise<void>;
clearTower: () => void;
}
export interface TowerDataTable extends Pick<Tower, "id" | "tower_code" | "longitude" | "latitude"> {
address: string;
device_code: string;
}
export interface IGetDeviceSelect {
id: string;
device_code: string;
}