import { useEffect, useState } from 'react' import { Link } from 'react-router-dom' import { useQuery } from '@tanstack/react-query' import { LineChart } from '@mui/x-charts/LineChart' import { client, errorMessage } from '../api/client' import type { Dashboard } from '../api/types' import { Icon } from '../components/Icon' import { Card, ErrorState, Spinner } from '../components/ui' import { formatWithZone } from '../lib/dates' import { WEBINAR_STATUS_LABEL } from '../lib/labels' import { panelPath } from '../lib/routes' /** * Ekran glowny panelu. * * Pierwsze, co widzi zalogowany uzytkownik, wiec pokazuje wylacznie rzeczy, * na ktore mozna zareagowac: co sie zbliza i co czeka na obsluge. Nie ma tu * liczb dla samych liczb. */ const HYDRO_AZURE = '#2C8ABD' export function DashboardPage() { const query = useQuery({ queryKey: ['dashboard'], queryFn: async () => { const response = await client.get('/admin/dashboard') return response.data }, }) if (query.isPending) return if (query.isError) { return } const data = query.data const statuses = data.webinarsByStatus return (

Podsumowanie

Co się zbliża i co czeka na obsługę

{/* Nieobsluzone zapytania na samej gorze i wyroznione: to jedyna pozycja na tym ekranie, ktora oznacza zalegla prace. */} {data.pendingOffers > 0 && ( {data.pendingOffers} {data.pendingOffers === 1 ? 'zapytanie ofertowe czeka na obsługę' : 'zapytań ofertowych czeka na obsługę'} )}
{data.upcoming ? ( ) : (

Nie zaplanowano żadnego webinaru.{' '} Dodaj pierwszy .

)}
{(['PUBLISHED', 'DRAFT', 'FINISHED', 'CANCELLED'] as const).map((status) => (
{WEBINAR_STATUS_LABEL[status]}
{statuses[status] ?? 0}
))}
Pełna lista
{data.dailyRegistrations.every((d) => d.count === 0) ? (

W ostatnich 30 dniach nikt się nie zapisał.

) : (
d.count), label: 'Zapisy', color: HYDRO_AZURE, area: true, showMark: false, }, ]} xAxis={[ { scaleType: 'point', data: data.dailyRegistrations.map((d) => formatDay(d.day)), }, ]} yAxis={[{ min: 0 }]} margin={{ left: 40, right: 16, top: 16, bottom: 40 }} />
)}
) } /** Najblizszy webinar wraz z odliczaniem. */ function UpcomingCard({ upcoming }: { upcoming: NonNullable }) { const [countdown, setCountdown] = useState(() => describeCountdown(upcoming.startsAt)) useEffect(() => { // odliczanie odswiezane co minute: sekundy przy odleglosci dni nic nie wnosza const timer = window.setInterval(() => { setCountdown(describeCountdown(upcoming.startsAt)) }, 60_000) return () => window.clearInterval(timer) }, [upcoming.startsAt]) return (
{upcoming.title}

{formatWithZone(upcoming.startsAt, upcoming.timezone)}

Rozpocznie się za

{countdown}

{upcoming.registered} {upcoming.capacity !== null ? `z ${upcoming.capacity} miejsc` : 'zapisanych, bez limitu'}
) } /** * Ile zostalo do startu, zwyklym zdaniem. * * Bez sekund: przy odleglosci liczonej w dniach sekundy sa szumem, a przy * odleglosci minut i tak wystarcza dokladnosc do minuty. */ function describeCountdown(startsAt: string): string { const diffMs = new Date(startsAt).getTime() - Date.now() if (diffMs <= 0) return 'już trwa' const minutes = Math.floor(diffMs / 60_000) if (minutes < 60) { return `${minutes} ${plural(minutes, 'minutę', 'minuty', 'minut')}` } const hours = Math.floor(minutes / 60) if (hours < 24) { const rest = minutes % 60 const hourPart = `${hours} ${plural(hours, 'godzinę', 'godziny', 'godzin')}` return rest > 0 ? `${hourPart} i ${rest} min` : hourPart } const days = Math.floor(hours / 24) return `${days} ${plural(days, 'dzień', 'dni', 'dni')}` } /** Odmiana przez liczbę, wedlug reguly polskiej. */ function plural(count: number, one: string, few: string, many: string): string { if (count === 1) return one const lastTwo = count % 100 const last = count % 10 if (lastTwo >= 12 && lastTwo <= 14) return many return last >= 2 && last <= 4 ? few : many } /** Dzien i miesiac, bez roku: wykres obejmuje 30 dni. */ function formatDay(iso: string): string { try { return new Intl.DateTimeFormat('pl-PL', { day: '2-digit', month: '2-digit' }) .format(new Date(iso)) } catch { return iso } }