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.
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
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 componentdate-fns- Date utilitieslucide-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
| Prop | Type | Description |
|---|---|---|
simulations | Simulation[] | Array of SGERP simulations to display on the calendar |
selected | Date | Optional. Currently selected date |
onSelect | (date: Date | undefined) => void | Optional. Callback when date is selected |
month | Date | Optional. Currently displayed month |
onMonthChange | (month: Date) => void | Optional. Callback when month changes |
showOutsideDays | boolean | Optional. Show days from adjacent months. Default: true |
className | string | Optional. Additional CSS classes |
locale | Locale | Optional. date-fns locale for internationalization |
projectId | number | null | Optional. Project ID for filtering simulations |
templateIds | number[] | 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:
- Parses
start_timeas an ISO 8601 date string - Extracts the date portion (ignoring time)
- Groups simulations by date for efficient lookup
- 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
classNameprop 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