2 Commits

2 changed files with 71 additions and 7 deletions

View File

@@ -144,7 +144,7 @@ describe('WellDashboardPage', () => {
expect(getProjectFullConfig).toHaveBeenCalledWith('project-1') expect(getProjectFullConfig).toHaveBeenCalledWith('project-1')
expect(fetchAssetTelemetryHistory).toHaveBeenCalledWith('well-1', 1, 100) expect(fetchAssetTelemetryHistory).toHaveBeenCalledWith('well-1', 1, 100)
expect(fetchAssetTelemetry).not.toHaveBeenCalled() expect(fetchAssetTelemetry).toHaveBeenCalledWith('well-1', 100)
expect(getAlarms).toHaveBeenCalledWith('project-1') expect(getAlarms).toHaveBeenCalledWith('project-1')
expect(container.textContent).toContain('Pozo Norte') expect(container.textContent).toContain('Pozo Norte')
expect(container.textContent).toContain('Activo') expect(container.textContent).toContain('Activo')
@@ -564,10 +564,61 @@ describe('WellDashboardPage', () => {
expect(lowFrequencySection?.textContent).not.toContain('No hay variables de baja frecuencia') expect(lowFrequencySection?.textContent).not.toContain('No hay variables de baja frecuencia')
}) })
it('includes LF capture fields from raw telemetry when history lacks origin metadata', async () => {
vi.mocked(fetchDashboardSummary).mockResolvedValue({
data: [{
asset_id: 'well-1',
asset: 'Pozo Norte',
source: 'SCADA',
frequency_class: 'HF',
last_updated: '2026-06-22T10:05:00Z',
telemetry: { values: { presion: 121 } },
}],
})
vi.mocked(fetchAssetTelemetryHistory).mockResolvedValue({ records: [
{
asset_id: 'well-1',
time: '2026-06-22T10:00:00Z',
data: { values: { caudal_bopd: 84 } },
},
] })
vi.mocked(fetchAssetTelemetry).mockResolvedValue([
{
asset_id: 'well-1',
time: '2026-06-22T10:00:00Z',
source: 'PWA',
data: { values: { presion: 120, caudal_bopd: 84 } },
},
])
await act(async () => {
root.render(
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={['/app/admin/wells/well-1']}>
<Routes>
<Route path="/app/admin/wells/:wellId" element={<WellDashboardPage />} />
</Routes>
</MemoryRouter>
</QueryClientProvider>
)
})
await flushQueries()
await waitForText(container, 'Caudal bopd')
const lowFrequencySection = container.querySelector('section[aria-label="Variables de baja frecuencia"]')
expect(fetchAssetTelemetry).toHaveBeenCalledWith('well-1', 100)
expect(lowFrequencySection?.textContent).toContain('Caudal bopd')
expect(lowFrequencySection?.textContent).toContain('84')
expect(lowFrequencySection?.textContent).toContain('Captura PWA')
expect(lowFrequencySection?.textContent).not.toContain('No hay variables de baja frecuencia')
})
it('shows loading copy before rendering empty high and low frequency states', async () => { it('shows loading copy before rendering empty high and low frequency states', async () => {
vi.mocked(getProjectFullConfig).mockReturnValue(new Promise(() => undefined) as ReturnType<typeof getProjectFullConfig>) vi.mocked(getProjectFullConfig).mockReturnValue(new Promise(() => undefined) as ReturnType<typeof getProjectFullConfig>)
vi.mocked(fetchDashboardSummary).mockReturnValue(new Promise(() => undefined) as ReturnType<typeof fetchDashboardSummary>) vi.mocked(fetchDashboardSummary).mockReturnValue(new Promise(() => undefined) as ReturnType<typeof fetchDashboardSummary>)
vi.mocked(fetchAssetTelemetryHistory).mockReturnValue(new Promise(() => undefined) as ReturnType<typeof fetchAssetTelemetryHistory>) vi.mocked(fetchAssetTelemetryHistory).mockReturnValue(new Promise(() => undefined) as ReturnType<typeof fetchAssetTelemetryHistory>)
vi.mocked(fetchAssetTelemetry).mockReturnValue(new Promise(() => undefined) as ReturnType<typeof fetchAssetTelemetry>)
await act(async () => { await act(async () => {
root.render( root.render(
@@ -619,6 +670,6 @@ describe('WellDashboardPage', () => {
expect(highFrequencySection?.textContent).not.toContain('presion') expect(highFrequencySection?.textContent).not.toContain('presion')
expect(highFrequencySection?.textContent).not.toContain('Temperatura motor') expect(highFrequencySection?.textContent).not.toContain('Temperatura motor')
expect(fetchAssetTelemetryHistory).toHaveBeenCalledWith('well-1', 1, 100) expect(fetchAssetTelemetryHistory).toHaveBeenCalledWith('well-1', 1, 100)
expect(fetchAssetTelemetry).not.toHaveBeenCalled() expect(fetchAssetTelemetry).toHaveBeenCalledWith('well-1', 100)
}) })
}) })

View File

@@ -6,7 +6,7 @@ import { CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YA
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { useAuthStore } from '@/features/auth/stores/auth-store' import { useAuthStore } from '@/features/auth/stores/auth-store'
import { getAlarms } from '@/features/admin/alerts/api/alerts-api' import { getAlarms } from '@/features/admin/alerts/api/alerts-api'
import { fetchAssetTelemetryHistory, fetchDashboardSummary, type TelemetryHistoryRecord } from '../api/dashboard-api' import { fetchAssetTelemetry, fetchAssetTelemetryHistory, fetchDashboardSummary, type TelemetryHistoryRecord } from '../api/dashboard-api'
import { DashboardEmptyState } from '../components/DashboardEmptyState' import { DashboardEmptyState } from '../components/DashboardEmptyState'
import { TelemetryChart } from '../components/TelemetryChart' import { TelemetryChart } from '../components/TelemetryChart'
import type { AssetData } from '../utils/telemetry-normalization' import type { AssetData } from '../utils/telemetry-normalization'
@@ -269,6 +269,12 @@ export default function WellDashboardPage() {
queryFn: () => fetchAssetTelemetryHistory(wellId!, 1, 100), queryFn: () => fetchAssetTelemetryHistory(wellId!, 1, 100),
enabled: Boolean(token && wellId), enabled: Boolean(token && wellId),
}) })
const rawTelemetryQuery = useQuery({
queryKey: ['well-dashboard-raw-telemetry', wellId],
queryFn: () => fetchAssetTelemetry(wellId!, 100),
enabled: Boolean(token && wellId),
refetchInterval: 30000,
})
const wellConfig = useMemo(() => { const wellConfig = useMemo(() => {
return projectConfigQuery.data?.assets.find(({ asset }) => asset.id === wellId && asset.asset_type === 'POZO') ?? null return projectConfigQuery.data?.assets.find(({ asset }) => asset.id === wellId && asset.asset_type === 'POZO') ?? null
@@ -284,6 +290,12 @@ export default function WellDashboardPage() {
return records.map(normalizeHistoryRecord) return records.map(normalizeHistoryRecord)
}, [historyQuery.data]) }, [historyQuery.data])
const rawTelemetry = useMemo(() => {
const raw = rawTelemetryQuery.data as TelemetryHistoryRecord[] | { records?: TelemetryHistoryRecord[] } | undefined
const records = Array.isArray(raw) ? raw : Array.isArray(raw?.records) ? raw.records : []
return records.map(normalizeHistoryRecord)
}, [rawTelemetryQuery.data])
const latestAssetData = useMemo<AssetData | null>(() => { const latestAssetData = useMemo<AssetData | null>(() => {
const asset = summaryQuery.data?.data.find((item) => item.asset_id === wellId) const asset = summaryQuery.data?.data.find((item) => item.asset_id === wellId)
if (asset) return asset if (asset) return asset
@@ -331,14 +343,14 @@ export default function WellDashboardPage() {
const remainingHighFrequencySignals = Math.max(highFrequencySignals.length - visibleHighFrequencySignals.length, 0) const remainingHighFrequencySignals = Math.max(highFrequencySignals.length - visibleHighFrequencySignals.length, 0)
const lowFrequencySignals = useMemo(() => variableSignals.filter((signal) => signal.frequencyClass === 'low'), [variableSignals]) const lowFrequencySignals = useMemo(() => variableSignals.filter((signal) => signal.frequencyClass === 'low'), [variableSignals])
const lowFrequencyCaptureSignals = useMemo(() => { const lowFrequencyCaptureSignals = useMemo(() => {
if (!latestAssetData && history.length === 0) return [] if (!latestAssetData && history.length === 0 && rawTelemetry.length === 0) return []
const catalogAliases = new Set(enabledVariables.map((variable) => variable.alias)) const catalogAliases = new Set(enabledVariables.map((variable) => variable.alias))
const captureFieldsByAlias = new Map<string, ReturnType<typeof deriveCaptureFields>[number]>() const captureFieldsByAlias = new Map<string, ReturnType<typeof deriveCaptureFields>[number]>()
const captureCandidates: AssetData[] = [] const captureCandidates: AssetData[] = []
if (latestAssetData) captureCandidates.push(latestAssetData) if (latestAssetData) captureCandidates.push(latestAssetData)
for (const record of history) { for (const record of [...rawTelemetry, ...history]) {
if (!record.data || !wellId) continue if (!record.data || !wellId) continue
if (record.asset_id && record.asset_id !== wellId) continue if (record.asset_id && record.asset_id !== wellId) continue
@@ -372,7 +384,7 @@ export default function WellDashboardPage() {
sparklinePath: '', sparklinePath: '',
sparklineValues: [] as number[], sparklineValues: [] as number[],
})) }))
}, [enabledVariables, history, latestAssetData, resolvedProjectId, wellId, wellName]) }, [enabledVariables, history, latestAssetData, rawTelemetry, resolvedProjectId, wellId, wellName])
const allLowFrequencySignals = useMemo(() => [...lowFrequencySignals, ...lowFrequencyCaptureSignals], [lowFrequencyCaptureSignals, lowFrequencySignals]) const allLowFrequencySignals = useMemo(() => [...lowFrequencySignals, ...lowFrequencyCaptureSignals], [lowFrequencyCaptureSignals, lowFrequencySignals])
const gaugeSignals = useMemo(() => { const gaugeSignals = useMemo(() => {
return variableSignals return variableSignals
@@ -410,6 +422,7 @@ export default function WellDashboardPage() {
.filter((point): point is { time: string; value: number } => typeof point.value === 'number' && Number.isFinite(point.value)) .filter((point): point is { time: string; value: number } => typeof point.value === 'number' && Number.isFinite(point.value))
}, [history, selectedSignal]) }, [history, selectedSignal])
const loading = projectConfigQuery.isLoading || summaryQuery.isLoading || historyQuery.isLoading const loading = projectConfigQuery.isLoading || summaryQuery.isLoading || historyQuery.isLoading
const lowFrequencyLoading = loading || rawTelemetryQuery.isLoading
const hasProject = Boolean(resolvedProjectId) const hasProject = Boolean(resolvedProjectId)
useEffect(() => { useEffect(() => {
@@ -766,7 +779,7 @@ export default function WellDashboardPage() {
</div> </div>
<span className="text-[11px] font-semibold uppercase tracking-[0.18em] text-[#9aa8ba]">{allLowFrequencySignals.length} señales</span> <span className="text-[11px] font-semibold uppercase tracking-[0.18em] text-[#9aa8ba]">{allLowFrequencySignals.length} señales</span>
</div> </div>
{loading && allLowFrequencySignals.length === 0 ? ( {lowFrequencyLoading && allLowFrequencySignals.length === 0 ? (
<p className="rounded-2xl border border-[#2a3038] bg-[#080b0f] p-4 text-sm text-[#9aa8ba]">Cargando variables de baja frecuencia</p> <p className="rounded-2xl border border-[#2a3038] bg-[#080b0f] p-4 text-sm text-[#9aa8ba]">Cargando variables de baja frecuencia</p>
) : allLowFrequencySignals.length === 0 ? ( ) : allLowFrequencySignals.length === 0 ? (
<p className="rounded-2xl border border-[#2a3038] bg-[#080b0f] p-4 text-sm text-[#9aa8ba]">No hay variables de baja frecuencia ni capturas LF para este pozo.</p> <p className="rounded-2xl border border-[#2a3038] bg-[#080b0f] p-4 text-sm text-[#9aa8ba]">No hay variables de baja frecuencia ni capturas LF para este pozo.</p>