Skip to main content
The Dashboard page showcases Tambo360’s production metrics and alerts. It consists of reusable components that display KPIs, production logs, and TamboEngine alerts.

Dashboard Overview

The Dashboard provides a comprehensive view of farm operations:

Production Metrics

Monthly cheese, milk, waste, and cost statistics

Historical Comparison

Month-over-month trend analysis

Daily Production Log

Recent batch production entries

TamboEngine Alerts

AI-generated production alerts

StatCard Component

Displays key performance indicators with trend comparisons.

File Location

apps/frontend/src/components/shared/StatCard.tsx

Props Interface

interface StatCardProps {
  title: string                                    // KPI name
  value: string                                    // Main value to display
  unit: string                                     // Unit of measurement (Kg, L, %, $)
  trend?: {                                        // Optional trend data
    value: number                                  // Percentage change
    isPositive: boolean                            // Direction of change
  }
  description?: string                             // Context (e.g., "vs Octubre")
  icon?: React.ReactNode                           // Optional icon
  isPending?: boolean                              // Loading state
}

Implementation

StatCard.tsx
import { AlertCircle } from 'lucide-react'
import { Card, CardContent } from '../common/card'
import { Skeleton } from '@/src/components/common/skeleton'

export const StatCard = ({
  title,
  value,
  trend,
  description,
  icon,
  unit,
  isPending,
}: StatCardProps) => {
  return (
    <Card className="border-slate-200 shadow-sm bg-white rounded-xl">
      <CardContent className="p-5">
        <div className="flex flex-col gap-4">
          {icon}

          {/* Title */}
          <span className="text-[12px] font-bold text-slate-500 uppercase tracking-wider">
            {title}
          </span>

          <div className="flex flex-col gap-1">
            {/* Main Value */}
            {isPending ? (
              <Skeleton className="h-8 w-[50%]" />
            ) : unit === '$ ' ? (
              <span className="text-3xl font-bold text-slate-900 tracking-tight">
                {unit} {Number(value).toLocaleString('es-AR')}
              </span>
            ) : (
              <span className="text-3xl font-bold text-slate-900 tracking-tight">
                {Number(value).toLocaleString('es-AR')} {unit}
              </span>
            )}

            {/* Trend Indicator */}
            <div className="flex items-center gap-1.5 mt-1">
              {trend ? (
                <div className="flex items-center gap-1 text-[13px] font-bold text-slate-600">
                  <span>{trend.isPositive ? '↑' : '↓'}</span>
                  <span>{trend.value?.toString().split('.')[0]}% </span>
                  <span className="text-[12px] text-slate-400 font-medium">
                    {description}
                  </span>
                </div>
              ) : (
                <span className="flex items-center gap-1 text-[12px] text-slate-400 font-medium">
                  <AlertCircle className="h-4 w-4" />
                  Sin datos suficientes
                </span>
              )}
            </div>
          </div>
        </div>
      </CardContent>
    </Card>
  )
}

Usage Example

Dashboard.tsx
import { StatCard } from '../components/shared/StatCard'
import { useCurrentMonth } from '@/src/hooks/dashboard/useCurrentMonth'

const Dashboard = () => {
  const { data, isPending } = useCurrentMonth()

  return (
    <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-4">
      <StatCard
        title="Queso Producido"
        value={data?.data.actual.quesos}
        unit=" Kg"
        trend={
          data?.data.variaciones.quesos !== null
            ? {
                value: data?.data.variaciones.quesos,
                isPositive: data?.data.variaciones.quesos >= 0,
              }
            : undefined
        }
        description={'vs ' + data?.data.mesPrevio}
        isPending={isPending}
      />

      <StatCard
        title="Leche Vendida"
        value={data?.data.actual.leches}
        unit="L"
        trend={
          data?.data.variaciones.leches !== null
            ? {
                value: data?.data.variaciones.leches,
                isPositive: data?.data.variaciones.leches >= 0,
              }
            : undefined
        }
        description={'vs ' + data?.data.mesPrevio}
        isPending={isPending}
      />

      <StatCard
        title="Mermas Totales"
        value={porcentajeMermas.toFixed(1)}
        unit="%"
        trend={
          data?.data.variaciones.mermas !== null
            ? {
                value: data?.data.variaciones.mermas,
                isPositive: data?.data.variaciones.mermas <= 0, // Negative is good
              }
            : undefined
        }
        description={`vs ${data?.data.mesPrevio}`}
        isPending={isPending}
      />

      <StatCard
        title="Costos totales"
        value={data?.data.actual.costos}
        unit="$ "
        trend={
          data?.data.variaciones.costos !== null
            ? {
                value: data?.data.variaciones.costos,
                isPositive: data?.data.variaciones.costos >= 0,
              }
            : undefined
        }
        description={'vs ' + data?.data.mesPrevio}
        isPending={isPending}
      />
    </div>
  )
}
Location: apps/frontend/src/pages/Dashboard.tsx:36

