Simulation Calendar

A calendar component that displays simulations as dots on their start dates

Live Example

Simulation Calendar

Click on dates with blue dots to see simulations for that day.

April 2026

Filtered Calendar Example

Basic Filters

Select project, template, and date to filter simulations

With Simulation Selector

All four filters including individual simulation selection

Prerequisites

This component requires sgerp-frontend-lib to be installed in your project. See the Installation guide for setup instructions.

Dependencies

This component will automatically install:

  • react-day-picker - Calendar component
  • date-fns - Date utilities
  • lucide-react - For icons
  • shadcn/ui components: button

Usage

import { SimulationCalendar } from "@/components/ui/sgerp-simulation-calendar"
import { useState } from "react"

export function MyComponent() {
  const [selectedDate, setSelectedDate] = useState<Date>()
  const simulations = [] // Your simulations array

  return (
    <SimulationCalendar
      simulations={simulations}
      selected={selectedDate}
      onSelect={setSelectedDate}
    />
  )
}

Props

PropTypeDescription
simulationsSimulation[]Array of SGERP simulations to display on the calendar
selectedDateOptional. Currently selected date
onSelect(date: Date | undefined) => voidOptional. Callback when date is selected
monthDateOptional. Currently displayed month
onMonthChange(month: Date) => voidOptional. Callback when month changes
showOutsideDaysbooleanOptional. Show days from adjacent months. Default: true
classNamestringOptional. Additional CSS classes
localeLocaleOptional. date-fns locale for internationalization
projectIdnumber | nullOptional. Project ID for filtering simulations
templateIdsnumber[]Optional. Array of template IDs for filtering simulations

Features

  • Visual Indicators: Blue dots appear on dates with simulations
  • Multiple Simulations: Handles multiple simulations per day
  • Date Selection: Click dates to select them
  • Month Navigation: Arrow buttons to navigate months
  • Efficient Rendering: Uses memoization for performance
  • ISO 8601 Support: Parses simulation start_time in ISO format
  • Responsive: Works on all screen sizes
  • Accessible: Built on react-day-picker with ARIA support

Examples

Basic Usage with SGERP Hook

import { SimulationCalendar } from "@/components/ui/sgerp-simulation-calendar"
import { useSGERP } from "sgerp-frontend-lib"
import { useState, useEffect } from "react"

export function MyCalendar() {
  const api = useSGERP()
  const simulations = api?.collections.simulation
  const [selectedDate, setSelectedDate] = useState<Date>()

  useEffect(() => {
    // Fetch simulations on mount
    simulations?.fetch({ limit: 100 })
  }, [])

  return (
    <SimulationCalendar
      simulations={simulations?.models || []}
      selected={selectedDate}
      onSelect={setSelectedDate}
    />
  )
}

With Selected Date Details

import { SimulationCalendar } from "@/components/ui/sgerp-simulation-calendar"
import { useSGERP } from "sgerp-frontend-lib"
import { useState, useMemo } from "react"
import { format } from "date-fns"

export function CalendarWithDetails() {
  const api = useSGERP()
  const simulations = api?.collections.simulation
  const [selectedDate, setSelectedDate] = useState<Date>()

  // Filter simulations for selected date
  const selectedSimulations = useMemo(() => {
    if (!selectedDate || !simulations?.models) return []

    const dateStr = format(selectedDate, "yyyy-MM-dd")
    return simulations.models.filter(sim =>
      sim.start_time?.startsWith(dateStr)
    )
  }, [selectedDate, simulations?.models])

  return (
    <div className="grid gap-4 md:grid-cols-2">
      <SimulationCalendar
        simulations={simulations?.models || []}
        selected={selectedDate}
        onSelect={setSelectedDate}
      />

      <div>
        <h3>Simulations on {selectedDate ? format(selectedDate, "PP") : "..."}</h3>
        {selectedSimulations.map(sim => (
          <div key={sim.id}>
            {sim.name} - {sim.state}
          </div>
        ))}
      </div>
    </div>
  )
}

Custom Styling

<SimulationCalendar
  simulations={simulations}
  selected={selectedDate}
  onSelect={setSelectedDate}
  className="border rounded-lg shadow-lg"
/>

Filtered by Project

import { SimulationCalendar } from "@/components/ui/sgerp-simulation-calendar"
import { useSGERP } from "sgerp-frontend-lib"
import { useState, useEffect } from "react"

export function ProjectSimulationCalendar({ projectId }: { projectId: number }) {
  const api = useSGERP()
  const simulations = api?.collections.simulation
  const [selectedDate, setSelectedDate] = useState<Date>()

  useEffect(() => {
    // Fetch only simulations for this project
    simulations?.fetch({
      limit: 100,
      project_id: projectId
    })
  }, [projectId])

  return (
    <SimulationCalendar
      simulations={simulations?.models || []}
      selected={selectedDate}
      onSelect={setSelectedDate}
    />
  )
}

Controlled Month Navigation

import { SimulationCalendar } from "@/components/ui/sgerp-simulation-calendar"
import { useState } from "react"
import { addMonths, subMonths } from "date-fns"

export function ControlledCalendar() {
  const [month, setMonth] = useState(new Date())
  const simulations = [] // Your simulations

  return (
    <div>
      <div className="flex gap-2 mb-4">
        <button onClick={() => setMonth(subMonths(month, 1))}>
          Previous Month
        </button>
        <button onClick={() => setMonth(addMonths(month, 1))}>
          Next Month
        </button>
      </div>

      <SimulationCalendar
        simulations={simulations}
        month={month}
        onMonthChange={setMonth}
      />
    </div>
  )
}

With Project and Template Filters (Dropdown Style)

