+
+
+
+
+
+
+
+
+
+
+
+ date.toLocaleString('ru-RU', { weekday: 'short' }).replace('.', '').toUpperCase(),
+ }}
+ classNames={{
+ month_caption: 'hidden',
+ nav: 'hidden',
+ weekdays: 'flex gap-1',
+ weekday:
+ 'flex-1 flex items-center justify-center h-6 text-[0.65rem] font-medium text-muted-foreground',
+ week: 'flex gap-1 mt-1',
+ day: 'flex-1 aspect-square p-0',
+ day_button: cn(
+ 'bg-muted/50 hover:bg-muted rounded-md',
+ 'data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[selected-single=true]:hover:bg-primary data-[selected-single=true]:hover:text-primary-foreground!',
+ ),
+ outside: 'opacity-60',
+ disabled: 'opacity-60',
+ today: cn('bg-accent text-foreground rounded-md'),
+ }}
+ components={{
+ Weekday: ({ children, className: cls, ...props }: ComponentPropsWithoutRef<'th'>) => {
+ const isToday = children === TODAY_WEEKDAY
+ return (
+
+ {children}
+ |
+ )
+ },
+ DayButton: ({
+ children,
+ modifiers,
+ day,
+ ...props
+ }: React.ComponentProps) => {
+ const dateKey = format(day.date, 'yyyy-MM-dd')
+ const hasEvents = !modifiers.outside && datesWithEvents.has(dateKey)
+
+ return (
+
+ {hasEvents ? (
+
+ ) : (
+
+ )}
+ {children}
+
+ )
+ },
+ }}
+ />
+
+ )
+}
diff --git a/apps/web/src/components/schedule/schedule-jobs-card.tsx b/apps/web/src/components/schedule/schedule-jobs-card.tsx
new file mode 100644
index 0000000..ea6f664
--- /dev/null
+++ b/apps/web/src/components/schedule/schedule-jobs-card.tsx
@@ -0,0 +1,77 @@
+import { useMemo, useState } from 'react'
+
+import { DataGridCard } from '@/components/data-grid-shell'
+import { QueryState } from '@/components/query-state'
+import { TableSkeleton } from '@/components/skeletons'
+import { Tabs, TabsList, TabsTrigger } from '@evobgp/ui/components/tabs'
+import type { JobRow } from '@/types/api'
+
+import { ScheduleJobsGrid } from './schedule-jobs-grid'
+
+type JobTab = 'all' | 'refresh' | 'failed'
+
+function filterJobs(items: JobRow[], tab: JobTab): JobRow[] {
+ if (tab === 'refresh') return items.filter((j) => j.kind === 'module_refresh')
+ if (tab === 'failed')
+ return items.filter((j) => ['failed', 'error', 'cancelled'].includes(j.status.toLowerCase()))
+ return items
+}
+
+function tabCounts(items: JobRow[]) {
+ return {
+ all: items.length,
+ refresh: items.filter((j) => j.kind === 'module_refresh').length,
+ failed: items.filter((j) => ['failed', 'error', 'cancelled'].includes(j.status.toLowerCase()))
+ .length,
+ }
+}
+
+/** Jobs data-grid with status tabs (data-grid-filtering pattern). */
+export function ScheduleJobsCard({
+ jobs,
+ isLoading,
+ isError,
+ error,
+ onRetry,
+}: {
+ jobs: JobRow[]
+ isLoading: boolean
+ isError: boolean
+ error: unknown
+ onRetry: () => void
+}) {
+ const [tab, setTab] = useState