Add Sensor Hub functionality with components for managing sensors, including a service for API calls, a modal for selection, and a form for adding new sensors. Updated layout to integrate SensorHub component.
This commit is contained in:
@@ -11,6 +11,7 @@ import HorizontalLayout from '@layouts/HorizontalLayout'
|
|||||||
|
|
||||||
// Component Imports
|
// Component Imports
|
||||||
import Providers from '@components/Providers'
|
import Providers from '@components/Providers'
|
||||||
|
import SensorHub from '@components/SensorHub'
|
||||||
import Navigation from '@components/layout/vertical/Navigation'
|
import Navigation from '@components/layout/vertical/Navigation'
|
||||||
import Header from '@components/layout/horizontal/Header'
|
import Header from '@components/layout/horizontal/Header'
|
||||||
import Navbar from '@components/layout/vertical/Navbar'
|
import Navbar from '@components/layout/vertical/Navbar'
|
||||||
@@ -31,6 +32,7 @@ const Layout = async (props: ChildrenType) => {
|
|||||||
return (
|
return (
|
||||||
<Providers direction={direction}>
|
<Providers direction={direction}>
|
||||||
<AuthGuard>
|
<AuthGuard>
|
||||||
|
<SensorHub>
|
||||||
<LayoutWrapper
|
<LayoutWrapper
|
||||||
systemMode={systemMode}
|
systemMode={systemMode}
|
||||||
verticalLayout={
|
verticalLayout={
|
||||||
@@ -55,6 +57,7 @@ const Layout = async (props: ChildrenType) => {
|
|||||||
<i className='tabler-arrow-up' />
|
<i className='tabler-arrow-up' />
|
||||||
</Button>
|
</Button>
|
||||||
</ScrollToTop>
|
</ScrollToTop>
|
||||||
|
</SensorHub>
|
||||||
</AuthGuard>
|
</AuthGuard>
|
||||||
</Providers>
|
</Providers>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
// React Imports
|
||||||
|
import { useEffect, type ReactNode } from 'react'
|
||||||
|
|
||||||
|
// Hook Imports
|
||||||
|
import { useSensorHub } from '@/hooks/useSensorHub'
|
||||||
|
import { Box } from '@mui/material'
|
||||||
|
import SensorHubView from '@/views/sensorHub'
|
||||||
|
interface SensorHubProps {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SensorHub({ children }: SensorHubProps) {
|
||||||
|
const { hasSensorHub } = useSensorHub()
|
||||||
|
|
||||||
|
|
||||||
|
return <> {children}
|
||||||
|
{!hasSensorHub && <SensorHubView />}</>
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
// React Imports
|
||||||
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
|
|
||||||
|
const SENSOR_HUB_STORAGE_KEY = 'sensor_hub'
|
||||||
|
|
||||||
|
export interface SensorHubInfo {
|
||||||
|
id: string
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseSensorHubReturn {
|
||||||
|
/** Sensor hub data from localStorage */
|
||||||
|
sensorHub: SensorHubInfo | null
|
||||||
|
/** Whether sensor_hub exists in localStorage */
|
||||||
|
hasSensorHub: boolean
|
||||||
|
/** Save sensor hub to localStorage */
|
||||||
|
setSensorHub: (data: SensorHubInfo | null) => void
|
||||||
|
/** Get headers to attach to API requests (e.g. X-Sensor-Hub-Id) */
|
||||||
|
getSensorHubHeaders: () => Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseSensorHub = (raw: string | null): SensorHubInfo | null => {
|
||||||
|
if (!raw) return null
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw)
|
||||||
|
if (parsed && typeof parsed === 'object' && typeof parsed.id === 'string') {
|
||||||
|
return parsed as SensorHubInfo
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore invalid JSON
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSensorHub = (): UseSensorHubReturn => {
|
||||||
|
const [sensorHub, setSensorHubState] = useState<SensorHubInfo | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') return
|
||||||
|
const stored = localStorage.getItem(SENSOR_HUB_STORAGE_KEY)
|
||||||
|
setSensorHubState(parseSensorHub(stored))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const setSensorHub = useCallback((data: SensorHubInfo | null) => {
|
||||||
|
if (typeof window === 'undefined') return
|
||||||
|
if (data === null) {
|
||||||
|
localStorage.removeItem(SENSOR_HUB_STORAGE_KEY)
|
||||||
|
setSensorHubState(null)
|
||||||
|
} else {
|
||||||
|
localStorage.setItem(SENSOR_HUB_STORAGE_KEY, JSON.stringify(data))
|
||||||
|
setSensorHubState(data)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const getSensorHubHeaders = useCallback((): Record<string, string> => {
|
||||||
|
const hub = sensorHub ?? (typeof window !== 'undefined' ? parseSensorHub(localStorage.getItem(SENSOR_HUB_STORAGE_KEY)) : null)
|
||||||
|
if (!hub?.id) return {}
|
||||||
|
return {
|
||||||
|
'X-Sensor-Hub-Id': hub.id
|
||||||
|
}
|
||||||
|
}, [sensorHub])
|
||||||
|
|
||||||
|
return {
|
||||||
|
sensorHub,
|
||||||
|
hasSensorHub: sensorHub !== null,
|
||||||
|
setSensorHub,
|
||||||
|
getSensorHubHeaders
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,4 +14,5 @@ export * from './services/kanbanService'
|
|||||||
export * from './services/todoService'
|
export * from './services/todoService'
|
||||||
export * from './services/userManagementService'
|
export * from './services/userManagementService'
|
||||||
export * from './services/rolesPermissionsService'
|
export * from './services/rolesPermissionsService'
|
||||||
|
export * from './services/sensorHubService'
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Sensor Hub Service
|
||||||
|
* Handles sensor hub API calls
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { apiClient } from '../client'
|
||||||
|
|
||||||
|
export interface Sensor {
|
||||||
|
name: string
|
||||||
|
uuid_sensor: string
|
||||||
|
last_updated: string
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListSensorsResponse {
|
||||||
|
status?: string
|
||||||
|
data: Sensor | Sensor[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sensorHubService = {
|
||||||
|
/**
|
||||||
|
* Get list of sensors
|
||||||
|
*/
|
||||||
|
async listSensors(): Promise<Sensor[]> {
|
||||||
|
const response = await apiClient.get<ListSensorsResponse>('/api/sensor-hub/')
|
||||||
|
const data = response?.data
|
||||||
|
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data && typeof data === 'object') {
|
||||||
|
return [data as Sensor]
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
// React Imports
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
// MUI Imports
|
||||||
|
import Grid from '@mui/material/Grid2'
|
||||||
|
import Button from '@mui/material/Button'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
|
||||||
|
// Component Imports
|
||||||
|
import CustomTextField from '@core/components/mui/TextField'
|
||||||
|
|
||||||
|
type FormSensorHubProps = {
|
||||||
|
onBack: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormSensorHub = ({ onBack }: FormSensorHubProps) => {
|
||||||
|
const [name, setName] = useState('')
|
||||||
|
const [uuidSensor, setUuidSensor] = useState('')
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
// TODO: Call API to add sensor
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col gap-4'>
|
||||||
|
<div className='flex items-center gap-2 mbe-2'>
|
||||||
|
<Button
|
||||||
|
variant='text'
|
||||||
|
color='secondary'
|
||||||
|
size='small'
|
||||||
|
startIcon={<i className='tabler-arrow-right text-xl' />}
|
||||||
|
onClick={onBack}
|
||||||
|
>
|
||||||
|
بازگشت
|
||||||
|
</Button>
|
||||||
|
{/* <Typography variant='h6'>افزودن سنسور جدید</Typography> */}
|
||||||
|
</div>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<Grid container spacing={4}>
|
||||||
|
<Grid size={{ xs: 12 }}>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
label='نام سنسور'
|
||||||
|
placeholder='نام سنسور را وارد کنید'
|
||||||
|
value={name}
|
||||||
|
onChange={e => setName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12 }}>
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
label='شناسه سنسور (UUID)'
|
||||||
|
placeholder='شناسه سنسور را وارد کنید'
|
||||||
|
value={uuidSensor}
|
||||||
|
onChange={e => setUuidSensor(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12 }} className='flex gap-2'>
|
||||||
|
<Button variant='tonal' color='secondary' onClick={onBack}>
|
||||||
|
انصراف
|
||||||
|
</Button>
|
||||||
|
<Button variant='contained' type='submit' startIcon={<i className='tabler-plus' />}>
|
||||||
|
ذخیره سنسور
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FormSensorHub
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
// React Imports
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import type { ChangeEvent } from 'react'
|
||||||
|
|
||||||
|
// MUI Imports
|
||||||
|
import Grid from '@mui/material/Grid2'
|
||||||
|
import Button from '@mui/material/Button'
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress'
|
||||||
|
|
||||||
|
// Third-party Imports
|
||||||
|
import DateObject from 'react-date-object'
|
||||||
|
|
||||||
|
// Type Imports
|
||||||
|
import type { CustomInputHorizontalData } from '@core/components/custom-inputs/types'
|
||||||
|
|
||||||
|
// API Imports
|
||||||
|
import { sensorHubService } from '@/libs/api'
|
||||||
|
import type { Sensor } from '@/libs/api/services/sensorHubService'
|
||||||
|
|
||||||
|
// Component Imports
|
||||||
|
import CustomInputHorizontal from '@core/components/custom-inputs/Horizontal'
|
||||||
|
|
||||||
|
type OptionSensorHubProps = {
|
||||||
|
onConfirm?: (sensor: Sensor) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatLastUpdated = (dateStr: string | null | undefined): string => {
|
||||||
|
if (!dateStr) return '—'
|
||||||
|
try {
|
||||||
|
const d = new DateObject(new Date(dateStr)).convert('persian').setLocale('fa')
|
||||||
|
|
||||||
|
return d.format('YYYY/MM/DD')
|
||||||
|
} catch {
|
||||||
|
return dateStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sensorToOption = (sensor: Sensor, isFirst: boolean): CustomInputHorizontalData => ({
|
||||||
|
title: sensor.name,
|
||||||
|
meta: formatLastUpdated(sensor.last_updated),
|
||||||
|
content: sensor.uuid_sensor,
|
||||||
|
value: sensor.uuid_sensor,
|
||||||
|
isSelected: isFirst
|
||||||
|
})
|
||||||
|
|
||||||
|
const OptionSensorHub = ({ onConfirm }: OptionSensorHubProps) => {
|
||||||
|
const [sensors, setSensors] = useState<Sensor[]>([])
|
||||||
|
const [data, setData] = useState<CustomInputHorizontalData[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [selectedOption, setSelectedOption] = useState<string>('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchSensors = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
const sensorsList = await sensorHubService.listSensors()
|
||||||
|
setSensors(sensorsList)
|
||||||
|
const options = sensorsList.map((s, i) => sensorToOption(s, i === 0))
|
||||||
|
setData(options)
|
||||||
|
if (options.length > 0) {
|
||||||
|
const selected = options.find(o => o.isSelected) ?? options[0]
|
||||||
|
|
||||||
|
setSelectedOption(selected.value)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setSensors([])
|
||||||
|
setData([])
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchSensors()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleOptionChange = (prop: string | ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (typeof prop === 'string') {
|
||||||
|
setSelectedOption(prop)
|
||||||
|
} else {
|
||||||
|
setSelectedOption((prop.target as HTMLInputElement).value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className='flex items-center justify-center py-12'>
|
||||||
|
<CircularProgress />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
const selected = sensors.find(s => s.uuid_sensor === selectedOption)
|
||||||
|
if (selected && onConfirm) {
|
||||||
|
onConfirm(selected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={e => e.preventDefault()}>
|
||||||
|
<Grid container>
|
||||||
|
{data.map(item => (
|
||||||
|
<CustomInputHorizontal
|
||||||
|
type='radio'
|
||||||
|
key={item.value}
|
||||||
|
data={item}
|
||||||
|
gridProps={{
|
||||||
|
size: { xs: 12 },
|
||||||
|
className:
|
||||||
|
'[&:first-of-type>*]:rounded-be-none [&:last-of-type>*]:rounded-bs-none [&:nth-of-type(2)>*]:rounded-none'
|
||||||
|
}}
|
||||||
|
selected={selectedOption}
|
||||||
|
name='sensor-hub-option'
|
||||||
|
handleChange={handleOptionChange}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
{onConfirm && (
|
||||||
|
<div className='flex justify-end mts-4'>
|
||||||
|
<Button
|
||||||
|
variant='contained'
|
||||||
|
sx={{mt:4}}
|
||||||
|
color='primary'
|
||||||
|
onClick={handleConfirm}
|
||||||
|
startIcon={<i className='tabler-check text-xl' />}
|
||||||
|
>
|
||||||
|
تایید
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OptionSensorHub
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
// React Imports
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
|
// MUI Imports
|
||||||
|
import Card from '@mui/material/Card'
|
||||||
|
import CardHeader from '@mui/material/CardHeader'
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress'
|
||||||
|
|
||||||
|
// Third-party Imports
|
||||||
|
import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'
|
||||||
|
import DateObject from 'react-date-object'
|
||||||
|
|
||||||
|
// API Imports
|
||||||
|
import { sensorHubService } from '@/libs/api'
|
||||||
|
import type { Sensor } from '@/libs/api/services/sensorHubService'
|
||||||
|
|
||||||
|
// Style Imports
|
||||||
|
import styles from '@core/styles/table.module.css'
|
||||||
|
|
||||||
|
const formatToShamsi = (dateStr: string | null | undefined): string => {
|
||||||
|
if (!dateStr) return '—'
|
||||||
|
try {
|
||||||
|
const d = new DateObject(new Date(dateStr)).convert('persian').setLocale('fa')
|
||||||
|
|
||||||
|
return d.format('YYYY/MM/DD')
|
||||||
|
} catch {
|
||||||
|
return dateStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Column Definitions
|
||||||
|
const columnHelper = createColumnHelper<Sensor>()
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
columnHelper.accessor('name', {
|
||||||
|
cell: info => info.getValue(),
|
||||||
|
header: 'Name'
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('last_updated', {
|
||||||
|
cell: info => formatToShamsi(info.getValue()),
|
||||||
|
header: 'Last Update'
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('uuid_sensor', {
|
||||||
|
cell: info => info.getValue(),
|
||||||
|
header: 'UUID'
|
||||||
|
})
|
||||||
|
]
|
||||||
|
|
||||||
|
const SensorHubTable = () => {
|
||||||
|
const [data, setData] = useState<Sensor[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchSensors = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
const sensors = await sensorHubService.listSensors()
|
||||||
|
setData(sensors)
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to load sensors')
|
||||||
|
setData([])
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchSensors()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
filterFns: {
|
||||||
|
fuzzy: () => false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader title='Sensor Hub' />
|
||||||
|
<div className='flex items-center justify-center p-12'>
|
||||||
|
<CircularProgress />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader title='Sensor Hub' subheader={error} />
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader title='Sensor Hub' />
|
||||||
|
<div className='overflow-x-auto'>
|
||||||
|
<table className={styles.table}>
|
||||||
|
<thead>
|
||||||
|
{table.getHeaderGroups().map(headerGroup => (
|
||||||
|
<tr key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map(header => (
|
||||||
|
<th key={header.id}>
|
||||||
|
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{table.getRowModel().rows.map(row => (
|
||||||
|
<tr key={row.id}>
|
||||||
|
{row.getVisibleCells().map(cell => (
|
||||||
|
<td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SensorHubTable
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
// React Imports
|
||||||
|
import { useState } from 'react'
|
||||||
|
import type { Theme } from '@mui/material/styles'
|
||||||
|
|
||||||
|
// Hook Imports
|
||||||
|
import { useSensorHub } from '@/hooks/useSensorHub'
|
||||||
|
|
||||||
|
// API Imports
|
||||||
|
import type { Sensor } from '@/libs/api/services/sensorHubService'
|
||||||
|
import Dialog from '@mui/material/Dialog'
|
||||||
|
import DialogContent from '@mui/material/DialogContent'
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
|
import Drawer from '@mui/material/Drawer'
|
||||||
|
import Button from '@mui/material/Button'
|
||||||
|
import IconButton from '@mui/material/IconButton'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import useMediaQuery from '@mui/material/useMediaQuery'
|
||||||
|
import Fade from '@mui/material/Fade'
|
||||||
|
|
||||||
|
// Component Imports
|
||||||
|
import OptionSensorHub from './OptionSensorHub'
|
||||||
|
import FormSensorHub from './FormSensorHub'
|
||||||
|
|
||||||
|
type TableModalSheetProps = {
|
||||||
|
open: boolean
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const backdropBlurSx = { backdropFilter: 'blur(4px)' }
|
||||||
|
const transitionTimeout = { enter: 300, exit: 200 }
|
||||||
|
|
||||||
|
const DialogContentWithTransition = ({
|
||||||
|
showAddForm,
|
||||||
|
onShowAddForm,
|
||||||
|
onBack,
|
||||||
|
onConfirm
|
||||||
|
}: {
|
||||||
|
showAddForm: boolean
|
||||||
|
onShowAddForm: () => void
|
||||||
|
onBack: () => void
|
||||||
|
onConfirm: (sensor: Sensor) => void
|
||||||
|
}) => (
|
||||||
|
<Fade key={showAddForm ? 'form' : 'options'} in timeout={transitionTimeout}>
|
||||||
|
<div>
|
||||||
|
{showAddForm ? (
|
||||||
|
<FormSensorHub onBack={onBack} />
|
||||||
|
) : (
|
||||||
|
<div className='flex flex-col gap-4'>
|
||||||
|
<div className='grid grid-cols-[1fr_auto] items-center gap-4'>
|
||||||
|
<div className='flex flex-col gap-0.5'>
|
||||||
|
<Typography variant='h6' fontWeight={600}>
|
||||||
|
انتخاب سنسور
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body2' color='text.secondary' sx={{ lineHeight: 1.5 }}>
|
||||||
|
سنسور مورد نظر را انتخاب کنید یا سنسور جدید اضافه کنید
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant='contained'
|
||||||
|
color='primary'
|
||||||
|
startIcon={<i className='tabler-plus text-xl' />}
|
||||||
|
onClick={onShowAddForm}
|
||||||
|
>
|
||||||
|
اضافه کردن سنسور
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<OptionSensorHub onConfirm={onConfirm} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Fade>
|
||||||
|
)
|
||||||
|
|
||||||
|
const DrawerContentWithTransition = ({
|
||||||
|
showAddForm,
|
||||||
|
onShowAddForm,
|
||||||
|
onBack,
|
||||||
|
onConfirm
|
||||||
|
}: {
|
||||||
|
showAddForm: boolean
|
||||||
|
onShowAddForm: () => void
|
||||||
|
onBack: () => void
|
||||||
|
onConfirm: (sensor: Sensor) => void
|
||||||
|
}) => (
|
||||||
|
<Fade key={showAddForm ? 'form' : 'options'} in timeout={transitionTimeout}>
|
||||||
|
<div>
|
||||||
|
{showAddForm ? (
|
||||||
|
<FormSensorHub onBack={onBack} />
|
||||||
|
) : (
|
||||||
|
<div className='flex flex-col gap-4'>
|
||||||
|
<div className='grid grid-cols-[1fr_auto] items-center gap-4'>
|
||||||
|
<div className='flex flex-col gap-0.5'>
|
||||||
|
<Typography variant='h6' fontWeight={600}>
|
||||||
|
انتخاب سنسور
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body2' color='text.secondary' sx={{ lineHeight: 1.5 }}>
|
||||||
|
سنسور مورد نظر را انتخاب کنید یا سنسور جدید اضافه کنید
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant='contained'
|
||||||
|
color='primary'
|
||||||
|
startIcon={<i className='tabler-plus text-xl' />}
|
||||||
|
onClick={onShowAddForm}
|
||||||
|
>
|
||||||
|
اضافه کردن سنسور
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<OptionSensorHub onConfirm={onConfirm} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Fade>
|
||||||
|
)
|
||||||
|
|
||||||
|
const TableModalSheet = ({ open, onClose }: TableModalSheetProps) => {
|
||||||
|
const [showAddForm, setShowAddForm] = useState(false)
|
||||||
|
const isMobile = useMediaQuery((theme: Theme) => theme.breakpoints.down('sm'))
|
||||||
|
const { setSensorHub } = useSensorHub()
|
||||||
|
|
||||||
|
const handleBack = () => setShowAddForm(false)
|
||||||
|
|
||||||
|
const handleConfirm = (sensor: Sensor) => {
|
||||||
|
setSensorHub({ id: sensor.uuid_sensor, ...sensor })
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
anchor='bottom'
|
||||||
|
variant='temporary'
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
ModalProps={{ keepMounted: true }}
|
||||||
|
slotProps={{
|
||||||
|
backdrop: {
|
||||||
|
sx: backdropBlurSx
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
PaperProps={{
|
||||||
|
sx: {
|
||||||
|
maxHeight: '80vh',
|
||||||
|
borderTopLeftRadius: 'var(--mui-shape-borderRadius)',
|
||||||
|
borderTopRightRadius: 'var(--mui-shape-borderRadius)'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='flex items-center justify-between plb-4 pli-6 border-bs'>
|
||||||
|
<Typography variant='h5'>Sensor Data</Typography>
|
||||||
|
<IconButton size='small' onClick={onClose} aria-label='close'>
|
||||||
|
<i className='tabler-x text-2xl' />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
<div className='overflow-y-auto pli-6 plb-6' style={{ maxHeight: 'calc(80vh - 72px)' }}>
|
||||||
|
<DrawerContentWithTransition
|
||||||
|
showAddForm={showAddForm}
|
||||||
|
onShowAddForm={() => setShowAddForm(true)}
|
||||||
|
onBack={handleBack}
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
fullWidth
|
||||||
|
maxWidth='lg'
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
slotProps={{
|
||||||
|
backdrop: {
|
||||||
|
sx: backdropBlurSx
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentWithTransition
|
||||||
|
showAddForm={showAddForm}
|
||||||
|
onShowAddForm={() => setShowAddForm(true)}
|
||||||
|
onBack={handleBack}
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TableModalSheet
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
// React Imports
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
// MUI Imports
|
||||||
|
import Button from '@mui/material/Button'
|
||||||
|
|
||||||
|
// Component Imports
|
||||||
|
import TableModalSheet from './TableModalSheet'
|
||||||
|
|
||||||
|
const SensorHubView = () => {
|
||||||
|
const [open, setOpen] = useState(true)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TableModalSheet open={open} onClose={() => setOpen(false)} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SensorHubView
|
||||||
Reference in New Issue
Block a user