@open-mercato/core 0.7.1-develop.7201.1.9bd6c88e3d → 0.7.1-develop.7203.1.9396450b78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- [build:core] found 4931 entry points
1
+ [build:core] found 4932 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 232 entry points
4
4
  [build:core:generated] built successfully
@@ -2,13 +2,13 @@
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { Calendar, CalendarClock, Clock, Mail, Phone, StickyNote, Users } from "lucide-react";
5
- import { toZonedTime } from "date-fns-tz";
6
5
  import { cn } from "@open-mercato/shared/lib/utils";
7
6
  import { useT } from "@open-mercato/shared/lib/i18n/context";
8
7
  import { readApiResultOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
9
8
  import { ActivitiesDayStrip } from "./ActivitiesDayStrip.js";
10
9
  import { ActivitiesAddNewMenu } from "./ActivitiesAddNewMenu.js";
11
10
  import { isOpenInteractionStatus } from "../../lib/interactionStatus.js";
11
+ import { isSameDay, toLocalZonedDate } from "../../lib/localDay.js";
12
12
  import { createLogger } from "@open-mercato/shared/lib/logger";
13
13
  const logger = createLogger("customers");
14
14
  const TYPE_ICONS = {
@@ -17,24 +17,11 @@ const TYPE_ICONS = {
17
17
  meeting: Users,
18
18
  note: StickyNote
19
19
  };
20
- const USER_TIMEZONE = (() => {
21
- try {
22
- return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
23
- } catch {
24
- return "UTC";
25
- }
26
- })();
27
- function toLocalZonedDate(value) {
28
- return toZonedTime(value, USER_TIMEZONE);
29
- }
30
20
  function startOfDay(date) {
31
21
  const next = new Date(date);
32
22
  next.setHours(0, 0, 0, 0);
33
23
  return next;
34
24
  }
35
- function isSameDay(a, b) {
36
- return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
37
- }
38
25
  function isOverdue(activity, now) {
39
26
  const scheduled = activity.scheduledAt ?? activity.occurredAt;
40
27
  if (!scheduled) return false;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/customers/components/detail/ActivitiesCard.tsx"],
4
- "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { Calendar, CalendarClock, Clock, Mail, Phone, StickyNote, Users } from 'lucide-react'\nimport { toZonedTime } from 'date-fns-tz'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { ActivitiesDayStrip } from './ActivitiesDayStrip'\nimport { ActivitiesAddNewMenu, type ActivityKind } from './ActivitiesAddNewMenu'\nimport type { InteractionSummary } from './types'\nimport { isOpenInteractionStatus } from '../../lib/interactionStatus'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\n\ninterface ActivitiesCardProps {\n entityId: string\n /**\n * Initial planned activities (from the parent route's `plannedActivitiesPreview`).\n * Used as the seed value before the broader `/api/customers/interactions` fetch\n * resolves, and as the fallback when the fetch fails. The card always prefers\n * its own fetched window (issue #1809 \u2014 fixes E1 status alignment and E2 type\n * coverage by sourcing from the same endpoint as the day strip rather than the\n * 5-item server preview that excluded most types in practice).\n */\n plannedActivities: InteractionSummary[]\n refreshKey?: number\n onAddNew: (kind: ActivityKind) => void\n onEditActivity?: (activity: InteractionSummary) => void\n /**\n * Optional company name for the parent entity. When the planned activity has no `dealTitle`,\n * the row subtitle falls back to \"{type} \u00B7 {company}\" to mirror Figma 784:809.\n */\n entityCompanyName?: string | null\n}\n\nconst TYPE_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {\n call: Phone,\n email: Mail,\n meeting: Users,\n note: StickyNote,\n}\n\nconst USER_TIMEZONE = (() => {\n try {\n return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'\n } catch {\n return 'UTC'\n }\n})()\n\n// Project a UTC instant to the user's local timezone before extracting day/month/year\n// for \"same day\" comparisons (issue #1809 \u2014 E3 timezone drift).\nfunction toLocalZonedDate(value: string | Date): Date {\n return toZonedTime(value, USER_TIMEZONE)\n}\n\nfunction startOfDay(date: Date): Date {\n const next = new Date(date)\n next.setHours(0, 0, 0, 0)\n return next\n}\n\nfunction isSameDay(a: Date, b: Date): boolean {\n return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()\n}\n\nfunction isOverdue(activity: InteractionSummary, now: Date): boolean {\n const scheduled = activity.scheduledAt ?? activity.occurredAt\n if (!scheduled) return false\n const date = new Date(scheduled)\n if (Number.isNaN(date.getTime())) return false\n return date.getTime() < now.getTime() && isOpenInteractionStatus(activity.status)\n}\n\n// Visible window for the day-strip + activity list. Mirrors `VISIBLE_DAYS = 5`\n// in ActivitiesDayStrip with extra padding so navigation forward/back doesn't\n// race the fetch.\nconst FETCH_WINDOW_DAYS = 31\n\nfunction formatTime(date: Date): string {\n return date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })\n}\n\nfunction formatRelativeDay(date: Date, t: TranslateFn): string {\n const now = new Date()\n const today = startOfDay(now)\n const target = startOfDay(date)\n const diff = Math.round((target.getTime() - today.getTime()) / (1000 * 60 * 60 * 24))\n if (diff === 0) return t('customers.timeline.date.today', 'today')\n if (diff === 1) return t('customers.timeline.date.tomorrow', 'tomorrow')\n if (diff === -1) return t('customers.timeline.date.yesterday', 'yesterday')\n return target.toLocaleDateString(undefined, { day: 'numeric', month: 'short' })\n}\n\nfunction formatDuration(minutes: number, t: TranslateFn): string {\n if (minutes >= 60) {\n const hours = Math.round((minutes / 60) * 10) / 10\n return t('customers.activities.calendar.hoursShort', '{hours}h', { hours })\n }\n return t('customers.activities.calendar.minutesShort', '{minutes}m', { minutes })\n}\n\nexport function ActivitiesCard({\n entityId,\n plannedActivities,\n refreshKey = 0,\n onAddNew,\n onEditActivity,\n entityCompanyName,\n}: ActivitiesCardProps) {\n const t = useT()\n const [selectedDate, setSelectedDate] = React.useState<Date>(() => startOfDay(new Date()))\n // Fetch the same broader window as the day strip via the canonical interactions\n // endpoint. This single source of truth aligns the day-strip count with the\n // visible event list (issue #1809 \u2014 E1) and surfaces all interaction types\n // (issue #1809 \u2014 E2: the previous reliance on the server-side 5-item preview\n // produced \"Person view shows only Calls\" because the limit happened to drop\n // every non-call entry from the prefix-window).\n const [fetchedEvents, setFetchedEvents] = React.useState<InteractionSummary[] | null>(null)\n\n React.useEffect(() => {\n if (!entityId) {\n setFetchedEvents(null)\n return\n }\n const controller = new AbortController()\n const today = startOfDay(new Date())\n const fromDate = new Date(today)\n fromDate.setDate(today.getDate() - FETCH_WINDOW_DAYS)\n const toDate = new Date(today)\n toDate.setDate(today.getDate() + FETCH_WINDOW_DAYS)\n toDate.setHours(23, 59, 59, 999)\n const params = new URLSearchParams({\n entityId,\n from: fromDate.toISOString(),\n to: toDate.toISOString(),\n // Server caps at 100 (interactions querySchema). 100 is well above what\n // an active CRM record accumulates in a 31-day window of meetings/calls,\n // and the day strip + list naturally degrade to truncation if exceeded.\n limit: '100',\n sortField: 'scheduledAt',\n sortDir: 'asc',\n excludeInteractionType: 'task',\n })\n void (async () => {\n try {\n const payload = await readApiResultOrThrow<{ items?: InteractionSummary[] }>(\n `/api/customers/interactions?${params.toString()}`,\n { signal: controller.signal },\n )\n setFetchedEvents(Array.isArray(payload?.items) ? payload.items : [])\n } catch (err) {\n if ((err as { name?: string } | null)?.name !== 'AbortError') {\n logger.warn('failed to load interactions', { component: 'ActivitiesCard', err })\n setFetchedEvents(null)\n }\n }\n })()\n return () => controller.abort()\n }, [entityId, refreshKey])\n\n // Prefer the broader fetch when it has resolved; fall back to the seed prop\n // (route-supplied preview) only while the fetch is in flight or after a\n // hard failure. This guarantees that the rare prop-only render path keeps\n // backwards-compat with existing unit tests while live UI uses the broader fetch.\n const effectiveEvents: InteractionSummary[] = fetchedEvents ?? plannedActivities\n\n const eventsForSelectedDay = React.useMemo(() => {\n const items = effectiveEvents.filter((activity) => {\n const scheduled = activity.scheduledAt ?? activity.occurredAt\n if (!scheduled) return false\n const date = new Date(scheduled)\n if (Number.isNaN(date.getTime())) return false\n // Compare in the user's local timezone so a 23:30 local activity stays\n // on its local-day chip instead of bleeding into the next UTC day\n // (issue #1809 \u2014 E3).\n return isSameDay(toLocalZonedDate(scheduled), selectedDate)\n })\n return items.sort((left, right) => {\n const leftTime = new Date(left.scheduledAt ?? left.occurredAt ?? left.createdAt).getTime()\n const rightTime = new Date(right.scheduledAt ?? right.occurredAt ?? right.createdAt).getTime()\n return leftTime - rightTime\n })\n }, [effectiveEvents, selectedDate])\n\n const overdueCount = React.useMemo(() => {\n const now = new Date()\n return effectiveEvents.filter((activity) => isOverdue(activity, now)).length\n }, [effectiveEvents])\n\n return (\n <div className=\"flex flex-col gap-3 rounded-lg border border-border bg-card pt-4 pb-4 px-4\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <Calendar className=\"size-4 text-foreground\" />\n <h3 className=\"text-sm font-semibold leading-none text-foreground\">\n {t('customers.activities.card.title', 'Activities')}\n </h3>\n {overdueCount > 0 ? (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-status-error-bg px-1.5 py-0.5 text-xs font-medium text-status-error-text\">\n <CalendarClock className=\"size-3\" />\n {t('customers.activities.card.overdue', '{count} overdue', { count: overdueCount })}\n </span>\n ) : null}\n </div>\n <ActivitiesAddNewMenu onSelect={onAddNew} />\n </div>\n\n <ActivitiesDayStrip\n entityId={entityId}\n selectedDate={selectedDate}\n onSelectDate={setSelectedDate}\n refreshKey={refreshKey}\n events={fetchedEvents ?? undefined}\n />\n\n {eventsForSelectedDay.length > 0 ? (\n <>\n <div className=\"h-px w-full bg-border\" />\n <ul className=\"flex flex-col\">\n {eventsForSelectedDay.map((activity) => (\n <PlannedEventRow\n key={activity.id}\n activity={activity}\n onClick={onEditActivity}\n entityCompanyName={entityCompanyName ?? null}\n t={t}\n />\n ))}\n </ul>\n </>\n ) : (\n <>\n <div className=\"h-px w-full bg-border\" />\n <p className=\"px-1 py-2 text-xs text-muted-foreground\">\n {t('customers.activities.card.empty', 'Nothing scheduled for this day.')}\n </p>\n </>\n )}\n </div>\n )\n}\n\ninterface PlannedEventRowProps {\n activity: InteractionSummary\n onClick?: (activity: InteractionSummary) => void\n entityCompanyName: string | null\n t: TranslateFn\n}\n\nfunction PlannedEventRow({ activity, onClick, entityCompanyName, t }: PlannedEventRowProps) {\n const dateStr = activity.scheduledAt ?? activity.occurredAt ?? activity.createdAt\n const date = new Date(dateStr)\n const validDate = !Number.isNaN(date.getTime())\n const Icon = TYPE_ICONS[activity.interactionType] ?? Users\n const duration = typeof activity.duration === 'number' && activity.duration > 0 ? activity.duration : null\n const overdue = validDate && date.getTime() < Date.now() && isOpenInteractionStatus(activity.status)\n const typeLabel = labelForType(activity.interactionType, t)\n const subtitleSuffix = activity.dealTitle ?? entityCompanyName ?? null\n const subtitle = subtitleSuffix ? `${typeLabel} \u00B7 ${subtitleSuffix}` : typeLabel\n const interactive = !!onClick\n\n return (\n <li>\n <button\n type=\"button\"\n onClick={interactive ? () => onClick?.(activity) : undefined}\n disabled={!interactive}\n className={cn(\n 'flex w-full items-start gap-[9px] pt-[8px] text-left transition-colors',\n interactive ? 'cursor-pointer rounded-md hover:bg-accent/30 px-1' : 'px-1',\n )}\n >\n <div className=\"flex h-[44px] w-[43px] shrink-0 flex-col gap-[2px] pt-[2px]\">\n <span className=\"text-xs font-semibold leading-none text-foreground\">\n {validDate ? formatTime(date) : ''}\n </span>\n <span className=\"text-[10px] leading-none font-normal text-muted-foreground\">\n {validDate ? formatRelativeDay(date, t) : ''}\n </span>\n </div>\n <div className=\"flex shrink-0 items-center justify-center rounded-full bg-muted border-4 border-background size-7\">\n <Icon className=\"size-4 text-muted-foreground\" />\n </div>\n <div className=\"min-w-0 flex flex-1 flex-col gap-[4px]\">\n <span className=\"text-sm leading-5 tracking-[-0.084px] text-foreground\">\n {activity.title ?? activity.body ?? labelForType(activity.interactionType, t)}\n </span>\n {duration ? (\n <span className={cn(\n 'inline-flex w-fit items-center gap-[2px] rounded-full pl-[4px] pr-[8px] py-[2px] text-xs font-medium leading-[16px]',\n overdue\n ? 'bg-status-error-bg text-status-error-text'\n : 'bg-status-warning-bg text-status-warning-text',\n )}>\n <Clock className=\"size-4\" />\n {formatDuration(duration, t)}\n </span>\n ) : null}\n <span className=\"text-[11px] font-normal text-muted-foreground\">{subtitle}</span>\n </div>\n </button>\n </li>\n )\n}\n\nfunction labelForType(type: string, t: TranslateFn): string {\n const map: Record<string, [string, string]> = {\n meeting: ['customers.timeline.filter.meeting', 'Meeting'],\n call: ['customers.timeline.filter.call', 'Call'],\n email: ['customers.timeline.filter.email', 'Email'],\n note: ['customers.timeline.filter.note', 'Note'],\n task: ['customers.timeline.filter.task', 'Task'],\n }\n const entry = map[type]\n return entry ? t(entry[0], entry[1]) : type\n}\n\nexport default ActivitiesCard\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { Calendar, CalendarClock, Clock, Mail, Phone, StickyNote, Users } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { ActivitiesDayStrip } from './ActivitiesDayStrip'\nimport { ActivitiesAddNewMenu, type ActivityKind } from './ActivitiesAddNewMenu'\nimport type { InteractionSummary } from './types'\nimport { isOpenInteractionStatus } from '../../lib/interactionStatus'\nimport { isSameDay, toLocalZonedDate } from '../../lib/localDay'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\n\ninterface ActivitiesCardProps {\n entityId: string\n /**\n * Initial planned activities (from the parent route's `plannedActivitiesPreview`).\n * Used as the seed value before the broader `/api/customers/interactions` fetch\n * resolves, and as the fallback when the fetch fails. The card always prefers\n * its own fetched window (issue #1809 \u2014 fixes E1 status alignment and E2 type\n * coverage by sourcing from the same endpoint as the day strip rather than the\n * 5-item server preview that excluded most types in practice).\n */\n plannedActivities: InteractionSummary[]\n refreshKey?: number\n onAddNew: (kind: ActivityKind) => void\n onEditActivity?: (activity: InteractionSummary) => void\n /**\n * Optional company name for the parent entity. When the planned activity has no `dealTitle`,\n * the row subtitle falls back to \"{type} \u00B7 {company}\" to mirror Figma 784:809.\n */\n entityCompanyName?: string | null\n}\n\nconst TYPE_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {\n call: Phone,\n email: Mail,\n meeting: Users,\n note: StickyNote,\n}\n\nfunction startOfDay(date: Date): Date {\n const next = new Date(date)\n next.setHours(0, 0, 0, 0)\n return next\n}\n\nfunction isOverdue(activity: InteractionSummary, now: Date): boolean {\n const scheduled = activity.scheduledAt ?? activity.occurredAt\n if (!scheduled) return false\n const date = new Date(scheduled)\n if (Number.isNaN(date.getTime())) return false\n return date.getTime() < now.getTime() && isOpenInteractionStatus(activity.status)\n}\n\n// Visible window for the day-strip + activity list. Mirrors `VISIBLE_DAYS = 5`\n// in ActivitiesDayStrip with extra padding so navigation forward/back doesn't\n// race the fetch.\nconst FETCH_WINDOW_DAYS = 31\n\nfunction formatTime(date: Date): string {\n return date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })\n}\n\nfunction formatRelativeDay(date: Date, t: TranslateFn): string {\n const now = new Date()\n const today = startOfDay(now)\n const target = startOfDay(date)\n const diff = Math.round((target.getTime() - today.getTime()) / (1000 * 60 * 60 * 24))\n if (diff === 0) return t('customers.timeline.date.today', 'today')\n if (diff === 1) return t('customers.timeline.date.tomorrow', 'tomorrow')\n if (diff === -1) return t('customers.timeline.date.yesterday', 'yesterday')\n return target.toLocaleDateString(undefined, { day: 'numeric', month: 'short' })\n}\n\nfunction formatDuration(minutes: number, t: TranslateFn): string {\n if (minutes >= 60) {\n const hours = Math.round((minutes / 60) * 10) / 10\n return t('customers.activities.calendar.hoursShort', '{hours}h', { hours })\n }\n return t('customers.activities.calendar.minutesShort', '{minutes}m', { minutes })\n}\n\nexport function ActivitiesCard({\n entityId,\n plannedActivities,\n refreshKey = 0,\n onAddNew,\n onEditActivity,\n entityCompanyName,\n}: ActivitiesCardProps) {\n const t = useT()\n const [selectedDate, setSelectedDate] = React.useState<Date>(() => startOfDay(new Date()))\n // Fetch the same broader window as the day strip via the canonical interactions\n // endpoint. This single source of truth aligns the day-strip count with the\n // visible event list (issue #1809 \u2014 E1) and surfaces all interaction types\n // (issue #1809 \u2014 E2: the previous reliance on the server-side 5-item preview\n // produced \"Person view shows only Calls\" because the limit happened to drop\n // every non-call entry from the prefix-window).\n const [fetchedEvents, setFetchedEvents] = React.useState<InteractionSummary[] | null>(null)\n\n React.useEffect(() => {\n if (!entityId) {\n setFetchedEvents(null)\n return\n }\n const controller = new AbortController()\n const today = startOfDay(new Date())\n const fromDate = new Date(today)\n fromDate.setDate(today.getDate() - FETCH_WINDOW_DAYS)\n const toDate = new Date(today)\n toDate.setDate(today.getDate() + FETCH_WINDOW_DAYS)\n toDate.setHours(23, 59, 59, 999)\n const params = new URLSearchParams({\n entityId,\n from: fromDate.toISOString(),\n to: toDate.toISOString(),\n // Server caps at 100 (interactions querySchema). 100 is well above what\n // an active CRM record accumulates in a 31-day window of meetings/calls,\n // and the day strip + list naturally degrade to truncation if exceeded.\n limit: '100',\n sortField: 'scheduledAt',\n sortDir: 'asc',\n excludeInteractionType: 'task',\n })\n void (async () => {\n try {\n const payload = await readApiResultOrThrow<{ items?: InteractionSummary[] }>(\n `/api/customers/interactions?${params.toString()}`,\n { signal: controller.signal },\n )\n setFetchedEvents(Array.isArray(payload?.items) ? payload.items : [])\n } catch (err) {\n if ((err as { name?: string } | null)?.name !== 'AbortError') {\n logger.warn('failed to load interactions', { component: 'ActivitiesCard', err })\n setFetchedEvents(null)\n }\n }\n })()\n return () => controller.abort()\n }, [entityId, refreshKey])\n\n // Prefer the broader fetch when it has resolved; fall back to the seed prop\n // (route-supplied preview) only while the fetch is in flight or after a\n // hard failure. This guarantees that the rare prop-only render path keeps\n // backwards-compat with existing unit tests while live UI uses the broader fetch.\n const effectiveEvents: InteractionSummary[] = fetchedEvents ?? plannedActivities\n\n const eventsForSelectedDay = React.useMemo(() => {\n const items = effectiveEvents.filter((activity) => {\n const scheduled = activity.scheduledAt ?? activity.occurredAt\n if (!scheduled) return false\n const date = new Date(scheduled)\n if (Number.isNaN(date.getTime())) return false\n // Compare in the user's local timezone so a 23:30 local activity stays\n // on its local-day chip instead of bleeding into the next UTC day\n // (issue #1809 \u2014 E3).\n return isSameDay(toLocalZonedDate(scheduled), selectedDate)\n })\n return items.sort((left, right) => {\n const leftTime = new Date(left.scheduledAt ?? left.occurredAt ?? left.createdAt).getTime()\n const rightTime = new Date(right.scheduledAt ?? right.occurredAt ?? right.createdAt).getTime()\n return leftTime - rightTime\n })\n }, [effectiveEvents, selectedDate])\n\n const overdueCount = React.useMemo(() => {\n const now = new Date()\n return effectiveEvents.filter((activity) => isOverdue(activity, now)).length\n }, [effectiveEvents])\n\n return (\n <div className=\"flex flex-col gap-3 rounded-lg border border-border bg-card pt-4 pb-4 px-4\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <Calendar className=\"size-4 text-foreground\" />\n <h3 className=\"text-sm font-semibold leading-none text-foreground\">\n {t('customers.activities.card.title', 'Activities')}\n </h3>\n {overdueCount > 0 ? (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-status-error-bg px-1.5 py-0.5 text-xs font-medium text-status-error-text\">\n <CalendarClock className=\"size-3\" />\n {t('customers.activities.card.overdue', '{count} overdue', { count: overdueCount })}\n </span>\n ) : null}\n </div>\n <ActivitiesAddNewMenu onSelect={onAddNew} />\n </div>\n\n <ActivitiesDayStrip\n entityId={entityId}\n selectedDate={selectedDate}\n onSelectDate={setSelectedDate}\n refreshKey={refreshKey}\n events={fetchedEvents ?? undefined}\n />\n\n {eventsForSelectedDay.length > 0 ? (\n <>\n <div className=\"h-px w-full bg-border\" />\n <ul className=\"flex flex-col\">\n {eventsForSelectedDay.map((activity) => (\n <PlannedEventRow\n key={activity.id}\n activity={activity}\n onClick={onEditActivity}\n entityCompanyName={entityCompanyName ?? null}\n t={t}\n />\n ))}\n </ul>\n </>\n ) : (\n <>\n <div className=\"h-px w-full bg-border\" />\n <p className=\"px-1 py-2 text-xs text-muted-foreground\">\n {t('customers.activities.card.empty', 'Nothing scheduled for this day.')}\n </p>\n </>\n )}\n </div>\n )\n}\n\ninterface PlannedEventRowProps {\n activity: InteractionSummary\n onClick?: (activity: InteractionSummary) => void\n entityCompanyName: string | null\n t: TranslateFn\n}\n\nfunction PlannedEventRow({ activity, onClick, entityCompanyName, t }: PlannedEventRowProps) {\n const dateStr = activity.scheduledAt ?? activity.occurredAt ?? activity.createdAt\n const date = new Date(dateStr)\n const validDate = !Number.isNaN(date.getTime())\n const Icon = TYPE_ICONS[activity.interactionType] ?? Users\n const duration = typeof activity.duration === 'number' && activity.duration > 0 ? activity.duration : null\n const overdue = validDate && date.getTime() < Date.now() && isOpenInteractionStatus(activity.status)\n const typeLabel = labelForType(activity.interactionType, t)\n const subtitleSuffix = activity.dealTitle ?? entityCompanyName ?? null\n const subtitle = subtitleSuffix ? `${typeLabel} \u00B7 ${subtitleSuffix}` : typeLabel\n const interactive = !!onClick\n\n return (\n <li>\n <button\n type=\"button\"\n onClick={interactive ? () => onClick?.(activity) : undefined}\n disabled={!interactive}\n className={cn(\n 'flex w-full items-start gap-[9px] pt-[8px] text-left transition-colors',\n interactive ? 'cursor-pointer rounded-md hover:bg-accent/30 px-1' : 'px-1',\n )}\n >\n <div className=\"flex h-[44px] w-[43px] shrink-0 flex-col gap-[2px] pt-[2px]\">\n <span className=\"text-xs font-semibold leading-none text-foreground\">\n {validDate ? formatTime(date) : ''}\n </span>\n <span className=\"text-[10px] leading-none font-normal text-muted-foreground\">\n {validDate ? formatRelativeDay(date, t) : ''}\n </span>\n </div>\n <div className=\"flex shrink-0 items-center justify-center rounded-full bg-muted border-4 border-background size-7\">\n <Icon className=\"size-4 text-muted-foreground\" />\n </div>\n <div className=\"min-w-0 flex flex-1 flex-col gap-[4px]\">\n <span className=\"text-sm leading-5 tracking-[-0.084px] text-foreground\">\n {activity.title ?? activity.body ?? labelForType(activity.interactionType, t)}\n </span>\n {duration ? (\n <span className={cn(\n 'inline-flex w-fit items-center gap-[2px] rounded-full pl-[4px] pr-[8px] py-[2px] text-xs font-medium leading-[16px]',\n overdue\n ? 'bg-status-error-bg text-status-error-text'\n : 'bg-status-warning-bg text-status-warning-text',\n )}>\n <Clock className=\"size-4\" />\n {formatDuration(duration, t)}\n </span>\n ) : null}\n <span className=\"text-[11px] font-normal text-muted-foreground\">{subtitle}</span>\n </div>\n </button>\n </li>\n )\n}\n\nfunction labelForType(type: string, t: TranslateFn): string {\n const map: Record<string, [string, string]> = {\n meeting: ['customers.timeline.filter.meeting', 'Meeting'],\n call: ['customers.timeline.filter.call', 'Call'],\n email: ['customers.timeline.filter.email', 'Email'],\n note: ['customers.timeline.filter.note', 'Note'],\n task: ['customers.timeline.filter.task', 'Task'],\n }\n const entry = map[type]\n return entry ? t(entry[0], entry[1]) : type\n}\n\nexport default ActivitiesCard\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -2,22 +2,12 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { ChevronLeft, ChevronRight } from "lucide-react";
5
- import { toZonedTime } from "date-fns-tz";
6
5
  import { cn } from "@open-mercato/shared/lib/utils";
7
6
  import { useT } from "@open-mercato/shared/lib/i18n/context";
8
7
  import { readApiResultOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
8
+ import { isSameDay, toLocalZonedDate } from "../../lib/localDay.js";
9
9
  import { createLogger } from "@open-mercato/shared/lib/logger";
10
10
  const logger = createLogger("customers");
11
- const USER_TIMEZONE = (() => {
12
- try {
13
- return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
14
- } catch {
15
- return "UTC";
16
- }
17
- })();
18
- function toLocalZonedDate(value) {
19
- return toZonedTime(value, USER_TIMEZONE);
20
- }
21
11
  const VISIBLE_DAYS = 5;
22
12
  const BUSYNESS_SLOTS = 10;
23
13
  const SLOT_START_HOUR = 7;
@@ -55,9 +45,6 @@ function endOfDay(date) {
55
45
  next.setHours(23, 59, 59, 999);
56
46
  return next;
57
47
  }
58
- function isSameDay(a, b) {
59
- return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
60
- }
61
48
  function isWeekend(date) {
62
49
  const day = date.getDay();
63
50
  return day === 0 || day === 6;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/customers/components/detail/ActivitiesDayStrip.tsx"],
4
- "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { ChevronLeft, ChevronRight } from 'lucide-react'\nimport { toZonedTime } from 'date-fns-tz'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport type { InteractionSummary } from './types'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\n\ninterface ActivitiesDayStripProps {\n entityId: string\n selectedDate: Date\n onSelectDate: (date: Date) => void\n refreshKey?: number\n /**\n * Optional pre-fetched events. When provided, the day strip skips its own fetch\n * and uses the supplied list, ensuring its busyness count agrees with the\n * activity list rendered alongside it (issue #1809 \u2014 E1 status filter alignment).\n */\n events?: InteractionSummary[]\n}\n\nconst USER_TIMEZONE = (() => {\n try {\n return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'\n } catch {\n return 'UTC'\n }\n})()\n\n// Project a UTC ISO timestamp to the user's local timezone before comparing\n// \"same day\" (issue #1809 \u2014 E3). The browser's `new Date(iso)` treats the\n// instant correctly, but `getDate()/getMonth()/getFullYear()` reflect the\n// user's local day, so for activities scheduled at e.g. 23:30 local on a UTC\n// boundary the day-strip and list now agree.\nfunction toLocalZonedDate(value: string | Date): Date {\n return toZonedTime(value, USER_TIMEZONE)\n}\n\nconst VISIBLE_DAYS = 5\nconst BUSYNESS_SLOTS = 10\nconst SLOT_START_HOUR = 7\nconst SLOT_END_HOUR = 22\n\nconst DAY_LABEL_KEYS: Array<[number, string, string]> = [\n [0, 'customers.calendar.day.sun', 'SUN'],\n [1, 'customers.calendar.day.mon', 'MON'],\n [2, 'customers.calendar.day.tue', 'TUE'],\n [3, 'customers.calendar.day.wed', 'WED'],\n [4, 'customers.calendar.day.thu', 'THU'],\n [5, 'customers.calendar.day.fri', 'FRI'],\n [6, 'customers.calendar.day.sat', 'SAT'],\n]\n\nconst MONTH_KEYS: Array<[number, string, string]> = [\n [0, 'customers.calendar.month.january', 'January'],\n [1, 'customers.calendar.month.february', 'February'],\n [2, 'customers.calendar.month.march', 'March'],\n [3, 'customers.calendar.month.april', 'April'],\n [4, 'customers.calendar.month.may', 'May'],\n [5, 'customers.calendar.month.june', 'June'],\n [6, 'customers.calendar.month.july', 'July'],\n [7, 'customers.calendar.month.august', 'August'],\n [8, 'customers.calendar.month.september', 'September'],\n [9, 'customers.calendar.month.october', 'October'],\n [10, 'customers.calendar.month.november', 'November'],\n [11, 'customers.calendar.month.december', 'December'],\n]\n\nfunction startOfDay(date: Date): Date {\n const next = new Date(date)\n next.setHours(0, 0, 0, 0)\n return next\n}\n\nfunction endOfDay(date: Date): Date {\n const next = new Date(date)\n next.setHours(23, 59, 59, 999)\n return next\n}\n\nfunction isSameDay(a: Date, b: Date): boolean {\n return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()\n}\n\nfunction isWeekend(date: Date): boolean {\n const day = date.getDay()\n return day === 0 || day === 6\n}\n\nfunction addDays(date: Date, delta: number): Date {\n const next = new Date(date)\n next.setDate(date.getDate() + delta)\n return next\n}\n\nfunction buildVisibleDays(anchor: Date): Date[] {\n const start = startOfDay(anchor)\n return Array.from({ length: VISIBLE_DAYS }, (_, index) => addDays(start, index))\n}\n\n// Anchor the visible window so that the given focal date lands at the center slot\n// (position 2 out of 5). Matches Figma 784:809 where the selected day is centered.\nfunction anchorCenteredOn(focalDate: Date): Date {\n return startOfDay(addDays(focalDate, -Math.floor(VISIBLE_DAYS / 2)))\n}\n\ntype SlotState = 'empty' | 'partial' | 'full' | 'conflict'\n\ntype DayBusyness = {\n totalMinutes: number\n eventCount: number\n slots: SlotState[]\n}\n\nfunction emptyBusyness(): DayBusyness {\n return {\n totalMinutes: 0,\n eventCount: 0,\n slots: Array<SlotState>(BUSYNESS_SLOTS).fill('empty'),\n }\n}\n\nfunction computeDayBusyness(events: InteractionSummary[], day: Date): DayBusyness {\n if (events.length === 0) return emptyBusyness()\n const dayStart = startOfDay(day).getTime()\n const slotMs = ((SLOT_END_HOUR - SLOT_START_HOUR) * 60 * 60 * 1000) / BUSYNESS_SLOTS\n const slotMinutes = slotMs / 60000\n const slotCounts: number[] = Array(BUSYNESS_SLOTS).fill(0)\n const slotMinutesUsed: number[] = Array(BUSYNESS_SLOTS).fill(0)\n let totalMinutes = 0\n let eventCount = 0\n\n for (const event of events) {\n const startIso = event.scheduledAt ?? event.occurredAt ?? event.createdAt\n if (!startIso) continue\n const start = new Date(startIso)\n if (Number.isNaN(start.getTime())) continue\n // Compare in the user's local timezone so an activity at 23:30 local time\n // doesn't bleed into the next UTC day's chip (issue #1809 \u2014 E3).\n const localStart = toLocalZonedDate(startIso)\n if (!isSameDay(localStart, day)) continue\n eventCount += 1\n const durationMinutes = typeof event.duration === 'number' && event.duration > 0 ? event.duration : 30\n totalMinutes += durationMinutes\n const eventStartMs = start.getTime()\n const eventEndMs = eventStartMs + durationMinutes * 60000\n const slotsStartMs = dayStart + SLOT_START_HOUR * 60 * 60 * 1000\n for (let slot = 0; slot < BUSYNESS_SLOTS; slot += 1) {\n const slotStart = slotsStartMs + slot * slotMs\n const slotEnd = slotStart + slotMs\n const overlapStart = Math.max(slotStart, eventStartMs)\n const overlapEnd = Math.min(slotEnd, eventEndMs)\n const overlapMinutes = Math.max(0, (overlapEnd - overlapStart) / 60000)\n if (overlapMinutes <= 0) continue\n slotCounts[slot] += 1\n slotMinutesUsed[slot] += overlapMinutes\n }\n }\n\n const slots: SlotState[] = slotCounts.map((count, index) => {\n if (count === 0) return 'empty'\n if (count > 1) return 'conflict'\n const used = slotMinutesUsed[index]\n if (used >= slotMinutes * 0.5) return 'full'\n return 'partial'\n })\n\n return { totalMinutes, eventCount, slots }\n}\n\nfunction formatBusyLabel(busy: DayBusyness, t: TranslateFn): string {\n if (busy.eventCount === 0) return ''\n // Match Figma 784:809 label format: \"Xm\" when under an hour, \"Xh\" otherwise.\n // Mixed \"Xh Ym\" overflows the 101px card and is not part of the visual spec.\n const durationLabel = busy.totalMinutes < 60\n ? t('customers.activities.calendar.minutesShort', '{minutes}m', { minutes: Math.max(Math.round(busy.totalMinutes), 1) })\n : t('customers.activities.calendar.hoursShort', '{hours}h', { hours: Math.floor(busy.totalMinutes / 60) })\n return t('customers.activities.calendar.eventsSummary', '{count} {countLabel} \u00B7 {duration}', {\n count: busy.eventCount,\n countLabel: busy.eventCount === 1\n ? t('customers.activities.calendar.eventSingular', 'event')\n : t('customers.activities.calendar.eventPlural', 'events'),\n duration: durationLabel,\n })\n}\n\nfunction formatMonthLabel(date: Date, t: TranslateFn): string {\n const monthEntry = MONTH_KEYS.find(([index]) => index === date.getMonth())\n const monthName = monthEntry ? t(monthEntry[1], monthEntry[2]) : ''\n return t('customers.activities.calendar.monthYear', '{month} {year}', { month: monthName, year: date.getFullYear() })\n}\n\nfunction formatDayLabel(date: Date, t: TranslateFn): string {\n const entry = DAY_LABEL_KEYS.find(([index]) => index === date.getDay())\n return entry ? t(entry[1], entry[2]) : ''\n}\n\nexport function ActivitiesDayStrip({ entityId, selectedDate, onSelectDate, refreshKey = 0, events: providedEvents }: ActivitiesDayStripProps) {\n const t = useT()\n const [anchor, setAnchor] = React.useState<Date>(() => anchorCenteredOn(selectedDate))\n const [fetchedEvents, setFetchedEvents] = React.useState<InteractionSummary[]>([])\n // When the parent supplies `events` (preferred path \u2014 keeps day strip and\n // the list in lockstep, fixes #1809 E1), skip the local fetch entirely.\n const useProvidedEvents = providedEvents !== undefined\n const events = useProvidedEvents ? providedEvents : fetchedEvents\n\n React.useEffect(() => {\n setAnchor((current) => {\n const days = buildVisibleDays(current)\n const visible = days.some((day) => isSameDay(day, selectedDate))\n if (visible) return current\n return anchorCenteredOn(selectedDate)\n })\n }, [selectedDate])\n\n const visibleDays = React.useMemo(() => buildVisibleDays(anchor), [anchor])\n const headerLabel = React.useMemo(() => formatMonthLabel(visibleDays[0], t), [visibleDays, t])\n\n React.useEffect(() => {\n if (useProvidedEvents) return\n if (!entityId || visibleDays.length === 0) return\n const controller = new AbortController()\n const fromIso = startOfDay(visibleDays[0]).toISOString()\n const toIso = endOfDay(visibleDays[visibleDays.length - 1]).toISOString()\n const params = new URLSearchParams({\n entityId,\n from: fromIso,\n to: toIso,\n limit: '100',\n sortField: 'scheduledAt',\n sortDir: 'asc',\n excludeInteractionType: 'task',\n })\n void (async () => {\n try {\n const payload = await readApiResultOrThrow<{ items?: InteractionSummary[] }>(\n `/api/customers/interactions?${params.toString()}`,\n { signal: controller.signal },\n )\n setFetchedEvents(Array.isArray(payload?.items) ? payload.items : [])\n } catch (err) {\n if ((err as { name?: string } | null)?.name === 'AbortError') return\n logger.warn('failed to load interactions', { component: 'ActivitiesDayStrip', err })\n setFetchedEvents([])\n }\n })()\n return () => controller.abort()\n }, [entityId, visibleDays, refreshKey, useProvidedEvents])\n\n const todayDate = React.useMemo(() => startOfDay(new Date()), [])\n\n const handlePrev = React.useCallback(() => {\n setAnchor((current) => addDays(current, -VISIBLE_DAYS))\n }, [])\n const handleNext = React.useCallback(() => {\n setAnchor((current) => addDays(current, VISIBLE_DAYS))\n }, [])\n const handleHeaderPrev = React.useCallback(() => {\n setAnchor((current) => {\n const next = new Date(current)\n next.setMonth(current.getMonth() - 1)\n return startOfDay(next)\n })\n }, [])\n const handleHeaderNext = React.useCallback(() => {\n setAnchor((current) => {\n const next = new Date(current)\n next.setMonth(current.getMonth() + 1)\n return startOfDay(next)\n })\n }, [])\n\n return (\n <div className=\"flex flex-col gap-2.5 rounded-md px-3.5 py-3 w-full\">\n <div className=\"flex items-center justify-center gap-1.5 rounded-md bg-muted px-1.5 py-1.5\">\n <button\n type=\"button\"\n onClick={handleHeaderPrev}\n aria-label={t('customers.activities.calendar.prevMonth', 'Previous month')}\n className=\"flex size-6 items-center justify-center rounded-md border border-border bg-card shadow-xs hover:bg-accent/40\"\n >\n <ChevronLeft className=\"size-4 text-foreground\" />\n </button>\n <span className=\"flex-1 text-center text-sm font-medium leading-5 text-foreground\">{headerLabel}</span>\n <button\n type=\"button\"\n onClick={handleHeaderNext}\n aria-label={t('customers.activities.calendar.nextMonth', 'Next month')}\n className=\"flex size-6 items-center justify-center rounded-md border border-border bg-card shadow-xs hover:bg-accent/40\"\n >\n <ChevronRight className=\"size-4 text-foreground\" />\n </button>\n </div>\n <div className=\"flex w-full items-center gap-2\">\n <button\n type=\"button\"\n onClick={handlePrev}\n aria-label={t('customers.activities.calendar.prevWindow', 'Previous days')}\n className=\"flex size-6 shrink-0 items-center justify-center rounded-md border border-border bg-card shadow-xs hover:bg-accent/40\"\n >\n <ChevronLeft className=\"size-4 text-foreground\" />\n </button>\n <div className=\"flex flex-1 items-stretch justify-center gap-1\">\n {visibleDays.map((day) => {\n const busy = computeDayBusyness(events, day)\n const isSelected = isSameDay(day, selectedDate)\n const isToday = isSameDay(day, todayDate)\n const weekend = isWeekend(day)\n const busyLabel = busy.eventCount > 0\n ? formatBusyLabel(busy, t)\n : weekend\n ? t('customers.activities.calendar.weekend', 'Weekend')\n : ''\n return (\n <DayCard\n key={day.toISOString()}\n day={day}\n isActive={isSelected}\n isToday={isToday}\n busyness={busy}\n label={busyLabel}\n dayName={formatDayLabel(day, t)}\n onSelect={() => onSelectDate(day)}\n />\n )\n })}\n </div>\n <button\n type=\"button\"\n onClick={handleNext}\n aria-label={t('customers.activities.calendar.nextWindow', 'Next days')}\n className=\"flex size-6 shrink-0 items-center justify-center rounded-md border border-border bg-card shadow-xs hover:bg-accent/40\"\n >\n <ChevronRight className=\"size-4 text-foreground\" />\n </button>\n </div>\n </div>\n )\n}\n\ninterface DayCardProps {\n day: Date\n isActive: boolean\n isToday: boolean\n busyness: DayBusyness\n label: string\n dayName: string\n onSelect: () => void\n}\n\nfunction DayCard({ day, isActive, isToday, busyness, label, dayName, onSelect }: DayCardProps) {\n const dayNumber = String(day.getDate()).padStart(2, '0')\n return (\n <button\n type=\"button\"\n onClick={onSelect}\n aria-pressed={isActive}\n aria-label={`${dayName} ${dayNumber}`}\n className={cn(\n 'flex h-[104px] w-[101px] flex-col items-center gap-[6px] overflow-hidden rounded-[10px] border p-[12px] transition-colors',\n isActive\n ? 'border-transparent bg-foreground'\n : 'border-border bg-card hover:border-foreground/40',\n )}\n >\n <span className=\"text-[11px] font-medium leading-none tracking-[0.44px] text-muted-foreground\">\n {dayName}\n </span>\n <div className=\"flex items-center gap-[5px]\">\n <span\n className={cn(\n 'text-2xl font-semibold leading-7',\n isActive ? 'text-background' : 'text-foreground',\n )}\n >\n {dayNumber}\n </span>\n {isToday ? (\n <span\n className=\"inline-block size-1.5 rounded-full bg-status-info-icon\"\n aria-hidden\n />\n ) : null}\n </div>\n <div className=\"flex h-4 w-[82px] items-end gap-[1.5px]\">\n {busyness.slots.map((state, index) => (\n <BusySlot key={index} state={state} active={isActive} />\n ))}\n </div>\n <span className=\"text-[11px] leading-[14px] font-normal whitespace-nowrap text-muted-foreground\">\n {label}\n </span>\n </button>\n )\n}\n\nfunction BusySlot({ state, active }: { state: SlotState; active: boolean }) {\n const heightClass = state === 'empty'\n ? 'h-0.5'\n : state === 'partial'\n ? 'h-2'\n : 'h-3.5'\n let bgClass: string\n if (state === 'conflict') {\n bgClass = 'bg-status-error-icon'\n } else if (active) {\n if (state === 'empty') bgClass = 'bg-background/30'\n else if (state === 'partial') bgClass = 'bg-background/60'\n else bgClass = 'bg-background'\n } else {\n if (state === 'empty') bgClass = 'bg-border'\n else if (state === 'partial') bgClass = 'bg-muted-foreground'\n else bgClass = 'bg-foreground'\n }\n return <div className={cn('w-[7px] shrink-0 rounded-[1.5px]', heightClass, bgClass)} aria-hidden />\n}\n\nexport default ActivitiesDayStrip\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { ChevronLeft, ChevronRight } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport type { InteractionSummary } from './types'\nimport { isSameDay, toLocalZonedDate } from '../../lib/localDay'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\n\ninterface ActivitiesDayStripProps {\n entityId: string\n selectedDate: Date\n onSelectDate: (date: Date) => void\n refreshKey?: number\n /**\n * Optional pre-fetched events. When provided, the day strip skips its own fetch\n * and uses the supplied list, ensuring its busyness count agrees with the\n * activity list rendered alongside it (issue #1809 \u2014 E1 status filter alignment).\n */\n events?: InteractionSummary[]\n}\n\nconst VISIBLE_DAYS = 5\nconst BUSYNESS_SLOTS = 10\nconst SLOT_START_HOUR = 7\nconst SLOT_END_HOUR = 22\n\nconst DAY_LABEL_KEYS: Array<[number, string, string]> = [\n [0, 'customers.calendar.day.sun', 'SUN'],\n [1, 'customers.calendar.day.mon', 'MON'],\n [2, 'customers.calendar.day.tue', 'TUE'],\n [3, 'customers.calendar.day.wed', 'WED'],\n [4, 'customers.calendar.day.thu', 'THU'],\n [5, 'customers.calendar.day.fri', 'FRI'],\n [6, 'customers.calendar.day.sat', 'SAT'],\n]\n\nconst MONTH_KEYS: Array<[number, string, string]> = [\n [0, 'customers.calendar.month.january', 'January'],\n [1, 'customers.calendar.month.february', 'February'],\n [2, 'customers.calendar.month.march', 'March'],\n [3, 'customers.calendar.month.april', 'April'],\n [4, 'customers.calendar.month.may', 'May'],\n [5, 'customers.calendar.month.june', 'June'],\n [6, 'customers.calendar.month.july', 'July'],\n [7, 'customers.calendar.month.august', 'August'],\n [8, 'customers.calendar.month.september', 'September'],\n [9, 'customers.calendar.month.october', 'October'],\n [10, 'customers.calendar.month.november', 'November'],\n [11, 'customers.calendar.month.december', 'December'],\n]\n\nfunction startOfDay(date: Date): Date {\n const next = new Date(date)\n next.setHours(0, 0, 0, 0)\n return next\n}\n\nfunction endOfDay(date: Date): Date {\n const next = new Date(date)\n next.setHours(23, 59, 59, 999)\n return next\n}\n\nfunction isWeekend(date: Date): boolean {\n const day = date.getDay()\n return day === 0 || day === 6\n}\n\nfunction addDays(date: Date, delta: number): Date {\n const next = new Date(date)\n next.setDate(date.getDate() + delta)\n return next\n}\n\nfunction buildVisibleDays(anchor: Date): Date[] {\n const start = startOfDay(anchor)\n return Array.from({ length: VISIBLE_DAYS }, (_, index) => addDays(start, index))\n}\n\n// Anchor the visible window so that the given focal date lands at the center slot\n// (position 2 out of 5). Matches Figma 784:809 where the selected day is centered.\nfunction anchorCenteredOn(focalDate: Date): Date {\n return startOfDay(addDays(focalDate, -Math.floor(VISIBLE_DAYS / 2)))\n}\n\ntype SlotState = 'empty' | 'partial' | 'full' | 'conflict'\n\ntype DayBusyness = {\n totalMinutes: number\n eventCount: number\n slots: SlotState[]\n}\n\nfunction emptyBusyness(): DayBusyness {\n return {\n totalMinutes: 0,\n eventCount: 0,\n slots: Array<SlotState>(BUSYNESS_SLOTS).fill('empty'),\n }\n}\n\nfunction computeDayBusyness(events: InteractionSummary[], day: Date): DayBusyness {\n if (events.length === 0) return emptyBusyness()\n const dayStart = startOfDay(day).getTime()\n const slotMs = ((SLOT_END_HOUR - SLOT_START_HOUR) * 60 * 60 * 1000) / BUSYNESS_SLOTS\n const slotMinutes = slotMs / 60000\n const slotCounts: number[] = Array(BUSYNESS_SLOTS).fill(0)\n const slotMinutesUsed: number[] = Array(BUSYNESS_SLOTS).fill(0)\n let totalMinutes = 0\n let eventCount = 0\n\n for (const event of events) {\n const startIso = event.scheduledAt ?? event.occurredAt ?? event.createdAt\n if (!startIso) continue\n const start = new Date(startIso)\n if (Number.isNaN(start.getTime())) continue\n // Compare in the user's local timezone so an activity at 23:30 local time\n // doesn't bleed into the next UTC day's chip (issue #1809 \u2014 E3).\n const localStart = toLocalZonedDate(startIso)\n if (!isSameDay(localStart, day)) continue\n eventCount += 1\n const durationMinutes = typeof event.duration === 'number' && event.duration > 0 ? event.duration : 30\n totalMinutes += durationMinutes\n const eventStartMs = start.getTime()\n const eventEndMs = eventStartMs + durationMinutes * 60000\n const slotsStartMs = dayStart + SLOT_START_HOUR * 60 * 60 * 1000\n for (let slot = 0; slot < BUSYNESS_SLOTS; slot += 1) {\n const slotStart = slotsStartMs + slot * slotMs\n const slotEnd = slotStart + slotMs\n const overlapStart = Math.max(slotStart, eventStartMs)\n const overlapEnd = Math.min(slotEnd, eventEndMs)\n const overlapMinutes = Math.max(0, (overlapEnd - overlapStart) / 60000)\n if (overlapMinutes <= 0) continue\n slotCounts[slot] += 1\n slotMinutesUsed[slot] += overlapMinutes\n }\n }\n\n const slots: SlotState[] = slotCounts.map((count, index) => {\n if (count === 0) return 'empty'\n if (count > 1) return 'conflict'\n const used = slotMinutesUsed[index]\n if (used >= slotMinutes * 0.5) return 'full'\n return 'partial'\n })\n\n return { totalMinutes, eventCount, slots }\n}\n\nfunction formatBusyLabel(busy: DayBusyness, t: TranslateFn): string {\n if (busy.eventCount === 0) return ''\n // Match Figma 784:809 label format: \"Xm\" when under an hour, \"Xh\" otherwise.\n // Mixed \"Xh Ym\" overflows the 101px card and is not part of the visual spec.\n const durationLabel = busy.totalMinutes < 60\n ? t('customers.activities.calendar.minutesShort', '{minutes}m', { minutes: Math.max(Math.round(busy.totalMinutes), 1) })\n : t('customers.activities.calendar.hoursShort', '{hours}h', { hours: Math.floor(busy.totalMinutes / 60) })\n return t('customers.activities.calendar.eventsSummary', '{count} {countLabel} \u00B7 {duration}', {\n count: busy.eventCount,\n countLabel: busy.eventCount === 1\n ? t('customers.activities.calendar.eventSingular', 'event')\n : t('customers.activities.calendar.eventPlural', 'events'),\n duration: durationLabel,\n })\n}\n\nfunction formatMonthLabel(date: Date, t: TranslateFn): string {\n const monthEntry = MONTH_KEYS.find(([index]) => index === date.getMonth())\n const monthName = monthEntry ? t(monthEntry[1], monthEntry[2]) : ''\n return t('customers.activities.calendar.monthYear', '{month} {year}', { month: monthName, year: date.getFullYear() })\n}\n\nfunction formatDayLabel(date: Date, t: TranslateFn): string {\n const entry = DAY_LABEL_KEYS.find(([index]) => index === date.getDay())\n return entry ? t(entry[1], entry[2]) : ''\n}\n\nexport function ActivitiesDayStrip({ entityId, selectedDate, onSelectDate, refreshKey = 0, events: providedEvents }: ActivitiesDayStripProps) {\n const t = useT()\n const [anchor, setAnchor] = React.useState<Date>(() => anchorCenteredOn(selectedDate))\n const [fetchedEvents, setFetchedEvents] = React.useState<InteractionSummary[]>([])\n // When the parent supplies `events` (preferred path \u2014 keeps day strip and\n // the list in lockstep, fixes #1809 E1), skip the local fetch entirely.\n const useProvidedEvents = providedEvents !== undefined\n const events = useProvidedEvents ? providedEvents : fetchedEvents\n\n React.useEffect(() => {\n setAnchor((current) => {\n const days = buildVisibleDays(current)\n const visible = days.some((day) => isSameDay(day, selectedDate))\n if (visible) return current\n return anchorCenteredOn(selectedDate)\n })\n }, [selectedDate])\n\n const visibleDays = React.useMemo(() => buildVisibleDays(anchor), [anchor])\n const headerLabel = React.useMemo(() => formatMonthLabel(visibleDays[0], t), [visibleDays, t])\n\n React.useEffect(() => {\n if (useProvidedEvents) return\n if (!entityId || visibleDays.length === 0) return\n const controller = new AbortController()\n const fromIso = startOfDay(visibleDays[0]).toISOString()\n const toIso = endOfDay(visibleDays[visibleDays.length - 1]).toISOString()\n const params = new URLSearchParams({\n entityId,\n from: fromIso,\n to: toIso,\n limit: '100',\n sortField: 'scheduledAt',\n sortDir: 'asc',\n excludeInteractionType: 'task',\n })\n void (async () => {\n try {\n const payload = await readApiResultOrThrow<{ items?: InteractionSummary[] }>(\n `/api/customers/interactions?${params.toString()}`,\n { signal: controller.signal },\n )\n setFetchedEvents(Array.isArray(payload?.items) ? payload.items : [])\n } catch (err) {\n if ((err as { name?: string } | null)?.name === 'AbortError') return\n logger.warn('failed to load interactions', { component: 'ActivitiesDayStrip', err })\n setFetchedEvents([])\n }\n })()\n return () => controller.abort()\n }, [entityId, visibleDays, refreshKey, useProvidedEvents])\n\n const todayDate = React.useMemo(() => startOfDay(new Date()), [])\n\n const handlePrev = React.useCallback(() => {\n setAnchor((current) => addDays(current, -VISIBLE_DAYS))\n }, [])\n const handleNext = React.useCallback(() => {\n setAnchor((current) => addDays(current, VISIBLE_DAYS))\n }, [])\n const handleHeaderPrev = React.useCallback(() => {\n setAnchor((current) => {\n const next = new Date(current)\n next.setMonth(current.getMonth() - 1)\n return startOfDay(next)\n })\n }, [])\n const handleHeaderNext = React.useCallback(() => {\n setAnchor((current) => {\n const next = new Date(current)\n next.setMonth(current.getMonth() + 1)\n return startOfDay(next)\n })\n }, [])\n\n return (\n <div className=\"flex flex-col gap-2.5 rounded-md px-3.5 py-3 w-full\">\n <div className=\"flex items-center justify-center gap-1.5 rounded-md bg-muted px-1.5 py-1.5\">\n <button\n type=\"button\"\n onClick={handleHeaderPrev}\n aria-label={t('customers.activities.calendar.prevMonth', 'Previous month')}\n className=\"flex size-6 items-center justify-center rounded-md border border-border bg-card shadow-xs hover:bg-accent/40\"\n >\n <ChevronLeft className=\"size-4 text-foreground\" />\n </button>\n <span className=\"flex-1 text-center text-sm font-medium leading-5 text-foreground\">{headerLabel}</span>\n <button\n type=\"button\"\n onClick={handleHeaderNext}\n aria-label={t('customers.activities.calendar.nextMonth', 'Next month')}\n className=\"flex size-6 items-center justify-center rounded-md border border-border bg-card shadow-xs hover:bg-accent/40\"\n >\n <ChevronRight className=\"size-4 text-foreground\" />\n </button>\n </div>\n <div className=\"flex w-full items-center gap-2\">\n <button\n type=\"button\"\n onClick={handlePrev}\n aria-label={t('customers.activities.calendar.prevWindow', 'Previous days')}\n className=\"flex size-6 shrink-0 items-center justify-center rounded-md border border-border bg-card shadow-xs hover:bg-accent/40\"\n >\n <ChevronLeft className=\"size-4 text-foreground\" />\n </button>\n <div className=\"flex flex-1 items-stretch justify-center gap-1\">\n {visibleDays.map((day) => {\n const busy = computeDayBusyness(events, day)\n const isSelected = isSameDay(day, selectedDate)\n const isToday = isSameDay(day, todayDate)\n const weekend = isWeekend(day)\n const busyLabel = busy.eventCount > 0\n ? formatBusyLabel(busy, t)\n : weekend\n ? t('customers.activities.calendar.weekend', 'Weekend')\n : ''\n return (\n <DayCard\n key={day.toISOString()}\n day={day}\n isActive={isSelected}\n isToday={isToday}\n busyness={busy}\n label={busyLabel}\n dayName={formatDayLabel(day, t)}\n onSelect={() => onSelectDate(day)}\n />\n )\n })}\n </div>\n <button\n type=\"button\"\n onClick={handleNext}\n aria-label={t('customers.activities.calendar.nextWindow', 'Next days')}\n className=\"flex size-6 shrink-0 items-center justify-center rounded-md border border-border bg-card shadow-xs hover:bg-accent/40\"\n >\n <ChevronRight className=\"size-4 text-foreground\" />\n </button>\n </div>\n </div>\n )\n}\n\ninterface DayCardProps {\n day: Date\n isActive: boolean\n isToday: boolean\n busyness: DayBusyness\n label: string\n dayName: string\n onSelect: () => void\n}\n\nfunction DayCard({ day, isActive, isToday, busyness, label, dayName, onSelect }: DayCardProps) {\n const dayNumber = String(day.getDate()).padStart(2, '0')\n return (\n <button\n type=\"button\"\n onClick={onSelect}\n aria-pressed={isActive}\n aria-label={`${dayName} ${dayNumber}`}\n className={cn(\n 'flex h-[104px] w-[101px] flex-col items-center gap-[6px] overflow-hidden rounded-[10px] border p-[12px] transition-colors',\n isActive\n ? 'border-transparent bg-foreground'\n : 'border-border bg-card hover:border-foreground/40',\n )}\n >\n <span className=\"text-[11px] font-medium leading-none tracking-[0.44px] text-muted-foreground\">\n {dayName}\n </span>\n <div className=\"flex items-center gap-[5px]\">\n <span\n className={cn(\n 'text-2xl font-semibold leading-7',\n isActive ? 'text-background' : 'text-foreground',\n )}\n >\n {dayNumber}\n </span>\n {isToday ? (\n <span\n className=\"inline-block size-1.5 rounded-full bg-status-info-icon\"\n aria-hidden\n />\n ) : null}\n </div>\n <div className=\"flex h-4 w-[82px] items-end gap-[1.5px]\">\n {busyness.slots.map((state, index) => (\n <BusySlot key={index} state={state} active={isActive} />\n ))}\n </div>\n <span className=\"text-[11px] leading-[14px] font-normal whitespace-nowrap text-muted-foreground\">\n {label}\n </span>\n </button>\n )\n}\n\nfunction BusySlot({ state, active }: { state: SlotState; active: boolean }) {\n const heightClass = state === 'empty'\n ? 'h-0.5'\n : state === 'partial'\n ? 'h-2'\n : 'h-3.5'\n let bgClass: string\n if (state === 'conflict') {\n bgClass = 'bg-status-error-icon'\n } else if (active) {\n if (state === 'empty') bgClass = 'bg-background/30'\n else if (state === 'partial') bgClass = 'bg-background/60'\n else bgClass = 'bg-background'\n } else {\n if (state === 'empty') bgClass = 'bg-border'\n else if (state === 'partial') bgClass = 'bg-muted-foreground'\n else bgClass = 'bg-foreground'\n }\n return <div className={cn('w-[7px] shrink-0 rounded-[1.5px]', heightClass, bgClass)} aria-hidden />\n}\n\nexport default ActivitiesDayStrip\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -8,6 +8,7 @@ import { readApiResultOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
8
8
  import { Button } from "@open-mercato/ui/primitives/button";
9
9
  import { IconButton } from "@open-mercato/ui/primitives/icon-button";
10
10
  import { loadLegacyActivitiesInRange } from "./legacyActivities.js";
11
+ import { isSameDay } from "../../lib/localDay.js";
11
12
  function getWeekDays(baseDate) {
12
13
  const day = baseDate.getDay();
13
14
  const monday = new Date(baseDate);
@@ -21,9 +22,6 @@ function getWeekDays(baseDate) {
21
22
  }
22
23
  return days;
23
24
  }
24
- function isSameDay(a, b) {
25
- return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
26
- }
27
25
  function getDayLabels(t) {
28
26
  return [
29
27
  t("customers.calendar.day.mon", "MON"),
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/customers/components/detail/MiniWeekCalendar.tsx"],
4
- "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { Calendar, ChevronLeft, ChevronRight } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport type { InteractionSummary } from './types'\nimport { loadLegacyActivitiesInRange } from './legacyActivities'\n\ninterface MiniWeekCalendarProps {\n entityId: string\n useCanonicalInteractions?: boolean\n refreshRef?: React.RefObject<(() => void) | null>\n}\n\nfunction getWeekDays(baseDate: Date): Date[] {\n const day = baseDate.getDay()\n const monday = new Date(baseDate)\n monday.setDate(baseDate.getDate() - ((day === 0 ? 7 : day) - 1))\n monday.setHours(0, 0, 0, 0)\n const days: Date[] = []\n for (let i = 0; i < 7; i++) {\n const d = new Date(monday)\n d.setDate(monday.getDate() + i)\n days.push(d)\n }\n return days\n}\n\nfunction isSameDay(a: Date, b: Date): boolean {\n return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()\n}\n\nfunction getDayLabels(t: ReturnType<typeof useT>): string[] {\n return [\n t('customers.calendar.day.mon', 'MON'),\n t('customers.calendar.day.tue', 'TUE'),\n t('customers.calendar.day.wed', 'WED'),\n t('customers.calendar.day.thu', 'THU'),\n t('customers.calendar.day.fri', 'FRI'),\n t('customers.calendar.day.sat', 'SAT'),\n t('customers.calendar.day.sun', 'SUN'),\n ]\n}\n\nconst INTERACTION_TYPE_COLORS: Record<string, string> = {\n call: 'bg-chart-orange',\n email: 'bg-chart-blue',\n meeting: 'bg-chart-emerald',\n task: 'bg-chart-violet',\n note: 'bg-muted-foreground',\n}\n\nconst INTERACTION_TYPE_FALLBACK_COLOR = 'bg-chart-orange'\n\nfunction dotColorForType(type: string, isToday: boolean): string {\n if (isToday) return 'bg-background'\n return INTERACTION_TYPE_COLORS[type] ?? INTERACTION_TYPE_FALLBACK_COLOR\n}\n\nexport function MiniWeekCalendar({ entityId, useCanonicalInteractions = true, refreshRef }: MiniWeekCalendarProps) {\n const t = useT()\n const today = React.useMemo(() => new Date(), [])\n const [weekOffset, setWeekOffset] = React.useState(0)\n const [events, setEvents] = React.useState<InteractionSummary[]>([])\n\n const baseDate = React.useMemo(() => {\n const d = new Date(today)\n d.setDate(d.getDate() + weekOffset * 7)\n return d\n }, [today, weekOffset])\n\n const weekDays = React.useMemo(() => getWeekDays(baseDate), [baseDate])\n const weekStart = weekDays[0]\n const weekEnd = weekDays[6]\n\n const monthLabel = React.useMemo(() => {\n const month = baseDate.toLocaleDateString(undefined, { month: 'long', year: 'numeric' })\n return weekOffset === 0\n ? t('customers.calendar.thisWeek', 'This week') + ' \u2014 ' + month\n : month\n }, [baseDate, t, weekOffset])\n\n const [refreshKey, setRefreshKey] = React.useState(0)\n const refresh = React.useCallback(() => setRefreshKey((k) => k + 1), [])\n\n React.useEffect(() => {\n if (refreshRef) (refreshRef as React.MutableRefObject<(() => void) | null>).current = refresh\n }, [refresh, refreshRef])\n\n React.useEffect(() => {\n if (!entityId) return\n const controller = new AbortController()\n const rangeStart = new Date(weekStart)\n rangeStart.setHours(0, 0, 0, 0)\n const rangeEnd = new Date(weekEnd)\n rangeEnd.setDate(rangeEnd.getDate() + 2)\n rangeEnd.setHours(23, 59, 59, 999)\n\n function isWithinRange(item: InteractionSummary): boolean {\n const rawDate = item.scheduledAt ?? item.occurredAt ?? item.createdAt\n if (!rawDate) return false\n const eventDate = new Date(rawDate)\n if (Number.isNaN(eventDate.getTime())) return false\n return eventDate >= rangeStart && eventDate <= rangeEnd\n }\n\n async function loadCanonicalInteractions(): Promise<InteractionSummary[]> {\n const items: InteractionSummary[] = []\n let cursor: string | undefined\n do {\n const params = new URLSearchParams({\n entityId,\n sortField: 'occurredAt',\n sortDir: 'desc',\n limit: '100',\n excludeInteractionType: 'task',\n from: rangeStart.toISOString(),\n to: rangeEnd.toISOString(),\n })\n if (cursor) params.set('cursor', cursor)\n const payload = await readApiResultOrThrow<{\n items?: InteractionSummary[]\n nextCursor?: string\n }>(`/api/customers/interactions?${params.toString()}`, { signal: controller.signal })\n const pageItems = Array.isArray(payload?.items) ? payload.items : []\n items.push(...pageItems)\n cursor = typeof payload?.nextCursor === 'string' ? payload.nextCursor : undefined\n } while (cursor)\n return items.filter(isWithinRange)\n }\n\n void (async () => {\n try {\n const canonicalItems = await loadCanonicalInteractions()\n if (useCanonicalInteractions) {\n setEvents(canonicalItems)\n return\n }\n const legacyItems = await loadLegacyActivitiesInRange({\n entityId,\n rangeStart,\n isWithinRange,\n signal: controller.signal,\n })\n const seen = new Set<string>()\n const merged: InteractionSummary[] = []\n for (const item of [...canonicalItems, ...legacyItems]) {\n if (seen.has(item.id)) continue\n seen.add(item.id)\n merged.push(item)\n }\n setEvents(merged.filter(isWithinRange))\n } catch {\n setEvents([])\n }\n })()\n return () => controller.abort()\n }, [entityId, weekStart, weekEnd, useCanonicalInteractions, refreshKey])\n\n const eventsByDay = React.useMemo(() => {\n const map = new Map<string, InteractionSummary[]>()\n for (const event of events) {\n const dateStr = event.scheduledAt ?? event.occurredAt ?? event.createdAt\n if (!dateStr) continue\n const key = new Date(dateStr).toDateString()\n const list = map.get(key) ?? []\n list.push(event)\n map.set(key, list)\n }\n return map\n }, [events])\n\n // Selected day for summary (defaults to today)\n const [selectedDay, setSelectedDay] = React.useState<Date | null>(null)\n const activeDay = selectedDay ?? today\n\n // Collect events for the active day + tomorrow for the summary list\n const summaryEvents = React.useMemo(() => {\n const result: Array<{ event: InteractionSummary; dayLabel: string; isToday: boolean }> = []\n for (let offset = 0; offset <= 2 && result.length < 3; offset++) {\n const d = new Date(activeDay)\n d.setDate(d.getDate() + offset)\n const dayEvents = eventsByDay.get(d.toDateString()) ?? []\n const isActiveDay = offset === 0\n const label = isActiveDay\n ? ''\n : offset === 1\n ? t('customers.calendar.tomorrow', 'Tomorrow') + ' '\n : d.toLocaleDateString(undefined, { weekday: 'short' }) + ' '\n for (const ev of dayEvents) {\n if (result.length >= 3) break\n result.push({ event: ev, dayLabel: label, isToday: isActiveDay })\n }\n }\n return result\n }, [eventsByDay, activeDay, t])\n\n const dayLabels = getDayLabels(t)\n\n return (\n <div className=\"rounded-lg border border-border/60 p-4\">\n {/* Header */}\n <div className=\"mb-3 flex items-center justify-between\">\n <div className=\"flex items-center gap-2 text-sm font-medium\">\n <Calendar className=\"size-4 text-muted-foreground\" />\n {monthLabel}\n </div>\n <div className=\"flex items-center gap-1\">\n <IconButton type=\"button\" variant=\"ghost\" size=\"xs\" onClick={() => setWeekOffset((w) => w - 1)} aria-label={t('customers.calendar.previousWeek', 'Previous week')}>\n <ChevronLeft className=\"size-3.5\" />\n </IconButton>\n <IconButton type=\"button\" variant=\"ghost\" size=\"xs\" onClick={() => setWeekOffset((w) => w + 1)} aria-label={t('customers.calendar.nextWeek', 'Next week')}>\n <ChevronRight className=\"size-3.5\" />\n </IconButton>\n </div>\n </div>\n\n {/* Day headers */}\n <div className=\"grid grid-cols-7 mb-1\">\n {dayLabels.map((label) => (\n <div key={label} className=\"text-center text-xs font-medium text-muted-foreground\">\n {label}\n </div>\n ))}\n </div>\n\n {/* Day cells \u2014 bordered grid like Figma */}\n <div className=\"grid grid-cols-7 border rounded-lg overflow-hidden\">\n {weekDays.map((day) => {\n const isToday = isSameDay(day, today)\n const isSelected = selectedDay ? isSameDay(day, selectedDay) : isToday\n const dayEvents = eventsByDay.get(day.toDateString()) ?? []\n const hasEvents = dayEvents.length > 0\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n key={day.toISOString()}\n onClick={() => setSelectedDay(isSameDay(day, selectedDay ?? today) ? null : day)}\n className={cn(\n 'h-auto flex flex-col items-center border-r last:border-r-0 py-3 text-sm transition-colors cursor-pointer rounded-none',\n isSelected ? 'bg-foreground text-background font-bold' : 'hover:bg-accent/50',\n )}\n >\n <span className=\"text-base\">{day.getDate()}</span>\n {hasEvents && (\n <div className=\"mt-1 flex gap-0.5\">\n {dayEvents.slice(0, 3).map((ev, i) => (\n <span\n key={i}\n className={cn(\n 'size-1 rounded-full',\n dotColorForType(ev.interactionType, isSelected),\n )}\n />\n ))}\n </div>\n )}\n </Button>\n )\n })}\n </div>\n\n {/* Day summary \u2014 events for selected/active day + upcoming */}\n {summaryEvents.length > 0 && (\n <div className=\"mt-3 space-y-1.5 border-t pt-3\">\n {summaryEvents.map(({ event, dayLabel, isToday: isActiveDay }) => {\n const dateStr = event.scheduledAt ?? event.occurredAt ?? event.createdAt\n const time = dateStr ? new Date(dateStr).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }) : ''\n return (\n <div key={event.id} className=\"flex items-center gap-2 text-xs\">\n <span className={cn('size-1.5 rounded-full shrink-0', INTERACTION_TYPE_COLORS[event.interactionType] ?? (isActiveDay ? INTERACTION_TYPE_FALLBACK_COLOR : 'bg-muted-foreground'))} />\n <span className=\"font-semibold text-muted-foreground shrink-0\">\n {dayLabel}{time}\n </span>\n <span className=\"truncate\">{event.title ?? event.body ?? event.interactionType}</span>\n </div>\n )\n })}\n </div>\n )}\n </div>\n )\n}\n"],
5
- "mappings": ";AA+MQ,SACE,KADF;AA7MR,YAAY,WAAW;AACvB,SAAS,UAAU,aAAa,oBAAoB;AACpD,SAAS,UAAU;AACnB,SAAS,YAAY;AACrB,SAAS,4BAA4B;AACrC,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAE3B,SAAS,mCAAmC;AAQ5C,SAAS,YAAY,UAAwB;AAC3C,QAAM,MAAM,SAAS,OAAO;AAC5B,QAAM,SAAS,IAAI,KAAK,QAAQ;AAChC,SAAO,QAAQ,SAAS,QAAQ,MAAM,QAAQ,IAAI,IAAI,OAAO,EAAE;AAC/D,SAAO,SAAS,GAAG,GAAG,GAAG,CAAC;AAC1B,QAAM,OAAe,CAAC;AACtB,WAAS,IAAI,GAAG,IAAI,GAAG,KAAK;AAC1B,UAAM,IAAI,IAAI,KAAK,MAAM;AACzB,MAAE,QAAQ,OAAO,QAAQ,IAAI,CAAC;AAC9B,SAAK,KAAK,CAAC;AAAA,EACb;AACA,SAAO;AACT;AAEA,SAAS,UAAU,GAAS,GAAkB;AAC5C,SAAO,EAAE,YAAY,MAAM,EAAE,YAAY,KAAK,EAAE,SAAS,MAAM,EAAE,SAAS,KAAK,EAAE,QAAQ,MAAM,EAAE,QAAQ;AAC3G;AAEA,SAAS,aAAa,GAAsC;AAC1D,SAAO;AAAA,IACL,EAAE,8BAA8B,KAAK;AAAA,IACrC,EAAE,8BAA8B,KAAK;AAAA,IACrC,EAAE,8BAA8B,KAAK;AAAA,IACrC,EAAE,8BAA8B,KAAK;AAAA,IACrC,EAAE,8BAA8B,KAAK;AAAA,IACrC,EAAE,8BAA8B,KAAK;AAAA,IACrC,EAAE,8BAA8B,KAAK;AAAA,EACvC;AACF;AAEA,MAAM,0BAAkD;AAAA,EACtD,MAAM;AAAA,EACN,OAAO;AAAA,EACP,SAAS;AAAA,EACT,MAAM;AAAA,EACN,MAAM;AACR;AAEA,MAAM,kCAAkC;AAExC,SAAS,gBAAgB,MAAc,SAA0B;AAC/D,MAAI,QAAS,QAAO;AACpB,SAAO,wBAAwB,IAAI,KAAK;AAC1C;AAEO,SAAS,iBAAiB,EAAE,UAAU,2BAA2B,MAAM,WAAW,GAA0B;AACjH,QAAM,IAAI,KAAK;AACf,QAAM,QAAQ,MAAM,QAAQ,MAAM,oBAAI,KAAK,GAAG,CAAC,CAAC;AAChD,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAS,CAAC;AACpD,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAA+B,CAAC,CAAC;AAEnE,QAAM,WAAW,MAAM,QAAQ,MAAM;AACnC,UAAM,IAAI,IAAI,KAAK,KAAK;AACxB,MAAE,QAAQ,EAAE,QAAQ,IAAI,aAAa,CAAC;AACtC,WAAO;AAAA,EACT,GAAG,CAAC,OAAO,UAAU,CAAC;AAEtB,QAAM,WAAW,MAAM,QAAQ,MAAM,YAAY,QAAQ,GAAG,CAAC,QAAQ,CAAC;AACtE,QAAM,YAAY,SAAS,CAAC;AAC5B,QAAM,UAAU,SAAS,CAAC;AAE1B,QAAM,aAAa,MAAM,QAAQ,MAAM;AACrC,UAAM,QAAQ,SAAS,mBAAmB,QAAW,EAAE,OAAO,QAAQ,MAAM,UAAU,CAAC;AACvF,WAAO,eAAe,IAClB,EAAE,+BAA+B,WAAW,IAAI,aAAQ,QACxD;AAAA,EACN,GAAG,CAAC,UAAU,GAAG,UAAU,CAAC;AAE5B,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAS,CAAC;AACpD,QAAM,UAAU,MAAM,YAAY,MAAM,cAAc,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC;AAEvE,QAAM,UAAU,MAAM;AACpB,QAAI,WAAY,CAAC,WAA2D,UAAU;AAAA,EACxF,GAAG,CAAC,SAAS,UAAU,CAAC;AAExB,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,SAAU;AACf,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,aAAa,IAAI,KAAK,SAAS;AACrC,eAAW,SAAS,GAAG,GAAG,GAAG,CAAC;AAC9B,UAAM,WAAW,IAAI,KAAK,OAAO;AACjC,aAAS,QAAQ,SAAS,QAAQ,IAAI,CAAC;AACvC,aAAS,SAAS,IAAI,IAAI,IAAI,GAAG;AAEjC,aAAS,cAAc,MAAmC;AACxD,YAAM,UAAU,KAAK,eAAe,KAAK,cAAc,KAAK;AAC5D,UAAI,CAAC,QAAS,QAAO;AACrB,YAAM,YAAY,IAAI,KAAK,OAAO;AAClC,UAAI,OAAO,MAAM,UAAU,QAAQ,CAAC,EAAG,QAAO;AAC9C,aAAO,aAAa,cAAc,aAAa;AAAA,IACjD;AAEA,mBAAe,4BAA2D;AACxE,YAAM,QAA8B,CAAC;AACrC,UAAI;AACJ,SAAG;AACD,cAAM,SAAS,IAAI,gBAAgB;AAAA,UACjC;AAAA,UACA,WAAW;AAAA,UACX,SAAS;AAAA,UACT,OAAO;AAAA,UACP,wBAAwB;AAAA,UACxB,MAAM,WAAW,YAAY;AAAA,UAC7B,IAAI,SAAS,YAAY;AAAA,QAC3B,CAAC;AACD,YAAI,OAAQ,QAAO,IAAI,UAAU,MAAM;AACvC,cAAM,UAAU,MAAM,qBAGnB,+BAA+B,OAAO,SAAS,CAAC,IAAI,EAAE,QAAQ,WAAW,OAAO,CAAC;AACpF,cAAM,YAAY,MAAM,QAAQ,SAAS,KAAK,IAAI,QAAQ,QAAQ,CAAC;AACnE,cAAM,KAAK,GAAG,SAAS;AACvB,iBAAS,OAAO,SAAS,eAAe,WAAW,QAAQ,aAAa;AAAA,MAC1E,SAAS;AACT,aAAO,MAAM,OAAO,aAAa;AAAA,IACnC;AAEA,UAAM,YAAY;AAChB,UAAI;AACF,cAAM,iBAAiB,MAAM,0BAA0B;AACvD,YAAI,0BAA0B;AAC5B,oBAAU,cAAc;AACxB;AAAA,QACF;AACA,cAAM,cAAc,MAAM,4BAA4B;AAAA,UACpD;AAAA,UACA;AAAA,UACA;AAAA,UACA,QAAQ,WAAW;AAAA,QACrB,CAAC;AACD,cAAM,OAAO,oBAAI,IAAY;AAC7B,cAAM,SAA+B,CAAC;AACtC,mBAAW,QAAQ,CAAC,GAAG,gBAAgB,GAAG,WAAW,GAAG;AACtD,cAAI,KAAK,IAAI,KAAK,EAAE,EAAG;AACvB,eAAK,IAAI,KAAK,EAAE;AAChB,iBAAO,KAAK,IAAI;AAAA,QAClB;AACA,kBAAU,OAAO,OAAO,aAAa,CAAC;AAAA,MACxC,QAAQ;AACN,kBAAU,CAAC,CAAC;AAAA,MACd;AAAA,IACF,GAAG;AACH,WAAO,MAAM,WAAW,MAAM;AAAA,EAChC,GAAG,CAAC,UAAU,WAAW,SAAS,0BAA0B,UAAU,CAAC;AAEvE,QAAM,cAAc,MAAM,QAAQ,MAAM;AACtC,UAAM,MAAM,oBAAI,IAAkC;AAClD,eAAW,SAAS,QAAQ;AAC1B,YAAM,UAAU,MAAM,eAAe,MAAM,cAAc,MAAM;AAC/D,UAAI,CAAC,QAAS;AACd,YAAM,MAAM,IAAI,KAAK,OAAO,EAAE,aAAa;AAC3C,YAAM,OAAO,IAAI,IAAI,GAAG,KAAK,CAAC;AAC9B,WAAK,KAAK,KAAK;AACf,UAAI,IAAI,KAAK,IAAI;AAAA,IACnB;AACA,WAAO;AAAA,EACT,GAAG,CAAC,MAAM,CAAC;AAGX,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAsB,IAAI;AACtE,QAAM,YAAY,eAAe;AAGjC,QAAM,gBAAgB,MAAM,QAAQ,MAAM;AACxC,UAAM,SAAmF,CAAC;AAC1F,aAAS,SAAS,GAAG,UAAU,KAAK,OAAO,SAAS,GAAG,UAAU;AAC/D,YAAM,IAAI,IAAI,KAAK,SAAS;AAC5B,QAAE,QAAQ,EAAE,QAAQ,IAAI,MAAM;AAC9B,YAAM,YAAY,YAAY,IAAI,EAAE,aAAa,CAAC,KAAK,CAAC;AACxD,YAAM,cAAc,WAAW;AAC/B,YAAM,QAAQ,cACV,KACA,WAAW,IACT,EAAE,+BAA+B,UAAU,IAAI,MAC/C,EAAE,mBAAmB,QAAW,EAAE,SAAS,QAAQ,CAAC,IAAI;AAC9D,iBAAW,MAAM,WAAW;AAC1B,YAAI,OAAO,UAAU,EAAG;AACxB,eAAO,KAAK,EAAE,OAAO,IAAI,UAAU,OAAO,SAAS,YAAY,CAAC;AAAA,MAClE;AAAA,IACF;AACA,WAAO;AAAA,EACT,GAAG,CAAC,aAAa,WAAW,CAAC,CAAC;AAE9B,QAAM,YAAY,aAAa,CAAC;AAEhC,SACE,qBAAC,SAAI,WAAU,0CAEb;AAAA,yBAAC,SAAI,WAAU,0CACb;AAAA,2BAAC,SAAI,WAAU,+CACb;AAAA,4BAAC,YAAS,WAAU,gCAA+B;AAAA,QAClD;AAAA,SACH;AAAA,MACA,qBAAC,SAAI,WAAU,2BACb;AAAA,4BAAC,cAAW,MAAK,UAAS,SAAQ,SAAQ,MAAK,MAAK,SAAS,MAAM,cAAc,CAAC,MAAM,IAAI,CAAC,GAAG,cAAY,EAAE,mCAAmC,eAAe,GAC9J,8BAAC,eAAY,WAAU,YAAW,GACpC;AAAA,QACA,oBAAC,cAAW,MAAK,UAAS,SAAQ,SAAQ,MAAK,MAAK,SAAS,MAAM,cAAc,CAAC,MAAM,IAAI,CAAC,GAAG,cAAY,EAAE,+BAA+B,WAAW,GACtJ,8BAAC,gBAAa,WAAU,YAAW,GACrC;AAAA,SACF;AAAA,OACF;AAAA,IAGA,oBAAC,SAAI,WAAU,yBACZ,oBAAU,IAAI,CAAC,UACd,oBAAC,SAAgB,WAAU,yDACxB,mBADO,KAEV,CACD,GACH;AAAA,IAGA,oBAAC,SAAI,WAAU,sDACZ,mBAAS,IAAI,CAAC,QAAQ;AACrB,YAAM,UAAU,UAAU,KAAK,KAAK;AACpC,YAAM,aAAa,cAAc,UAAU,KAAK,WAAW,IAAI;AAC/D,YAAM,YAAY,YAAY,IAAI,IAAI,aAAa,CAAC,KAAK,CAAC;AAC1D,YAAM,YAAY,UAAU,SAAS;AACrC,aACE;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,MAAK;AAAA,UAEL,SAAS,MAAM,eAAe,UAAU,KAAK,eAAe,KAAK,IAAI,OAAO,GAAG;AAAA,UAC/E,WAAW;AAAA,YACT;AAAA,YACA,aAAa,4CAA4C;AAAA,UAC3D;AAAA,UAEA;AAAA,gCAAC,UAAK,WAAU,aAAa,cAAI,QAAQ,GAAE;AAAA,YAC1C,aACC,oBAAC,SAAI,WAAU,qBACZ,oBAAU,MAAM,GAAG,CAAC,EAAE,IAAI,CAAC,IAAI,MAC9B;AAAA,cAAC;AAAA;AAAA,gBAEC,WAAW;AAAA,kBACT;AAAA,kBACA,gBAAgB,GAAG,iBAAiB,UAAU;AAAA,gBAChD;AAAA;AAAA,cAJK;AAAA,YAKP,CACD,GACH;AAAA;AAAA;AAAA,QAnBG,IAAI,YAAY;AAAA,MAqBvB;AAAA,IAEJ,CAAC,GACH;AAAA,IAGC,cAAc,SAAS,KACtB,oBAAC,SAAI,WAAU,kCACZ,wBAAc,IAAI,CAAC,EAAE,OAAO,UAAU,SAAS,YAAY,MAAM;AAChE,YAAM,UAAU,MAAM,eAAe,MAAM,cAAc,MAAM;AAC/D,YAAM,OAAO,UAAU,IAAI,KAAK,OAAO,EAAE,mBAAmB,QAAW,EAAE,MAAM,WAAW,QAAQ,UAAU,CAAC,IAAI;AACjH,aACE,qBAAC,SAAmB,WAAU,mCAC5B;AAAA,4BAAC,UAAK,WAAW,GAAG,kCAAkC,wBAAwB,MAAM,eAAe,MAAM,cAAc,kCAAkC,sBAAsB,GAAG;AAAA,QAClL,qBAAC,UAAK,WAAU,gDACb;AAAA;AAAA,UAAU;AAAA,WACb;AAAA,QACA,oBAAC,UAAK,WAAU,YAAY,gBAAM,SAAS,MAAM,QAAQ,MAAM,iBAAgB;AAAA,WALvE,MAAM,EAMhB;AAAA,IAEJ,CAAC,GACH;AAAA,KAEJ;AAEJ;",
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { Calendar, ChevronLeft, ChevronRight } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport type { InteractionSummary } from './types'\nimport { loadLegacyActivitiesInRange } from './legacyActivities'\nimport { isSameDay } from '../../lib/localDay'\n\ninterface MiniWeekCalendarProps {\n entityId: string\n useCanonicalInteractions?: boolean\n refreshRef?: React.RefObject<(() => void) | null>\n}\n\nfunction getWeekDays(baseDate: Date): Date[] {\n const day = baseDate.getDay()\n const monday = new Date(baseDate)\n monday.setDate(baseDate.getDate() - ((day === 0 ? 7 : day) - 1))\n monday.setHours(0, 0, 0, 0)\n const days: Date[] = []\n for (let i = 0; i < 7; i++) {\n const d = new Date(monday)\n d.setDate(monday.getDate() + i)\n days.push(d)\n }\n return days\n}\n\nfunction getDayLabels(t: ReturnType<typeof useT>): string[] {\n return [\n t('customers.calendar.day.mon', 'MON'),\n t('customers.calendar.day.tue', 'TUE'),\n t('customers.calendar.day.wed', 'WED'),\n t('customers.calendar.day.thu', 'THU'),\n t('customers.calendar.day.fri', 'FRI'),\n t('customers.calendar.day.sat', 'SAT'),\n t('customers.calendar.day.sun', 'SUN'),\n ]\n}\n\nconst INTERACTION_TYPE_COLORS: Record<string, string> = {\n call: 'bg-chart-orange',\n email: 'bg-chart-blue',\n meeting: 'bg-chart-emerald',\n task: 'bg-chart-violet',\n note: 'bg-muted-foreground',\n}\n\nconst INTERACTION_TYPE_FALLBACK_COLOR = 'bg-chart-orange'\n\nfunction dotColorForType(type: string, isToday: boolean): string {\n if (isToday) return 'bg-background'\n return INTERACTION_TYPE_COLORS[type] ?? INTERACTION_TYPE_FALLBACK_COLOR\n}\n\nexport function MiniWeekCalendar({ entityId, useCanonicalInteractions = true, refreshRef }: MiniWeekCalendarProps) {\n const t = useT()\n const today = React.useMemo(() => new Date(), [])\n const [weekOffset, setWeekOffset] = React.useState(0)\n const [events, setEvents] = React.useState<InteractionSummary[]>([])\n\n const baseDate = React.useMemo(() => {\n const d = new Date(today)\n d.setDate(d.getDate() + weekOffset * 7)\n return d\n }, [today, weekOffset])\n\n const weekDays = React.useMemo(() => getWeekDays(baseDate), [baseDate])\n const weekStart = weekDays[0]\n const weekEnd = weekDays[6]\n\n const monthLabel = React.useMemo(() => {\n const month = baseDate.toLocaleDateString(undefined, { month: 'long', year: 'numeric' })\n return weekOffset === 0\n ? t('customers.calendar.thisWeek', 'This week') + ' \u2014 ' + month\n : month\n }, [baseDate, t, weekOffset])\n\n const [refreshKey, setRefreshKey] = React.useState(0)\n const refresh = React.useCallback(() => setRefreshKey((k) => k + 1), [])\n\n React.useEffect(() => {\n if (refreshRef) (refreshRef as React.MutableRefObject<(() => void) | null>).current = refresh\n }, [refresh, refreshRef])\n\n React.useEffect(() => {\n if (!entityId) return\n const controller = new AbortController()\n const rangeStart = new Date(weekStart)\n rangeStart.setHours(0, 0, 0, 0)\n const rangeEnd = new Date(weekEnd)\n rangeEnd.setDate(rangeEnd.getDate() + 2)\n rangeEnd.setHours(23, 59, 59, 999)\n\n function isWithinRange(item: InteractionSummary): boolean {\n const rawDate = item.scheduledAt ?? item.occurredAt ?? item.createdAt\n if (!rawDate) return false\n const eventDate = new Date(rawDate)\n if (Number.isNaN(eventDate.getTime())) return false\n return eventDate >= rangeStart && eventDate <= rangeEnd\n }\n\n async function loadCanonicalInteractions(): Promise<InteractionSummary[]> {\n const items: InteractionSummary[] = []\n let cursor: string | undefined\n do {\n const params = new URLSearchParams({\n entityId,\n sortField: 'occurredAt',\n sortDir: 'desc',\n limit: '100',\n excludeInteractionType: 'task',\n from: rangeStart.toISOString(),\n to: rangeEnd.toISOString(),\n })\n if (cursor) params.set('cursor', cursor)\n const payload = await readApiResultOrThrow<{\n items?: InteractionSummary[]\n nextCursor?: string\n }>(`/api/customers/interactions?${params.toString()}`, { signal: controller.signal })\n const pageItems = Array.isArray(payload?.items) ? payload.items : []\n items.push(...pageItems)\n cursor = typeof payload?.nextCursor === 'string' ? payload.nextCursor : undefined\n } while (cursor)\n return items.filter(isWithinRange)\n }\n\n void (async () => {\n try {\n const canonicalItems = await loadCanonicalInteractions()\n if (useCanonicalInteractions) {\n setEvents(canonicalItems)\n return\n }\n const legacyItems = await loadLegacyActivitiesInRange({\n entityId,\n rangeStart,\n isWithinRange,\n signal: controller.signal,\n })\n const seen = new Set<string>()\n const merged: InteractionSummary[] = []\n for (const item of [...canonicalItems, ...legacyItems]) {\n if (seen.has(item.id)) continue\n seen.add(item.id)\n merged.push(item)\n }\n setEvents(merged.filter(isWithinRange))\n } catch {\n setEvents([])\n }\n })()\n return () => controller.abort()\n }, [entityId, weekStart, weekEnd, useCanonicalInteractions, refreshKey])\n\n const eventsByDay = React.useMemo(() => {\n const map = new Map<string, InteractionSummary[]>()\n for (const event of events) {\n const dateStr = event.scheduledAt ?? event.occurredAt ?? event.createdAt\n if (!dateStr) continue\n const key = new Date(dateStr).toDateString()\n const list = map.get(key) ?? []\n list.push(event)\n map.set(key, list)\n }\n return map\n }, [events])\n\n // Selected day for summary (defaults to today)\n const [selectedDay, setSelectedDay] = React.useState<Date | null>(null)\n const activeDay = selectedDay ?? today\n\n // Collect events for the active day + tomorrow for the summary list\n const summaryEvents = React.useMemo(() => {\n const result: Array<{ event: InteractionSummary; dayLabel: string; isToday: boolean }> = []\n for (let offset = 0; offset <= 2 && result.length < 3; offset++) {\n const d = new Date(activeDay)\n d.setDate(d.getDate() + offset)\n const dayEvents = eventsByDay.get(d.toDateString()) ?? []\n const isActiveDay = offset === 0\n const label = isActiveDay\n ? ''\n : offset === 1\n ? t('customers.calendar.tomorrow', 'Tomorrow') + ' '\n : d.toLocaleDateString(undefined, { weekday: 'short' }) + ' '\n for (const ev of dayEvents) {\n if (result.length >= 3) break\n result.push({ event: ev, dayLabel: label, isToday: isActiveDay })\n }\n }\n return result\n }, [eventsByDay, activeDay, t])\n\n const dayLabels = getDayLabels(t)\n\n return (\n <div className=\"rounded-lg border border-border/60 p-4\">\n {/* Header */}\n <div className=\"mb-3 flex items-center justify-between\">\n <div className=\"flex items-center gap-2 text-sm font-medium\">\n <Calendar className=\"size-4 text-muted-foreground\" />\n {monthLabel}\n </div>\n <div className=\"flex items-center gap-1\">\n <IconButton type=\"button\" variant=\"ghost\" size=\"xs\" onClick={() => setWeekOffset((w) => w - 1)} aria-label={t('customers.calendar.previousWeek', 'Previous week')}>\n <ChevronLeft className=\"size-3.5\" />\n </IconButton>\n <IconButton type=\"button\" variant=\"ghost\" size=\"xs\" onClick={() => setWeekOffset((w) => w + 1)} aria-label={t('customers.calendar.nextWeek', 'Next week')}>\n <ChevronRight className=\"size-3.5\" />\n </IconButton>\n </div>\n </div>\n\n {/* Day headers */}\n <div className=\"grid grid-cols-7 mb-1\">\n {dayLabels.map((label) => (\n <div key={label} className=\"text-center text-xs font-medium text-muted-foreground\">\n {label}\n </div>\n ))}\n </div>\n\n {/* Day cells \u2014 bordered grid like Figma */}\n <div className=\"grid grid-cols-7 border rounded-lg overflow-hidden\">\n {weekDays.map((day) => {\n const isToday = isSameDay(day, today)\n const isSelected = selectedDay ? isSameDay(day, selectedDay) : isToday\n const dayEvents = eventsByDay.get(day.toDateString()) ?? []\n const hasEvents = dayEvents.length > 0\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n key={day.toISOString()}\n onClick={() => setSelectedDay(isSameDay(day, selectedDay ?? today) ? null : day)}\n className={cn(\n 'h-auto flex flex-col items-center border-r last:border-r-0 py-3 text-sm transition-colors cursor-pointer rounded-none',\n isSelected ? 'bg-foreground text-background font-bold' : 'hover:bg-accent/50',\n )}\n >\n <span className=\"text-base\">{day.getDate()}</span>\n {hasEvents && (\n <div className=\"mt-1 flex gap-0.5\">\n {dayEvents.slice(0, 3).map((ev, i) => (\n <span\n key={i}\n className={cn(\n 'size-1 rounded-full',\n dotColorForType(ev.interactionType, isSelected),\n )}\n />\n ))}\n </div>\n )}\n </Button>\n )\n })}\n </div>\n\n {/* Day summary \u2014 events for selected/active day + upcoming */}\n {summaryEvents.length > 0 && (\n <div className=\"mt-3 space-y-1.5 border-t pt-3\">\n {summaryEvents.map(({ event, dayLabel, isToday: isActiveDay }) => {\n const dateStr = event.scheduledAt ?? event.occurredAt ?? event.createdAt\n const time = dateStr ? new Date(dateStr).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }) : ''\n return (\n <div key={event.id} className=\"flex items-center gap-2 text-xs\">\n <span className={cn('size-1.5 rounded-full shrink-0', INTERACTION_TYPE_COLORS[event.interactionType] ?? (isActiveDay ? INTERACTION_TYPE_FALLBACK_COLOR : 'bg-muted-foreground'))} />\n <span className=\"font-semibold text-muted-foreground shrink-0\">\n {dayLabel}{time}\n </span>\n <span className=\"truncate\">{event.title ?? event.body ?? event.interactionType}</span>\n </div>\n )\n })}\n </div>\n )}\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -5,6 +5,7 @@ import { useT } from "@open-mercato/shared/lib/i18n/context";
5
5
  import { Button } from "@open-mercato/ui/primitives/button";
6
6
  import { IconButton } from "@open-mercato/ui/primitives/icon-button";
7
7
  import { Popover, PopoverContent, PopoverTrigger } from "@open-mercato/ui/primitives/popover";
8
+ import { isSameDay } from "../../lib/localDay.js";
8
9
  const TYPE_ICONS = {
9
10
  call: Phone,
10
11
  email: Mail,
@@ -208,7 +209,7 @@ function formatScheduledDate(isoString) {
208
209
  const dayName = date.toLocaleDateString(void 0, { weekday: "short" });
209
210
  const dateStr = date.toLocaleDateString(void 0, { day: "numeric", month: "short" });
210
211
  const timeStr = date.toLocaleTimeString(void 0, { hour: "2-digit", minute: "2-digit" });
211
- if (date.toDateString() === tomorrow.toDateString()) {
212
+ if (isSameDay(date, tomorrow)) {
212
213
  return `Tomorrow ${timeStr}`;
213
214
  }
214
215
  return `${dayName}, ${dateStr} \xB7 ${timeStr}`;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/customers/components/detail/PlannedActivitiesSection.tsx"],
4
- "sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { Phone, Mail, Users, StickyNote, Clock, AlertCircle, CalendarClock, Check, MoreHorizontal, Plus } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'\nimport type { InteractionSummary } from './types'\n\nconst TYPE_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {\n call: Phone,\n email: Mail,\n meeting: Users,\n note: StickyNote,\n}\n\ninterface PlannedActivitiesSectionProps {\n activities: InteractionSummary[]\n onComplete?: (id: string) => void\n onSchedule?: () => void\n onEdit?: (activity: InteractionSummary) => void\n onCancel?: (id: string) => void\n}\n\nexport function PlannedActivitiesSection({ activities, onComplete, onSchedule, onEdit, onCancel }: PlannedActivitiesSectionProps) {\n const t = useT()\n\n if (activities.length === 0) return null\n\n const now = new Date()\n\n const classified = activities.map((activity) => {\n const scheduledDate = activity.scheduledAt ? new Date(activity.scheduledAt) : null\n const isOverdue = scheduledDate ? scheduledDate < now : false\n return { ...activity, isOverdue }\n })\n\n const overdue = classified.filter((a) => a.isOverdue)\n const upcoming = classified.filter((a) => !a.isOverdue)\n\n return (\n <div className=\"rounded-lg border bg-card\">\n {/* Header */}\n <div className=\"flex items-center justify-between px-4 py-3 border-b\">\n <div className=\"flex items-center gap-2 text-sm font-medium\">\n <CalendarClock className=\"size-4 text-muted-foreground\" />\n {t('customers.timeline.planned.title', 'Planned activities')}\n <span className=\"rounded-full bg-muted px-1.5 py-0.5 text-xs font-medium text-muted-foreground\">\n {activities.length}\n </span>\n {overdue.length > 0 && (\n <span className=\"flex items-center gap-1 text-xs font-medium text-destructive\">\n <AlertCircle className=\"size-3\" />\n {overdue.length} {t('customers.timeline.planned.overdueCount', 'overdue')}\n </span>\n )}\n </div>\n {onSchedule && (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" className=\"h-7 text-xs\" onClick={onSchedule}>\n <Plus className=\"mr-1 size-3\" />\n {t('customers.timeline.planned.schedule', 'Schedule')}\n </Button>\n )}\n </div>\n\n {/* Items */}\n <div className=\"divide-y\">\n {overdue.map((activity) => {\n const TypeIcon = TYPE_ICONS[activity.interactionType]\n return (\n <div\n key={activity.id}\n className=\"flex items-center gap-3 border-l-2 border-l-destructive px-4 py-3 cursor-pointer hover:bg-accent/50 transition-colors\"\n onClick={() => onEdit?.(activity)}\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => { if (e.key === 'Enter') onEdit?.(activity) }}\n >\n <div className=\"flex items-center justify-center size-8 rounded-full bg-destructive/10 shrink-0\">\n {TypeIcon ? <TypeIcon className=\"size-4 text-destructive\" /> : <AlertCircle className=\"size-4 text-destructive\" />}\n </div>\n <div className=\"min-w-0 flex-1\">\n <span className=\"text-sm font-medium block truncate\">\n {activity.title ?? activity.body ?? activity.interactionType}\n </span>\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <span className=\"inline-flex items-center gap-1 font-medium text-destructive\">\n {t('customers.timeline.planned.overdue', 'Overdue')}\n </span>\n <span>\n {activity.scheduledAt ? formatRelativeOverdue(activity.scheduledAt, t) : ''}\n </span>\n {activity.authorName && (\n <>\n <span>\u00B7</span>\n <span>{activity.authorName}</span>\n </>\n )}\n </div>\n </div>\n {onComplete && (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-7 shrink-0 text-xs\"\n onClick={(event) => {\n event.stopPropagation()\n onComplete(activity.id)\n }}\n >\n <Check className=\"mr-1 size-3\" />\n {t('customers.timeline.planned.markDone', 'Mark done')}\n </Button>\n )}\n <Popover>\n <PopoverTrigger asChild>\n <IconButton type=\"button\" variant=\"ghost\" size=\"xs\" aria-label={t('customers.timeline.more', 'More')} onClick={(e) => e.stopPropagation()}>\n <MoreHorizontal className=\"size-3.5\" />\n </IconButton>\n </PopoverTrigger>\n <PopoverContent align=\"end\" className=\"w-40 p-1\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start text-xs\"\n onClick={(event) => {\n event.stopPropagation()\n onEdit?.(activity)\n }}\n >\n {t('customers.timeline.planned.reschedule', 'Reschedule')}\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start text-xs text-destructive hover:text-destructive\"\n onClick={(event) => {\n event.stopPropagation()\n onCancel?.(activity.id)\n }}\n >\n {t('customers.timeline.planned.cancel', 'Cancel')}\n </Button>\n </PopoverContent>\n </Popover>\n </div>\n )\n })}\n\n {upcoming.map((activity) => {\n const TypeIcon = TYPE_ICONS[activity.interactionType]\n return (\n <div\n key={activity.id}\n className=\"flex items-center gap-3 px-4 py-3 cursor-pointer hover:bg-accent/50 transition-colors\"\n onClick={() => onEdit?.(activity)}\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => { if (e.key === 'Enter') onEdit?.(activity) }}\n >\n <div className=\"flex items-center justify-center size-8 rounded-full bg-muted shrink-0\">\n {TypeIcon ? <TypeIcon className=\"size-4 text-muted-foreground\" /> : <Clock className=\"size-4 text-muted-foreground\" />}\n </div>\n <div className=\"min-w-0 flex-1\">\n <span className=\"text-sm font-medium block truncate\">\n {activity.title ?? activity.body ?? activity.interactionType}\n </span>\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <span>{activity.scheduledAt ? formatScheduledDate(activity.scheduledAt) : ''}</span>\n {activity.authorName && (\n <>\n <span>\u00B7</span>\n <span>{activity.authorName}</span>\n </>\n )}\n </div>\n </div>\n <Popover>\n <PopoverTrigger asChild>\n <IconButton type=\"button\" variant=\"ghost\" size=\"xs\" aria-label={t('customers.timeline.more', 'More')} onClick={(e) => e.stopPropagation()}>\n <MoreHorizontal className=\"size-3.5\" />\n </IconButton>\n </PopoverTrigger>\n <PopoverContent align=\"end\" className=\"w-40 p-1\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start text-xs\"\n onClick={(event) => {\n event.stopPropagation()\n onEdit?.(activity)\n }}\n >\n {t('customers.timeline.planned.reschedule', 'Reschedule')}\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start text-xs text-destructive hover:text-destructive\"\n onClick={(event) => {\n event.stopPropagation()\n onCancel?.(activity.id)\n }}\n >\n {t('customers.timeline.planned.cancel', 'Cancel')}\n </Button>\n </PopoverContent>\n </Popover>\n </div>\n )\n })}\n </div>\n </div>\n )\n}\n\nfunction formatRelativeOverdue(isoString: string, t: TranslateFn): string {\n try {\n const date = new Date(isoString)\n const now = new Date()\n const diffMs = now.getTime() - date.getTime()\n const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))\n if (diffDays <= 0) return t('customers.timeline.planned.overdueToday', 'due today')\n if (diffDays === 1) return t('customers.timeline.planned.overdueSince', 'since yesterday')\n return t('customers.timeline.planned.overdueDays', 'overdue {{days}} days', { days: diffDays })\n } catch {\n return ''\n }\n}\n\nfunction formatScheduledDate(isoString: string): string {\n try {\n const date = new Date(isoString)\n const now = new Date()\n const tomorrow = new Date(now)\n tomorrow.setDate(tomorrow.getDate() + 1)\n\n const dayName = date.toLocaleDateString(undefined, { weekday: 'short' })\n const dateStr = date.toLocaleDateString(undefined, { day: 'numeric', month: 'short' })\n const timeStr = date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })\n\n if (date.toDateString() === tomorrow.toDateString()) {\n return `Tomorrow ${timeStr}`\n }\n return `${dayName}, ${dateStr} \u00B7 ${timeStr}`\n } catch {\n return isoString\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { Phone, Mail, Users, StickyNote, Clock, AlertCircle, CalendarClock, Check, MoreHorizontal, Plus } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'\nimport type { InteractionSummary } from './types'\nimport { isSameDay } from '../../lib/localDay'\n\nconst TYPE_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {\n call: Phone,\n email: Mail,\n meeting: Users,\n note: StickyNote,\n}\n\ninterface PlannedActivitiesSectionProps {\n activities: InteractionSummary[]\n onComplete?: (id: string) => void\n onSchedule?: () => void\n onEdit?: (activity: InteractionSummary) => void\n onCancel?: (id: string) => void\n}\n\nexport function PlannedActivitiesSection({ activities, onComplete, onSchedule, onEdit, onCancel }: PlannedActivitiesSectionProps) {\n const t = useT()\n\n if (activities.length === 0) return null\n\n const now = new Date()\n\n const classified = activities.map((activity) => {\n const scheduledDate = activity.scheduledAt ? new Date(activity.scheduledAt) : null\n const isOverdue = scheduledDate ? scheduledDate < now : false\n return { ...activity, isOverdue }\n })\n\n const overdue = classified.filter((a) => a.isOverdue)\n const upcoming = classified.filter((a) => !a.isOverdue)\n\n return (\n <div className=\"rounded-lg border bg-card\">\n {/* Header */}\n <div className=\"flex items-center justify-between px-4 py-3 border-b\">\n <div className=\"flex items-center gap-2 text-sm font-medium\">\n <CalendarClock className=\"size-4 text-muted-foreground\" />\n {t('customers.timeline.planned.title', 'Planned activities')}\n <span className=\"rounded-full bg-muted px-1.5 py-0.5 text-xs font-medium text-muted-foreground\">\n {activities.length}\n </span>\n {overdue.length > 0 && (\n <span className=\"flex items-center gap-1 text-xs font-medium text-destructive\">\n <AlertCircle className=\"size-3\" />\n {overdue.length} {t('customers.timeline.planned.overdueCount', 'overdue')}\n </span>\n )}\n </div>\n {onSchedule && (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" className=\"h-7 text-xs\" onClick={onSchedule}>\n <Plus className=\"mr-1 size-3\" />\n {t('customers.timeline.planned.schedule', 'Schedule')}\n </Button>\n )}\n </div>\n\n {/* Items */}\n <div className=\"divide-y\">\n {overdue.map((activity) => {\n const TypeIcon = TYPE_ICONS[activity.interactionType]\n return (\n <div\n key={activity.id}\n className=\"flex items-center gap-3 border-l-2 border-l-destructive px-4 py-3 cursor-pointer hover:bg-accent/50 transition-colors\"\n onClick={() => onEdit?.(activity)}\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => { if (e.key === 'Enter') onEdit?.(activity) }}\n >\n <div className=\"flex items-center justify-center size-8 rounded-full bg-destructive/10 shrink-0\">\n {TypeIcon ? <TypeIcon className=\"size-4 text-destructive\" /> : <AlertCircle className=\"size-4 text-destructive\" />}\n </div>\n <div className=\"min-w-0 flex-1\">\n <span className=\"text-sm font-medium block truncate\">\n {activity.title ?? activity.body ?? activity.interactionType}\n </span>\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <span className=\"inline-flex items-center gap-1 font-medium text-destructive\">\n {t('customers.timeline.planned.overdue', 'Overdue')}\n </span>\n <span>\n {activity.scheduledAt ? formatRelativeOverdue(activity.scheduledAt, t) : ''}\n </span>\n {activity.authorName && (\n <>\n <span>\u00B7</span>\n <span>{activity.authorName}</span>\n </>\n )}\n </div>\n </div>\n {onComplete && (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-7 shrink-0 text-xs\"\n onClick={(event) => {\n event.stopPropagation()\n onComplete(activity.id)\n }}\n >\n <Check className=\"mr-1 size-3\" />\n {t('customers.timeline.planned.markDone', 'Mark done')}\n </Button>\n )}\n <Popover>\n <PopoverTrigger asChild>\n <IconButton type=\"button\" variant=\"ghost\" size=\"xs\" aria-label={t('customers.timeline.more', 'More')} onClick={(e) => e.stopPropagation()}>\n <MoreHorizontal className=\"size-3.5\" />\n </IconButton>\n </PopoverTrigger>\n <PopoverContent align=\"end\" className=\"w-40 p-1\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start text-xs\"\n onClick={(event) => {\n event.stopPropagation()\n onEdit?.(activity)\n }}\n >\n {t('customers.timeline.planned.reschedule', 'Reschedule')}\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start text-xs text-destructive hover:text-destructive\"\n onClick={(event) => {\n event.stopPropagation()\n onCancel?.(activity.id)\n }}\n >\n {t('customers.timeline.planned.cancel', 'Cancel')}\n </Button>\n </PopoverContent>\n </Popover>\n </div>\n )\n })}\n\n {upcoming.map((activity) => {\n const TypeIcon = TYPE_ICONS[activity.interactionType]\n return (\n <div\n key={activity.id}\n className=\"flex items-center gap-3 px-4 py-3 cursor-pointer hover:bg-accent/50 transition-colors\"\n onClick={() => onEdit?.(activity)}\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => { if (e.key === 'Enter') onEdit?.(activity) }}\n >\n <div className=\"flex items-center justify-center size-8 rounded-full bg-muted shrink-0\">\n {TypeIcon ? <TypeIcon className=\"size-4 text-muted-foreground\" /> : <Clock className=\"size-4 text-muted-foreground\" />}\n </div>\n <div className=\"min-w-0 flex-1\">\n <span className=\"text-sm font-medium block truncate\">\n {activity.title ?? activity.body ?? activity.interactionType}\n </span>\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <span>{activity.scheduledAt ? formatScheduledDate(activity.scheduledAt) : ''}</span>\n {activity.authorName && (\n <>\n <span>\u00B7</span>\n <span>{activity.authorName}</span>\n </>\n )}\n </div>\n </div>\n <Popover>\n <PopoverTrigger asChild>\n <IconButton type=\"button\" variant=\"ghost\" size=\"xs\" aria-label={t('customers.timeline.more', 'More')} onClick={(e) => e.stopPropagation()}>\n <MoreHorizontal className=\"size-3.5\" />\n </IconButton>\n </PopoverTrigger>\n <PopoverContent align=\"end\" className=\"w-40 p-1\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start text-xs\"\n onClick={(event) => {\n event.stopPropagation()\n onEdit?.(activity)\n }}\n >\n {t('customers.timeline.planned.reschedule', 'Reschedule')}\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start text-xs text-destructive hover:text-destructive\"\n onClick={(event) => {\n event.stopPropagation()\n onCancel?.(activity.id)\n }}\n >\n {t('customers.timeline.planned.cancel', 'Cancel')}\n </Button>\n </PopoverContent>\n </Popover>\n </div>\n )\n })}\n </div>\n </div>\n )\n}\n\nfunction formatRelativeOverdue(isoString: string, t: TranslateFn): string {\n try {\n const date = new Date(isoString)\n const now = new Date()\n const diffMs = now.getTime() - date.getTime()\n const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))\n if (diffDays <= 0) return t('customers.timeline.planned.overdueToday', 'due today')\n if (diffDays === 1) return t('customers.timeline.planned.overdueSince', 'since yesterday')\n return t('customers.timeline.planned.overdueDays', 'overdue {{days}} days', { days: diffDays })\n } catch {\n return ''\n }\n}\n\nfunction formatScheduledDate(isoString: string): string {\n try {\n const date = new Date(isoString)\n const now = new Date()\n const tomorrow = new Date(now)\n tomorrow.setDate(tomorrow.getDate() + 1)\n\n const dayName = date.toLocaleDateString(undefined, { weekday: 'short' })\n const dateStr = date.toLocaleDateString(undefined, { day: 'numeric', month: 'short' })\n const timeStr = date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })\n\n if (isSameDay(date, tomorrow)) {\n return `Tomorrow ${timeStr}`\n }\n return `${dayName}, ${dateStr} \u00B7 ${timeStr}`\n } catch {\n return isoString\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -14,6 +14,7 @@ import {
14
14
  SelectValue
15
15
  } from "@open-mercato/ui/primitives/select";
16
16
  import { isVisible, getFieldLabel } from "./fieldConfig.js";
17
+ import { USER_TIMEZONE } from "../../../lib/localDay.js";
17
18
  function parseIsoDate(value) {
18
19
  if (!value) return null;
19
20
  const parts = value.split("-");
@@ -143,7 +144,7 @@ function DateTimeFields({
143
144
  /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "\xB7" }),
144
145
  /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5", children: [
145
146
  /* @__PURE__ */ jsx(Globe, { className: "size-3.5" }),
146
- Intl.DateTimeFormat().resolvedOptions().timeZone,
147
+ USER_TIMEZONE,
147
148
  " (GMT",
148
149
  (/* @__PURE__ */ new Date()).getTimezoneOffset() <= 0 ? "+" : "-",
149
150
  String(Math.abs(Math.floor((/* @__PURE__ */ new Date()).getTimezoneOffset() / 60))).padStart(1, "0"),
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/customers/components/detail/schedule/DateTimeFields.tsx"],
4
- "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { Globe, Repeat } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { DatePicker } from '@open-mercato/ui/primitives/date-picker'\nimport { TimePicker } from '@open-mercato/ui/backend/inputs/TimePicker'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport type { ActivityType, ScheduleFieldId } from './fieldConfig'\nimport { isVisible, getFieldLabel } from './fieldConfig'\n\nfunction parseIsoDate(value: string): Date | null {\n if (!value) return null\n const parts = value.split('-')\n if (parts.length !== 3) return null\n const [y, m, d] = parts.map((p) => parseInt(p, 10))\n if (!Number.isFinite(y) || !Number.isFinite(m) || !Number.isFinite(d)) return null\n const date = new Date(y, m - 1, d)\n return Number.isNaN(date.getTime()) ? null : date\n}\n\nfunction formatIsoDate(date: Date | null): string {\n if (!date) return ''\n const y = date.getFullYear()\n const m = String(date.getMonth() + 1).padStart(2, '0')\n const d = String(date.getDate()).padStart(2, '0')\n return `${y}-${m}-${d}`\n}\n\nconst DURATION_OPTIONS: Array<{ value: number; key: string; fallback: string }> = [\n { value: 15, key: 'customers.schedule.duration.option.15min', fallback: '15 min' },\n { value: 30, key: 'customers.schedule.duration.option.30min', fallback: '30 min' },\n { value: 45, key: 'customers.schedule.duration.option.45min', fallback: '45 min' },\n { value: 60, key: 'customers.schedule.duration.option.1hour', fallback: '1 hour' },\n { value: 90, key: 'customers.schedule.duration.option.1h30m', fallback: '1h 30m' },\n { value: 120, key: 'customers.schedule.duration.option.2hours', fallback: '2 hours' },\n]\nconst DAYS_OF_WEEK = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']\n\ninterface DateTimeFieldsProps {\n visible: Set<ScheduleFieldId>\n activityType: ActivityType\n date: string\n setDate: (value: string) => void\n startTime: string\n setStartTime: (value: string) => void\n duration: number\n setDuration: (value: number) => void\n allDay: boolean\n setAllDay: (value: boolean) => void\n recurrenceEnabled: boolean\n setRecurrenceEnabled: (value: boolean) => void\n recurrenceDays: boolean[]\n toggleRecurrenceDay: (index: number) => void\n recurrenceEndType: 'never' | 'count' | 'date'\n setRecurrenceEndType: (value: 'never' | 'count' | 'date') => void\n recurrenceCount: number\n setRecurrenceCount: (value: number) => void\n recurrenceEndDate: string\n setRecurrenceEndDate: (value: string) => void\n}\n\nexport function DateTimeFields({\n visible,\n activityType,\n date,\n setDate,\n startTime,\n setStartTime,\n duration,\n setDuration,\n allDay,\n setAllDay,\n recurrenceEnabled,\n setRecurrenceEnabled,\n recurrenceDays,\n toggleRecurrenceDay,\n recurrenceEndType,\n setRecurrenceEndType,\n recurrenceCount,\n setRecurrenceCount,\n recurrenceEndDate,\n setRecurrenceEndDate,\n}: DateTimeFieldsProps) {\n const t = useT()\n\n if (!visible.has('date')) return null\n\n const showStartTime = isVisible(activityType, 'startTime')\n const showDuration = isVisible(activityType, 'duration')\n const showAllDay = isVisible(activityType, 'allDay')\n const showRecurrence = isVisible(activityType, 'recurrence')\n\n const dateMissing = !date.trim()\n const timeMissing = showStartTime && !allDay && !startTime.trim()\n const dateErrorId = 'schedule-date-error'\n const timeErrorId = 'schedule-time-error'\n\n return (\n <>\n {/* Date / Time / Duration */}\n <div className=\"flex flex-wrap gap-3\">\n <div className=\"flex min-w-0 flex-[1.5] flex-col gap-1.5\">\n <label className=\"text-overline font-semibold text-muted-foreground tracking-wider\">\n {getFieldLabel(activityType, 'date', t, 'customers.schedule.date', 'Date')}\n <span aria-hidden=\"true\" className=\"ml-1 text-status-error-foreground\">*</span>\n </label>\n <DatePicker\n value={parseIsoDate(date)}\n onChange={(next) => setDate(formatIsoDate(next))}\n placeholder={t('customers.schedule.date.placeholder', 'Pick a date')}\n required\n aria-describedby={dateMissing ? dateErrorId : undefined}\n className={cn(\n 'h-10',\n dateMissing && 'border-status-error-border',\n )}\n />\n {dateMissing ? (\n <p id={dateErrorId} className=\"text-xs text-status-error-foreground\">\n {t('customers.activities.errors.dateRequired', 'Date is required')}\n </p>\n ) : null}\n </div>\n {showStartTime && (\n <div className=\"flex min-w-0 flex-1 flex-col gap-1.5\">\n <label className=\"text-overline font-semibold text-muted-foreground tracking-wider\">\n {getFieldLabel(activityType, 'startTime', t, 'customers.schedule.start', 'Start')}\n <span aria-hidden=\"true\" className=\"ml-1 text-status-error-foreground\">*</span>\n </label>\n <TimePicker\n value={startTime || null}\n onChange={(next) => setStartTime(next ?? '')}\n disabled={allDay}\n placeholder={t('customers.schedule.start.placeholder', 'Pick a time')}\n className={cn(\n 'py-2.5',\n timeMissing ? 'border-status-error-border' : undefined,\n )}\n showNowButton\n showClearButton={false}\n />\n {timeMissing ? (\n <p id={timeErrorId} className=\"text-xs text-status-error-foreground\">\n {t('customers.activities.errors.timeRequired', 'Time is required')}\n </p>\n ) : null}\n </div>\n )}\n {showDuration && (\n <div className=\"flex min-w-0 flex-1 flex-col gap-1.5\">\n <label className=\"text-overline font-semibold text-muted-foreground tracking-wider\">\n {getFieldLabel(activityType, 'duration', t, 'customers.schedule.duration', 'Duration')}\n </label>\n <Select\n value={String(duration)}\n onValueChange={(next) => {\n const parsed = Number(next)\n if (Number.isFinite(parsed)) setDuration(parsed)\n }}\n disabled={allDay}\n >\n <SelectTrigger className=\"h-10\">\n <SelectValue placeholder={t('customers.schedule.duration.placeholder', 'Pick duration')} />\n </SelectTrigger>\n <SelectContent>\n {DURATION_OPTIONS.map((option) => (\n <SelectItem key={option.value} value={String(option.value)}>\n {t(option.key, option.fallback)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n )}\n </div>\n\n {/* All day + timezone + recurrence */}\n {showAllDay && (\n <div className=\"flex flex-wrap items-center gap-3.5 text-xs text-muted-foreground\">\n <label className=\"flex items-center gap-2 cursor-pointer\">\n <input type=\"checkbox\" checked={allDay} onChange={(e) => setAllDay(e.target.checked)} className=\"rounded\" />\n {t('customers.schedule.allDay', 'All day')}\n </label>\n <span className=\"text-muted-foreground\">&middot;</span>\n <span className=\"flex items-center gap-1.5\">\n <Globe className=\"size-3.5\" />\n {Intl.DateTimeFormat().resolvedOptions().timeZone} (GMT{new Date().getTimezoneOffset() <= 0 ? '+' : '-'}{String(Math.abs(Math.floor(new Date().getTimezoneOffset() / 60))).padStart(1, '0')})\n </span>\n {showRecurrence && (\n <>\n <span className=\"text-muted-foreground\">&middot;</span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => setRecurrenceEnabled(!recurrenceEnabled)}\n className={cn('h-auto flex items-center gap-1.5', recurrenceEnabled && 'font-medium text-foreground')}\n >\n <Repeat className=\"size-3.5\" />\n {recurrenceEnabled\n ? t('customers.schedule.recurrence.active', 'Repeats')\n : t('customers.schedule.recurrence.none', 'No repeat')}\n </Button>\n </>\n )}\n </div>\n )}\n\n {/* Recurrence config */}\n {showRecurrence && recurrenceEnabled && (\n <div className=\"rounded-lg border border-status-warning-border bg-status-warning-bg p-4 space-y-3\">\n <div className=\"flex items-center justify-between\">\n <span className=\"flex items-center gap-2 text-sm font-semibold text-foreground\">\n <Repeat className=\"size-3.5\" />\n {t('customers.schedule.recurrence.title', 'Recurrence')}\n </span>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" className=\"h-auto text-xs font-medium text-foreground\">\n {t('customers.schedule.recurrence.edit', 'Edit')}\n </Button>\n </div>\n <div className=\"flex gap-2\">\n {DAYS_OF_WEEK.map((day, i) => (\n <Button\n key={day}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => toggleRecurrenceDay(i)}\n className={cn(\n 'h-auto flex size-8 items-center justify-center rounded-full text-xs font-medium transition-colors p-0',\n recurrenceDays[i] ? 'bg-primary text-primary-foreground' : 'border border-border bg-background text-muted-foreground hover:bg-muted',\n )}\n >\n {day.slice(0, 2)}\n </Button>\n ))}\n </div>\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <span>{t('customers.schedule.recurrence.ends', 'Ends')}:</span>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => setRecurrenceEndType('never')} className={cn('h-auto rounded-full px-3 py-1 text-xs font-medium', recurrenceEndType === 'never' ? 'bg-background border border-border text-foreground' : 'text-muted-foreground')}>\n {t('customers.schedule.recurrence.never', 'Never')}\n </Button>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => setRecurrenceEndType('count')} className={cn('h-auto rounded-full px-3 py-1 text-xs font-medium', recurrenceEndType === 'count' ? 'bg-primary text-primary-foreground' : 'text-muted-foreground')}>\n {t('customers.schedule.recurrence.afterCount', 'After {{count}} occurrences', { count: recurrenceCount })}\n </Button>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => setRecurrenceEndType('date')} className={cn('h-auto rounded-full px-3 py-1 text-xs font-medium', recurrenceEndType === 'date' ? 'bg-background border border-border text-foreground' : 'text-muted-foreground')}>\n {recurrenceEndDate || t('customers.schedule.recurrence.onDate', 'On date')}\n </Button>\n </div>\n </div>\n )}\n </>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { Globe, Repeat } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { DatePicker } from '@open-mercato/ui/primitives/date-picker'\nimport { TimePicker } from '@open-mercato/ui/backend/inputs/TimePicker'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport type { ActivityType, ScheduleFieldId } from './fieldConfig'\nimport { isVisible, getFieldLabel } from './fieldConfig'\nimport { USER_TIMEZONE } from '../../../lib/localDay'\n\nfunction parseIsoDate(value: string): Date | null {\n if (!value) return null\n const parts = value.split('-')\n if (parts.length !== 3) return null\n const [y, m, d] = parts.map((p) => parseInt(p, 10))\n if (!Number.isFinite(y) || !Number.isFinite(m) || !Number.isFinite(d)) return null\n const date = new Date(y, m - 1, d)\n return Number.isNaN(date.getTime()) ? null : date\n}\n\nfunction formatIsoDate(date: Date | null): string {\n if (!date) return ''\n const y = date.getFullYear()\n const m = String(date.getMonth() + 1).padStart(2, '0')\n const d = String(date.getDate()).padStart(2, '0')\n return `${y}-${m}-${d}`\n}\n\nconst DURATION_OPTIONS: Array<{ value: number; key: string; fallback: string }> = [\n { value: 15, key: 'customers.schedule.duration.option.15min', fallback: '15 min' },\n { value: 30, key: 'customers.schedule.duration.option.30min', fallback: '30 min' },\n { value: 45, key: 'customers.schedule.duration.option.45min', fallback: '45 min' },\n { value: 60, key: 'customers.schedule.duration.option.1hour', fallback: '1 hour' },\n { value: 90, key: 'customers.schedule.duration.option.1h30m', fallback: '1h 30m' },\n { value: 120, key: 'customers.schedule.duration.option.2hours', fallback: '2 hours' },\n]\nconst DAYS_OF_WEEK = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']\n\ninterface DateTimeFieldsProps {\n visible: Set<ScheduleFieldId>\n activityType: ActivityType\n date: string\n setDate: (value: string) => void\n startTime: string\n setStartTime: (value: string) => void\n duration: number\n setDuration: (value: number) => void\n allDay: boolean\n setAllDay: (value: boolean) => void\n recurrenceEnabled: boolean\n setRecurrenceEnabled: (value: boolean) => void\n recurrenceDays: boolean[]\n toggleRecurrenceDay: (index: number) => void\n recurrenceEndType: 'never' | 'count' | 'date'\n setRecurrenceEndType: (value: 'never' | 'count' | 'date') => void\n recurrenceCount: number\n setRecurrenceCount: (value: number) => void\n recurrenceEndDate: string\n setRecurrenceEndDate: (value: string) => void\n}\n\nexport function DateTimeFields({\n visible,\n activityType,\n date,\n setDate,\n startTime,\n setStartTime,\n duration,\n setDuration,\n allDay,\n setAllDay,\n recurrenceEnabled,\n setRecurrenceEnabled,\n recurrenceDays,\n toggleRecurrenceDay,\n recurrenceEndType,\n setRecurrenceEndType,\n recurrenceCount,\n setRecurrenceCount,\n recurrenceEndDate,\n setRecurrenceEndDate,\n}: DateTimeFieldsProps) {\n const t = useT()\n\n if (!visible.has('date')) return null\n\n const showStartTime = isVisible(activityType, 'startTime')\n const showDuration = isVisible(activityType, 'duration')\n const showAllDay = isVisible(activityType, 'allDay')\n const showRecurrence = isVisible(activityType, 'recurrence')\n\n const dateMissing = !date.trim()\n const timeMissing = showStartTime && !allDay && !startTime.trim()\n const dateErrorId = 'schedule-date-error'\n const timeErrorId = 'schedule-time-error'\n\n return (\n <>\n {/* Date / Time / Duration */}\n <div className=\"flex flex-wrap gap-3\">\n <div className=\"flex min-w-0 flex-[1.5] flex-col gap-1.5\">\n <label className=\"text-overline font-semibold text-muted-foreground tracking-wider\">\n {getFieldLabel(activityType, 'date', t, 'customers.schedule.date', 'Date')}\n <span aria-hidden=\"true\" className=\"ml-1 text-status-error-foreground\">*</span>\n </label>\n <DatePicker\n value={parseIsoDate(date)}\n onChange={(next) => setDate(formatIsoDate(next))}\n placeholder={t('customers.schedule.date.placeholder', 'Pick a date')}\n required\n aria-describedby={dateMissing ? dateErrorId : undefined}\n className={cn(\n 'h-10',\n dateMissing && 'border-status-error-border',\n )}\n />\n {dateMissing ? (\n <p id={dateErrorId} className=\"text-xs text-status-error-foreground\">\n {t('customers.activities.errors.dateRequired', 'Date is required')}\n </p>\n ) : null}\n </div>\n {showStartTime && (\n <div className=\"flex min-w-0 flex-1 flex-col gap-1.5\">\n <label className=\"text-overline font-semibold text-muted-foreground tracking-wider\">\n {getFieldLabel(activityType, 'startTime', t, 'customers.schedule.start', 'Start')}\n <span aria-hidden=\"true\" className=\"ml-1 text-status-error-foreground\">*</span>\n </label>\n <TimePicker\n value={startTime || null}\n onChange={(next) => setStartTime(next ?? '')}\n disabled={allDay}\n placeholder={t('customers.schedule.start.placeholder', 'Pick a time')}\n className={cn(\n 'py-2.5',\n timeMissing ? 'border-status-error-border' : undefined,\n )}\n showNowButton\n showClearButton={false}\n />\n {timeMissing ? (\n <p id={timeErrorId} className=\"text-xs text-status-error-foreground\">\n {t('customers.activities.errors.timeRequired', 'Time is required')}\n </p>\n ) : null}\n </div>\n )}\n {showDuration && (\n <div className=\"flex min-w-0 flex-1 flex-col gap-1.5\">\n <label className=\"text-overline font-semibold text-muted-foreground tracking-wider\">\n {getFieldLabel(activityType, 'duration', t, 'customers.schedule.duration', 'Duration')}\n </label>\n <Select\n value={String(duration)}\n onValueChange={(next) => {\n const parsed = Number(next)\n if (Number.isFinite(parsed)) setDuration(parsed)\n }}\n disabled={allDay}\n >\n <SelectTrigger className=\"h-10\">\n <SelectValue placeholder={t('customers.schedule.duration.placeholder', 'Pick duration')} />\n </SelectTrigger>\n <SelectContent>\n {DURATION_OPTIONS.map((option) => (\n <SelectItem key={option.value} value={String(option.value)}>\n {t(option.key, option.fallback)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n )}\n </div>\n\n {/* All day + timezone + recurrence */}\n {showAllDay && (\n <div className=\"flex flex-wrap items-center gap-3.5 text-xs text-muted-foreground\">\n <label className=\"flex items-center gap-2 cursor-pointer\">\n <input type=\"checkbox\" checked={allDay} onChange={(e) => setAllDay(e.target.checked)} className=\"rounded\" />\n {t('customers.schedule.allDay', 'All day')}\n </label>\n <span className=\"text-muted-foreground\">&middot;</span>\n <span className=\"flex items-center gap-1.5\">\n <Globe className=\"size-3.5\" />\n {USER_TIMEZONE} (GMT{new Date().getTimezoneOffset() <= 0 ? '+' : '-'}{String(Math.abs(Math.floor(new Date().getTimezoneOffset() / 60))).padStart(1, '0')})\n </span>\n {showRecurrence && (\n <>\n <span className=\"text-muted-foreground\">&middot;</span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => setRecurrenceEnabled(!recurrenceEnabled)}\n className={cn('h-auto flex items-center gap-1.5', recurrenceEnabled && 'font-medium text-foreground')}\n >\n <Repeat className=\"size-3.5\" />\n {recurrenceEnabled\n ? t('customers.schedule.recurrence.active', 'Repeats')\n : t('customers.schedule.recurrence.none', 'No repeat')}\n </Button>\n </>\n )}\n </div>\n )}\n\n {/* Recurrence config */}\n {showRecurrence && recurrenceEnabled && (\n <div className=\"rounded-lg border border-status-warning-border bg-status-warning-bg p-4 space-y-3\">\n <div className=\"flex items-center justify-between\">\n <span className=\"flex items-center gap-2 text-sm font-semibold text-foreground\">\n <Repeat className=\"size-3.5\" />\n {t('customers.schedule.recurrence.title', 'Recurrence')}\n </span>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" className=\"h-auto text-xs font-medium text-foreground\">\n {t('customers.schedule.recurrence.edit', 'Edit')}\n </Button>\n </div>\n <div className=\"flex gap-2\">\n {DAYS_OF_WEEK.map((day, i) => (\n <Button\n key={day}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => toggleRecurrenceDay(i)}\n className={cn(\n 'h-auto flex size-8 items-center justify-center rounded-full text-xs font-medium transition-colors p-0',\n recurrenceDays[i] ? 'bg-primary text-primary-foreground' : 'border border-border bg-background text-muted-foreground hover:bg-muted',\n )}\n >\n {day.slice(0, 2)}\n </Button>\n ))}\n </div>\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <span>{t('customers.schedule.recurrence.ends', 'Ends')}:</span>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => setRecurrenceEndType('never')} className={cn('h-auto rounded-full px-3 py-1 text-xs font-medium', recurrenceEndType === 'never' ? 'bg-background border border-border text-foreground' : 'text-muted-foreground')}>\n {t('customers.schedule.recurrence.never', 'Never')}\n </Button>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => setRecurrenceEndType('count')} className={cn('h-auto rounded-full px-3 py-1 text-xs font-medium', recurrenceEndType === 'count' ? 'bg-primary text-primary-foreground' : 'text-muted-foreground')}>\n {t('customers.schedule.recurrence.afterCount', 'After {{count}} occurrences', { count: recurrenceCount })}\n </Button>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => setRecurrenceEndType('date')} className={cn('h-auto rounded-full px-3 py-1 text-xs font-medium', recurrenceEndType === 'date' ? 'bg-background border border-border text-foreground' : 'text-muted-foreground')}>\n {recurrenceEndDate || t('customers.schedule.recurrence.onDate', 'On date')}\n </Button>\n </div>\n </div>\n )}\n </>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,20 @@
1
+ import { toZonedTime } from "date-fns-tz";
2
+ const USER_TIMEZONE = (() => {
3
+ try {
4
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
5
+ } catch {
6
+ return "UTC";
7
+ }
8
+ })();
9
+ function toLocalZonedDate(value) {
10
+ return toZonedTime(value, USER_TIMEZONE);
11
+ }
12
+ function isSameDay(a, b) {
13
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
14
+ }
15
+ export {
16
+ USER_TIMEZONE,
17
+ isSameDay,
18
+ toLocalZonedDate
19
+ };
20
+ //# sourceMappingURL=localDay.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/customers/lib/localDay.ts"],
4
+ "sourcesContent": ["// Single source of truth for \"what timezone is the reader in, and is this the same\n// calendar day\" across the customers detail components.\n//\n// Every activity surface in `components/detail/` answers the same two questions, and\n// until issue #6011 each answered them with its own byte-identical copy. They are kept\n// together here because they are one decision, not two: `isSameDay` is only meaningful\n// once both operands have been projected into the same zone, which is what\n// `toLocalZonedDate` does with `USER_TIMEZONE`.\n//\n// The zone is deliberately the reader's own runtime zone. Host/tenant-configured\n// display timezones are planned in `.ai/specs/2026-05-18-date-locale-settings.md`\n// (issue #1964); when that lands, threading a configured zone through the detail\n// components is an edit to this file rather than a hunt through every component.\n\nimport { toZonedTime } from 'date-fns-tz'\n\nexport const USER_TIMEZONE = (() => {\n try {\n return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'\n } catch {\n return 'UTC'\n }\n})()\n\n// Project a UTC instant to the user's local timezone before extracting day/month/year\n// for \"same day\" comparisons (issue #1809 \u2014 E3 timezone drift). The browser's\n// `new Date(iso)` treats the instant correctly, but `getDate()/getMonth()/getFullYear()`\n// reflect the user's local day, so an activity scheduled at e.g. 23:30 local on a UTC\n// boundary lands on the day the reader would call it.\nexport function toLocalZonedDate(value: string | Date): Date {\n return toZonedTime(value, USER_TIMEZONE)\n}\n\nexport function isSameDay(a: Date, b: Date): boolean {\n return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()\n}\n"],
5
+ "mappings": "AAcA,SAAS,mBAAmB;AAErB,MAAM,iBAAiB,MAAM;AAClC,MAAI;AACF,WAAO,KAAK,eAAe,EAAE,gBAAgB,EAAE,YAAY;AAAA,EAC7D,QAAQ;AACN,WAAO;AAAA,EACT;AACF,GAAG;AAOI,SAAS,iBAAiB,OAA4B;AAC3D,SAAO,YAAY,OAAO,aAAa;AACzC;AAEO,SAAS,UAAU,GAAS,GAAkB;AACnD,SAAO,EAAE,YAAY,MAAM,EAAE,YAAY,KAAK,EAAE,SAAS,MAAM,EAAE,SAAS,KAAK,EAAE,QAAQ,MAAM,EAAE,QAAQ;AAC3G;",
6
+ "names": []
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.7.1-develop.7201.1.9bd6c88e3d",
3
+ "version": "0.7.1-develop.7203.1.9396450b78",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -253,16 +253,16 @@
253
253
  "zod": "^4.4.3"
254
254
  },
255
255
  "peerDependencies": {
256
- "@open-mercato/ai-assistant": "0.7.1-develop.7201.1.9bd6c88e3d",
257
- "@open-mercato/shared": "0.7.1-develop.7201.1.9bd6c88e3d",
258
- "@open-mercato/ui": "0.7.1-develop.7201.1.9bd6c88e3d",
256
+ "@open-mercato/ai-assistant": "0.7.1-develop.7203.1.9396450b78",
257
+ "@open-mercato/shared": "0.7.1-develop.7203.1.9396450b78",
258
+ "@open-mercato/ui": "0.7.1-develop.7203.1.9396450b78",
259
259
  "react": "^19.0.0",
260
260
  "react-dom": "^19.0.0"
261
261
  },
262
262
  "devDependencies": {
263
- "@open-mercato/ai-assistant": "0.7.1-develop.7201.1.9bd6c88e3d",
264
- "@open-mercato/shared": "0.7.1-develop.7201.1.9bd6c88e3d",
265
- "@open-mercato/ui": "0.7.1-develop.7201.1.9bd6c88e3d",
263
+ "@open-mercato/ai-assistant": "0.7.1-develop.7203.1.9396450b78",
264
+ "@open-mercato/shared": "0.7.1-develop.7203.1.9396450b78",
265
+ "@open-mercato/ui": "0.7.1-develop.7203.1.9396450b78",
266
266
  "@testing-library/dom": "^10.4.1",
267
267
  "@testing-library/jest-dom": "^7.0.1",
268
268
  "@testing-library/react": "^16.3.3",
@@ -2,7 +2,6 @@
2
2
 
3
3
  import * as React from 'react'
4
4
  import { Calendar, CalendarClock, Clock, Mail, Phone, StickyNote, Users } from 'lucide-react'
5
- import { toZonedTime } from 'date-fns-tz'
6
5
  import { cn } from '@open-mercato/shared/lib/utils'
7
6
  import { useT } from '@open-mercato/shared/lib/i18n/context'
8
7
  import type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'
@@ -11,6 +10,7 @@ import { ActivitiesDayStrip } from './ActivitiesDayStrip'
11
10
  import { ActivitiesAddNewMenu, type ActivityKind } from './ActivitiesAddNewMenu'
12
11
  import type { InteractionSummary } from './types'
13
12
  import { isOpenInteractionStatus } from '../../lib/interactionStatus'
13
+ import { isSameDay, toLocalZonedDate } from '../../lib/localDay'
14
14
  import { createLogger } from '@open-mercato/shared/lib/logger'
15
15
 
16
16
  const logger = createLogger('customers')
@@ -43,30 +43,12 @@ const TYPE_ICONS: Record<string, React.ComponentType<{ className?: string }>> =
43
43
  note: StickyNote,
44
44
  }
45
45
 
46
- const USER_TIMEZONE = (() => {
47
- try {
48
- return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
49
- } catch {
50
- return 'UTC'
51
- }
52
- })()
53
-
54
- // Project a UTC instant to the user's local timezone before extracting day/month/year
55
- // for "same day" comparisons (issue #1809 — E3 timezone drift).
56
- function toLocalZonedDate(value: string | Date): Date {
57
- return toZonedTime(value, USER_TIMEZONE)
58
- }
59
-
60
46
  function startOfDay(date: Date): Date {
61
47
  const next = new Date(date)
62
48
  next.setHours(0, 0, 0, 0)
63
49
  return next
64
50
  }
65
51
 
66
- function isSameDay(a: Date, b: Date): boolean {
67
- return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
68
- }
69
-
70
52
  function isOverdue(activity: InteractionSummary, now: Date): boolean {
71
53
  const scheduled = activity.scheduledAt ?? activity.occurredAt
72
54
  if (!scheduled) return false
@@ -2,12 +2,12 @@
2
2
 
3
3
  import * as React from 'react'
4
4
  import { ChevronLeft, ChevronRight } from 'lucide-react'
5
- import { toZonedTime } from 'date-fns-tz'
6
5
  import { cn } from '@open-mercato/shared/lib/utils'
7
6
  import { useT } from '@open-mercato/shared/lib/i18n/context'
8
7
  import type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'
9
8
  import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
10
9
  import type { InteractionSummary } from './types'
10
+ import { isSameDay, toLocalZonedDate } from '../../lib/localDay'
11
11
  import { createLogger } from '@open-mercato/shared/lib/logger'
12
12
 
13
13
  const logger = createLogger('customers')
@@ -25,23 +25,6 @@ interface ActivitiesDayStripProps {
25
25
  events?: InteractionSummary[]
26
26
  }
27
27
 
28
- const USER_TIMEZONE = (() => {
29
- try {
30
- return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
31
- } catch {
32
- return 'UTC'
33
- }
34
- })()
35
-
36
- // Project a UTC ISO timestamp to the user's local timezone before comparing
37
- // "same day" (issue #1809 — E3). The browser's `new Date(iso)` treats the
38
- // instant correctly, but `getDate()/getMonth()/getFullYear()` reflect the
39
- // user's local day, so for activities scheduled at e.g. 23:30 local on a UTC
40
- // boundary the day-strip and list now agree.
41
- function toLocalZonedDate(value: string | Date): Date {
42
- return toZonedTime(value, USER_TIMEZONE)
43
- }
44
-
45
28
  const VISIBLE_DAYS = 5
46
29
  const BUSYNESS_SLOTS = 10
47
30
  const SLOT_START_HOUR = 7
@@ -84,10 +67,6 @@ function endOfDay(date: Date): Date {
84
67
  return next
85
68
  }
86
69
 
87
- function isSameDay(a: Date, b: Date): boolean {
88
- return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
89
- }
90
-
91
70
  function isWeekend(date: Date): boolean {
92
71
  const day = date.getDay()
93
72
  return day === 0 || day === 6
@@ -9,6 +9,7 @@ import { Button } from '@open-mercato/ui/primitives/button'
9
9
  import { IconButton } from '@open-mercato/ui/primitives/icon-button'
10
10
  import type { InteractionSummary } from './types'
11
11
  import { loadLegacyActivitiesInRange } from './legacyActivities'
12
+ import { isSameDay } from '../../lib/localDay'
12
13
 
13
14
  interface MiniWeekCalendarProps {
14
15
  entityId: string
@@ -30,10 +31,6 @@ function getWeekDays(baseDate: Date): Date[] {
30
31
  return days
31
32
  }
32
33
 
33
- function isSameDay(a: Date, b: Date): boolean {
34
- return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
35
- }
36
-
37
34
  function getDayLabels(t: ReturnType<typeof useT>): string[] {
38
35
  return [
39
36
  t('customers.calendar.day.mon', 'MON'),
@@ -7,6 +7,7 @@ import { Button } from '@open-mercato/ui/primitives/button'
7
7
  import { IconButton } from '@open-mercato/ui/primitives/icon-button'
8
8
  import { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'
9
9
  import type { InteractionSummary } from './types'
10
+ import { isSameDay } from '../../lib/localDay'
10
11
 
11
12
  const TYPE_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
12
13
  call: Phone,
@@ -245,7 +246,7 @@ function formatScheduledDate(isoString: string): string {
245
246
  const dateStr = date.toLocaleDateString(undefined, { day: 'numeric', month: 'short' })
246
247
  const timeStr = date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })
247
248
 
248
- if (date.toDateString() === tomorrow.toDateString()) {
249
+ if (isSameDay(date, tomorrow)) {
249
250
  return `Tomorrow ${timeStr}`
250
251
  }
251
252
  return `${dayName}, ${dateStr} · ${timeStr}`
@@ -16,6 +16,7 @@ import {
16
16
  } from '@open-mercato/ui/primitives/select'
17
17
  import type { ActivityType, ScheduleFieldId } from './fieldConfig'
18
18
  import { isVisible, getFieldLabel } from './fieldConfig'
19
+ import { USER_TIMEZONE } from '../../../lib/localDay'
19
20
 
20
21
  function parseIsoDate(value: string): Date | null {
21
22
  if (!value) return null
@@ -193,7 +194,7 @@ export function DateTimeFields({
193
194
  <span className="text-muted-foreground">&middot;</span>
194
195
  <span className="flex items-center gap-1.5">
195
196
  <Globe className="size-3.5" />
196
- {Intl.DateTimeFormat().resolvedOptions().timeZone} (GMT{new Date().getTimezoneOffset() <= 0 ? '+' : '-'}{String(Math.abs(Math.floor(new Date().getTimezoneOffset() / 60))).padStart(1, '0')})
197
+ {USER_TIMEZONE} (GMT{new Date().getTimezoneOffset() <= 0 ? '+' : '-'}{String(Math.abs(Math.floor(new Date().getTimezoneOffset() / 60))).padStart(1, '0')})
197
198
  </span>
198
199
  {showRecurrence && (
199
200
  <>
@@ -0,0 +1,36 @@
1
+ // Single source of truth for "what timezone is the reader in, and is this the same
2
+ // calendar day" across the customers detail components.
3
+ //
4
+ // Every activity surface in `components/detail/` answers the same two questions, and
5
+ // until issue #6011 each answered them with its own byte-identical copy. They are kept
6
+ // together here because they are one decision, not two: `isSameDay` is only meaningful
7
+ // once both operands have been projected into the same zone, which is what
8
+ // `toLocalZonedDate` does with `USER_TIMEZONE`.
9
+ //
10
+ // The zone is deliberately the reader's own runtime zone. Host/tenant-configured
11
+ // display timezones are planned in `.ai/specs/2026-05-18-date-locale-settings.md`
12
+ // (issue #1964); when that lands, threading a configured zone through the detail
13
+ // components is an edit to this file rather than a hunt through every component.
14
+
15
+ import { toZonedTime } from 'date-fns-tz'
16
+
17
+ export const USER_TIMEZONE = (() => {
18
+ try {
19
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
20
+ } catch {
21
+ return 'UTC'
22
+ }
23
+ })()
24
+
25
+ // Project a UTC instant to the user's local timezone before extracting day/month/year
26
+ // for "same day" comparisons (issue #1809 — E3 timezone drift). The browser's
27
+ // `new Date(iso)` treats the instant correctly, but `getDate()/getMonth()/getFullYear()`
28
+ // reflect the user's local day, so an activity scheduled at e.g. 23:30 local on a UTC
29
+ // boundary lands on the day the reader would call it.
30
+ export function toLocalZonedDate(value: string | Date): Date {
31
+ return toZonedTime(value, USER_TIMEZONE)
32
+ }
33
+
34
+ export function isSameDay(a: Date, b: Date): boolean {
35
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
36
+ }