Resolve merge conflicts: accept incoming changes (theirs)
This commit is contained in:
parent
f4e955ade4
commit
549e926493
|
|
@ -0,0 +1,18 @@
|
|||
import { RoleBasedRender } from '@/components/role-based/role-based-render'
|
||||
import { DeviceDeleteDialog } from './device-delete-dialog'
|
||||
import { DeviceEditDialog } from './device-edit-dialog'
|
||||
import { PortSettingsModal } from './port-settings-modal'
|
||||
import { DeviceBulkCreateDialog } from './device-bulk-create-dialog'
|
||||
|
||||
export function DevicesDialogs() {
|
||||
return (
|
||||
<>
|
||||
<DeviceEditDialog />
|
||||
<DeviceDeleteDialog />
|
||||
<RoleBasedRender allowedRoles={['Super Admin', 'Admin']}>
|
||||
<PortSettingsModal />
|
||||
<DeviceBulkCreateDialog />
|
||||
</RoleBasedRender>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { Button } from '@/components/ui/button'
|
||||
import { IconPlus, IconUpload } from '@tabler/icons-react'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { useDevicesContext } from '../context/devices-context'
|
||||
|
||||
export function DevicesPrimaryButtons() {
|
||||
const navigate = useNavigate()
|
||||
const { setIsBulkCreateDialogOpen } = useDevicesContext()
|
||||
|
||||
const handleCreateDevice = () => {
|
||||
navigate({ to: '/devices/create' })
|
||||
}
|
||||
|
||||
const handleBulkCreate = () => {
|
||||
setIsBulkCreateDialogOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex gap-2'>
|
||||
<Button size='sm' onClick={handleCreateDevice}>
|
||||
<IconPlus className='h-4 w-4' /> Tambah perangkat
|
||||
</Button>
|
||||
<Button size='sm' variant='outline' onClick={handleBulkCreate}>
|
||||
<IconUpload className='h-4 w-4' /> Bulk Import
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,265 @@
|
|||
import React, { createContext, useContext, useState } from 'react'
|
||||
import { DeviceComplete, DeviceFilters, DeviceSorting } from '../types/device'
|
||||
|
||||
interface DevicesContextType {
|
||||
// UI State
|
||||
selectedDevices: string[]
|
||||
setSelectedDevices: (devices: string[]) => void
|
||||
|
||||
// Dialog states
|
||||
isEditDialogOpen: boolean
|
||||
setIsEditDialogOpen: (open: boolean) => void
|
||||
|
||||
isDeleteDialogOpen: boolean
|
||||
setIsDeleteDialogOpen: (open: boolean) => void
|
||||
|
||||
isPortSettingsModalOpen: boolean
|
||||
setIsPortSettingsModalOpen: (open: boolean) => void
|
||||
|
||||
isBulkCreateDialogOpen: boolean
|
||||
setIsBulkCreateDialogOpen: (open: boolean) => void
|
||||
|
||||
// Current device for edit/delete
|
||||
currentDevice: DeviceComplete | null
|
||||
setCurrentDevice: (device: DeviceComplete | null) => void
|
||||
|
||||
// Frontend filtering and sorting
|
||||
filters: DeviceFilters
|
||||
setFilters: (filters: DeviceFilters) => void
|
||||
|
||||
sorting: DeviceSorting | null
|
||||
setSorting: (sorting: DeviceSorting | null) => void
|
||||
|
||||
// Pagination (frontend)
|
||||
currentPage: number
|
||||
setCurrentPage: (page: number) => void
|
||||
|
||||
pageSize: number
|
||||
setPageSize: (size: number) => void
|
||||
|
||||
// Region filtering state
|
||||
selectedProvince: string
|
||||
setSelectedProvince: (province: string) => void
|
||||
selectedCity: string
|
||||
setSelectedCity: (city: string) => void
|
||||
selectedDistrict: string
|
||||
setSelectedDistrict: (district: string) => void
|
||||
|
||||
// Actions
|
||||
openEditDialog: (device: DeviceComplete) => void
|
||||
openDeleteDialog: (device: DeviceComplete) => void
|
||||
closeAllDialogs: () => void
|
||||
|
||||
resetFilters: () => void
|
||||
|
||||
// Bulk operations
|
||||
isBulkMode: boolean
|
||||
setIsBulkMode: (mode: boolean) => void
|
||||
toggleDeviceSelection: (deviceId: string) => void
|
||||
selectAllDevices: (deviceIds: string[]) => void
|
||||
clearSelection: () => void
|
||||
bulkDeleteSelected: () => void
|
||||
|
||||
// Settings Port
|
||||
portSettingsDevice: DeviceComplete | null
|
||||
isPortSettingsOpen: boolean
|
||||
setIsPortSettingsOpen: (open: boolean) => void
|
||||
openPortSettingsDialog: (device: DeviceComplete) => void
|
||||
|
||||
// Region filter handlers
|
||||
handleProvinceChange: (provinceId: string, provinceName?: string) => void
|
||||
handleCityChange: (cityId: string, cityName?: string) => void
|
||||
handleDistrictChange: (districtId: string, districtName?: string) => void
|
||||
}
|
||||
|
||||
const DevicesContext = createContext<DevicesContextType | undefined>(undefined)
|
||||
|
||||
interface DevicesProviderProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const initialFilters: DeviceFilters = {
|
||||
search: '',
|
||||
status: '',
|
||||
device_type: '',
|
||||
province: '',
|
||||
city: '',
|
||||
district: '',
|
||||
}
|
||||
|
||||
export default function DevicesProvider({ children }: DevicesProviderProps) {
|
||||
// UI State
|
||||
const [selectedDevices, setSelectedDevices] = useState<string[]>([])
|
||||
|
||||
// Dialog states
|
||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
|
||||
const [isBulkCreateDialogOpen, setIsBulkCreateDialogOpen] = useState(false)
|
||||
|
||||
// Current device
|
||||
const [currentDevice, setCurrentDevice] = useState<DeviceComplete | null>(null)
|
||||
|
||||
// Frontend filtering and sorting
|
||||
const [filters, setFilters] = useState<DeviceFilters>(initialFilters)
|
||||
const [sorting, setSorting] = useState<DeviceSorting | null>(null)
|
||||
|
||||
// Pagination
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(10)
|
||||
|
||||
// Region filtering state
|
||||
const [selectedProvince, setSelectedProvince] = useState('')
|
||||
const [selectedCity, setSelectedCity] = useState('')
|
||||
const [selectedDistrict, setSelectedDistrict] = useState('')
|
||||
|
||||
// Bulk operations state
|
||||
const [isBulkMode, setIsBulkMode] = useState(false)
|
||||
|
||||
// Settings port
|
||||
const [portSettingsDevice, setPortSettingsDevice] = useState<DeviceComplete | null>(null)
|
||||
const [isPortSettingsOpen, setIsPortSettingsOpen] = useState(false)
|
||||
const [isPortSettingsModalOpen, setIsPortSettingsModalOpen] = useState(false)
|
||||
|
||||
// Actions
|
||||
const openEditDialog = (device: DeviceComplete) => {
|
||||
setCurrentDevice(device)
|
||||
setIsEditDialogOpen(true)
|
||||
}
|
||||
|
||||
const openDeleteDialog = (device: DeviceComplete) => {
|
||||
setCurrentDevice(device)
|
||||
setIsDeleteDialogOpen(true)
|
||||
}
|
||||
|
||||
const closeAllDialogs = () => {
|
||||
setIsEditDialogOpen(false)
|
||||
setIsDeleteDialogOpen(false)
|
||||
setCurrentDevice(null)
|
||||
}
|
||||
|
||||
const resetFilters = () => {
|
||||
setFilters(initialFilters)
|
||||
setSorting(null)
|
||||
setCurrentPage(1)
|
||||
}
|
||||
|
||||
const openPortSettingsDialog = (device: DeviceComplete) => {
|
||||
setPortSettingsDevice(device)
|
||||
setIsPortSettingsOpen(true)
|
||||
}
|
||||
|
||||
// Region filter handlers
|
||||
const handleProvinceChange = (provinceId: string, provinceName?: string) => {
|
||||
setSelectedProvince(provinceId) // ID for API calls
|
||||
setSelectedCity('')
|
||||
setSelectedDistrict('')
|
||||
setFilters(prev => ({
|
||||
...prev,
|
||||
province: provinceName || provinceId, // Name for filtering
|
||||
city: '',
|
||||
district: ''
|
||||
}))
|
||||
}
|
||||
|
||||
const handleCityChange = (cityId: string, cityName?: string) => {
|
||||
setSelectedCity(cityId) // ID for API calls
|
||||
setSelectedDistrict('')
|
||||
setFilters(prev => ({
|
||||
...prev,
|
||||
city: cityName || cityId, // Name for filtering
|
||||
district: ''
|
||||
}))
|
||||
}
|
||||
|
||||
const handleDistrictChange = (districtId: string, districtName?: string) => {
|
||||
setSelectedDistrict(districtId) // ID for API calls
|
||||
setFilters(prev => ({
|
||||
...prev,
|
||||
district: districtName || districtId // Name for filtering
|
||||
}))
|
||||
}
|
||||
|
||||
// Bulk operations functions
|
||||
const toggleDeviceSelection = (deviceId: string) => {
|
||||
setSelectedDevices(prev =>
|
||||
prev.includes(deviceId)
|
||||
? prev.filter(id => id !== deviceId)
|
||||
: [...prev, deviceId]
|
||||
)
|
||||
}
|
||||
|
||||
const selectAllDevices = (deviceIds: string[]) => {
|
||||
setSelectedDevices(deviceIds)
|
||||
}
|
||||
|
||||
const clearSelection = () => {
|
||||
setSelectedDevices([])
|
||||
setIsBulkMode(false)
|
||||
}
|
||||
|
||||
const bulkDeleteSelected = () => {
|
||||
// This will be handled by the bulk delete hook
|
||||
// The actual deletion logic is in the component that calls this
|
||||
console.log('Bulk delete selected devices:', selectedDevices)
|
||||
}
|
||||
|
||||
const value: DevicesContextType = {
|
||||
selectedDevices,
|
||||
setSelectedDevices,
|
||||
isEditDialogOpen,
|
||||
setIsEditDialogOpen,
|
||||
isDeleteDialogOpen,
|
||||
setIsDeleteDialogOpen,
|
||||
isPortSettingsModalOpen,
|
||||
setIsPortSettingsModalOpen,
|
||||
isBulkCreateDialogOpen,
|
||||
setIsBulkCreateDialogOpen,
|
||||
currentDevice,
|
||||
setCurrentDevice,
|
||||
filters,
|
||||
setFilters,
|
||||
sorting,
|
||||
setSorting,
|
||||
currentPage,
|
||||
setCurrentPage,
|
||||
pageSize,
|
||||
setPageSize,
|
||||
selectedProvince,
|
||||
setSelectedProvince,
|
||||
selectedCity,
|
||||
setSelectedCity,
|
||||
selectedDistrict,
|
||||
setSelectedDistrict,
|
||||
openEditDialog,
|
||||
openDeleteDialog,
|
||||
closeAllDialogs,
|
||||
resetFilters,
|
||||
portSettingsDevice,
|
||||
isPortSettingsOpen,
|
||||
setIsPortSettingsOpen,
|
||||
openPortSettingsDialog,
|
||||
handleProvinceChange,
|
||||
handleCityChange,
|
||||
handleDistrictChange,
|
||||
isBulkMode,
|
||||
setIsBulkMode,
|
||||
toggleDeviceSelection,
|
||||
selectAllDevices,
|
||||
clearSelection,
|
||||
bulkDeleteSelected
|
||||
}
|
||||
|
||||
return (
|
||||
<DevicesContext.Provider value={value}>
|
||||
{children}
|
||||
</DevicesContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useDevicesContext = () => {
|
||||
const context = useContext(DevicesContext)
|
||||
if (context === undefined) {
|
||||
throw new Error('useDevicesContext must be used within a DevicesProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
import { devicesService } from '../services/devices-service'
|
||||
import { DevicePortsUpdateRequest } from '../types/device'
|
||||
|
||||
export const useDevices = ({
|
||||
type = 'all',
|
||||
}: { type?: 'otb' | 'closure' | 'odp' | 'all' } = {}) => {
|
||||
return useQuery({
|
||||
queryKey: ['devices'],
|
||||
queryFn: devicesService.getDevices,
|
||||
select: (data) => {
|
||||
if (type === 'otb') {
|
||||
return data.data.filter(d => d.device_type.toLowerCase() === 'otb') || []
|
||||
}
|
||||
if (type === 'closure') {
|
||||
return data.data.filter(d => d.device_type.toLowerCase() === 'closure') || []
|
||||
}
|
||||
if (type === 'odp') {
|
||||
return data.data.filter(d => d.device_type.toLowerCase() === 'odp') || []
|
||||
}
|
||||
return data.data || []
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useDevice = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: ['device', id],
|
||||
queryFn: () => devicesService.getDevice(id),
|
||||
select: (data) => data.data,
|
||||
enabled: !!id,
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeviceWithPorts = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: ['device-with-ports', id],
|
||||
queryFn: () => devicesService.getDeviceWithPorts(id),
|
||||
select: (data) => data.data,
|
||||
enabled: !!id,
|
||||
})
|
||||
}
|
||||
|
||||
export const useCreateDevice = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: FormData) => devicesService.createDevice(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['devices'] })
|
||||
toast.success('Device created successfully')
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.response?.data?.status?.description || 'Failed to create device')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateDevice = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, formData }: { id: string; formData: FormData }) => devicesService.updateDevice(id, formData),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['devices'] })
|
||||
toast.success('Device updated successfully')
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.response?.data?.status?.description || 'Failed to update device')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteDeviceImage = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ deviceId, imageName }: { deviceId: string; imageName: string }) =>
|
||||
devicesService.deleteDeviceImage(deviceId, imageName),
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['devices'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['device', variables.deviceId] })
|
||||
queryClient.invalidateQueries({ queryKey: ['device-with-ports', variables.deviceId] })
|
||||
toast.success('Image deleted successfully')
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.response?.data?.status?.description || 'Failed to delete image')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Updated hook to use the new assign-customer endpoint
|
||||
export const useAssignCustomersToPorts = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: DevicePortsUpdateRequest) => devicesService.assignCustomersToPorts(data),
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['devices'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['device-with-ports', variables.device_id] })
|
||||
toast.success('Customer assignments updated successfully')
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.response?.data?.status?.description || 'Failed to update customer assignments')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Keep the old hook name for backward compatibility
|
||||
export const useUpdateDevicePorts = useAssignCustomersToPorts
|
||||
|
||||
export const useDeleteDevice = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => devicesService.deleteDevice(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['devices'] })
|
||||
toast.success('Device deleted successfully')
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.response?.data?.status?.description || 'Failed to delete device')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useBulkDeleteDevices = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (ids: string[]) => devicesService.bulkDeleteDevices(ids),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['devices'] })
|
||||
toast.success('Devices deleted successfully')
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.response?.data?.status?.description || 'Failed to delete devices')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useBulkCreateDevices = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (devices: any[]) => devicesService.bulkCreateDevices(devices),
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['devices'] })
|
||||
const successCount = data.data?.success_count || 0
|
||||
const failureCount = data.data?.failure_count || 0
|
||||
|
||||
if (failureCount > 0) {
|
||||
toast.warning(`${successCount} devices created successfully, ${failureCount} failed`)
|
||||
} else {
|
||||
toast.success(`${successCount} devices created successfully`)
|
||||
}
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.response?.data?.status?.description || 'Failed to create devices')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
import { useState, useMemo } from 'react'
|
||||
import { Header } from '@/components/layout/header'
|
||||
import { Main } from '@/components/layout/main'
|
||||
import { ProfileDropdown } from '@/components/profile-dropdown'
|
||||
import { Search } from '@/components/search'
|
||||
import { ThemeSwitch } from '@/components/theme-switch'
|
||||
import { columns } from './components/columns'
|
||||
import { DataTable } from './components/data-table'
|
||||
import { DevicesDialogs } from './components/devices-dialogs'
|
||||
import { DevicesPrimaryButtons } from './components/devices-primary-buttons'
|
||||
import { ViewSwitcher } from './components/view-switcher'
|
||||
import { DevicesCards } from './components/devices-cards'
|
||||
import { DevicesRegionFilter } from './components/devices-region-filter'
|
||||
import DevicesProvider, { useDevicesContext } from './context/devices-context'
|
||||
import { useDevices } from './hooks/use-devices'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { useEffect } from 'react'
|
||||
import { handleServerError } from '@/utils/handle-server-error'
|
||||
|
||||
|
||||
function DevicesContent() {
|
||||
const { data: devices, isLoading, error } = useDevices()
|
||||
const { filters } = useDevicesContext()
|
||||
const [view, setView] = useState<'table' | 'cards'>('table')
|
||||
|
||||
// Filter devices based on region filters
|
||||
const filteredDevices = useMemo(() => {
|
||||
if (!devices) return []
|
||||
|
||||
return devices.filter((device) => {
|
||||
const matchesProvince = !filters.province || device.province === filters.province
|
||||
const matchesCity = !filters.city || device.city === filters.city
|
||||
const matchesDistrict = !filters.district || device.district === filters.district
|
||||
|
||||
return matchesProvince && matchesCity && matchesDistrict
|
||||
})
|
||||
}, [devices, filters.province, filters.city, filters.district])
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
handleServerError(error)
|
||||
}
|
||||
}, [error])
|
||||
|
||||
const renderSkeletons = () => {
|
||||
if (view === 'table') {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<Skeleton className="h-64 w-full" />
|
||||
<div className="flex justify-between">
|
||||
<Skeleton className="h-8 w-32" />
|
||||
<Skeleton className="h-8 w-64" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-48 w-full" />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header fixed>
|
||||
<Search />
|
||||
<div className='ml-auto flex items-center space-x-4'>
|
||||
<ThemeSwitch />
|
||||
<ProfileDropdown />
|
||||
</div>
|
||||
</Header>
|
||||
|
||||
<Main>
|
||||
<div className='mb-2 flex flex-wrap items-center justify-between space-y-2 gap-x-4'>
|
||||
<div>
|
||||
<h2 className='text-2xl font-bold tracking-tight'>Perangkat</h2>
|
||||
<p className='text-muted-foreground'>
|
||||
Berikut adalah daftar perangkat Anda!
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<ViewSwitcher view={view} onViewChange={setView} />
|
||||
<DevicesPrimaryButtons />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Region Filter */}
|
||||
<div className="mb-4">
|
||||
<DevicesRegionFilter />
|
||||
</div>
|
||||
|
||||
<div className='-mx-4 flex-1 overflow-auto px-4 py-1 lg:flex-row lg:space-y-0 lg:space-x-12'>
|
||||
{isLoading ? (
|
||||
renderSkeletons()
|
||||
) : view === 'table' ? (
|
||||
<DataTable
|
||||
data={filteredDevices || []}
|
||||
columns={columns}
|
||||
/>
|
||||
) : (
|
||||
<DevicesCards devices={filteredDevices || []} />
|
||||
)}
|
||||
</div>
|
||||
</Main>
|
||||
|
||||
<DevicesDialogs />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Devices() {
|
||||
return (
|
||||
<DevicesProvider>
|
||||
<DevicesContent />
|
||||
</DevicesProvider>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
import { apiClient } from '@/lib/api'
|
||||
import { ApiResponse } from '@/lib/api/types'
|
||||
import {
|
||||
DeviceResponse,
|
||||
DevicePortsUpdateRequest,
|
||||
DeviceComplete,
|
||||
DevicesWithoutConnectionsResponse,
|
||||
DevicesWithoutTowersResponse
|
||||
} from '../types/device'
|
||||
|
||||
export const devicesService = {
|
||||
// Get all devices (no query params - get everything)
|
||||
getDevices: async (): Promise<ApiResponse<DeviceComplete[]>> => {
|
||||
return apiClient.get<DeviceComplete[]>('/device-details')
|
||||
},
|
||||
|
||||
// Get single device by ID
|
||||
getDevice: async (id: string): Promise<ApiResponse<DeviceComplete>> => {
|
||||
return apiClient.get<DeviceComplete>(`/device-details/${id}`)
|
||||
},
|
||||
|
||||
// Create new device
|
||||
createDevice: async (data: FormData): Promise<ApiResponse<DeviceResponse>> => {
|
||||
return apiClient.postFormData<DeviceResponse>('/devices', data)
|
||||
},
|
||||
|
||||
// Update device
|
||||
updateDevice: async (id: string, data: FormData): Promise<ApiResponse<DeviceResponse>> => {
|
||||
return apiClient.putFormData<DeviceResponse>(`/device-details/${id}`, data)
|
||||
},
|
||||
|
||||
// Delete device
|
||||
deleteDevice: async (id: string): Promise<ApiResponse<null>> => {
|
||||
return apiClient.delete<null>(`/devices/${id}`)
|
||||
},
|
||||
|
||||
// Delete device image - NEW
|
||||
deleteDeviceImage: async (deviceId: string, imageName: string): Promise<ApiResponse<null>> => {
|
||||
return apiClient.delete<null>(`/device-details/${deviceId}/images/${imageName}`)
|
||||
},
|
||||
|
||||
// Bulk delete devices
|
||||
bulkDeleteDevices: async (ids: string[]): Promise<ApiResponse<null>> => {
|
||||
return apiClient.post<null>('/devices/bulk-delete', { ids })
|
||||
},
|
||||
|
||||
// Bulk create devices
|
||||
bulkCreateDevices: async (devices: any[]): Promise<ApiResponse<{ success_count: number; failure_count: number; errors?: any[] }>> => {
|
||||
return apiClient.post<{ success_count: number; failure_count: number; errors?: any[] }>('/devices/bulk-create', { devices })
|
||||
},
|
||||
|
||||
// Assign customers to device ports - new endpoint
|
||||
assignCustomersToPorts: async (data: DevicePortsUpdateRequest): Promise<ApiResponse<DeviceComplete>> => {
|
||||
const payload = data.ports.map(port => ({
|
||||
new_customer_name: port.customer_name,
|
||||
port_number: port.port_number,
|
||||
bandwidth: port.bandwidth,
|
||||
is_occupied: port.is_occupied,
|
||||
status: port.status,
|
||||
}))
|
||||
|
||||
return apiClient.put<DeviceComplete>(`/device-details/${data.device_id}/bulk-update-customers-by-port`, { updates: payload })
|
||||
},
|
||||
|
||||
// Get device with port assignments - uses device-details endpoint
|
||||
getDeviceWithPorts: async (deviceId: string): Promise<ApiResponse<DeviceComplete>> => {
|
||||
return apiClient.get<DeviceComplete>(`/device-details/${deviceId}`)
|
||||
},
|
||||
|
||||
// Get devices without connections (not used by fishbone or backbone)
|
||||
getDevicesWithoutConnections: async (deviceTypes: string[]): Promise<ApiResponse<DevicesWithoutConnectionsResponse>> => {
|
||||
const params = new URLSearchParams()
|
||||
params.append('device_types', deviceTypes.join(','))
|
||||
|
||||
return apiClient.get<DevicesWithoutConnectionsResponse>(`/device-details/without-connections?${params.toString()}`)
|
||||
},
|
||||
|
||||
// Get devices without towers
|
||||
getDevicesWithoutTowers: async (deviceTypes: string[]): Promise<ApiResponse<DevicesWithoutTowersResponse>> => {
|
||||
const params = new URLSearchParams()
|
||||
params.append('device_types', deviceTypes.join(','))
|
||||
|
||||
return apiClient.get<DevicesWithoutTowersResponse>(`/device-details/without-towers?${params.toString()}`)
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
import { Backbone } from '@/features/backbones/types/backbone'
|
||||
import { Fishbone } from '@/features/fishbones/types/fishbone'
|
||||
import { Olt } from '@/features/olts/types/olt'
|
||||
import { Tower } from '@/features/towers/types/tower'
|
||||
|
||||
export interface Device {
|
||||
id: string
|
||||
device_code: string
|
||||
device_type: string
|
||||
longitude: number
|
||||
latitude: number
|
||||
address: string
|
||||
port_amount: number
|
||||
total_used_port: number
|
||||
province?: string
|
||||
city?: string
|
||||
district?: string
|
||||
status: 'active' | 'inactive' | 'maintenance'
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface PortAssignment {
|
||||
port_number: number
|
||||
customer_name: string | null
|
||||
is_occupied: boolean
|
||||
status: "dyingGasp" | "los" | "on" | "off"
|
||||
bandwidth: string | null
|
||||
}
|
||||
|
||||
export interface DeviceComplete {
|
||||
id: string
|
||||
olt: Olt | null
|
||||
device_code: string
|
||||
device_type: string
|
||||
address: string
|
||||
longitude: number
|
||||
latitude: number
|
||||
status: 'active' | 'inactive' | 'maintenance'
|
||||
port_amount: number
|
||||
port_used: number
|
||||
port_available: number
|
||||
customer_names: string[] | null
|
||||
port_assignments: PortAssignment[]
|
||||
province: string | null
|
||||
city: string | null
|
||||
district: string | null
|
||||
image_urls: string[]
|
||||
backbones: (Pick<Backbone, 'id' | 'backbone_code' | 'core_amount'> & {
|
||||
is_start_device: boolean
|
||||
connected_to: string
|
||||
})[]
|
||||
fishbones: (Pick<Fishbone, 'id' | 'fishbone_code' | 'core_amount' | 'backbone_code'> & {
|
||||
is_start_device: boolean
|
||||
connected_to: string
|
||||
})[]
|
||||
towers: (Omit<
|
||||
Tower,
|
||||
'latitude' | 'longitude' | 'device_code' | 'address' | 'created_at'
|
||||
> & { distance_km: number })[]
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface DeviceCreateRequest {
|
||||
device_code: string
|
||||
device_type: string
|
||||
longitude: number
|
||||
latitude: number
|
||||
port_amount?: number | null
|
||||
status: 'active' | 'inactive' | 'maintenance'
|
||||
province?: string
|
||||
city?: string
|
||||
district?: string
|
||||
image_urls?: File[]
|
||||
}
|
||||
|
||||
export interface DeviceUpdateRequest extends Partial<DeviceCreateRequest> {
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface DevicesListResponse {
|
||||
devices: Device[]
|
||||
// Remove pagination fields since we get all data
|
||||
}
|
||||
|
||||
export interface DeviceResponse {
|
||||
device: Device
|
||||
}
|
||||
|
||||
// Frontend filtering state
|
||||
export interface DeviceFilters {
|
||||
search: string
|
||||
status: string
|
||||
device_type: string
|
||||
port_status?: string // Add this new field
|
||||
province?: string
|
||||
city?: string
|
||||
district?: string
|
||||
}
|
||||
|
||||
export interface DeviceSorting {
|
||||
field: keyof Device
|
||||
direction: 'asc' | 'desc'
|
||||
}
|
||||
|
||||
export type PortAssignmentStatus = 'no_customer' | 'in_use'
|
||||
|
||||
// Add connection status from API
|
||||
export type PortConnectionStatus = 'dyingGasp' | 'los' | 'on' | 'off'
|
||||
|
||||
export interface Port {
|
||||
port_number: number
|
||||
assignment_status: PortAssignmentStatus
|
||||
connection_status?: PortConnectionStatus
|
||||
customer_name?: string
|
||||
is_occupied: boolean
|
||||
bandwidth?: string | null
|
||||
}
|
||||
|
||||
export interface DevicePort extends Device {
|
||||
ports?: Port[]
|
||||
}
|
||||
export interface DevicePortAssignmentRequest {
|
||||
customer_name: string | null
|
||||
port_number: number
|
||||
is_occupied: boolean
|
||||
status: "dyingGasp" | "los" | "on" | "off"
|
||||
bandwidth: string | null
|
||||
}
|
||||
|
||||
export interface DevicePortsUpdateRequest {
|
||||
device_id: string
|
||||
ports: DevicePortAssignmentRequest[]
|
||||
}
|
||||
|
||||
export interface DeviceBreakdown {
|
||||
closure: {
|
||||
count: number
|
||||
devices: DeviceComplete[]
|
||||
}
|
||||
otb: {
|
||||
count: number
|
||||
devices: DeviceComplete[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface DevicesWithoutConnectionsFilter {
|
||||
criteria: string
|
||||
device_types: string[]
|
||||
}
|
||||
|
||||
export interface DevicesWithoutConnectionsResponse {
|
||||
breakdown: DeviceBreakdown
|
||||
devices: DeviceComplete[]
|
||||
filter: DevicesWithoutConnectionsFilter
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface DevicesWithoutTowersFilter {
|
||||
criteria: string
|
||||
device_types: string[]
|
||||
}
|
||||
|
||||
export interface DevicesWithoutTowersResponse {
|
||||
devices: DeviceComplete[]
|
||||
filter: DevicesWithoutTowersFilter
|
||||
total: number
|
||||
}
|
||||
Loading…
Reference in New Issue