This example shows a horizontal layout with dependent selectors (project → template) and a calendar dropdown:

import { SimulationCalendar } from "@/components/ui/sgerp-simulation-calendar"
import { SGERPAutocomplete } from "@/components/ui/sgerp-autocomplete"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { Button } from "@/components/ui/button"
import { useSGERP } from "sgerp-frontend-lib"
import { useState, useEffect } from "react"
import { format, startOfMonth, endOfMonth } from "date-fns"
import { formatInTimeZone } from "date-fns-tz"
import { Calendar as CalendarIcon } from "lucide-react"
import type { Project } from "sgerp-frontend-lib"

export function FilteredCalendar() {
  const api = useSGERP()
  const simulations = api?.collections.simulation
  const [projectId, setProjectId] = useState<number | null>(null)
  const [project, setProject] = useState<Project | null>(null)
  const [templateId, setTemplateId] = useState<number | null>(null)
  const [selectedDate, setSelectedDate] = useState<Date>()
  const [currentMonth, setCurrentMonth] = useState<Date>(new Date())
  const [open, setOpen] = useState(false)

  const fetchSimulationsForMonth = async (month: Date) => {
    if (!simulations || !projectId || !project) return

    // Format dates in the project's timezone
    const timezone = project.timezone || "UTC"
    const startDate = formatInTimeZone(startOfMonth(month), timezone, "yyyy-MM-dd")
    const endDate = formatInTimeZone(endOfMonth(month), timezone, "yyyy-MM-dd")

    const filters: Record<string, any> = {
      limit: 1000,
      start_time__gte: startDate + "T00:00:00",
      start_time__lte: endDate + "T23:59:59",
      project_id: projectId,
    }

    if (templateId) {
      filters.template_id__in = templateId
    }

    await simulations.fetch(filters)
  }

  const handleProjectChange = (newProjectId: number | null, projectObject?: Project) => {
    setProjectId(newProjectId)
    setProject(projectObject || null)
    setTemplateId(null)
  }

  useEffect(() => {
    if (projectId && project) {
      fetchSimulationsForMonth(currentMonth)
    }
  }, [projectId, project, templateId])

  return (
    <div className="flex gap-3">
      {/* Project Selector */}
      <SGERPAutocomplete
        collectionName="project"
        onSelectionChange={handleProjectChange}
        placeholder="Select project..."
      />

      {/* Template Selector */}
      <SGERPAutocomplete
        collectionName="simulation"
        onSelectionChange={setTemplateId}
        placeholder="Select template..."
        disabled={!projectId}
        filters={projectId ? { project_id: projectId, simulation_mode: 'template' } : {}}
      />

      {/* Calendar Dropdown */}
      <Popover open={open} onOpenChange={setOpen}>
        <PopoverTrigger asChild>
          <Button variant="outline" className="w-full justify-start text-left font-normal h-[46px]" disabled={!projectId}>
            <CalendarIcon className="mr-2 h-4 w-4" />
            {selectedDate ? format(selectedDate, "MMM d, yyyy") : "Select date..."}
          </Button>
        </PopoverTrigger>
        <PopoverContent className="w-auto p-0" align="start">
          <SimulationCalendar
            simulations={simulations?.models || []}
            selected={selectedDate}
            onSelect={(date) => {
              setSelectedDate(date)
              setOpen(false)
            }}
            month={currentMonth}
            onMonthChange={(month) => {
              setCurrentMonth(month)
              fetchSimulationsForMonth(month)
            }}
            projectId={projectId}
            templateIds={templateId ? [templateId] : []}
          />
        </PopoverContent>
      </Popover>
    </div>
  )
}

Simulation Date Logic

The calendar uses the start_time field from the Simulation type to determine which dates should have dots:

  1. Parses start_time as an ISO 8601 date string
  2. Extracts the date portion (ignoring time)
  3. Groups simulations by date for efficient lookup
  4. Displays a blue dot on dates with one or more simulations

Example simulation object:

{
  id: 123,
  name: "Morning Rush Simulation",
  start_time: "2025-10-15T08:00:00Z",  // ← Used for calendar dots
  end_time: "2025-10-15T20:00:00Z",
  state: "completed",
  // ... other fields
}

Styling

The calendar uses Tailwind CSS and inherits styles from your theme. Key style points:

  • Dots: Blue (bg-blue-500), 1.5x1.5 rounded circles
  • Selected Date: Primary color background
  • Today: Accent color background
  • Navigation Buttons: Outline variant with hover effects

You can customize by:

  • Passing className prop for container styles
  • Modifying the component's internal classNames
  • Using CSS to override specific elements

API Integration

The component works with the SGERP Collections API:

// Fetch simulations
await api.collections.simulation.fetch({ limit: 100 })

// Pass to calendar
<SimulationCalendar simulations={api.collections.simulation.models} />

The Simulation type is imported from sgerp-frontend-lib:

import type { Simulation } from "sgerp-frontend-lib"

Performance

The component is optimized for performance:

  • Memoized date map: Simulations are indexed by date for O(1) lookups
  • Memoized callbacks: Prevent unnecessary re-renders
  • Efficient re-renders: Only updates when simulations array changes

For best performance with large datasets:

  • Limit the number of simulations fetched (use filters)
  • Use pagination to load simulations as needed
  • Consider filtering by date range on the backend

Accessibility

Built on react-day-picker, the component includes:

  • ARIA labels and roles
  • Keyboard navigation (arrow keys, Enter, Tab)
  • Screen reader support
  • Focus management

Localization

Use the locale prop with date-fns locales:

import { ja } from "date-fns/locale"

<SimulationCalendar
  simulations={simulations}
  locale={ja}
/>

This affects:

  • Month and day names
  • Week start day
  • Date formatting