AEB-71: Added 3D navigation in monitoring zones
This commit is contained in:
@@ -22,15 +22,7 @@ const AreaChart: React.FC<AreaChartProps> = ({ className = '', data }) => {
|
||||
|
||||
const safeData = (Array.isArray(data) && data.length > 0)
|
||||
? data
|
||||
: [
|
||||
{ value: 5 },
|
||||
{ value: 3 },
|
||||
{ value: 7 },
|
||||
{ value: 2 },
|
||||
{ value: 6 },
|
||||
{ value: 4 },
|
||||
{ value: 8 }
|
||||
]
|
||||
: Array.from({ length: 7 }, () => ({ value: 0 }))
|
||||
|
||||
const maxVal = Math.max(...safeData.map(d => d.value || 0), 1)
|
||||
const stepX = safeData.length > 1 ? width / (safeData.length - 1) : width
|
||||
|
||||
@@ -14,24 +14,9 @@ interface BarChartProps {
|
||||
}
|
||||
|
||||
const BarChart: React.FC<BarChartProps> = ({ className = '', data }) => {
|
||||
const defaultData = [
|
||||
{ value: 80, color: 'rgb(42, 157, 144)' },
|
||||
{ value: 65, color: 'rgb(42, 157, 144)' },
|
||||
{ value: 90, color: 'rgb(42, 157, 144)' },
|
||||
{ value: 45, color: 'rgb(42, 157, 144)' },
|
||||
{ value: 75, color: 'rgb(42, 157, 144)' },
|
||||
{ value: 55, color: 'rgb(42, 157, 144)' },
|
||||
{ value: 85, color: 'rgb(42, 157, 144)' },
|
||||
{ value: 70, color: 'rgb(42, 157, 144)' },
|
||||
{ value: 60, color: 'rgb(42, 157, 144)' },
|
||||
{ value: 95, color: 'rgb(42, 157, 144)' },
|
||||
{ value: 40, color: 'rgb(42, 157, 144)' },
|
||||
{ value: 80, color: 'rgb(42, 157, 144)' }
|
||||
]
|
||||
|
||||
const barData = (Array.isArray(data) && data.length > 0)
|
||||
? data.map(d => ({ value: d.value, color: d.color || 'rgb(42, 157, 144)' }))
|
||||
: defaultData
|
||||
: Array.from({ length: 12 }, () => ({ value: 0, color: 'rgb(42, 157, 144)' }))
|
||||
|
||||
const maxVal = Math.max(...barData.map(b => b.value || 0), 1)
|
||||
|
||||
|
||||
@@ -7,28 +7,49 @@ import useNavigationStore from '../../app/store/navigationStore'
|
||||
import ChartCard from './ChartCard'
|
||||
import AreaChart from './AreaChart'
|
||||
import BarChart from './BarChart'
|
||||
import DetectorChart from './DetectorChart'
|
||||
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
const router = useRouter()
|
||||
const { currentObject, setCurrentSubmenu, closeMonitoring, closeFloorNavigation, closeNotifications } = useNavigationStore()
|
||||
const objectId = currentObject?.id
|
||||
const objectTitle = currentObject?.title
|
||||
|
||||
const [dashboardAlerts, setDashboardAlerts] = useState<any[]>([])
|
||||
const [chartData, setChartData] = useState<{ timestamp: string; value: number }[]>([])
|
||||
const [sensorTypes] = useState<Array<{code: string, name: string}>>([
|
||||
{ code: '', name: 'Все датчики' },
|
||||
{ code: 'GA', name: 'Инклинометр' },
|
||||
{ code: 'PE', name: 'Танзометр' },
|
||||
{ code: 'GLE', name: 'Гидроуровень' }
|
||||
])
|
||||
const [selectedSensorType, setSelectedSensorType] = useState<string>('')
|
||||
const [selectedChartPeriod, setSelectedChartPeriod] = useState<string>('168')
|
||||
const [selectedTablePeriod, setSelectedTablePeriod] = useState<string>('168')
|
||||
|
||||
useEffect(() => {
|
||||
const loadDashboard = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/get-dashboard', { cache: 'no-store' })
|
||||
const params = new URLSearchParams()
|
||||
params.append('time_period', selectedChartPeriod)
|
||||
|
||||
const res = await fetch(`/api/get-dashboard?${params.toString()}`, { cache: 'no-store' })
|
||||
if (!res.ok) return
|
||||
const payload = await res.json()
|
||||
console.log('[Dashboard] GET /api/get-dashboard', { status: res.status, payload })
|
||||
const tableData = payload?.data?.table_data ?? []
|
||||
const arr = (Array.isArray(tableData) ? tableData : [])
|
||||
.filter((a: any) => (objectTitle ? a.object === objectTitle : true))
|
||||
setDashboardAlerts(arr as any[])
|
||||
|
||||
let tableData = payload?.data?.table_data ?? []
|
||||
tableData = Array.isArray(tableData) ? tableData : []
|
||||
|
||||
if (objectTitle) {
|
||||
tableData = tableData.filter((a: any) => a.object === objectTitle)
|
||||
}
|
||||
|
||||
if (selectedSensorType && selectedSensorType !== '') {
|
||||
tableData = tableData.filter((a: any) => {
|
||||
return a.detector_type?.toLowerCase() === selectedSensorType.toLowerCase()
|
||||
})
|
||||
}
|
||||
|
||||
setDashboardAlerts(tableData as any[])
|
||||
|
||||
const cd = Array.isArray(payload?.data?.chart_data) ? payload.data.chart_data : []
|
||||
setChartData(cd as any[])
|
||||
@@ -37,14 +58,52 @@ const Dashboard: React.FC = () => {
|
||||
}
|
||||
}
|
||||
loadDashboard()
|
||||
}, [objectTitle])
|
||||
}, [objectTitle, selectedChartPeriod, selectedSensorType])
|
||||
|
||||
// Separate effect for table data based on table period
|
||||
useEffect(() => {
|
||||
const loadTableData = async () => {
|
||||
try {
|
||||
const params = new URLSearchParams()
|
||||
params.append('time_period', selectedTablePeriod)
|
||||
|
||||
const res = await fetch(`/api/get-dashboard?${params.toString()}`, { cache: 'no-store' })
|
||||
if (!res.ok) return
|
||||
const payload = await res.json()
|
||||
console.log('[Dashboard] GET /api/get-dashboard (table)', { status: res.status, payload })
|
||||
|
||||
let tableData = payload?.data?.table_data ?? []
|
||||
tableData = Array.isArray(tableData) ? tableData : []
|
||||
|
||||
if (objectTitle) {
|
||||
tableData = tableData.filter((a: any) => a.object === objectTitle)
|
||||
}
|
||||
|
||||
if (selectedSensorType && selectedSensorType !== '') {
|
||||
tableData = tableData.filter((a: any) => {
|
||||
return a.detector_type?.toLowerCase() === selectedSensorType.toLowerCase()
|
||||
})
|
||||
}
|
||||
|
||||
setDashboardAlerts(tableData as any[])
|
||||
} catch (e) {
|
||||
console.error('Failed to load table data:', e)
|
||||
}
|
||||
}
|
||||
loadTableData()
|
||||
}, [objectTitle, selectedTablePeriod, selectedSensorType])
|
||||
|
||||
const handleBackClick = () => {
|
||||
router.push('/objects')
|
||||
}
|
||||
|
||||
|
||||
const filteredAlerts = dashboardAlerts.filter((alert: any) => {
|
||||
if (selectedSensorType === '') return true
|
||||
return alert.detector_type?.toLowerCase() === selectedSensorType.toLowerCase()
|
||||
})
|
||||
|
||||
// Статусы
|
||||
const statusCounts = dashboardAlerts.reduce((acc: { critical: number; warning: number; normal: number }, a: any) => {
|
||||
const statusCounts = filteredAlerts.reduce((acc: { critical: number; warning: number; normal: number }, a: any) => {
|
||||
if (a.severity === 'critical') acc.critical++
|
||||
else if (a.severity === 'warning') acc.warning++
|
||||
else acc.normal++
|
||||
@@ -58,6 +117,18 @@ const Dashboard: React.FC = () => {
|
||||
setCurrentSubmenu(null)
|
||||
router.push('/navigation')
|
||||
}
|
||||
|
||||
const handleSensorTypeChange = (sensorType: string) => {
|
||||
setSelectedSensorType(sensorType)
|
||||
}
|
||||
|
||||
const handleChartPeriodChange = (period: string) => {
|
||||
setSelectedChartPeriod(period)
|
||||
}
|
||||
|
||||
const handleTablePeriodChange = (period: string) => {
|
||||
setSelectedTablePeriod(period)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-[#0e111a]">
|
||||
@@ -87,17 +158,25 @@ const Dashboard: React.FC = () => {
|
||||
|
||||
<div className="flex-1 p-6 overflow-auto">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-white text-2xl font-semibold mb-6">Объект {objectId?.replace('object_', '')}</h1>
|
||||
<h1 className="text-white text-2xl font-semibold mb-6">{objectTitle || 'Объект'}</h1>
|
||||
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<button
|
||||
className="flex items-center gap-6 rounded-[10px] px-4 py-[18px] bg-[rgb(22,24,36)] text-white"
|
||||
>
|
||||
<span className="text-sm font-medium">Датчики : все</span>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div className="relative">
|
||||
<select
|
||||
value={selectedSensorType}
|
||||
onChange={(e) => handleSensorTypeChange(e.target.value)}
|
||||
className="flex items-center gap-6 rounded-[10px] px-4 py-[18px] bg-[rgb(22,24,36)] text-white appearance-none pr-8"
|
||||
>
|
||||
{sensorTypes.map((type) => (
|
||||
<option key={type.code} value={type.code}>
|
||||
{type.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<svg className="w-4 h-4 absolute right-3 top-1/2 transform -translate-y-1/2 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 ml-auto">
|
||||
<button
|
||||
@@ -107,12 +186,20 @@ const Dashboard: React.FC = () => {
|
||||
<span className="text-sm font-medium">Навигация</span>
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-2 bg-[rgb(22,24,36)] rounded-lg px-3 py-2">
|
||||
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
||||
<div className="relative">
|
||||
<select
|
||||
value={selectedChartPeriod}
|
||||
onChange={(e) => handleChartPeriodChange(e.target.value)}
|
||||
className="flex items-center gap-2 bg-[rgb(22,24,36)] rounded-lg px-3 py-2 text-white appearance-none pr-8"
|
||||
>
|
||||
<option value="24">День</option>
|
||||
<option value="72">3 дня</option>
|
||||
<option value="168">Неделя</option>
|
||||
<option value="720">Месяц</option>
|
||||
</select>
|
||||
<svg className="w-4 h-4 absolute right-3 top-1/2 transform -translate-y-1/2 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
<span className="text-white text-sm font-medium">Период</span>
|
||||
<div className="w-2 h-2 bg-white rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -121,14 +208,14 @@ const Dashboard: React.FC = () => {
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-[18px]">
|
||||
<ChartCard
|
||||
title="Показатель"
|
||||
subtitle="За последние 6 месяцев"
|
||||
// subtitle removed
|
||||
>
|
||||
<AreaChart data={chartData} />
|
||||
</ChartCard>
|
||||
|
||||
<ChartCard
|
||||
title="Статистика"
|
||||
subtitle="Данные за период"
|
||||
// subtitle removed
|
||||
>
|
||||
<BarChart data={chartData?.map((d: any) => ({ value: d.value }))} />
|
||||
</ChartCard>
|
||||
@@ -140,12 +227,18 @@ const Dashboard: React.FC = () => {
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-white text-2xl font-semibold">Тренды</h2>
|
||||
<div className="bg-[#161824] rounded-lg px-3 py-2 flex items-center gap-2">
|
||||
<svg className="w-4 h-4 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<span className="text-white text-sm font-medium">Месяц</span>
|
||||
<svg className="w-4 h-4 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<div className="relative">
|
||||
<select
|
||||
value={selectedTablePeriod}
|
||||
onChange={(e) => handleTablePeriodChange(e.target.value)}
|
||||
className="bg-[#161824] rounded-lg px-3 py-2 flex items-center gap-2 text-white appearance-none pr-8"
|
||||
>
|
||||
<option value="24">День</option>
|
||||
<option value="72">3 дня</option>
|
||||
<option value="168">Неделя</option>
|
||||
<option value="720">Месяц</option>
|
||||
</select>
|
||||
<svg className="w-4 h-4 absolute right-3 top-1/2 transform -translate-y-1/2 pointer-events-none" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
@@ -165,7 +258,7 @@ const Dashboard: React.FC = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{dashboardAlerts.map((alert: any) => (
|
||||
{filteredAlerts.map((alert: any) => (
|
||||
<tr key={alert.id} className="border-b border-gray-800">
|
||||
<td className="py-3 text-white text-sm">{alert.name}</td>
|
||||
<td className="py-3 text-gray-300 text-sm">{alert.message}</td>
|
||||
@@ -191,7 +284,7 @@ const Dashboard: React.FC = () => {
|
||||
{/* Статы */}
|
||||
<div className="mt-6 grid grid-cols-4 gap-4">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-white">{dashboardAlerts.length}</div>
|
||||
<div className="text-2xl font-bold text-white">{filteredAlerts.length}</div>
|
||||
<div className="text-sm text-gray-400">Всего</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
@@ -208,37 +301,6 @@ const Dashboard: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Графики с аналитикой */}
|
||||
<div className="mt-6 grid grid-cols-1 lg:grid-cols-4 gap-[18px]">
|
||||
<ChartCard
|
||||
title="Тренды детекторов"
|
||||
subtitle="За последний месяц"
|
||||
>
|
||||
<DetectorChart type="line" data={chartData?.map((d: any) => ({ value: d.value }))} />
|
||||
</ChartCard>
|
||||
|
||||
<ChartCard
|
||||
title="Статистика по месяцам"
|
||||
subtitle="Активность детекторов"
|
||||
>
|
||||
<DetectorChart type="bar" data={chartData?.map((d: any) => ({ value: d.value }))} />
|
||||
</ChartCard>
|
||||
|
||||
<ChartCard
|
||||
title="Анализ производительности"
|
||||
subtitle="Эффективность работы"
|
||||
>
|
||||
<DetectorChart type="line" data={chartData?.map((d: any) => ({ value: d.value }))} />
|
||||
</ChartCard>
|
||||
|
||||
<ChartCard
|
||||
title="Сводка по статусам"
|
||||
subtitle="Распределение состояний"
|
||||
>
|
||||
<DetectorChart type="bar" data={chartData?.map((d: any) => ({ value: d.value }))} />
|
||||
</ChartCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user