diff --git a/src/app/(dashboard)/(private)/layout.tsx b/src/app/(dashboard)/(private)/layout.tsx index 71d0dfb..b6a4d8d 100644 --- a/src/app/(dashboard)/(private)/layout.tsx +++ b/src/app/(dashboard)/(private)/layout.tsx @@ -11,6 +11,7 @@ import HorizontalLayout from '@layouts/HorizontalLayout' // Component Imports import Providers from '@components/Providers' +import SensorHub from '@components/SensorHub' import Navigation from '@components/layout/vertical/Navigation' import Header from '@components/layout/horizontal/Header' import Navbar from '@components/layout/vertical/Navbar' @@ -31,7 +32,8 @@ const Layout = async (props: ChildrenType) => { return ( - + { + ) diff --git a/src/components/SensorHub.tsx b/src/components/SensorHub.tsx new file mode 100644 index 0000000..55cb2fa --- /dev/null +++ b/src/components/SensorHub.tsx @@ -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 && }> +} diff --git a/src/hooks/useSensorHub.ts b/src/hooks/useSensorHub.ts new file mode 100644 index 0000000..2ebbaa1 --- /dev/null +++ b/src/hooks/useSensorHub.ts @@ -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 +} + +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(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 => { + 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 + } +} diff --git a/src/libs/api/index.ts b/src/libs/api/index.ts index 0424b6d..c96bba1 100644 --- a/src/libs/api/index.ts +++ b/src/libs/api/index.ts @@ -14,4 +14,5 @@ export * from './services/kanbanService' export * from './services/todoService' export * from './services/userManagementService' export * from './services/rolesPermissionsService' +export * from './services/sensorHubService' diff --git a/src/libs/api/services/sensorHubService.ts b/src/libs/api/services/sensorHubService.ts new file mode 100644 index 0000000..59c095c --- /dev/null +++ b/src/libs/api/services/sensorHubService.ts @@ -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 { + const response = await apiClient.get('/api/sensor-hub/') + const data = response?.data + + if (Array.isArray(data)) { + return data + } + + if (data && typeof data === 'object') { + return [data as Sensor] + } + + return [] + } +} diff --git a/src/views/sensorHub/FormSensorHub.tsx b/src/views/sensorHub/FormSensorHub.tsx new file mode 100644 index 0000000..476c29e --- /dev/null +++ b/src/views/sensorHub/FormSensorHub.tsx @@ -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 ( + + + } + onClick={onBack} + > + بازگشت + + {/* افزودن سنسور جدید */} + + + + + setName(e.target.value)} + /> + + + setUuidSensor(e.target.value)} + /> + + + + انصراف + + }> + ذخیره سنسور + + + + + + ) +} + +export default FormSensorHub diff --git a/src/views/sensorHub/OptionSensorHub.tsx b/src/views/sensorHub/OptionSensorHub.tsx new file mode 100644 index 0000000..f228b3d --- /dev/null +++ b/src/views/sensorHub/OptionSensorHub.tsx @@ -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([]) + const [data, setData] = useState([]) + const [loading, setLoading] = useState(true) + const [selectedOption, setSelectedOption] = useState('') + + 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) => { + if (typeof prop === 'string') { + setSelectedOption(prop) + } else { + setSelectedOption((prop.target as HTMLInputElement).value) + } + } + + if (loading) { + return ( + + + + ) + } + + if (data.length === 0) { + return null + } + + const handleConfirm = () => { + const selected = sensors.find(s => s.uuid_sensor === selectedOption) + if (selected && onConfirm) { + onConfirm(selected) + } + } + + return ( + e.preventDefault()}> + + {data.map(item => ( + *]:rounded-be-none [&:last-of-type>*]:rounded-bs-none [&:nth-of-type(2)>*]:rounded-none' + }} + selected={selectedOption} + name='sensor-hub-option' + handleChange={handleOptionChange} + /> + ))} + + {onConfirm && ( + + } + > + تایید + + + )} + + ) +} + +export default OptionSensorHub diff --git a/src/views/sensorHub/SensorHubTable.tsx b/src/views/sensorHub/SensorHubTable.tsx new file mode 100644 index 0000000..41adbe3 --- /dev/null +++ b/src/views/sensorHub/SensorHubTable.tsx @@ -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() + +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([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(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 ( + + + + + + + ) + } + + if (error) { + return ( + + + + ) + } + + return ( + + + + + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + + ))} + + ))} + + + {table.getRowModel().rows.map(row => ( + + {row.getVisibleCells().map(cell => ( + {flexRender(cell.column.columnDef.cell, cell.getContext())} + ))} + + ))} + + + + + ) +} + +export default SensorHubTable diff --git a/src/views/sensorHub/TableModalSheet.tsx b/src/views/sensorHub/TableModalSheet.tsx new file mode 100644 index 0000000..a45c75d --- /dev/null +++ b/src/views/sensorHub/TableModalSheet.tsx @@ -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 +}) => ( + + + {showAddForm ? ( + + ) : ( + + + + + انتخاب سنسور + + + سنسور مورد نظر را انتخاب کنید یا سنسور جدید اضافه کنید + + + } + onClick={onShowAddForm} + > + اضافه کردن سنسور + + + + + )} + + +) + +const DrawerContentWithTransition = ({ + showAddForm, + onShowAddForm, + onBack, + onConfirm +}: { + showAddForm: boolean + onShowAddForm: () => void + onBack: () => void + onConfirm: (sensor: Sensor) => void +}) => ( + + + {showAddForm ? ( + + ) : ( + + + + + انتخاب سنسور + + + سنسور مورد نظر را انتخاب کنید یا سنسور جدید اضافه کنید + + + } + onClick={onShowAddForm} + > + اضافه کردن سنسور + + + + + )} + + +) + +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 ( + + + Sensor Data + + + + + + setShowAddForm(true)} + onBack={handleBack} + onConfirm={handleConfirm} + /> + + + ) + } + + return ( + + + + setShowAddForm(true)} + onBack={handleBack} + onConfirm={handleConfirm} + /> + + + ) +} + +export default TableModalSheet diff --git a/src/views/sensorHub/index.tsx b/src/views/sensorHub/index.tsx new file mode 100644 index 0000000..0b61c4e --- /dev/null +++ b/src/views/sensorHub/index.tsx @@ -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 ( + <> + setOpen(false)} /> + > + ) +} + +export default SensorHubView