Features

  • Loading Skeletons: Shows skeleton UI while data is loading
  • Number Formatting: Uses toLocaleString('es-AR') for Spanish formatting
  • Currency Support: Special handling for currency units ($ prefix)
  • Trend Arrows: Visual up/down indicators
  • No Data State: Shows alert icon when trend data is unavailable

AlertCard Component

Displays TamboEngine alerts with action buttons.

File Location

apps/frontend/src/components/shared/dashboard/AlertCard.tsx

Props Interface

import { Alert } from '@/src/types/alerts'

interface AlertCardProps {
  alert: Alert  // Alert object from API
}

Alert Type Definition

alerts.ts
export interface Alert {
  id: string
  producto: string           // Product name (e.g., "Queso")
  categoria: string          // Category (e.g., "Dulce de leche")
  descripcion: string        // Alert description
  creado_en: string          // ISO date string
  idLote: string             // Batch ID
  visto: boolean             // Whether alert has been viewed
}

Implementation

AlertCard.tsx
import { Button } from '@/src/components/common/Button'
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from '@/src/components/common/card'
import { Alert } from '@/src/types/alerts'
import { History, Package } from 'lucide-react'
import { Link } from 'react-router-dom'

interface AlertCardProps {
  alert: Alert
}

const AlertCard = ({ alert }: AlertCardProps) => {
  return (
    <Card className="bg-alert-bg mx-4 rounded-lg gap-4">
      <CardHeader className="space-y-1">
        <CardTitle className="font-bold text-[16px]">
          {alert.producto} {alert.categoria}
        </CardTitle>
        <CardDescription className="flex justify-between items-center">
          <span className="flex gap-2 text-xs">
            <History className="size-3" />
            {new Date(alert.creado_en)
              .toLocaleString('es-ES', {
                month: 'long',
                year: 'numeric',
              })
              .replace(/^\w/, (c) => c.toUpperCase())}
          </span>

          <span className="flex gap-2 text-xs">
            <Package className="size-3" /> L-003, L006
          </span>
        </CardDescription>
      </CardHeader>

      <CardContent className="space-y-4 mx-4 p-1">
        <p>{alert.descripcion}</p>
      </CardContent>

      <CardFooter className="justify-between gap-2 w-full">
        <Button className="flex-1" asChild>
          <Link to={`/tambo-engine/#${alert.id}`}>Ver alerta</Link>
        </Button>
        <Button variant="outline" className="flex-1" asChild>
          <Link to={`/produccion/lote/${alert.idLote}`}>Ver lote</Link>
        </Button>
      </CardFooter>
    </Card>
  )
}

export default AlertCard

Features

  • Date Formatting: Displays month and year in Spanish
  • Quick Actions: Direct links to alert details and batch details
  • Icon Integration: Uses Lucide icons for visual context
  • Responsive Layout: Buttons stack on mobile, side-by-side on desktop

AlertsSection Component

Container for multiple AlertCards displayed in the Dashboard sidebar.

File Location

apps/frontend/src/components/shared/dashboard/AlertsSection.tsx

Usage

Dashboard.tsx
import AlertsSection from '@/src/components/shared/dashboard/AlertsSection'

<aside className="w-full lg:w-80 shrink-0">
  <AlertsSection />
</aside>

Card Components (Base)

All dashboard components use base card components:

File Location

apps/frontend/src/components/common/card.tsx

Available Components

import {
  Card,           // Container
  CardHeader,     // Top section
  CardTitle,      // Title text
  CardDescription,// Subtitle text
  CardContent,    // Main content
  CardFooter,     // Bottom section (buttons, etc.)
  CardAction,     // Top-right action area
} from '@/src/components/common/card'

