feat: impl cache & crud mock in devices
This commit is contained in:
parent
5e471b7e20
commit
778b05fd50
|
|
@ -1,2 +1,4 @@
|
||||||
VITE_API_URL="http://localhost:8000"
|
VITE_API_URL="http://localhost:8000"
|
||||||
VITE_APP_KEY="4ae931db63f0d18f5082781be5e2c622"
|
VITE_APP_KEY="4ae931db63f0d18f5082781be5e2c622"
|
||||||
|
VITE_APP_CACHE_TIME_VALUE=30
|
||||||
|
VITE_APP_CACHE_TIME_UNIT="seconds"
|
||||||
|
|
@ -17,7 +17,9 @@
|
||||||
"@tanstack/react-table": "^8.21.2",
|
"@tanstack/react-table": "^8.21.2",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"iconsax-react": "^0.0.8",
|
"iconsax-react": "^0.0.8",
|
||||||
|
"immer": "^10.1.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
|
@ -2414,6 +2416,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/dayjs": {
|
||||||
|
"version": "1.11.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||||
|
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||||
|
|
@ -2959,6 +2966,15 @@
|
||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/immer": {
|
||||||
|
"version": "10.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
|
||||||
|
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/immer"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/import-fresh": {
|
"node_modules/import-fresh": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,9 @@
|
||||||
"@tanstack/react-table": "^8.21.2",
|
"@tanstack/react-table": "^8.21.2",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"iconsax-react": "^0.0.8",
|
"iconsax-react": "^0.0.8",
|
||||||
|
"immer": "^10.1.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Alert from "@mui/material/Alert";
|
||||||
|
import { DeviceState } from "../types/device.types";
|
||||||
|
import { AuthState } from "../types/auth.types";
|
||||||
|
|
||||||
|
interface FlashMessageProps {
|
||||||
|
store: () => DeviceState | AuthState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FlashMessage(props: FlashMessageProps) {
|
||||||
|
const { error, clearError } = props.store();
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string | null>("");
|
||||||
|
|
||||||
|
const onCloseError = () => {
|
||||||
|
setErrorMessage(null);
|
||||||
|
clearError()
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (error) {
|
||||||
|
setErrorMessage(error as string);
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
clearError()
|
||||||
|
}, 500);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
if (!errorMessage) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box display={"flex"} mb={2}>
|
||||||
|
<Alert
|
||||||
|
severity="error"
|
||||||
|
variant="border"
|
||||||
|
onClose={onCloseError}
|
||||||
|
sx={{ width: "100%" }}
|
||||||
|
>
|
||||||
|
{errorMessage}
|
||||||
|
</Alert>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -58,7 +58,7 @@ function ComponentReactTable<T extends object>({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Box justifyContent={"space-between"} display="flex">
|
<Box justifyContent={"space-between"} display="flex" sx={{ px: 2}}>
|
||||||
<Box>
|
<Box>
|
||||||
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -106,7 +106,13 @@ function ComponentReactTable<T extends object>({
|
||||||
))}
|
))}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody className={striped ? "striped" : undefined}>
|
<TableBody className={striped ? "striped" : undefined}>
|
||||||
{table.getRowModel().rows.map((row) => (
|
{table.getRowModel().rows.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={columns.length} align="center">
|
||||||
|
No data.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : table.getRowModel().rows.map((row) => (
|
||||||
<TableRow key={row.id}>
|
<TableRow key={row.id}>
|
||||||
{row.getVisibleCells().map((cell) => (
|
{row.getVisibleCells().map((cell) => (
|
||||||
<TableCell key={cell.id}>
|
<TableCell key={cell.id}>
|
||||||
|
|
|
||||||
|
|
@ -14,33 +14,18 @@ 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 { useDeviceStore } from "../stores/deviceStore";
|
||||||
interface UserData {
|
import { Device } from "../types/device.types";
|
||||||
device_code: string;
|
import { useEffect } from "react";
|
||||||
device_type: string;
|
import FlashMessage from "../components/FlashMessage";
|
||||||
address: string;
|
|
||||||
port_amount: string;
|
|
||||||
status: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Devices() {
|
export default function Devices() {
|
||||||
useTitle("Devices");
|
useTitle("Devices");
|
||||||
|
|
||||||
const data: UserData[] = [
|
const { data, cacheExpired, getAll } = useDeviceStore();
|
||||||
{ device_code: 'DV-CODE-1', device_type: "OTB", address: "Jl. Contoh 1", port_amount: '16', status: "Active" },
|
|
||||||
{ device_code: 'DV-CODE-2', device_type: "OTB", address: "Jl. Contoh 2", port_amount: '16', status: "Active" },
|
|
||||||
{ device_code: 'DV-CODE-3', device_type: "OTB", address: "Jl. Contoh 3", port_amount: '16', status: "Active" },
|
|
||||||
{ device_code: 'DV-CODE-4', device_type: "OTB", address: "Jl. Contoh 4", port_amount: '16', status: "Active" },
|
|
||||||
{ device_code: 'DV-CODE-5', device_type: "OTB", address: "Jl. Contoh 5", port_amount: '16', status: "Active" },
|
|
||||||
{ device_code: 'DV-CODE-6', device_type: "OTB", address: "Jl. Contoh 6", port_amount: '16', status: "Active" },
|
|
||||||
{ device_code: 'DV-CODE-7', device_type: "OTB", address: "Jl. Contoh 7", port_amount: '16', status: "Active" },
|
|
||||||
{ device_code: 'DV-CODE-8', device_type: "OTB", address: "Jl. Contoh 8", port_amount: '16', status: "Active" },
|
|
||||||
{ device_code: 'DV-CODE-9', device_type: "OTB", address: "Jl. Contoh 9", port_amount: '16', status: "Active" },
|
|
||||||
{ device_code: 'DV-CODE-10', device_type: "OTB", address: "Jl. Contoh 10", port_amount: '16', status: "Active" },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Define columns
|
// Define columns
|
||||||
const columns: ColumnDef<UserData>[] = [
|
const columns: ColumnDef<Device>[] = [
|
||||||
{
|
{
|
||||||
header: "Device Code",
|
header: "Device Code",
|
||||||
accessorKey: "device_code",
|
accessorKey: "device_code",
|
||||||
|
|
@ -61,25 +46,41 @@ export default function Devices() {
|
||||||
header: "Status",
|
header: "Status",
|
||||||
accessorKey: "status",
|
accessorKey: "status",
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Typography color={row.original.status === "Active" ? "success.main" : "error.main"}>
|
<Typography
|
||||||
|
color={
|
||||||
|
row.original.status.toLowerCase() === "active"
|
||||||
|
? "success.main"
|
||||||
|
: row.original.status.toLowerCase() === "inactive"
|
||||||
|
? "error.main"
|
||||||
|
: "warning.main"
|
||||||
|
}
|
||||||
|
>
|
||||||
{row.original.status}
|
{row.original.status}
|
||||||
</Typography>
|
</Typography>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: '',
|
header: "",
|
||||||
accessorKey: "action",
|
accessorKey: "action",
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
cell: () => (
|
cell: ({ row }) => (
|
||||||
<>
|
<>
|
||||||
<IconButton variant="text" color="secondary" >
|
<IconButton variant="text" color="secondary" href={`/devices/${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">
|
||||||
<Grid container spacing={2} alignItems={"center"} mb={4}>
|
<Grid container spacing={2} alignItems={"center"} mb={4}>
|
||||||
|
|
@ -87,7 +88,11 @@ export default function Devices() {
|
||||||
<Typography variant="h3">Devices</Typography>
|
<Typography variant="h3">Devices</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size="auto">
|
<Grid size="auto">
|
||||||
<Button href="/devices/create" variant="contained" startIcon={<AddIcon color="inherit" />}>
|
<Button
|
||||||
|
href="/devices/create"
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<AddIcon color="inherit" />}
|
||||||
|
>
|
||||||
Add New
|
Add New
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
@ -101,16 +106,15 @@ export default function Devices() {
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
<FlashMessage store={useDeviceStore} />
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<Box sx={{ p: 2 }}>
|
<Box sx={{ p: 2 }}>
|
||||||
<Typography variant="h5">List Devices</Typography>
|
<Typography variant="h5">List Devices</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Divider />
|
<Divider />
|
||||||
<CardContent sx={{ flex: "1 0 auto", pt: 2 }}>
|
<CardContent sx={{ flex: "1 0 auto", pt: 2, px: 0 }}>
|
||||||
<ReactTable
|
<ReactTable data={data || []} columns={columns} />
|
||||||
data={data}
|
|
||||||
columns={columns}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import Alert from "@mui/material/Alert";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Breadcrumbs from "@mui/material/Breadcrumbs";
|
import Breadcrumbs from "@mui/material/Breadcrumbs";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
|
|
@ -14,14 +13,15 @@ import IconButton from "../../components/IconButton";
|
||||||
import { Back } from "iconsax-react";
|
import { Back } from "iconsax-react";
|
||||||
import { useParams } from "react-router";
|
import { useParams } from "react-router";
|
||||||
import Form from "./Form";
|
import Form from "./Form";
|
||||||
import { FormEvent } from "react";
|
import { FormEvent, useEffect } from "react";
|
||||||
import { useDeviceStore } from "../../stores/deviceStore";
|
import { useDeviceStore } from "../../stores/deviceStore";
|
||||||
import { DeviceRequest } from "../../types/device.types";
|
import { DeviceRequest } from "../../types/device.types";
|
||||||
|
import FlashMessage from "../../components/FlashMessage";
|
||||||
|
|
||||||
export default function DeviceCreate() {
|
export default function DeviceCreate() {
|
||||||
useTitle("Devices");
|
useTitle("Devices");
|
||||||
const { pid } = useParams();
|
const { pid } = useParams();
|
||||||
const { update, create, error } = useDeviceStore();
|
const { update, create, isLoading, find, clearDevice, device } = useDeviceStore();
|
||||||
|
|
||||||
const IS_EDIT_PAGE = pid!!;
|
const IS_EDIT_PAGE = pid!!;
|
||||||
|
|
||||||
|
|
@ -36,8 +36,21 @@ export default function DeviceCreate() {
|
||||||
await update(pid!!, data);
|
await update(pid!!, data);
|
||||||
} else {
|
} else {
|
||||||
await create(data);
|
await create(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (pid!!) {
|
||||||
|
(async () => {
|
||||||
|
await find(pid!!);
|
||||||
|
})()
|
||||||
|
} else {
|
||||||
|
(async () => {
|
||||||
|
await clearDevice()
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box component="main">
|
<Box component="main">
|
||||||
<Grid container spacing={2} alignItems={"center"} mb={4}>
|
<Grid container spacing={2} alignItems={"center"} mb={4}>
|
||||||
|
|
@ -62,11 +75,7 @@ export default function DeviceCreate() {
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{error && (
|
<FlashMessage store={useDeviceStore} />
|
||||||
<Box display={"flex"} mb={2}>
|
|
||||||
<Alert severity="error" variant="border" sx={{ width: "100%" }}>{error.toString()} asass</Alert>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Card component="form" onSubmit={onSubmit}>
|
<Card component="form" onSubmit={onSubmit}>
|
||||||
<Box
|
<Box
|
||||||
|
|
@ -75,8 +84,9 @@ export default function DeviceCreate() {
|
||||||
justifyContent={"space-between"}
|
justifyContent={"space-between"}
|
||||||
alignItems={"center"}
|
alignItems={"center"}
|
||||||
>
|
>
|
||||||
<Typography variant="h5">Fill the form</Typography>
|
<Typography variant="h5">Fill the form {device?.device_code}</Typography>
|
||||||
<Button
|
<Button
|
||||||
|
disabled={isLoading}
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
size="small"
|
size="small"
|
||||||
|
|
|
||||||
|
|
@ -6,21 +6,34 @@ import InputLabel from "@mui/material/InputLabel";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import Select from "@mui/material/Select";
|
import Select from "@mui/material/Select";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { DeviceStatus } from '../../types/device.types';
|
import { DeviceStatus } from '../../types/device.types';
|
||||||
|
import { useDeviceStore } from '../../stores/deviceStore';
|
||||||
|
|
||||||
export default function Form() {
|
export default function Form() {
|
||||||
|
const { device, clearDevice } = useDeviceStore();
|
||||||
const [select, setSelect] = useState<DeviceStatus>('active');
|
const [select, setSelect] = useState<DeviceStatus>('active');
|
||||||
|
|
||||||
const STATUS = ["active", "inactive", "maintenance"];
|
const STATUS = ["active", "inactive", "maintenance"];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelect(device?.status || 'active');
|
||||||
|
|
||||||
|
const clear = async () => {
|
||||||
|
await clearDevice();
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clear();
|
||||||
|
};
|
||||||
|
}, [device]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
{/* Device Code */}
|
{/* Device Code */}
|
||||||
<Grid size={{ md: 4, xs: 12 }}>
|
<Grid size={{ md: 4, xs: 12 }}>
|
||||||
<TextField fullWidth name='device_code' label="Device Code" variant="outlined" />
|
<TextField fullWidth name='device_code' label="Device Code" variant="outlined" value={device?.device_code} />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* Total Port */}
|
{/* Total Port */}
|
||||||
<Grid size={{ md: 4, xs: 12 }}>
|
<Grid size={{ md: 4, xs: 12 }}>
|
||||||
<TextField
|
<TextField
|
||||||
|
|
@ -29,6 +42,7 @@ export default function Form() {
|
||||||
label="Total Port"
|
label="Total Port"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
type="number"
|
type="number"
|
||||||
|
value={device?.port_amount}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
|
@ -75,6 +89,7 @@ export default function Form() {
|
||||||
label="Longitude"
|
label="Longitude"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
type="number"
|
type="number"
|
||||||
|
value={device?.longitude}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
|
@ -85,6 +100,7 @@ export default function Form() {
|
||||||
label="Latitude"
|
label="Latitude"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
type="number"
|
type="number"
|
||||||
|
value={device?.latitude}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,42 @@ const api = createAPI();
|
||||||
|
|
||||||
export const deviceRepository: DeviceRepositoryProps = {
|
export const deviceRepository: DeviceRepositoryProps = {
|
||||||
async getAll() {
|
async getAll() {
|
||||||
const response = await api.get<ResponseApi<Device[]>>("/api/v1/devices");
|
// const response = await api.get<ResponseApi<Device[]>>("/api/v1/devices");
|
||||||
|
|
||||||
return response.data.data as Device[];
|
// return response.data.data as Device[];
|
||||||
|
|
||||||
|
return Promise.resolve( [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
device_code: "DV-EX-001",
|
||||||
|
device_type: "OTB",
|
||||||
|
longitude: "1",
|
||||||
|
latitude: "1",
|
||||||
|
address: "Jl. Jend Sudirman No. XX",
|
||||||
|
port_amount: 10,
|
||||||
|
status: "active",
|
||||||
|
created_at: "2024-01-01",
|
||||||
|
updated_at: "2024-01-01",
|
||||||
|
}
|
||||||
|
])
|
||||||
},
|
},
|
||||||
|
|
||||||
async getDeviceById(id: string) {
|
async getDeviceById(id: string) {
|
||||||
const response = await api.get<ResponseApi<Device>>(`/api/v1/devices/${id}`);
|
// const response = await api.get<ResponseApi<Device>>(`/api/v1/devices/${id}`);
|
||||||
|
|
||||||
return response.data.data as Device;
|
// return response.data.data as Device;
|
||||||
|
return Promise.resolve({
|
||||||
|
id: "1",
|
||||||
|
device_code: "DV-EX-001",
|
||||||
|
device_type: "OTB",
|
||||||
|
longitude: "1",
|
||||||
|
latitude: "1",
|
||||||
|
address: "Jl. Jend Sudirman No. XX",
|
||||||
|
port_amount: 10,
|
||||||
|
status: "active",
|
||||||
|
created_at: "2024-01-01",
|
||||||
|
updated_at: "2024-01-01",
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
async createDevice(data: Device) {
|
async createDevice(data: Device) {
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,10 @@ export const useAuthStore = create<AuthState>()(
|
||||||
} finally {
|
} finally {
|
||||||
set({ token: null, user: null, error: null, isLoading: false });
|
set({ token: null, user: null, error: null, isLoading: false });
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearError: () => {
|
||||||
|
set({ error: null });
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,29 +2,34 @@ import { create } from "zustand";
|
||||||
import { deviceRepository } from "../repositories/deviceRepository";
|
import { deviceRepository } from "../repositories/deviceRepository";
|
||||||
import { persist } from "zustand/middleware";
|
import { persist } from "zustand/middleware";
|
||||||
import { createJSONStorage } from "zustand/middleware";
|
import { createJSONStorage } from "zustand/middleware";
|
||||||
|
import { immer } from 'zustand/middleware/immer'
|
||||||
import { secureStorage } from "./storage";
|
import { secureStorage } from "./storage";
|
||||||
import { Device, DeviceState } from "../types/device.types";
|
import { Device, DeviceState } from "../types/device.types";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
export const useDeviceStore = create<DeviceState>()(
|
export const useDeviceStore = create<DeviceState>()(
|
||||||
persist(
|
persist(
|
||||||
(set) => ({
|
immer((set, get) => ({
|
||||||
data: [],
|
data: [],
|
||||||
device: null,
|
device: null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
lastUpdate: null,
|
||||||
getAll: async () => {
|
getAll: async () => {
|
||||||
set({ isLoading: true });
|
set({ isLoading: true });
|
||||||
try {
|
try {
|
||||||
const devices = await deviceRepository.getAll();
|
const devices = await deviceRepository.getAll();
|
||||||
set({ data: devices });
|
set({ data: devices, lastUpdate: dayjs().valueOf() });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.status === 401) {
|
if (error.status === 401) {
|
||||||
set({ error: error.response.status.description });
|
set({ error: error.response?.status?.description });
|
||||||
} else {
|
} else {
|
||||||
set({ error: error.response.status.description || error.message });
|
set({
|
||||||
|
error: error.response?.status?.description || error.message,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
set({ isLoading: false });
|
set({ isLoading: false, lastUpdate: dayjs().valueOf() });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
find: async (id: string) => {
|
find: async (id: string) => {
|
||||||
|
|
@ -37,45 +42,69 @@ export const useDeviceStore = create<DeviceState>()(
|
||||||
if (error.status === 401) {
|
if (error.status === 401) {
|
||||||
set({ error: error.response.status.description });
|
set({ error: error.response.status.description });
|
||||||
} else {
|
} else {
|
||||||
set({ error: error.response?.status?.description || error.message });
|
set({
|
||||||
|
error: error.response?.status?.description || error.message,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
set({ isLoading: false });
|
set({ isLoading: false });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
create: async (device: Omit<Device, "id" & "created_at" & "updated_at">) => {
|
create: async (
|
||||||
|
device: Omit<Device, "id" & "created_at" & "updated_at">
|
||||||
|
) => {
|
||||||
set({ isLoading: true });
|
set({ isLoading: true });
|
||||||
try {
|
try {
|
||||||
await deviceRepository.createDevice(device);
|
await deviceRepository.createDevice(device);
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
|
||||||
if (error.status === 401) {
|
if (error.status === 401) {
|
||||||
set({ error: error.response.status.description });
|
set({ error: error.response.status.description });
|
||||||
} else {
|
} else {
|
||||||
set({ error: error.response?.status?.description || error.message });
|
set({
|
||||||
|
error: error.response?.status?.description || error.message,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
set({ isLoading: false });
|
set({ isLoading: false });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
update: async (id: string, device: Omit<Device, "id" & "created_at" & "updated_at">) => {
|
update: async (
|
||||||
|
id: string,
|
||||||
|
device: Omit<Device, "id" & "created_at" & "updated_at">
|
||||||
|
) => {
|
||||||
set({ isLoading: true });
|
set({ isLoading: true });
|
||||||
try {
|
try {
|
||||||
await deviceRepository.updateDevice(id, device);
|
await deviceRepository.updateDevice(id, device);
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.status === 401) {
|
if (error.status === 401) {
|
||||||
set({ error: error.response.status.description });
|
set({ error: error.response.status.description });
|
||||||
} else {
|
} else {
|
||||||
set({ error: error.response?.status?.description || error.message });
|
set({
|
||||||
|
error: error.response?.status?.description || error.message,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
set({ isLoading: false });
|
set({ isLoading: false });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
clearError: () => set({ error: null }),
|
||||||
|
clearDevice: () => {
|
||||||
|
set({ device: 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: "devices", storage: createJSONStorage(() => secureStorage) }
|
{ name: "devices", storage: createJSONStorage(() => secureStorage) }
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { ErrorState } from "./store.types";
|
||||||
|
|
||||||
export interface LoginPayload {
|
export interface LoginPayload {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
|
@ -22,11 +24,10 @@ export interface UserProfile {
|
||||||
* Auth state
|
* Auth state
|
||||||
* ===========
|
* ===========
|
||||||
*/
|
*/
|
||||||
export interface AuthState {
|
export interface AuthState extends ErrorState<AuthErrorInputState> {
|
||||||
token: string | null;
|
token: string | null;
|
||||||
user: UserProfile | null;
|
user: UserProfile | null;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
error: string | null | AuthErrorInputState;
|
|
||||||
login: (payload: LoginPayload) => Promise<boolean>;
|
login: (payload: LoginPayload) => Promise<boolean>;
|
||||||
logout: () => Promise<void>;
|
logout: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { ResponseApi } from "./response.types";
|
import { ResponseApi } from "./response.types";
|
||||||
|
import { CacheState, ErrorState } from "./store.types";
|
||||||
|
|
||||||
export interface DeviceRepositoryProps {
|
export interface DeviceRepositoryProps {
|
||||||
getAll: () => Promise<Device[] | []>;
|
getAll: () => Promise<Device[] | []>;
|
||||||
|
|
@ -14,7 +15,7 @@ export interface Device {
|
||||||
longitude: string | number;
|
longitude: string | number;
|
||||||
latitude: string | number;
|
latitude: string | number;
|
||||||
port_amount: number;
|
port_amount: number;
|
||||||
status: string;
|
status: DeviceStatus;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|
@ -27,15 +28,16 @@ export interface DeviceRequest extends Omit<Device, "id" & "created_at" & "updat
|
||||||
* Device State
|
* Device State
|
||||||
* =============
|
* =============
|
||||||
*/
|
*/
|
||||||
export interface DeviceState {
|
export interface DeviceState extends ErrorState, CacheState {
|
||||||
data?: Device[];
|
data?: Device[];
|
||||||
device?: Device | null;
|
device?: Device | null;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
error: string | null | DeviceErrorInputState;
|
lastUpdate: number | null;
|
||||||
getAll: () => Promise<void>;
|
getAll: () => Promise<void>;
|
||||||
find: (id: string) => Promise<void>;
|
find: (id: string) => Promise<void>;
|
||||||
create: (device: DeviceRequest) => Promise<void>;
|
create: (device: DeviceRequest) => Promise<void>;
|
||||||
update: (id: string, device: DeviceRequest) => Promise<void>;
|
update: (id: string, device: DeviceRequest) => Promise<void>;
|
||||||
|
clearDevice: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceErrorInputState {
|
export interface DeviceErrorInputState {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
export interface ErrorState<T = undefined> {
|
||||||
|
error: string | null | T;
|
||||||
|
clearError: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CacheState {
|
||||||
|
cacheExpired: () => boolean;
|
||||||
|
}
|
||||||
|
|
@ -20,7 +20,10 @@
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true,
|
||||||
|
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
import { ManipulateType } from "dayjs"
|
||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_APP_KEY: string
|
readonly VITE_APP_KEY: string
|
||||||
readonly VITE_API_URL: string
|
readonly VITE_API_URL: string
|
||||||
|
readonly VITE_APP_CACHE_TIME_VALUE: number
|
||||||
|
readonly VITE_APP_CACHE_TIME_UNIT: ManipulateType
|
||||||
// more env variables...
|
// more env variables...
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue