task ep1.6.1

This commit is contained in:
phaichayon
2026-07-13 17:01:54 +07:00
parent e0fcb3992b
commit 7d7548ebd4
3 changed files with 439 additions and 33 deletions

View File

@@ -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">