feat: crud devices (wip: notif success)
This commit is contained in:
parent
f2bb8eb2bf
commit
e13e236b4a
|
|
@ -11,6 +11,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.0.14",
|
"@tailwindcss/vite": "^4.0.14",
|
||||||
|
"@tanstack/react-form": "^1.1.2",
|
||||||
"@tanstack/react-query": "^5.69.0",
|
"@tanstack/react-query": "^5.69.0",
|
||||||
"@tanstack/react-table": "^8.21.2",
|
"@tanstack/react-table": "^8.21.2",
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ const Header: React.FC<HeaderProps> = ({
|
||||||
toggleSidebar,
|
toggleSidebar,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<header className="p-4 sticky top-0 z-[2]">
|
<header className="p-4 sticky top-0 z-[1]">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
{/* Mobile Sidebar Toggle */}
|
{/* Mobile Sidebar Toggle */}
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
|
|
@ -84,13 +84,13 @@ const Header: React.FC<HeaderProps> = ({
|
||||||
className="dropdown-content bg-base-100 rounded-box w-52"
|
className="dropdown-content bg-base-100 rounded-box w-52"
|
||||||
>
|
>
|
||||||
<Menu.Title>Hi, {user?.name}</Menu.Title>
|
<Menu.Title>Hi, {user?.name}</Menu.Title>
|
||||||
<Dropdown.Item>
|
<Dropdown.Item anchor={false}>
|
||||||
<NavLink to="#" className="justify-between">
|
<NavLink to="/" className="justify-between">
|
||||||
Profile
|
Profile
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
<Dropdown.Item>
|
<Dropdown.Item anchor={false}>
|
||||||
<NavLink to="#">Settings</NavLink>
|
<NavLink to="/">Settings</NavLink>
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
<Dropdown.Item anchor={false}>
|
<Dropdown.Item anchor={false}>
|
||||||
<button onClick={handleLogout}>
|
<button onClick={handleLogout}>
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ const Sidebar: React.FC<SidebarProps> = ({ sidebarOpen, user, handleLogout }) =>
|
||||||
>
|
>
|
||||||
<ReactSVG src={BackboneIcon} title="Backbone" className="w-5 h-5 mr-2" />
|
<ReactSVG src={BackboneIcon} title="Backbone" className="w-5 h-5 mr-2" />
|
||||||
Backbone
|
Backbone
|
||||||
|
<div className="badge badge-neutral">WIP</div>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
|
|
@ -80,6 +81,7 @@ const Sidebar: React.FC<SidebarProps> = ({ sidebarOpen, user, handleLogout }) =>
|
||||||
>
|
>
|
||||||
<ReactSVG src={FishboneIcon} title="Fishbone" className="w-5 h-5 mr-2" />
|
<ReactSVG src={FishboneIcon} title="Fishbone" className="w-5 h-5 mr-2" />
|
||||||
Fishbone
|
Fishbone
|
||||||
|
<div className="badge badge-neutral">WIP</div>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
|
|
@ -102,6 +104,7 @@ const Sidebar: React.FC<SidebarProps> = ({ sidebarOpen, user, handleLogout }) =>
|
||||||
>
|
>
|
||||||
<ReactSVG src={TowerIcon} title="Tower" className="w-5 h-5 mr-2" />
|
<ReactSVG src={TowerIcon} title="Tower" className="w-5 h-5 mr-2" />
|
||||||
Tower
|
Tower
|
||||||
|
<div className="badge badge-neutral">WIP</div>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
@ -120,6 +123,7 @@ const Sidebar: React.FC<SidebarProps> = ({ sidebarOpen, user, handleLogout }) =>
|
||||||
>
|
>
|
||||||
<ReactSVG src={UserIcon} title="User" className="w-5 h-5 mr-2" />
|
<ReactSVG src={UserIcon} title="User" className="w-5 h-5 mr-2" />
|
||||||
User
|
User
|
||||||
|
<div className="badge badge-neutral">WIP</div>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ const DashboardLayout: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Main content area */}
|
{/* Main content area */}
|
||||||
<main className="flex-1 p-6 pt-0 z-[1]">
|
<main className="flex-1 p-6 pt-0 z-[0]">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,6 @@ interface Callback {
|
||||||
|
|
||||||
export const login = async (credentials: LoginCredentials): Promise<LoginResponse> => {
|
export const login = async (credentials: LoginCredentials): Promise<LoginResponse> => {
|
||||||
const response = await apiClient.post<ApiResponse<LoginResponse>>('/api/v1/users/login', credentials);
|
const response = await apiClient.post<ApiResponse<LoginResponse>>('/api/v1/users/login', credentials);
|
||||||
|
|
||||||
console.log(response.data);
|
|
||||||
|
|
||||||
if (!response.data.data) {
|
if (!response.data.data) {
|
||||||
throw new Error('Authentication failed');
|
throw new Error('Authentication failed');
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// src/features/devices/api/devices.ts
|
// src/features/devices/api/devices.ts
|
||||||
import { apiClient } from '@/lib/api-client';
|
import { apiClient, extractApiErrors } from '@/lib/api-client';
|
||||||
import { DeviceDataTable } from '../types';
|
import { DeviceDataTable, DeviceRequest, DeviceSingleResponse } from '../types';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { ApiResponse } from '@/lib/api-client'; // Pastikan Anda memiliki tipe ApiResponse
|
import { ApiResponse } from '@/lib/api-client'; // Pastikan Anda memiliki tipe ApiResponse
|
||||||
import axios, { AxiosError } from 'axios';
|
import axios, { AxiosError } from 'axios';
|
||||||
import { DataTableDummy } from '../dummy';
|
import { DataTableDummy } from '../dummy';
|
||||||
|
|
@ -28,7 +28,7 @@ export const getDevices = async (): Promise<DeviceDataTable[]> => {
|
||||||
throw new Error(response.data.status.description || 'Unknown error');
|
throw new Error(response.data.status.description || 'Unknown error');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Tangani berbagai jenis error
|
// Tangani berbagai jenis error
|
||||||
if (DataTableDummy.length > 0) {
|
if (DataTableDummy.length > 0 && false) {
|
||||||
return DataTableDummy as unknown as DeviceDataTable[];
|
return DataTableDummy as unknown as DeviceDataTable[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,6 +73,71 @@ export const getDevices = async (): Promise<DeviceDataTable[]> => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createDevice = async (deviceData: DeviceRequest): Promise<Pick<ApiResponse<null>, "status">> => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.post<ApiResponse<null>>('/api/v1/devices', deviceData);
|
||||||
|
return response.data.status as unknown as Pick<ApiResponse<null>, "status">;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
const apiError = error.response?.data?.status;
|
||||||
|
throw {
|
||||||
|
code: apiError?.code || 500,
|
||||||
|
message: apiError?.description || 'An unexpected error occurred',
|
||||||
|
errors: apiError?.errors || {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateDevice = async (id: string, deviceData: Partial<DeviceRequest>): Promise<Pick<ApiResponse<null>, "status">> => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.put<ApiResponse<null>>(`/api/v1/devices/${id}`, deviceData);
|
||||||
|
return response.data.status as unknown as Pick<ApiResponse<null>, "status">;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
const apiError = error.response?.data?.status;
|
||||||
|
throw {
|
||||||
|
code: apiError?.code || 500,
|
||||||
|
message: apiError?.description || 'An unexpected error occurred',
|
||||||
|
errors: apiError?.errors || {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDeviceById = async (id: string): Promise<DeviceSingleResponse> => {
|
||||||
|
console.log(id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get<ApiResponse<DeviceSingleResponse>>(`/api/v1/devices/${id}`);
|
||||||
|
return response.data.data as DeviceSingleResponse;
|
||||||
|
} catch (error) {
|
||||||
|
const errors = extractApiErrors(error);
|
||||||
|
|
||||||
|
console.log(errors);
|
||||||
|
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
const apiError = error.response?.data?.status;
|
||||||
|
throw {
|
||||||
|
code: apiError?.code || 500,
|
||||||
|
message: apiError?.description || 'An unexpected error occurred',
|
||||||
|
errors: apiError?.errors || {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ===========================================
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* ===========================================
|
||||||
|
*/
|
||||||
|
|
||||||
export const useDevices = () => {
|
export const useDevices = () => {
|
||||||
return useQuery<DeviceDataTable[], Error>({
|
return useQuery<DeviceDataTable[], Error>({
|
||||||
queryKey: ['devices'],
|
queryKey: ['devices'],
|
||||||
|
|
@ -80,4 +145,41 @@ export const useDevices = () => {
|
||||||
retry: 1, // Mencoba ulang sekali jika error
|
retry: 1, // Mencoba ulang sekali jika error
|
||||||
retryDelay: 1000, // Delay 1 detik antara percobaan
|
retryDelay: 1000, // Delay 1 detik antara percobaan
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateDevice = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: createDevice,
|
||||||
|
onSuccess: () => {
|
||||||
|
// Invalidate and refetch devices list
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['devices'] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateDevice = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, deviceData }: { id: string; deviceData: Partial<DeviceRequest> }) =>
|
||||||
|
updateDevice(id, deviceData),
|
||||||
|
onSuccess: () => {
|
||||||
|
// Invalidate and refetch devices list
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['devices'] });
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
// Handle error (you might want to add toast or error handling)
|
||||||
|
console.error('Device update failed', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeviceById = (id: string) => {
|
||||||
|
return useQuery<DeviceSingleResponse, Error>({
|
||||||
|
queryKey: ['device', id],
|
||||||
|
queryFn: () => getDeviceById(id),
|
||||||
|
enabled: !!id, // Hanya jalankan jika id ada
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
// src/features/devices/types.ts
|
|
||||||
export interface DeviceDataTable {
|
export interface DeviceDataTable {
|
||||||
id: number;
|
id: string;
|
||||||
device_code: string;
|
device_code: string;
|
||||||
device_type: string;
|
device_type: string;
|
||||||
longitude: number;
|
longitude: number;
|
||||||
|
|
@ -11,3 +10,24 @@ export interface DeviceDataTable {
|
||||||
status: string;
|
status: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DeviceRequest {
|
||||||
|
device_code: string;
|
||||||
|
device_type: 'OTB' | 'ODP';
|
||||||
|
longitude: number;
|
||||||
|
latitude: number;
|
||||||
|
port_amount: number;
|
||||||
|
status: "maintenance" | "active" | "inactive";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeviceSingleResponse {
|
||||||
|
id: string;
|
||||||
|
device_code: string;
|
||||||
|
device_type: string;
|
||||||
|
longitude: number;
|
||||||
|
latitude: number;
|
||||||
|
port_amount: number;
|
||||||
|
total_used_port: number;
|
||||||
|
status: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
import DeviceForm from './DeviceForm';
|
||||||
|
|
||||||
|
export default function CreateDevicePage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="bg-base-100 shadow-md p-4 rounded-lg mb-4">
|
||||||
|
<h1 className="text-2xl font-bold">Create New Device</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<DeviceForm onClose={() => window.history.back()} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,247 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useForm } from '@tanstack/react-form';
|
||||||
|
import { useCreateDevice, useUpdateDevice } from '@/features/devices/api/devices';
|
||||||
|
import { DeviceRequest, DeviceSingleResponse } from '@/features/devices/types';
|
||||||
|
import Input from '@/components/forms/Input';
|
||||||
|
|
||||||
|
interface DeviceFormProps {
|
||||||
|
initialData?: Partial<DeviceSingleResponse> | null;
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeviceForm: React.FC<DeviceFormProps> = ({ initialData, onClose }) => {
|
||||||
|
const createDeviceMutation = useCreateDevice();
|
||||||
|
const updateDeviceMutation = useUpdateDevice();
|
||||||
|
|
||||||
|
const defaultValues = {
|
||||||
|
device_code: initialData?.device_code || '',
|
||||||
|
device_type: initialData?.device_type || 'ODP',
|
||||||
|
longitude: initialData?.longitude || 0,
|
||||||
|
latitude: initialData?.latitude || 0,
|
||||||
|
port_amount: initialData?.port_amount || 0,
|
||||||
|
status: initialData?.status || 'active',
|
||||||
|
};
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
defaultValues,
|
||||||
|
// validators: {
|
||||||
|
// onChange: ZodSchema
|
||||||
|
// },
|
||||||
|
onSubmit: async ({ value }) => {
|
||||||
|
try {
|
||||||
|
if (initialData?.id) {
|
||||||
|
// Update existing device
|
||||||
|
await updateDeviceMutation.mutateAsync({
|
||||||
|
id: initialData.id,
|
||||||
|
deviceData: value as Partial<DeviceRequest>
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Create new device
|
||||||
|
console.log(value);
|
||||||
|
|
||||||
|
// Create new device
|
||||||
|
await createDeviceMutation.mutateAsync(value as DeviceRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the form or reset
|
||||||
|
onClose?.();
|
||||||
|
} catch (error: any) {
|
||||||
|
|
||||||
|
// Error handling is done in the mutation hooks
|
||||||
|
if (error.errors) {
|
||||||
|
// set error field are {}
|
||||||
|
Object.entries(defaultValues).forEach(([key]) => {
|
||||||
|
form.setFieldMeta(key as keyof typeof defaultValues, (prev) => ({
|
||||||
|
...prev,
|
||||||
|
errorMessage: ''
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.entries(error.errors).forEach(([key, value]) => {
|
||||||
|
form.setFieldMeta(key as keyof typeof defaultValues, (prev) => ({
|
||||||
|
...prev,
|
||||||
|
errorMessage: value
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 bg-base-100 rounded-lg shadow-md">
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
form.handleSubmit();
|
||||||
|
}}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
<form.Field
|
||||||
|
name="device_code"
|
||||||
|
children={(field) => (
|
||||||
|
<div>
|
||||||
|
<label htmlFor={field.name} className="block mb-2">Device Code</label>
|
||||||
|
<Input
|
||||||
|
id={field.name}
|
||||||
|
type="text"
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(e) => field.handleChange(e.target.value)}
|
||||||
|
className='w-full'
|
||||||
|
bordered
|
||||||
|
color={(field.state.meta as unknown as Record<string, string>).errorMessage ? 'error' : undefined}
|
||||||
|
/>
|
||||||
|
{(field.state.meta as unknown as Record<string, string>).errorMessage && (
|
||||||
|
<div className="text-error mt-1">
|
||||||
|
{(field.state.meta as unknown as Record<string, string>).errorMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
<form.Field
|
||||||
|
name="device_type"
|
||||||
|
children={(field) => (
|
||||||
|
<div>
|
||||||
|
<label htmlFor={field.name} className="block mb-2">Device Type</label>
|
||||||
|
<select
|
||||||
|
id={field.name}
|
||||||
|
value={field.state.value}
|
||||||
|
onChange={(e) => field.handleChange(e.target.value as "ODP" | "OTB")}
|
||||||
|
className="select select-bordered w-full"
|
||||||
|
>
|
||||||
|
<option value="ODP">ODP</option>
|
||||||
|
<option value="OTB">OTB</option>
|
||||||
|
</select>
|
||||||
|
{(field.state.meta as unknown as Record<string, string>).errorMessage && (
|
||||||
|
<div className="text-error mt-1">
|
||||||
|
{(field.state.meta as unknown as Record<string, string>).errorMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<form.Field
|
||||||
|
name="longitude"
|
||||||
|
children={(field) => (
|
||||||
|
<div>
|
||||||
|
<label htmlFor={field.name} className="block mb-2">Longitude</label>
|
||||||
|
<Input
|
||||||
|
id={field.name}
|
||||||
|
type="number"
|
||||||
|
step="0.000001"
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(e) => field.handleChange(Number(e.target.value))}
|
||||||
|
className="input-bordered w-full"
|
||||||
|
color={field.state.meta.errors.length > 0 ? 'error' : undefined}
|
||||||
|
/>
|
||||||
|
{(field.state.meta as unknown as Record<string, string>).errorMessage && (
|
||||||
|
<div className="text-error mt-1">
|
||||||
|
{(field.state.meta as unknown as Record<string, string>).errorMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<form.Field
|
||||||
|
name="latitude"
|
||||||
|
children={(field) => (
|
||||||
|
<div>
|
||||||
|
<label htmlFor={field.name} className="block mb-2">Latitude</label>
|
||||||
|
<Input
|
||||||
|
id={field.name}
|
||||||
|
type="number"
|
||||||
|
step="0.000001"
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(e) => field.handleChange(Number(e.target.value))}
|
||||||
|
className="input-bordered w-full"
|
||||||
|
color={field.state.meta.errors.length > 0 ? 'error' : undefined}
|
||||||
|
/>
|
||||||
|
{(field.state.meta as unknown as Record<string, string>).errorMessage && (
|
||||||
|
<div className="text-error mt-1">
|
||||||
|
{(field.state.meta as unknown as Record<string, string>).errorMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<form.Field
|
||||||
|
name="port_amount"
|
||||||
|
children={(field) => (
|
||||||
|
<div>
|
||||||
|
<label htmlFor={field.name} className="block mb-2">Port Amount</label>
|
||||||
|
<Input
|
||||||
|
id={field.name}
|
||||||
|
type="number"
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(e) => field.handleChange(Number(e.target.value))}
|
||||||
|
className="input-bordered w-full"
|
||||||
|
color={field.state.meta.errors.length > 0 ? 'error' : undefined}
|
||||||
|
/>
|
||||||
|
{(field.state.meta as unknown as Record<string, string>).errorMessage && (
|
||||||
|
<div className="text-error mt-1">
|
||||||
|
{(field.state.meta as unknown as Record<string, string>).errorMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<form.Field
|
||||||
|
name="status"
|
||||||
|
children={(field) => (
|
||||||
|
<div>
|
||||||
|
<label htmlFor={field.name} className="block mb-2">Status</label>
|
||||||
|
<select
|
||||||
|
id={field.name}
|
||||||
|
value={field.state.value}
|
||||||
|
onChange={(e) => field.handleChange(e.target.value as "active" | "maintenance" | "inactive")}
|
||||||
|
className="select select-bordered w-full"
|
||||||
|
>
|
||||||
|
<option value="active">Active</option>
|
||||||
|
<option value="maintenance">Maintenance</option>
|
||||||
|
<option value="inactive">Inactive</option>
|
||||||
|
</select>
|
||||||
|
{(field.state.meta as unknown as Record<string, string>).errorMessage && (
|
||||||
|
<div className="text-error mt-1">
|
||||||
|
{(field.state.meta as unknown as Record<string, string>).errorMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end space-x-4 mt-6">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="btn btn-ghost"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-primary"
|
||||||
|
disabled={createDeviceMutation.isPending || updateDeviceMutation.isPending}
|
||||||
|
>
|
||||||
|
{initialData?.id ? 'Update Device' : 'Create Device'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default DeviceForm;
|
||||||
|
|
@ -20,19 +20,33 @@ import { Pencil } from "lucide-react";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import id from "dayjs/locale/id";
|
import id from "dayjs/locale/id";
|
||||||
import Dropdown from "@/components/actions/Dropdown/Dropdown";
|
import Dropdown from "@/components/actions/Dropdown/Dropdown";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
|
||||||
dayjs.locale(id);
|
dayjs.locale(id);
|
||||||
|
|
||||||
export default function DevicePage() {
|
export default function DevicePage() {
|
||||||
const { data: devices, isLoading, isError, error } = useDevices();
|
const { data: devices, isLoading, isError, error } = useDevices();
|
||||||
|
|
||||||
// State untuk sorting dan filtering
|
// State untuk sorting dan filtering
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting] = useState<SortingState>([]);
|
||||||
const [globalFilter, setGlobalFilter] = useState("");
|
const [globalFilter, setGlobalFilter] = useState("");
|
||||||
const [pagination, setPagination] = useState({
|
const [pagination, setPagination] = useState({
|
||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
pageSize: 10, // Default 10 data per halaman
|
pageSize: 10, // Default 10 data per halaman
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// Replace handleCreateDevice with navigation
|
||||||
|
const handleCreateDevice = () => {
|
||||||
|
navigate("/devices/create");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Replace handleEditDevice with navigation
|
||||||
|
const handleEditDevice = (deviceId: string) => {
|
||||||
|
navigate(`/devices/${deviceId}/edit`);
|
||||||
|
};
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<DeviceDataTable>();
|
const columnHelper = createColumnHelper<DeviceDataTable>();
|
||||||
|
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
|
|
@ -41,33 +55,30 @@ export default function DevicePage() {
|
||||||
id: "actions",
|
id: "actions",
|
||||||
header: "",
|
header: "",
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Dropdown hover horizontal="right">
|
<Dropdown hover horizontal="right">
|
||||||
<Dropdown.Toggle
|
<Dropdown.Toggle button={false} className="btn btn-ghost btn-sm">
|
||||||
button={false}
|
<svg
|
||||||
className="btn btn-ghost btn-sm"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="w-4 h-4"
|
||||||
>
|
>
|
||||||
<svg
|
<circle cx={12} cy={12} r={1} />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<circle cx={12} cy={5} r={1} />
|
||||||
viewBox="0 0 24 24"
|
<circle cx={12} cy={19} r={1} />
|
||||||
fill="none"
|
</svg>
|
||||||
stroke="currentColor"
|
</Dropdown.Toggle>
|
||||||
strokeWidth={2}
|
<Dropdown.Menu className="shadow-2xl border p-1 bg-base-100 w-32">
|
||||||
strokeLinecap="round"
|
<Dropdown.Item onClick={() => handleEditDevice(row.original.id)}>
|
||||||
strokeLinejoin="round"
|
|
||||||
className="w-4 h-4"
|
|
||||||
>
|
|
||||||
<circle cx={12} cy={12} r={1} />
|
|
||||||
<circle cx={12} cy={5} r={1} />
|
|
||||||
<circle cx={12} cy={19} r={1} />
|
|
||||||
</svg>
|
|
||||||
</Dropdown.Toggle>
|
|
||||||
<Dropdown.Menu className="shadow-2xl border p-1 bg-base-100 w-32">
|
|
||||||
<Dropdown.Item>
|
|
||||||
<Pencil className="w-4 h-4 mr-2 text-info" />
|
<Pencil className="w-4 h-4 mr-2 text-info" />
|
||||||
Edit
|
Edit
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
</Dropdown.Menu>
|
</Dropdown.Menu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
),
|
),
|
||||||
size: 10,
|
size: 10,
|
||||||
}),
|
}),
|
||||||
|
|
@ -143,10 +154,12 @@ export default function DevicePage() {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="z-10">
|
||||||
<div className="bg-base-100 shadow-md p-4 rounded-lg flex justify-between items-center">
|
<div className="bg-base-100 shadow-md p-4 rounded-lg flex justify-between items-center">
|
||||||
<h1 className="text-2xl font-bold">Devices</h1>
|
<h1 className="text-2xl font-bold">Devices</h1>
|
||||||
<Button color="primary">Add Device</Button>
|
<Button color="primary" onClick={handleCreateDevice}>
|
||||||
|
Add Device
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card className="bg-base-100 shadow-lg mt-8">
|
<Card className="bg-base-100 shadow-lg mt-8">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { useParams } from 'react-router';
|
||||||
|
import DeviceForm from './DeviceForm';
|
||||||
|
import { useDeviceById } from '@/features/devices/api/devices';
|
||||||
|
import Loading from '@/components/feedback/Loading';
|
||||||
|
|
||||||
|
export default function EditDevicePage() {
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
|
||||||
|
const { data: device, isLoading } = useDeviceById(id as string);
|
||||||
|
|
||||||
|
if (isLoading) return <Loading />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="bg-base-100 shadow-md p-4 rounded-lg mb-4">
|
||||||
|
<h1 className="text-2xl font-bold">Edit Device</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<DeviceForm
|
||||||
|
initialData={device}
|
||||||
|
onClose={() => window.history.back()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,8 @@ import UsersListPage from '@/pages/users/UsersListPage';
|
||||||
import UserFormPage from '@/pages/users/UserFormPage';
|
import UserFormPage from '@/pages/users/UserFormPage';
|
||||||
import NotFoundPage from '@/pages/NotFoundPage';
|
import NotFoundPage from '@/pages/NotFoundPage';
|
||||||
import DevicePage from '@/pages/device/DevicePage';
|
import DevicePage from '@/pages/device/DevicePage';
|
||||||
|
import CreateDevicePage from '@/pages/device/CreateDevicePage';
|
||||||
|
import EditDevicePage from '@/pages/device/EditDevicePage';
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
// Public routes
|
// Public routes
|
||||||
|
|
@ -33,6 +35,16 @@ const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
path: '/devices',
|
path: '/devices',
|
||||||
element: <DevicePage />,
|
element: <DevicePage />,
|
||||||
|
// children: [
|
||||||
|
// ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/devices/create',
|
||||||
|
element: <CreateDevicePage />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/devices/:id/edit',
|
||||||
|
element: <EditDevicePage />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/users',
|
path: '/users',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue