Skip to main content

Overview

The Incidents App analytics dashboard provides real-time insights into hotel operations, incident trends, and team performance. Built with interactive charts and data tables, it helps administrators make data-driven decisions.

Dashboard Metrics

The main dashboard displays four key performance indicators:

Total Incidents

Lifetime count of all incidents reported across the property

Pending

Incidents awaiting staff acceptance (status: pendiente)

In Progress

Incidents currently being worked on (status: en_progreso)

Resolved

Completed incidents (status: resuelta)

Implementation

The dashboard fetches metrics using Supabase server-side queries:
app/(dashboard)/dashboard/page.tsx
import { createClient } from "@/lib/supabase-server";

export default async function DashboardPage() {
    const supabase = await createClient();

    // Fetch incident counts
    const { count: totalIncidents } = await supabase
        .from("incidents")
        .select("*", { count: "exact", head: true });

    const { count: pendingCount } = await supabase
        .from("incidents")
        .select("*", { count: "exact", head: true })
        .eq("status", "pendiente");

    const { count: inProgressCount } = await supabase
        .from("incidents")
        .select("*", { count: "exact", head: true })
        .eq("status", "en_progreso");

    const { count: resolvedCount } = await supabase
        .from("incidents")
        .select("*", { count: "exact", head: true })
        .eq("status", "resuelta");

    const cardData = {
        total: totalIncidents || 0,
        pending: pendingCount || 0,
        inProgress: inProgressCount || 0,
        resolved: resolvedCount || 0,
    };

    return (
        <div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
            <SectionCards data={cardData} />
            {/* More components */}
        </div>
    );
}

Trend Charts

The dashboard includes an interactive area chart showing incident trends over the last 90 days.

Chart Features

  • Dual Series: Created vs. Resolved incidents
  • Interactive Tooltips: Hover to see daily details
  • Responsive Design: Adapts to screen size
  • Date Range: Last 90 days of data
  • Color Coding: Visual distinction between series

Implementation

page.tsx
// Fetch incident trend data (grouped by day, last 90 days)
const ninetyDaysAgo = new Date();
ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);

const { data: trendRaw } = await supabase
    .from("incidents")
    .select("created_at, status")
    .gte("created_at", ninetyDaysAgo.toISOString())
    .order("created_at", { ascending: true });

// Group by date
const trendMap = new Map<string, { created: number; resolved: number }>();
(trendRaw || []).forEach((inc: any) => {
    const date = inc.created_at?.split("T")[0];
    if (!date) return;
    if (!trendMap.has(date)) {
        trendMap.set(date, { created: 0, resolved: 0 });
    }
    const entry = trendMap.get(date)!;
    entry.created++;
    if (inc.status === "resuelta") {
        entry.resolved++;
    }
});

const chartData = Array.from(trendMap.entries()).map(([date, val]) => ({
    date,
    created: val.created,
    resolved: val.resolved,
}));
The chart component (ChartAreaInteractive) uses Recharts library:
components/chart-area-interactive.tsx
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
import {
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
} from "@/components/ui/chart";

export function ChartAreaInteractive({ data }) {
  return (
    <ChartContainer config={chartConfig}>
      <AreaChart data={data}>
        <CartesianGrid vertical={false} />
        <XAxis
          dataKey="date"
          tickLine={false}
          axisLine={false}
          tickMargin={8}
          tickFormatter={(value) => {
            const date = new Date(value);
            return date.toLocaleDateString("en-US", {
              month: "short",
              day: "numeric",
            });
          }}
        />
        <YAxis tickLine={false} axisLine={false} />
        <ChartTooltip
          cursor={false}
          content={<ChartTooltipContent indicator="dot" />}
        />
        <Area
          dataKey="created"
          type="monotone"
          fill="hsl(var(--chart-1))"
          fillOpacity={0.4}
          stroke="hsl(var(--chart-1))"
        />
        <Area
          dataKey="resolved"
          type="monotone"
          fill="hsl(var(--chart-2))"
          fillOpacity={0.4}
          stroke="hsl(var(--chart-2))"
        />
      </AreaChart>
    </ChartContainer>
  );
}

Data Tables

The dashboard includes a comprehensive data table powered by TanStack Table showing all incidents.

Table Features

Sorting

Click column headers to sort data

Filtering

Filter by status, priority, area, or room

Pagination

Navigate through large datasets

Column Visibility

Show/hide columns as needed

Data Structure

The table displays these columns:
ColumnDescriptionSource
IDSequential row numberGenerated
TitleIncident titleincidents.title
StatusCurrent status with badgeincidents.status
PriorityPriority level with colorincidents.priority
AreaDepartment/categoryareas.name (join)
RoomRoom coderooms.room_code (join)
Assigned ToStaff member nameprofiles.full_name (join)
CreatedTimestampincidents.created_at

Implementation

page.tsx
const { data: incidents } = await supabase
    .from("incidents")
    .select(`
        id,
        title,
        description,
        status,
        priority,
        created_at,
        updated_at,
        area:areas(name),
        room:rooms(room_code, floor),
        assignee:profiles!incidents_assigned_to_fkey(full_name, email)
    `)
    .order("created_at", { ascending: false });

// Transform incidents data for the table
const tableData = (incidents || []).map((incident: any, index: number) => ({
    id: index + 1,
    uuid: incident.id,
    title: incident.title || "Sin título",
    status: incident.status || "pendiente",
    priority: incident.priority || "media",
    area: incident.area?.name || "Sin área",
    room: incident.room?.room_code || "Sin habitación",
    assigned_to: incident.assignee?.full_name || "Sin asignar",
    created_at: incident.created_at || "",
}));

return <DataTable data={tableData} />;

Advanced Analytics

The app/(dashboard)/dashboard/analytics route provides deeper insights:

By Department

  • Incident volume per area
  • Average resolution time by department
  • Most common issue types
  • Staff workload distribution

By Time Period

  • Daily, weekly, monthly trends
  • Peak incident hours
  • Seasonal patterns
  • Year-over-year comparisons

By Priority

  • Distribution of priority levels
  • Response time by priority
  • Escalation rate
  • SLA compliance

By Room/Location

  • Incidents by floor
  • Problem rooms identification
  • Geographic heat maps
  • Maintenance needs forecasting

Performance Metrics

Key performance indicators for operational efficiency:

Response Time

-- Average time from creation to first response
SELECT 
  AVG(EXTRACT(EPOCH FROM (updated_at - created_at))) / 60 AS avg_response_minutes
FROM incidents
WHERE status != 'pendiente';

Resolution Time

-- Average time from creation to resolution
SELECT 
  AVG(EXTRACT(EPOCH FROM (updated_at - created_at))) / 3600 AS avg_resolution_hours
FROM incidents
WHERE status = 'resuelta';

Completion Rate

-- Percentage of incidents resolved
SELECT 
  COUNT(CASE WHEN status = 'resuelta' THEN 1 END) * 100.0 / COUNT(*) AS completion_rate
FROM incidents;

Export and Reporting

CSV Export

Export incident data for external analysis:
function exportToCSV(data: any[]) {
  const headers = Object.keys(data[0]);
  const csv = [
    headers.join(','),
    ...data.map(row => 
      headers.map(header => 
        JSON.stringify(row[header] ?? '')
      ).join(',')
    )
  ].join('\n');

  const blob = new Blob([csv], { type: 'text/csv' });
  const url = URL.createObjectURL(blob);
  const link = document.createElement('a');
  link.href = url;
  link.download = `incidents-${new Date().toISOString()}.csv`;
  link.click();
}

PDF Reports

Generate formatted PDF reports with charts and tables using libraries like react-pdf or server-side rendering.

Real-Time Updates

Future enhancement: Subscribe to Supabase Realtime for live dashboard updates without page refresh.
// Example real-time subscription
const supabase = createClient();

supabase
  .channel('incidents')
  .on(
    'postgres_changes',
    { event: '*', schema: 'public', table: 'incidents' },
    (payload) => {
      console.log('Change received!', payload);
      // Update dashboard metrics
    }
  )
  .subscribe();

Customization

Administrators can customize analytics:
  • Date Ranges: Select custom time periods
  • Filters: Focus on specific areas, priorities, or statuses
  • Saved Views: Create and save custom dashboard configurations
  • Scheduled Reports: Automate weekly/monthly report generation

Next Steps

Incident Management

Learn about incident management features

User Management

Manage employee accounts and permissions

Database Schema

Explore the underlying data structure

Deployment

Deploy your analytics dashboard

Build docs developers (and LLMs) love