Basic Structure

<Card>
  <CardHeader>
    <CardTitle>Title</CardTitle>
    <CardDescription>Subtitle</CardDescription>
  </CardHeader>
  <CardContent>
    Main content goes here
  </CardContent>
  <CardFooter>
    <Button>Action</Button>
  </CardFooter>
</Card>

Skeleton Component

Shows loading placeholders for better perceived performance.

File Location

apps/frontend/src/components/common/skeleton.tsx

Usage

import { Skeleton } from '@/src/components/common/skeleton'

{isPending ? (
  <Skeleton className="h-8 w-[50%]" />
) : (
  <span>{value}</span>
)}

Dashboard Data Hooks

useCurrentMonth

Fetches current month statistics with previous month comparison.
import { useCurrentMonth } from '@/src/hooks/dashboard/useCurrentMonth'

const { data, isPending } = useCurrentMonth()

// Data structure:
interface CurrentMonthData {
  actual: {
    quesos: number    // Cheese produced (kg)
    leches: number    // Milk sold (L)
    mermas: number    // Waste (kg/L)
    costos: number    // Total costs ($)
  }
  variaciones: {
    quesos: number | null    // % change from previous month
    leches: number | null
    mermas: number | null
    costos: number | null
  }
  mesPrevio: string   // Previous month name (e.g., "Octubre")
}
Location: apps/frontend/src/hooks/dashboard/useCurrentMonth.ts

useGraph

Fetches historical data for charts and graphs. Location: apps/frontend/src/hooks/dashboard/useGraph.ts

Dashboard Layout

The Dashboard uses a responsive grid layout:
Dashboard.tsx
const Dashboard = () => {
  return (
    <div className="space-y-6 w-full max-w-full overflow-x-hidden">
      {/* Header */}
      <div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b pb-6">
        <div>
          <p className="text-muted-foreground text-xs sm:text-sm">
            Dashboard / {user.establecimientos[0].nombre}
          </p>
          <h1 className="text-2xl sm:text-3xl font-bold text-[#252525] tracking-tight">
            Reporte Mensual
          </h1>
        </div>
      </div>

      {/* Stats Grid - 4 columns */}
      <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-4">
        <StatCard {...props} />
      </div>

      {/* Main Content - Two Columns */}
      <div className="flex flex-col lg:flex-row gap-6 w-full">
        {/* Left Column */}
        <div className="flex-1 flex flex-col gap-6 min-w-0">
          <ComparacionHistorica />
          <DailyProductionLog />
        </div>

        {/* Right Column - Alerts Sidebar */}
        <aside className="w-full lg:w-80 shrink-0">
          <AlertsSection />
        </aside>
      </div>
    </div>
  )
}
Location: apps/frontend/src/pages/Dashboard.tsx

Responsive Breakpoints

  • Mobile (< 640px): Single column, stacked cards
  • Tablet (640px - 1023px): 2-column stat grid
  • Desktop (1024px+): 4-column stat grid with sidebar

Styling Conventions

Color Palette

/* Primary brand colors */
text-[#669213]    /* Green - active state */
bg-[#D7ECAF]      /* Light green - active background */

/* Neutral colors */
text-slate-900    /* Primary text */
text-slate-600    /* Secondary text */
text-slate-400    /* Muted text */
bg-white          /* Card background */
bg-[#F8FAFC]      /* Page background */

/* Semantic colors */
text-[#B91C1C]    /* Error red */
bg-[#FCE8E5]      /* Error background */
bg-alert-bg       /* Alert card background */

Typography

/* Stat values */
text-3xl font-bold tracking-tight

/* Card titles */
text-[16px] font-bold

/* Labels */
text-[12px] font-bold uppercase tracking-wider

/* Body text */
text-sm font-medium

Best Practices

Always show loading states

Use Skeleton components while data is fetching

Handle missing data gracefully

Show “Sin datos suficientes” when trend data is unavailable

Format numbers consistently

Use toLocaleString('es-AR') for Spanish number formatting

Make cards responsive

Test all components at mobile, tablet, and desktop sizes

React Query Hooks

Learn about data fetching with TanStack Query

Layout System

Understand how Dashboard fits into the layout

Card Components

Reusable Card primitives

TypeScript Types

Type definitions for dashboard data

Build docs developers (and LLMs) love