task ep1.6.1
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -95,16 +96,24 @@ export function CalendarWorkspace({ filters }: CalendarWorkspaceProps) {
|
||||
<AgendaView groupedEvents={groupedEvents} />
|
||||
</TabsContent>
|
||||
<TabsContent value="day">
|
||||
<CompactCalendarGrid events={events.slice(0, 12)} columns={1} />
|
||||
<CompactCalendarGrid
|
||||
events={events}
|
||||
view="day"
|
||||
anchorDate={filters.startFrom}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="week">
|
||||
<CompactCalendarGrid events={events.slice(0, 28)} columns={7} />
|
||||
<CompactCalendarGrid
|
||||
events={events}
|
||||
view="week"
|
||||
anchorDate={filters.startFrom}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="month">
|
||||
<CompactCalendarGrid
|
||||
events={events.slice(0, 42)}
|
||||
columns={7}
|
||||
month
|
||||
events={events}
|
||||
view="month"
|
||||
anchorDate={filters.startFrom}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
@@ -191,43 +200,153 @@ function AgendaView({
|
||||
|
||||
function CompactCalendarGrid({
|
||||
events,
|
||||
columns,
|
||||
month,
|
||||
view,
|
||||
anchorDate,
|
||||
}: {
|
||||
events: BigCalendarEvent[];
|
||||
columns: number;
|
||||
month?: boolean;
|
||||
view: "day" | "week" | "month";
|
||||
anchorDate: string;
|
||||
}) {
|
||||
if (!events.length) {
|
||||
return <CalendarEmptyState />;
|
||||
}
|
||||
const cells = buildCalendarGridCells({ events, view, anchorDate });
|
||||
const isMonth = view === "month";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"grid gap-3",
|
||||
columns === 1 && "grid-cols-1",
|
||||
columns === 7 && "grid-cols-1 md:grid-cols-7",
|
||||
)}
|
||||
>
|
||||
{events.map((event) => (
|
||||
<div
|
||||
key={event.id}
|
||||
className={cn(
|
||||
"border-border/70 bg-muted/20 min-h-28 rounded-lg border p-2",
|
||||
month && "min-h-36",
|
||||
)}
|
||||
>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{formatDate(event.start)}
|
||||
</p>
|
||||
<CalendarEventPill event={event} />
|
||||
</div>
|
||||
))}
|
||||
<div className="relative">
|
||||
<div
|
||||
className={cn(
|
||||
"grid gap-3",
|
||||
view === "day" && "grid-cols-1",
|
||||
view !== "day" && "grid-cols-1 md:grid-cols-7",
|
||||
)}
|
||||
>
|
||||
{cells.map((cell) => (
|
||||
<button
|
||||
key={cell.id}
|
||||
type="button"
|
||||
aria-label={`Select calendar slot ${cell.label}`}
|
||||
className={cn(
|
||||
"border-border/70 bg-muted/20 hover:bg-muted/40 min-h-28 rounded-lg border p-2 text-left transition-colors",
|
||||
isMonth && "min-h-36",
|
||||
cell.isToday && "ring-primary/30 ring-2",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-muted-foreground text-xs">{cell.label}</p>
|
||||
{cell.subLabel ? (
|
||||
<span className="text-muted-foreground text-[11px]">
|
||||
{cell.subLabel}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-2 space-y-2">
|
||||
{cell.events.map((event) => (
|
||||
<CalendarEventPill key={event.id} event={event} />
|
||||
))}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{!events.length ? <CalendarEmptyOverlay view={view} /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface CalendarGridCell {
|
||||
id: string;
|
||||
label: string;
|
||||
subLabel: string | null;
|
||||
isToday: boolean;
|
||||
events: BigCalendarEvent[];
|
||||
}
|
||||
|
||||
function buildCalendarGridCells({
|
||||
events,
|
||||
view,
|
||||
anchorDate,
|
||||
}: {
|
||||
events: BigCalendarEvent[];
|
||||
view: "day" | "week" | "month";
|
||||
anchorDate: string;
|
||||
}): CalendarGridCell[] {
|
||||
if (view === "day") {
|
||||
const day = startOfUtcDay(anchorDate);
|
||||
return Array.from({ length: 12 }, (_, index) => {
|
||||
const slot = new Date(day);
|
||||
slot.setUTCHours(8 + index, 0, 0, 0);
|
||||
return {
|
||||
id: `day-${slot.toISOString()}`,
|
||||
label: formatTime(slot.toISOString()),
|
||||
subLabel: index === 0 ? formatDate(slot.toISOString()) : null,
|
||||
isToday: isSameUtcDate(slot.toISOString(), new Date().toISOString()),
|
||||
events: events.filter((event) => isSameHour(event.start, slot.toISOString())),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const start = view === "week" ? startOfUtcDay(anchorDate) : startOfMonthGrid(anchorDate);
|
||||
const length = view === "week" ? 7 : 42;
|
||||
|
||||
return Array.from({ length }, (_, index) => {
|
||||
const date = new Date(start);
|
||||
date.setUTCDate(start.getUTCDate() + index);
|
||||
const iso = date.toISOString();
|
||||
return {
|
||||
id: `${view}-${iso}`,
|
||||
label: formatDate(iso),
|
||||
subLabel: view === "month" ? null : "Working day",
|
||||
isToday: isSameUtcDate(iso, new Date().toISOString()),
|
||||
events: events.filter((event) => isSameUtcDate(event.start, iso)),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function CalendarEmptyOverlay({ view }: { view: "day" | "week" | "month" }) {
|
||||
return (
|
||||
<div className="pointer-events-none absolute inset-x-4 top-1/2 -translate-y-1/2">
|
||||
<div className="border-border/70 bg-background/95 mx-auto max-w-md rounded-xl border border-dashed p-4 text-center shadow-sm backdrop-blur">
|
||||
<Icons.calendar className="text-muted-foreground mx-auto mb-2 h-6 w-6" />
|
||||
<p className="font-medium">No scheduled work. This {view} is free.</p>
|
||||
<p className="text-muted-foreground mt-1 text-sm">
|
||||
The calendar grid is still available for selecting dates and planning Activities.
|
||||
</p>
|
||||
<Button asChild className="pointer-events-auto mt-3" size="sm">
|
||||
<Link href="/dashboard/crm/activities">
|
||||
<Icons.add className="mr-1 h-4 w-4" />
|
||||
Add Activity
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function startOfUtcDay(value: string): Date {
|
||||
const date = new Date(value);
|
||||
date.setUTCHours(0, 0, 0, 0);
|
||||
return date;
|
||||
}
|
||||
|
||||
function startOfMonthGrid(value: string): Date {
|
||||
const firstOfMonth = startOfUtcDay(value);
|
||||
firstOfMonth.setUTCDate(1);
|
||||
const gridStart = new Date(firstOfMonth);
|
||||
gridStart.setUTCDate(firstOfMonth.getUTCDate() - firstOfMonth.getUTCDay());
|
||||
return gridStart;
|
||||
}
|
||||
|
||||
function isSameUtcDate(left: string, right: string): boolean {
|
||||
return left.slice(0, 10) === right.slice(0, 10);
|
||||
}
|
||||
|
||||
function isSameHour(left: string, right: string): boolean {
|
||||
const leftDate = new Date(left);
|
||||
const rightDate = new Date(right);
|
||||
return (
|
||||
isSameUtcDate(leftDate.toISOString(), rightDate.toISOString()) &&
|
||||
leftDate.getUTCHours() === rightDate.getUTCHours()
|
||||
);
|
||||
}
|
||||
|
||||
function CalendarEventCard({ event }: { event: BigCalendarEvent }) {
|
||||
return (
|
||||
<article className="border-border/70 hover:bg-muted/30 rounded-lg border p-4 transition-colors">
|
||||
|
||||
Reference in New Issue
Block a user