@open-mercato/core 0.6.6-develop.6241.1.47c01ad4ed → 0.6.6-develop.6245.1.2be3b151f8

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 3404 entry points
1
+ [build:core] found 3405 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 185 entry points
4
4
  [build:core:generated] built successfully
@@ -1,7 +1,6 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
- import { format } from "date-fns/format";
5
4
  import { CalendarRange, ListFilter, Settings } from "lucide-react";
6
5
  import { Badge } from "@open-mercato/ui/primitives/badge";
7
6
  import { Button } from "@open-mercato/ui/primitives/button";
@@ -18,14 +17,12 @@ import {
18
17
  SelectTrigger,
19
18
  SelectValue
20
19
  } from "@open-mercato/ui/primitives/select";
21
- import { useT } from "@open-mercato/shared/lib/i18n/context";
20
+ import { useLocale, useT } from "@open-mercato/shared/lib/i18n/context";
21
+ import { formatDateRangeLabel } from "../../lib/calendar/format.js";
22
22
  const RANGE_PRESETS = ["thisWeek", "next7", "thisMonth", "next30"];
23
23
  const STATUS_OPTIONS = ["planned", "done", "canceled"];
24
24
  const ALL_OPTION = "all";
25
25
  const EMPTY_FILTERS = { types: [], status: null, ownerUserId: null };
26
- function formatRangeLabel(from, to) {
27
- return `${format(from, "MMM dd")} \u2013 ${format(to, "MMM dd, yyyy")}`;
28
- }
29
26
  function CalendarToolbar(props) {
30
27
  const {
31
28
  anchor,
@@ -43,6 +40,7 @@ function CalendarToolbar(props) {
43
40
  onOpenSettings
44
41
  } = props;
45
42
  const t = useT();
43
+ const locale = useLocale();
46
44
  const [rangeOpen, setRangeOpen] = React.useState(false);
47
45
  const [filtersOpen, setFiltersOpen] = React.useState(false);
48
46
  const [pendingFilters, setPendingFilters] = React.useState(filters);
@@ -108,7 +106,7 @@ function CalendarToolbar(props) {
108
106
  className: "min-w-0 text-muted-foreground sm:-ml-px sm:rounded-l-none",
109
107
  children: [
110
108
  /* @__PURE__ */ jsx(CalendarRange, { "aria-hidden": "true" }),
111
- /* @__PURE__ */ jsx("span", { className: "truncate", children: formatRangeLabel(range.from, range.to) })
109
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: formatDateRangeLabel(locale, range.from, range.to) })
112
110
  ]
113
111
  }
114
112
  ) }),
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/customers/components/calendar/CalendarToolbar.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { format } from 'date-fns/format'\nimport { CalendarRange, ListFilter, Settings } from 'lucide-react'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Calendar } from '@open-mercato/ui/primitives/calendar'\nimport { CheckboxField } from '@open-mercato/ui/primitives/checkbox-field'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Kbd } from '@open-mercato/ui/primitives/kbd'\nimport { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'\nimport { SearchInput } from '@open-mercato/ui/primitives/search-input'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type {\n CalendarFiltersValue,\n CalendarRangePreset,\n CalendarToolbarProps,\n} from './types'\n\nconst RANGE_PRESETS: CalendarRangePreset[] = ['thisWeek', 'next7', 'thisMonth', 'next30']\n\nconst STATUS_OPTIONS = ['planned', 'done', 'canceled'] as const\n\nconst ALL_OPTION = 'all'\n\nconst EMPTY_FILTERS: CalendarFiltersValue = { types: [], status: null, ownerUserId: null }\n\nfunction formatRangeLabel(from: Date, to: Date): string {\n return `${format(from, 'MMM dd')} \u2013 ${format(to, 'MMM dd, yyyy')}`\n}\n\nexport function CalendarToolbar(props: CalendarToolbarProps) {\n const {\n anchor,\n range,\n preset,\n search,\n filters,\n typeOptions,\n ownerOptions,\n onToday,\n onPresetChange,\n onAnchorChange,\n onSearchChange,\n onFiltersChange,\n onOpenSettings,\n } = props\n const t = useT()\n const [rangeOpen, setRangeOpen] = React.useState(false)\n const [filtersOpen, setFiltersOpen] = React.useState(false)\n const [pendingFilters, setPendingFilters] = React.useState<CalendarFiltersValue>(filters)\n\n const presetLabels: Record<CalendarRangePreset, string> = {\n thisWeek: t('customers.calendar.toolbar.presets.thisWeek', 'This week'),\n next7: t('customers.calendar.toolbar.presets.next7', 'Next 7 days'),\n thisMonth: t('customers.calendar.toolbar.presets.thisMonth', 'This month'),\n next30: t('customers.calendar.toolbar.presets.next30', 'Next 30 days'),\n }\n\n const activeFilterCount =\n filters.types.length + (filters.status ? 1 : 0) + (filters.ownerUserId ? 1 : 0)\n\n const handleFiltersOpenChange = (open: boolean) => {\n if (open) setPendingFilters(filters)\n setFiltersOpen(open)\n }\n\n const applyFilters = () => {\n onFiltersChange(pendingFilters)\n setFiltersOpen(false)\n }\n\n const clearFilters = () => {\n setPendingFilters(EMPTY_FILTERS)\n onFiltersChange(EMPTY_FILTERS)\n setFiltersOpen(false)\n }\n\n const togglePendingType = (value: string, checked: boolean) => {\n setPendingFilters((current) => ({\n ...current,\n types: checked\n ? [...current.types, value]\n : current.types.filter((entry) => entry !== value),\n }))\n }\n\n return (\n <div className=\"flex flex-wrap items-center gap-2 sm:gap-3\">\n <div className=\"flex min-w-0 flex-wrap items-center gap-2 sm:gap-3\">\n <Button type=\"button\" variant=\"outline\" onClick={onToday}>\n {t('customers.calendar.toolbar.today', 'Today')}\n </Button>\n <div className=\"flex min-w-0 items-center\">\n <Select\n value={preset ?? ''}\n onValueChange={(value) => onPresetChange(value as CalendarRangePreset)}\n >\n <SelectTrigger\n className=\"hidden w-auto min-w-32 rounded-r-none sm:flex\"\n aria-label={t('customers.calendar.toolbar.presetLabel', 'Date range preset')}\n >\n <SelectValue\n placeholder={t('customers.calendar.toolbar.presetPlaceholder', 'Custom range')}\n />\n </SelectTrigger>\n <SelectContent>\n {RANGE_PRESETS.map((value) => (\n <SelectItem key={value} value={value}>\n {presetLabels[value]}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Popover open={rangeOpen} onOpenChange={setRangeOpen}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n variant=\"outline\"\n className=\"min-w-0 text-muted-foreground sm:-ml-px sm:rounded-l-none\"\n >\n <CalendarRange aria-hidden=\"true\" />\n <span className=\"truncate\">{formatRangeLabel(range.from, range.to)}</span>\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-auto min-w-0 p-2\">\n <Calendar\n mode=\"single\"\n selected={anchor}\n defaultMonth={anchor}\n onSelect={(date) => {\n if (!date) return\n onAnchorChange(date)\n setRangeOpen(false)\n }}\n />\n </PopoverContent>\n </Popover>\n </div>\n </div>\n <div className=\"flex w-full min-w-0 items-center gap-2 sm:ml-auto sm:w-auto sm:flex-none sm:gap-3\">\n <div className=\"relative min-w-0 flex-1 sm:w-72 sm:flex-none\">\n <SearchInput\n value={search}\n onChange={onSearchChange}\n placeholder={t('customers.calendar.toolbar.searchPlaceholder', 'Search\u2026')}\n aria-label={t('customers.calendar.toolbar.searchPlaceholder', 'Search\u2026')}\n data-calendar-search=\"\"\n />\n {search.length === 0 ? (\n <Kbd className=\"pointer-events-none absolute right-2 top-1/2 hidden -translate-y-1/2 sm:inline-flex\">/</Kbd>\n ) : null}\n </div>\n <Popover open={filtersOpen} onOpenChange={handleFiltersOpenChange}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n variant=\"outline\"\n aria-label={t('customers.calendar.toolbar.filters.label', 'Filter')}\n >\n <ListFilter aria-hidden=\"true\" />\n <span className=\"hidden sm:inline\">\n {t('customers.calendar.toolbar.filters.label', 'Filter')}\n </span>\n {activeFilterCount > 0 ? (\n <Badge variant=\"secondary\" size=\"sm\">\n {activeFilterCount}\n </Badge>\n ) : null}\n </Button>\n </PopoverTrigger>\n <PopoverContent\n align=\"end\"\n className=\"w-72 p-3\"\n onKeyDown={(event) => {\n if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {\n event.preventDefault()\n applyFilters()\n }\n }}\n >\n <div className=\"flex flex-col gap-4\">\n {typeOptions.length > 0 ? (\n <fieldset className=\"flex flex-col gap-2\">\n <legend className=\"pb-1 text-overline font-semibold uppercase tracking-widest text-muted-foreground\">\n {t('customers.calendar.toolbar.filters.types', 'Type')}\n </legend>\n {typeOptions.map((option) => (\n <CheckboxField\n key={option.value}\n label={option.label}\n size=\"sm\"\n checked={pendingFilters.types.includes(option.value)}\n onCheckedChange={(checked) =>\n togglePendingType(option.value, checked === true)\n }\n />\n ))}\n </fieldset>\n ) : null}\n <div className=\"flex flex-col gap-1.5\">\n <span className=\"text-overline font-semibold uppercase tracking-widest text-muted-foreground\">\n {t('customers.calendar.toolbar.filters.status', 'Status')}\n </span>\n <Select\n value={pendingFilters.status ?? ALL_OPTION}\n onValueChange={(value) =>\n setPendingFilters((current) => ({\n ...current,\n status: value === ALL_OPTION ? null : value,\n }))\n }\n >\n <SelectTrigger\n size=\"sm\"\n aria-label={t('customers.calendar.toolbar.filters.status', 'Status')}\n >\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL_OPTION}>\n {t('customers.calendar.toolbar.filters.allStatuses', 'All statuses')}\n </SelectItem>\n {STATUS_OPTIONS.map((status) => (\n <SelectItem key={status} value={status}>\n {t(`customers.calendar.toolbar.filters.statuses.${status}`, status)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n {ownerOptions.length > 0 ? (\n <div className=\"flex flex-col gap-1.5\">\n <span className=\"text-overline font-semibold uppercase tracking-widest text-muted-foreground\">\n {t('customers.calendar.toolbar.filters.owner', 'Owner')}\n </span>\n <Select\n value={pendingFilters.ownerUserId ?? ALL_OPTION}\n onValueChange={(value) =>\n setPendingFilters((current) => ({\n ...current,\n ownerUserId: value === ALL_OPTION ? null : value,\n }))\n }\n >\n <SelectTrigger\n size=\"sm\"\n aria-label={t('customers.calendar.toolbar.filters.owner', 'Owner')}\n >\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL_OPTION}>\n {t('customers.calendar.toolbar.filters.allOwners', 'All owners')}\n </SelectItem>\n {ownerOptions.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n ) : null}\n <div className=\"flex items-center justify-end gap-2 border-t pt-3\">\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={clearFilters}>\n {t('customers.calendar.toolbar.filters.clear', 'Clear')}\n </Button>\n <Button type=\"button\" size=\"sm\" onClick={applyFilters}>\n {t('customers.calendar.toolbar.filters.apply', 'Apply')}\n </Button>\n </div>\n </div>\n </PopoverContent>\n </Popover>\n <IconButton\n type=\"button\"\n variant=\"outline\"\n aria-label={t('customers.calendar.toolbar.settings', 'Calendar settings')}\n onClick={onOpenSettings}\n >\n <Settings aria-hidden />\n </IconButton>\n </div>\n </div>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { CalendarRange, ListFilter, Settings } from 'lucide-react'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Calendar } from '@open-mercato/ui/primitives/calendar'\nimport { CheckboxField } from '@open-mercato/ui/primitives/checkbox-field'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Kbd } from '@open-mercato/ui/primitives/kbd'\nimport { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'\nimport { SearchInput } from '@open-mercato/ui/primitives/search-input'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { formatDateRangeLabel } from '../../lib/calendar/format'\nimport type {\n CalendarFiltersValue,\n CalendarRangePreset,\n CalendarToolbarProps,\n} from './types'\n\nconst RANGE_PRESETS: CalendarRangePreset[] = ['thisWeek', 'next7', 'thisMonth', 'next30']\n\nconst STATUS_OPTIONS = ['planned', 'done', 'canceled'] as const\n\nconst ALL_OPTION = 'all'\n\nconst EMPTY_FILTERS: CalendarFiltersValue = { types: [], status: null, ownerUserId: null }\n\nexport function CalendarToolbar(props: CalendarToolbarProps) {\n const {\n anchor,\n range,\n preset,\n search,\n filters,\n typeOptions,\n ownerOptions,\n onToday,\n onPresetChange,\n onAnchorChange,\n onSearchChange,\n onFiltersChange,\n onOpenSettings,\n } = props\n const t = useT()\n const locale = useLocale()\n const [rangeOpen, setRangeOpen] = React.useState(false)\n const [filtersOpen, setFiltersOpen] = React.useState(false)\n const [pendingFilters, setPendingFilters] = React.useState<CalendarFiltersValue>(filters)\n\n const presetLabels: Record<CalendarRangePreset, string> = {\n thisWeek: t('customers.calendar.toolbar.presets.thisWeek', 'This week'),\n next7: t('customers.calendar.toolbar.presets.next7', 'Next 7 days'),\n thisMonth: t('customers.calendar.toolbar.presets.thisMonth', 'This month'),\n next30: t('customers.calendar.toolbar.presets.next30', 'Next 30 days'),\n }\n\n const activeFilterCount =\n filters.types.length + (filters.status ? 1 : 0) + (filters.ownerUserId ? 1 : 0)\n\n const handleFiltersOpenChange = (open: boolean) => {\n if (open) setPendingFilters(filters)\n setFiltersOpen(open)\n }\n\n const applyFilters = () => {\n onFiltersChange(pendingFilters)\n setFiltersOpen(false)\n }\n\n const clearFilters = () => {\n setPendingFilters(EMPTY_FILTERS)\n onFiltersChange(EMPTY_FILTERS)\n setFiltersOpen(false)\n }\n\n const togglePendingType = (value: string, checked: boolean) => {\n setPendingFilters((current) => ({\n ...current,\n types: checked\n ? [...current.types, value]\n : current.types.filter((entry) => entry !== value),\n }))\n }\n\n return (\n <div className=\"flex flex-wrap items-center gap-2 sm:gap-3\">\n <div className=\"flex min-w-0 flex-wrap items-center gap-2 sm:gap-3\">\n <Button type=\"button\" variant=\"outline\" onClick={onToday}>\n {t('customers.calendar.toolbar.today', 'Today')}\n </Button>\n <div className=\"flex min-w-0 items-center\">\n <Select\n value={preset ?? ''}\n onValueChange={(value) => onPresetChange(value as CalendarRangePreset)}\n >\n <SelectTrigger\n className=\"hidden w-auto min-w-32 rounded-r-none sm:flex\"\n aria-label={t('customers.calendar.toolbar.presetLabel', 'Date range preset')}\n >\n <SelectValue\n placeholder={t('customers.calendar.toolbar.presetPlaceholder', 'Custom range')}\n />\n </SelectTrigger>\n <SelectContent>\n {RANGE_PRESETS.map((value) => (\n <SelectItem key={value} value={value}>\n {presetLabels[value]}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Popover open={rangeOpen} onOpenChange={setRangeOpen}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n variant=\"outline\"\n className=\"min-w-0 text-muted-foreground sm:-ml-px sm:rounded-l-none\"\n >\n <CalendarRange aria-hidden=\"true\" />\n <span className=\"truncate\">{formatDateRangeLabel(locale, range.from, range.to)}</span>\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-auto min-w-0 p-2\">\n <Calendar\n mode=\"single\"\n selected={anchor}\n defaultMonth={anchor}\n onSelect={(date) => {\n if (!date) return\n onAnchorChange(date)\n setRangeOpen(false)\n }}\n />\n </PopoverContent>\n </Popover>\n </div>\n </div>\n <div className=\"flex w-full min-w-0 items-center gap-2 sm:ml-auto sm:w-auto sm:flex-none sm:gap-3\">\n <div className=\"relative min-w-0 flex-1 sm:w-72 sm:flex-none\">\n <SearchInput\n value={search}\n onChange={onSearchChange}\n placeholder={t('customers.calendar.toolbar.searchPlaceholder', 'Search\u2026')}\n aria-label={t('customers.calendar.toolbar.searchPlaceholder', 'Search\u2026')}\n data-calendar-search=\"\"\n />\n {search.length === 0 ? (\n <Kbd className=\"pointer-events-none absolute right-2 top-1/2 hidden -translate-y-1/2 sm:inline-flex\">/</Kbd>\n ) : null}\n </div>\n <Popover open={filtersOpen} onOpenChange={handleFiltersOpenChange}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n variant=\"outline\"\n aria-label={t('customers.calendar.toolbar.filters.label', 'Filter')}\n >\n <ListFilter aria-hidden=\"true\" />\n <span className=\"hidden sm:inline\">\n {t('customers.calendar.toolbar.filters.label', 'Filter')}\n </span>\n {activeFilterCount > 0 ? (\n <Badge variant=\"secondary\" size=\"sm\">\n {activeFilterCount}\n </Badge>\n ) : null}\n </Button>\n </PopoverTrigger>\n <PopoverContent\n align=\"end\"\n className=\"w-72 p-3\"\n onKeyDown={(event) => {\n if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {\n event.preventDefault()\n applyFilters()\n }\n }}\n >\n <div className=\"flex flex-col gap-4\">\n {typeOptions.length > 0 ? (\n <fieldset className=\"flex flex-col gap-2\">\n <legend className=\"pb-1 text-overline font-semibold uppercase tracking-widest text-muted-foreground\">\n {t('customers.calendar.toolbar.filters.types', 'Type')}\n </legend>\n {typeOptions.map((option) => (\n <CheckboxField\n key={option.value}\n label={option.label}\n size=\"sm\"\n checked={pendingFilters.types.includes(option.value)}\n onCheckedChange={(checked) =>\n togglePendingType(option.value, checked === true)\n }\n />\n ))}\n </fieldset>\n ) : null}\n <div className=\"flex flex-col gap-1.5\">\n <span className=\"text-overline font-semibold uppercase tracking-widest text-muted-foreground\">\n {t('customers.calendar.toolbar.filters.status', 'Status')}\n </span>\n <Select\n value={pendingFilters.status ?? ALL_OPTION}\n onValueChange={(value) =>\n setPendingFilters((current) => ({\n ...current,\n status: value === ALL_OPTION ? null : value,\n }))\n }\n >\n <SelectTrigger\n size=\"sm\"\n aria-label={t('customers.calendar.toolbar.filters.status', 'Status')}\n >\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL_OPTION}>\n {t('customers.calendar.toolbar.filters.allStatuses', 'All statuses')}\n </SelectItem>\n {STATUS_OPTIONS.map((status) => (\n <SelectItem key={status} value={status}>\n {t(`customers.calendar.toolbar.filters.statuses.${status}`, status)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n {ownerOptions.length > 0 ? (\n <div className=\"flex flex-col gap-1.5\">\n <span className=\"text-overline font-semibold uppercase tracking-widest text-muted-foreground\">\n {t('customers.calendar.toolbar.filters.owner', 'Owner')}\n </span>\n <Select\n value={pendingFilters.ownerUserId ?? ALL_OPTION}\n onValueChange={(value) =>\n setPendingFilters((current) => ({\n ...current,\n ownerUserId: value === ALL_OPTION ? null : value,\n }))\n }\n >\n <SelectTrigger\n size=\"sm\"\n aria-label={t('customers.calendar.toolbar.filters.owner', 'Owner')}\n >\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL_OPTION}>\n {t('customers.calendar.toolbar.filters.allOwners', 'All owners')}\n </SelectItem>\n {ownerOptions.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n ) : null}\n <div className=\"flex items-center justify-end gap-2 border-t pt-3\">\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={clearFilters}>\n {t('customers.calendar.toolbar.filters.clear', 'Clear')}\n </Button>\n <Button type=\"button\" size=\"sm\" onClick={applyFilters}>\n {t('customers.calendar.toolbar.filters.apply', 'Apply')}\n </Button>\n </div>\n </div>\n </PopoverContent>\n </Popover>\n <IconButton\n type=\"button\"\n variant=\"outline\"\n aria-label={t('customers.calendar.toolbar.settings', 'Calendar settings')}\n onClick={onOpenSettings}\n >\n <Settings aria-hidden />\n </IconButton>\n </div>\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -6,6 +6,7 @@ import { cn } from "@open-mercato/shared/lib/utils";
6
6
  import { useLocale, useT } from "@open-mercato/shared/lib/i18n/context";
7
7
  import { Avatar, AvatarStack } from "@open-mercato/ui/primitives/avatar";
8
8
  import { Button } from "@open-mercato/ui/primitives/button";
9
+ import { formatTimeRangeLabel } from "../../lib/calendar/format.js";
9
10
  const SHOW_TIME_MIN_HEIGHT_PX = 44;
10
11
  const SHOW_META_MIN_HEIGHT_PX = 96;
11
12
  const WRAP_TITLE_MIN_HEIGHT_PX = 128;
@@ -50,12 +51,7 @@ function resolveEventTone(item, nowMs) {
50
51
  };
51
52
  }
52
53
  function formatTimeRange(locale, start, end) {
53
- const formatter = new Intl.DateTimeFormat(locale, { hour: "numeric", minute: "2-digit" });
54
- try {
55
- return formatter.formatRange(start, end);
56
- } catch {
57
- return `${formatter.format(start)} - ${formatter.format(end)}`;
58
- }
54
+ return formatTimeRangeLabel(locale, start, end);
59
55
  }
60
56
  function participantLabel(participant) {
61
57
  return participant.name ?? participant.email ?? "?";
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/customers/components/calendar/EventBlock.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Globe, MapPin } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Avatar, AvatarStack } from '@open-mercato/ui/primitives/avatar'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport type { CalendarItem, CalendarPlatform } from './types'\n\nconst SHOW_TIME_MIN_HEIGHT_PX = 44\nconst SHOW_META_MIN_HEIGHT_PX = 96\nconst WRAP_TITLE_MIN_HEIGHT_PX = 128\nconst MAX_VISIBLE_AVATARS = 3\n\nconst PLATFORM_LABELS: Record<CalendarPlatform, { key: string; fallback: string }> = {\n zoom: { key: 'customers.calendar.platform.zoom', fallback: 'Zoom' },\n meet: { key: 'customers.calendar.platform.meet', fallback: 'Meet' },\n slack: { key: 'customers.calendar.platform.slack', fallback: 'Slack' },\n teams: { key: 'customers.calendar.platform.teams', fallback: 'Teams' },\n}\n\nexport type EventTone = {\n surfaceClassName: string\n titleClassName: string\n subClassName: string\n style?: React.CSSProperties\n}\n\nfunction softTintStyle(color: string): React.CSSProperties {\n if (/^#[0-9a-fA-F]{6}$/.test(color)) return { backgroundColor: `${color}1A` }\n return { backgroundColor: `color-mix(in srgb, ${color} 10%, transparent)` }\n}\n\nexport function resolveEventTone(item: CalendarItem, nowMs: number): EventTone {\n if (item.status === 'canceled') {\n return {\n surfaceClassName: 'bg-muted/60',\n titleClassName: 'text-muted-foreground line-through',\n subClassName: 'text-muted-foreground/70',\n }\n }\n if (item.status === 'done' || item.end.getTime() < nowMs) {\n return {\n surfaceClassName: 'bg-muted',\n titleClassName: 'text-muted-foreground',\n subClassName: 'text-muted-foreground',\n }\n }\n if (item.color) {\n return {\n surfaceClassName: '',\n titleClassName: 'text-foreground',\n subClassName: 'text-muted-foreground',\n style: softTintStyle(item.color),\n }\n }\n return {\n surfaceClassName: 'bg-muted/60',\n titleClassName: 'text-foreground',\n subClassName: 'text-muted-foreground',\n }\n}\n\nexport function formatTimeRange(locale: string, start: Date, end: Date): string {\n const formatter = new Intl.DateTimeFormat(locale, { hour: 'numeric', minute: '2-digit' })\n try {\n return formatter.formatRange(start, end)\n } catch {\n return `${formatter.format(start)} - ${formatter.format(end)}`\n }\n}\n\nfunction participantLabel(participant: CalendarItem['participants'][number]): string {\n return participant.name ?? participant.email ?? '?'\n}\n\ntype LocationMetaProps = {\n item: CalendarItem\n subClassName: string\n accentColor: string | null\n}\n\nfunction LocationMeta({ item, subClassName, accentColor }: LocationMetaProps) {\n const t = useT()\n if (!item.location || !item.locationKind) return null\n const iconStyle = accentColor ? { color: accentColor } : undefined\n if (item.locationKind === 'platform' && item.platform) {\n const platform = PLATFORM_LABELS[item.platform]\n return (\n <span className={cn('truncate text-xs', subClassName)}>\n {t('customers.calendar.grid.onPlatform', 'on {platform}', { platform: t(platform.key, platform.fallback) })}\n </span>\n )\n }\n if (item.locationKind === 'url') {\n return (\n <>\n <Globe className={cn('size-4 shrink-0', !accentColor && subClassName)} style={iconStyle} aria-hidden />\n <span className=\"truncate text-xs text-foreground\">{item.location}</span>\n </>\n )\n }\n return (\n <>\n <MapPin className={cn('size-4 shrink-0', !accentColor && subClassName)} style={iconStyle} aria-hidden />\n <span className=\"truncate text-xs text-foreground\">\n {t('customers.calendar.grid.venue', 'Venue: {name}', { name: item.location })}\n </span>\n </>\n )\n}\n\nexport type EventBlockProps = {\n item: CalendarItem\n top: number\n height: number\n insetInlineStart: string\n width: string\n conflicted: boolean\n highlighted: boolean\n selected?: boolean\n nowMs: number\n} & Omit<React.ComponentProps<typeof Button>, 'style' | 'children'>\n\nexport const EventBlock = React.forwardRef<HTMLButtonElement, EventBlockProps>(function EventBlock(\n {\n item,\n top,\n height,\n insetInlineStart,\n width,\n conflicted,\n highlighted,\n selected,\n nowMs,\n className,\n ...buttonProps\n },\n ref,\n) {\n const t = useT()\n const locale = useLocale()\n const tone = resolveEventTone(item, nowMs)\n const title = item.title || t('customers.calendar.grid.untitled', 'Untitled')\n const timeRange = formatTimeRange(locale, item.start, item.end)\n const showTime = height >= SHOW_TIME_MIN_HEIGHT_PX\n const wrapTitle = height >= WRAP_TITLE_MIN_HEIGHT_PX\n const hasMetaContent = item.participants.length > 0 || (item.location !== null && item.locationKind !== null)\n const showMeta = height >= SHOW_META_MIN_HEIGHT_PX && hasMetaContent\n const visibleParticipants = item.participants.slice(0, MAX_VISIBLE_AVATARS)\n const overflowCount = item.participants.length - visibleParticipants.length\n\n return (\n <Button\n ref={ref}\n type=\"button\"\n variant=\"ghost\"\n aria-label={`${title}, ${timeRange}`}\n className={cn(\n 'pointer-events-auto absolute h-auto flex-col items-start justify-start gap-1 whitespace-normal overflow-hidden rounded-md px-1.5 text-start outline-none hover:bg-muted/70 sm:px-3',\n height >= SHOW_TIME_MIN_HEIGHT_PX ? 'py-1 sm:py-2' : 'py-0.5 sm:py-1',\n tone.surfaceClassName,\n conflicted && 'ring-1 ring-status-warning-icon',\n selected && 'shadow-md ring-2 ring-foreground',\n highlighted && 'motion-safe:animate-pulse',\n conflicted || highlighted || selected ? 'z-30' : 'z-10',\n 'focus-visible:z-30 focus-visible:ring-2 focus-visible:ring-ring',\n className,\n )}\n style={{ top, height, insetInlineStart, width, ...tone.style }}\n {...buttonProps}\n >\n <span className={cn('w-full text-xs font-medium leading-4', wrapTitle ? 'line-clamp-2' : 'truncate', tone.titleClassName)}>\n {title}\n </span>\n {showTime ? (\n <span className={cn('w-full truncate text-overline uppercase tracking-wide', tone.subClassName)}>\n {timeRange}\n </span>\n ) : null}\n {showMeta ? (\n <span className=\"mt-auto flex w-full min-w-0 items-center gap-1.5\">\n {visibleParticipants.length > 0 ? (\n <>\n <AvatarStack size=\"xs\" max={MAX_VISIBLE_AVATARS} className=\"shrink-0 gap-px [&>*:not(:first-child)]:-ml-1\">\n {visibleParticipants.map((participant) => (\n <Avatar key={participant.userId} size=\"xs\" label={participantLabel(participant)} />\n ))}\n </AvatarStack>\n {overflowCount > 0 ? (\n <span className={cn('shrink-0 text-xs', tone.subClassName)}>+{overflowCount}</span>\n ) : null}\n </>\n ) : null}\n <span className=\"ms-auto flex min-w-0 items-center gap-1.5\">\n <LocationMeta item={item} subClassName={tone.subClassName} accentColor={item.color} />\n </span>\n </span>\n ) : null}\n </Button>\n )\n})\n\nEventBlock.displayName = 'EventBlock'\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Globe, MapPin } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Avatar, AvatarStack } from '@open-mercato/ui/primitives/avatar'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { formatTimeRangeLabel } from '../../lib/calendar/format'\nimport type { CalendarItem, CalendarPlatform } from './types'\n\nconst SHOW_TIME_MIN_HEIGHT_PX = 44\nconst SHOW_META_MIN_HEIGHT_PX = 96\nconst WRAP_TITLE_MIN_HEIGHT_PX = 128\nconst MAX_VISIBLE_AVATARS = 3\n\nconst PLATFORM_LABELS: Record<CalendarPlatform, { key: string; fallback: string }> = {\n zoom: { key: 'customers.calendar.platform.zoom', fallback: 'Zoom' },\n meet: { key: 'customers.calendar.platform.meet', fallback: 'Meet' },\n slack: { key: 'customers.calendar.platform.slack', fallback: 'Slack' },\n teams: { key: 'customers.calendar.platform.teams', fallback: 'Teams' },\n}\n\nexport type EventTone = {\n surfaceClassName: string\n titleClassName: string\n subClassName: string\n style?: React.CSSProperties\n}\n\nfunction softTintStyle(color: string): React.CSSProperties {\n if (/^#[0-9a-fA-F]{6}$/.test(color)) return { backgroundColor: `${color}1A` }\n return { backgroundColor: `color-mix(in srgb, ${color} 10%, transparent)` }\n}\n\nexport function resolveEventTone(item: CalendarItem, nowMs: number): EventTone {\n if (item.status === 'canceled') {\n return {\n surfaceClassName: 'bg-muted/60',\n titleClassName: 'text-muted-foreground line-through',\n subClassName: 'text-muted-foreground/70',\n }\n }\n if (item.status === 'done' || item.end.getTime() < nowMs) {\n return {\n surfaceClassName: 'bg-muted',\n titleClassName: 'text-muted-foreground',\n subClassName: 'text-muted-foreground',\n }\n }\n if (item.color) {\n return {\n surfaceClassName: '',\n titleClassName: 'text-foreground',\n subClassName: 'text-muted-foreground',\n style: softTintStyle(item.color),\n }\n }\n return {\n surfaceClassName: 'bg-muted/60',\n titleClassName: 'text-foreground',\n subClassName: 'text-muted-foreground',\n }\n}\n\nexport function formatTimeRange(locale: string, start: Date, end: Date): string {\n return formatTimeRangeLabel(locale, start, end)\n}\n\nfunction participantLabel(participant: CalendarItem['participants'][number]): string {\n return participant.name ?? participant.email ?? '?'\n}\n\ntype LocationMetaProps = {\n item: CalendarItem\n subClassName: string\n accentColor: string | null\n}\n\nfunction LocationMeta({ item, subClassName, accentColor }: LocationMetaProps) {\n const t = useT()\n if (!item.location || !item.locationKind) return null\n const iconStyle = accentColor ? { color: accentColor } : undefined\n if (item.locationKind === 'platform' && item.platform) {\n const platform = PLATFORM_LABELS[item.platform]\n return (\n <span className={cn('truncate text-xs', subClassName)}>\n {t('customers.calendar.grid.onPlatform', 'on {platform}', { platform: t(platform.key, platform.fallback) })}\n </span>\n )\n }\n if (item.locationKind === 'url') {\n return (\n <>\n <Globe className={cn('size-4 shrink-0', !accentColor && subClassName)} style={iconStyle} aria-hidden />\n <span className=\"truncate text-xs text-foreground\">{item.location}</span>\n </>\n )\n }\n return (\n <>\n <MapPin className={cn('size-4 shrink-0', !accentColor && subClassName)} style={iconStyle} aria-hidden />\n <span className=\"truncate text-xs text-foreground\">\n {t('customers.calendar.grid.venue', 'Venue: {name}', { name: item.location })}\n </span>\n </>\n )\n}\n\nexport type EventBlockProps = {\n item: CalendarItem\n top: number\n height: number\n insetInlineStart: string\n width: string\n conflicted: boolean\n highlighted: boolean\n selected?: boolean\n nowMs: number\n} & Omit<React.ComponentProps<typeof Button>, 'style' | 'children'>\n\nexport const EventBlock = React.forwardRef<HTMLButtonElement, EventBlockProps>(function EventBlock(\n {\n item,\n top,\n height,\n insetInlineStart,\n width,\n conflicted,\n highlighted,\n selected,\n nowMs,\n className,\n ...buttonProps\n },\n ref,\n) {\n const t = useT()\n const locale = useLocale()\n const tone = resolveEventTone(item, nowMs)\n const title = item.title || t('customers.calendar.grid.untitled', 'Untitled')\n const timeRange = formatTimeRange(locale, item.start, item.end)\n const showTime = height >= SHOW_TIME_MIN_HEIGHT_PX\n const wrapTitle = height >= WRAP_TITLE_MIN_HEIGHT_PX\n const hasMetaContent = item.participants.length > 0 || (item.location !== null && item.locationKind !== null)\n const showMeta = height >= SHOW_META_MIN_HEIGHT_PX && hasMetaContent\n const visibleParticipants = item.participants.slice(0, MAX_VISIBLE_AVATARS)\n const overflowCount = item.participants.length - visibleParticipants.length\n\n return (\n <Button\n ref={ref}\n type=\"button\"\n variant=\"ghost\"\n aria-label={`${title}, ${timeRange}`}\n className={cn(\n 'pointer-events-auto absolute h-auto flex-col items-start justify-start gap-1 whitespace-normal overflow-hidden rounded-md px-1.5 text-start outline-none hover:bg-muted/70 sm:px-3',\n height >= SHOW_TIME_MIN_HEIGHT_PX ? 'py-1 sm:py-2' : 'py-0.5 sm:py-1',\n tone.surfaceClassName,\n conflicted && 'ring-1 ring-status-warning-icon',\n selected && 'shadow-md ring-2 ring-foreground',\n highlighted && 'motion-safe:animate-pulse',\n conflicted || highlighted || selected ? 'z-30' : 'z-10',\n 'focus-visible:z-30 focus-visible:ring-2 focus-visible:ring-ring',\n className,\n )}\n style={{ top, height, insetInlineStart, width, ...tone.style }}\n {...buttonProps}\n >\n <span className={cn('w-full text-xs font-medium leading-4', wrapTitle ? 'line-clamp-2' : 'truncate', tone.titleClassName)}>\n {title}\n </span>\n {showTime ? (\n <span className={cn('w-full truncate text-overline uppercase tracking-wide', tone.subClassName)}>\n {timeRange}\n </span>\n ) : null}\n {showMeta ? (\n <span className=\"mt-auto flex w-full min-w-0 items-center gap-1.5\">\n {visibleParticipants.length > 0 ? (\n <>\n <AvatarStack size=\"xs\" max={MAX_VISIBLE_AVATARS} className=\"shrink-0 gap-px [&>*:not(:first-child)]:-ml-1\">\n {visibleParticipants.map((participant) => (\n <Avatar key={participant.userId} size=\"xs\" label={participantLabel(participant)} />\n ))}\n </AvatarStack>\n {overflowCount > 0 ? (\n <span className={cn('shrink-0 text-xs', tone.subClassName)}>+{overflowCount}</span>\n ) : null}\n </>\n ) : null}\n <span className=\"ms-auto flex min-w-0 items-center gap-1.5\">\n <LocationMeta item={item} subClassName={tone.subClassName} accentColor={item.color} />\n </span>\n </span>\n ) : null}\n </Button>\n )\n})\n\nEventBlock.displayName = 'EventBlock'\n"],
5
+ "mappings": "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",
6
6
  "names": ["EventBlock"]
7
7
  }
@@ -1,7 +1,6 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { differenceInCalendarDays } from "date-fns/differenceInCalendarDays";
4
- import { format } from "date-fns/format";
5
4
  import { AlertTriangle, ChevronDown, Clock } from "lucide-react";
6
5
  import { Button } from "@open-mercato/ui/primitives/button";
7
6
  import { IconButton } from "@open-mercato/ui/primitives/icon-button";
@@ -12,10 +11,11 @@ import {
12
11
  PopoverContent,
13
12
  PopoverTrigger
14
13
  } from "@open-mercato/ui/primitives/popover";
15
- import { useT } from "@open-mercato/shared/lib/i18n/context";
16
- function formatTimeRange(item, allDayLabel) {
14
+ import { useLocale, useT } from "@open-mercato/shared/lib/i18n/context";
15
+ import { formatDateLabel, formatTimeRangeLabel } from "../../lib/calendar/format.js";
16
+ function formatTimeRange(locale, item, allDayLabel) {
17
17
  if (item.allDay) return allDayLabel;
18
- return `${format(item.start, "h:mm a")} \u2013 ${format(item.end, "h:mm a")}`;
18
+ return formatTimeRangeLabel(locale, item.start, item.end);
19
19
  }
20
20
  function canJoin(item) {
21
21
  return item.locationKind === "url" || item.locationKind === "platform";
@@ -26,6 +26,7 @@ function UpcomingCardStatus({
26
26
  onSeeConflict
27
27
  }) {
28
28
  const t = useT();
29
+ const locale = useLocale();
29
30
  const { item, kind, conflictCount } = card;
30
31
  if (kind === "today") {
31
32
  return /* @__PURE__ */ jsxs("div", { className: "flex w-full items-center gap-1.5 rounded-lg bg-status-success-bg py-2 pl-2 pr-3", children: [
@@ -67,14 +68,14 @@ function UpcomingCardStatus({
67
68
  return /* @__PURE__ */ jsxs("div", { className: "flex w-full items-center gap-1.5 rounded-lg bg-status-error-bg py-2 pl-2 pr-3", children: [
68
69
  /* @__PURE__ */ jsx(Clock, { className: "size-4 shrink-0 text-status-error-icon", "aria-hidden": "true" }),
69
70
  /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-xs font-medium text-foreground", children: t("customers.calendar.cards.cancelled", "Cancelled") }),
70
- /* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: format(item.start, "MMM dd, yyyy") })
71
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: formatDateLabel(locale, item.start) })
71
72
  ] });
72
73
  }
73
74
  const daysLater = Math.max(0, differenceInCalendarDays(item.start, /* @__PURE__ */ new Date()));
74
75
  return /* @__PURE__ */ jsxs("div", { className: "flex w-full items-center gap-1.5 rounded-lg bg-muted py-2 pl-2 pr-3", children: [
75
76
  /* @__PURE__ */ jsx(Clock, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": "true" }),
76
77
  /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-xs text-muted-foreground", children: daysLater === 1 ? t("customers.calendar.cards.dayLater", "1 day later") : t("customers.calendar.cards.daysLater", "{days} days later", { days: daysLater }) }),
77
- /* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: format(item.start, "MMM dd, yyyy") })
78
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: formatDateLabel(locale, item.start) })
78
79
  ] });
79
80
  }
80
81
  function UpcomingCardItem({
@@ -87,12 +88,13 @@ function UpcomingCardItem({
87
88
  onCancel
88
89
  }) {
89
90
  const t = useT();
91
+ const locale = useLocale();
90
92
  const { item } = card;
91
93
  return /* @__PURE__ */ jsxs("article", { className: "flex w-[260px] shrink-0 snap-start flex-col gap-3 rounded-xl border bg-card px-2 pb-2 pt-4 shadow-xs sm:w-auto sm:shrink", children: [
92
94
  /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2 px-2", children: [
93
95
  /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [
94
96
  /* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium text-foreground", children: item.title }),
95
- /* @__PURE__ */ jsx("p", { className: "truncate text-xs text-muted-foreground", children: formatTimeRange(item, t("customers.calendar.cards.allDay", "All day")) })
97
+ /* @__PURE__ */ jsx("p", { className: "truncate text-xs text-muted-foreground", children: formatTimeRange(locale, item, t("customers.calendar.cards.allDay", "All day")) })
96
98
  ] }),
97
99
  /* @__PURE__ */ jsxs(Popover, { children: [
98
100
  /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/customers/components/calendar/UpcomingCards.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { differenceInCalendarDays } from 'date-fns/differenceInCalendarDays'\nimport { format } from 'date-fns/format'\nimport { AlertTriangle, ChevronDown, Clock } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { LinkButton } from '@open-mercato/ui/primitives/link-button'\nimport {\n Popover,\n PopoverClose,\n PopoverContent,\n PopoverTrigger,\n} from '@open-mercato/ui/primitives/popover'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { CalendarItem, UpcomingCard, UpcomingCardsProps } from './types'\n\nfunction formatTimeRange(item: CalendarItem, allDayLabel: string): string {\n if (item.allDay) return allDayLabel\n return `${format(item.start, 'h:mm a')} \u2013 ${format(item.end, 'h:mm a')}`\n}\n\nfunction canJoin(item: CalendarItem): boolean {\n return item.locationKind === 'url' || item.locationKind === 'platform'\n}\n\ntype CardCallbacks = Pick<UpcomingCardsProps, 'canManage' | 'onJoin' | 'onSeeConflict' | 'onOpen' | 'onEdit' | 'onCancel'>\n\nfunction UpcomingCardStatus({\n card,\n onJoin,\n onSeeConflict,\n}: {\n card: UpcomingCard\n onJoin: UpcomingCardsProps['onJoin']\n onSeeConflict: UpcomingCardsProps['onSeeConflict']\n}) {\n const t = useT()\n const { item, kind, conflictCount } = card\n\n if (kind === 'today') {\n return (\n <div className=\"flex w-full items-center gap-1.5 rounded-lg bg-status-success-bg py-2 pl-2 pr-3\">\n <Clock className=\"size-4 shrink-0 text-status-success-icon\" aria-hidden=\"true\" />\n <span className=\"min-w-0 flex-1 truncate text-xs font-medium text-foreground\">\n {t('customers.calendar.cards.today', 'Today')}\n </span>\n {canJoin(item) ? (\n <LinkButton\n variant=\"gray\"\n size=\"sm\"\n underline=\"always\"\n onClick={() => onJoin(item)}\n className=\"shrink-0\"\n >\n {t('customers.calendar.cards.join', 'Join Meeting')}\n </LinkButton>\n ) : null}\n </div>\n )\n }\n\n if (kind === 'conflicted') {\n return (\n <div className=\"flex w-full items-center gap-1.5 rounded-lg bg-status-warning-bg py-2 pl-2 pr-3\">\n <AlertTriangle className=\"size-4 shrink-0 text-status-warning-icon\" aria-hidden=\"true\" />\n <span className=\"min-w-0 flex-1 truncate text-xs font-medium text-foreground\">\n {t('customers.calendar.cards.conflictedCount', '{count} Conflicted', {\n count: conflictCount,\n })}\n </span>\n <LinkButton\n variant=\"gray\"\n size=\"sm\"\n underline=\"always\"\n onClick={() => onSeeConflict(item)}\n className=\"shrink-0\"\n >\n {t('customers.calendar.cards.seeConflict', 'See Conflict')}\n </LinkButton>\n </div>\n )\n }\n\n if (kind === 'cancelled') {\n return (\n <div className=\"flex w-full items-center gap-1.5 rounded-lg bg-status-error-bg py-2 pl-2 pr-3\">\n <Clock className=\"size-4 shrink-0 text-status-error-icon\" aria-hidden=\"true\" />\n <span className=\"min-w-0 flex-1 truncate text-xs font-medium text-foreground\">\n {t('customers.calendar.cards.cancelled', 'Cancelled')}\n </span>\n <span className=\"shrink-0 text-xs text-muted-foreground\">\n {format(item.start, 'MMM dd, yyyy')}\n </span>\n </div>\n )\n }\n\n const daysLater = Math.max(0, differenceInCalendarDays(item.start, new Date()))\n return (\n <div className=\"flex w-full items-center gap-1.5 rounded-lg bg-muted py-2 pl-2 pr-3\">\n <Clock className=\"size-4 shrink-0 text-muted-foreground\" aria-hidden=\"true\" />\n <span className=\"min-w-0 flex-1 truncate text-xs text-muted-foreground\">\n {daysLater === 1\n ? t('customers.calendar.cards.dayLater', '1 day later')\n : t('customers.calendar.cards.daysLater', '{days} days later', { days: daysLater })}\n </span>\n <span className=\"shrink-0 text-xs text-muted-foreground\">\n {format(item.start, 'MMM dd, yyyy')}\n </span>\n </div>\n )\n}\n\nfunction UpcomingCardItem({\n card,\n canManage,\n onJoin,\n onSeeConflict,\n onOpen,\n onEdit,\n onCancel,\n}: { card: UpcomingCard } & CardCallbacks) {\n const t = useT()\n const { item } = card\n\n return (\n <article className=\"flex w-[260px] shrink-0 snap-start flex-col gap-3 rounded-xl border bg-card px-2 pb-2 pt-4 shadow-xs sm:w-auto sm:shrink\">\n <div className=\"flex items-start gap-2 px-2\">\n <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n <p className=\"truncate text-sm font-medium text-foreground\">{item.title}</p>\n <p className=\"truncate text-xs text-muted-foreground\">\n {formatTimeRange(item, t('customers.calendar.cards.allDay', 'All day'))}\n </p>\n </div>\n <Popover>\n <PopoverTrigger asChild>\n <IconButton\n variant=\"outline\"\n size=\"xs\"\n fullRadius\n aria-label={t('customers.calendar.cards.menu.label', 'Event actions')}\n >\n <ChevronDown aria-hidden=\"true\" />\n </IconButton>\n </PopoverTrigger>\n <PopoverContent align=\"end\" className=\"w-40 min-w-0 p-1\">\n <PopoverClose asChild>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start\"\n onClick={() => onOpen(item)}\n >\n {t('customers.calendar.cards.menu.open', 'Open')}\n </Button>\n </PopoverClose>\n {canManage ? (\n <PopoverClose asChild>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start\"\n onClick={() => onEdit(item)}\n >\n {t('customers.calendar.cards.menu.edit', 'Edit')}\n </Button>\n </PopoverClose>\n ) : null}\n {canManage ? (\n <PopoverClose asChild>\n <Button\n type=\"button\"\n variant=\"destructive-ghost\"\n size=\"sm\"\n className=\"w-full justify-start\"\n onClick={() => onCancel(item)}\n >\n {t('customers.calendar.cards.menu.cancel', 'Cancel')}\n </Button>\n </PopoverClose>\n ) : null}\n </PopoverContent>\n </Popover>\n </div>\n <UpcomingCardStatus card={card} onJoin={onJoin} onSeeConflict={onSeeConflict} />\n </article>\n )\n}\n\nexport function UpcomingCards({\n cards,\n canManage = true,\n onJoin,\n onSeeConflict,\n onOpen,\n onEdit,\n onCancel,\n}: UpcomingCardsProps) {\n if (cards.length === 0) return null\n return (\n <div className=\"flex snap-x snap-mandatory gap-3 overflow-x-auto pb-1 sm:grid sm:snap-none sm:grid-cols-2 sm:gap-4 sm:overflow-visible sm:pb-0 xl:grid-cols-4\">\n {cards.map((card) => (\n <UpcomingCardItem\n key={card.item.id}\n card={card}\n canManage={canManage}\n onJoin={onJoin}\n onSeeConflict={onSeeConflict}\n onOpen={onOpen}\n onEdit={onEdit}\n onCancel={onCancel}\n />\n ))}\n </div>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { differenceInCalendarDays } from 'date-fns/differenceInCalendarDays'\nimport { AlertTriangle, ChevronDown, Clock } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { LinkButton } from '@open-mercato/ui/primitives/link-button'\nimport {\n Popover,\n PopoverClose,\n PopoverContent,\n PopoverTrigger,\n} from '@open-mercato/ui/primitives/popover'\nimport { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { formatDateLabel, formatTimeRangeLabel } from '../../lib/calendar/format'\nimport type { CalendarItem, UpcomingCard, UpcomingCardsProps } from './types'\n\nfunction formatTimeRange(locale: string, item: CalendarItem, allDayLabel: string): string {\n if (item.allDay) return allDayLabel\n return formatTimeRangeLabel(locale, item.start, item.end)\n}\n\nfunction canJoin(item: CalendarItem): boolean {\n return item.locationKind === 'url' || item.locationKind === 'platform'\n}\n\ntype CardCallbacks = Pick<UpcomingCardsProps, 'canManage' | 'onJoin' | 'onSeeConflict' | 'onOpen' | 'onEdit' | 'onCancel'>\n\nfunction UpcomingCardStatus({\n card,\n onJoin,\n onSeeConflict,\n}: {\n card: UpcomingCard\n onJoin: UpcomingCardsProps['onJoin']\n onSeeConflict: UpcomingCardsProps['onSeeConflict']\n}) {\n const t = useT()\n const locale = useLocale()\n const { item, kind, conflictCount } = card\n\n if (kind === 'today') {\n return (\n <div className=\"flex w-full items-center gap-1.5 rounded-lg bg-status-success-bg py-2 pl-2 pr-3\">\n <Clock className=\"size-4 shrink-0 text-status-success-icon\" aria-hidden=\"true\" />\n <span className=\"min-w-0 flex-1 truncate text-xs font-medium text-foreground\">\n {t('customers.calendar.cards.today', 'Today')}\n </span>\n {canJoin(item) ? (\n <LinkButton\n variant=\"gray\"\n size=\"sm\"\n underline=\"always\"\n onClick={() => onJoin(item)}\n className=\"shrink-0\"\n >\n {t('customers.calendar.cards.join', 'Join Meeting')}\n </LinkButton>\n ) : null}\n </div>\n )\n }\n\n if (kind === 'conflicted') {\n return (\n <div className=\"flex w-full items-center gap-1.5 rounded-lg bg-status-warning-bg py-2 pl-2 pr-3\">\n <AlertTriangle className=\"size-4 shrink-0 text-status-warning-icon\" aria-hidden=\"true\" />\n <span className=\"min-w-0 flex-1 truncate text-xs font-medium text-foreground\">\n {t('customers.calendar.cards.conflictedCount', '{count} Conflicted', {\n count: conflictCount,\n })}\n </span>\n <LinkButton\n variant=\"gray\"\n size=\"sm\"\n underline=\"always\"\n onClick={() => onSeeConflict(item)}\n className=\"shrink-0\"\n >\n {t('customers.calendar.cards.seeConflict', 'See Conflict')}\n </LinkButton>\n </div>\n )\n }\n\n if (kind === 'cancelled') {\n return (\n <div className=\"flex w-full items-center gap-1.5 rounded-lg bg-status-error-bg py-2 pl-2 pr-3\">\n <Clock className=\"size-4 shrink-0 text-status-error-icon\" aria-hidden=\"true\" />\n <span className=\"min-w-0 flex-1 truncate text-xs font-medium text-foreground\">\n {t('customers.calendar.cards.cancelled', 'Cancelled')}\n </span>\n <span className=\"shrink-0 text-xs text-muted-foreground\">\n {formatDateLabel(locale, item.start)}\n </span>\n </div>\n )\n }\n\n const daysLater = Math.max(0, differenceInCalendarDays(item.start, new Date()))\n return (\n <div className=\"flex w-full items-center gap-1.5 rounded-lg bg-muted py-2 pl-2 pr-3\">\n <Clock className=\"size-4 shrink-0 text-muted-foreground\" aria-hidden=\"true\" />\n <span className=\"min-w-0 flex-1 truncate text-xs text-muted-foreground\">\n {daysLater === 1\n ? t('customers.calendar.cards.dayLater', '1 day later')\n : t('customers.calendar.cards.daysLater', '{days} days later', { days: daysLater })}\n </span>\n <span className=\"shrink-0 text-xs text-muted-foreground\">\n {formatDateLabel(locale, item.start)}\n </span>\n </div>\n )\n}\n\nfunction UpcomingCardItem({\n card,\n canManage,\n onJoin,\n onSeeConflict,\n onOpen,\n onEdit,\n onCancel,\n}: { card: UpcomingCard } & CardCallbacks) {\n const t = useT()\n const locale = useLocale()\n const { item } = card\n\n return (\n <article className=\"flex w-[260px] shrink-0 snap-start flex-col gap-3 rounded-xl border bg-card px-2 pb-2 pt-4 shadow-xs sm:w-auto sm:shrink\">\n <div className=\"flex items-start gap-2 px-2\">\n <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n <p className=\"truncate text-sm font-medium text-foreground\">{item.title}</p>\n <p className=\"truncate text-xs text-muted-foreground\">\n {formatTimeRange(locale, item, t('customers.calendar.cards.allDay', 'All day'))}\n </p>\n </div>\n <Popover>\n <PopoverTrigger asChild>\n <IconButton\n variant=\"outline\"\n size=\"xs\"\n fullRadius\n aria-label={t('customers.calendar.cards.menu.label', 'Event actions')}\n >\n <ChevronDown aria-hidden=\"true\" />\n </IconButton>\n </PopoverTrigger>\n <PopoverContent align=\"end\" className=\"w-40 min-w-0 p-1\">\n <PopoverClose asChild>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start\"\n onClick={() => onOpen(item)}\n >\n {t('customers.calendar.cards.menu.open', 'Open')}\n </Button>\n </PopoverClose>\n {canManage ? (\n <PopoverClose asChild>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start\"\n onClick={() => onEdit(item)}\n >\n {t('customers.calendar.cards.menu.edit', 'Edit')}\n </Button>\n </PopoverClose>\n ) : null}\n {canManage ? (\n <PopoverClose asChild>\n <Button\n type=\"button\"\n variant=\"destructive-ghost\"\n size=\"sm\"\n className=\"w-full justify-start\"\n onClick={() => onCancel(item)}\n >\n {t('customers.calendar.cards.menu.cancel', 'Cancel')}\n </Button>\n </PopoverClose>\n ) : null}\n </PopoverContent>\n </Popover>\n </div>\n <UpcomingCardStatus card={card} onJoin={onJoin} onSeeConflict={onSeeConflict} />\n </article>\n )\n}\n\nexport function UpcomingCards({\n cards,\n canManage = true,\n onJoin,\n onSeeConflict,\n onOpen,\n onEdit,\n onCancel,\n}: UpcomingCardsProps) {\n if (cards.length === 0) return null\n return (\n <div className=\"flex snap-x snap-mandatory gap-3 overflow-x-auto pb-1 sm:grid sm:snap-none sm:grid-cols-2 sm:gap-4 sm:overflow-visible sm:pb-0 xl:grid-cols-4\">\n {cards.map((card) => (\n <UpcomingCardItem\n key={card.item.id}\n card={card}\n canManage={canManage}\n onJoin={onJoin}\n onSeeConflict={onSeeConflict}\n onOpen={onOpen}\n onEdit={onEdit}\n onCancel={onCancel}\n />\n ))}\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,27 @@
1
+ const DATE_OPTIONS = { day: "numeric", month: "short", year: "numeric" };
2
+ const TIME_OPTIONS = { hour: "numeric", minute: "2-digit" };
3
+ function formatDateLabel(locale, date) {
4
+ return new Intl.DateTimeFormat(locale, DATE_OPTIONS).format(date);
5
+ }
6
+ function formatDateRangeLabel(locale, from, to) {
7
+ const formatter = new Intl.DateTimeFormat(locale, DATE_OPTIONS);
8
+ try {
9
+ return formatter.formatRange(from, to);
10
+ } catch {
11
+ return `${formatter.format(from)} \u2013 ${formatter.format(to)}`;
12
+ }
13
+ }
14
+ function formatTimeRangeLabel(locale, start, end) {
15
+ const formatter = new Intl.DateTimeFormat(locale, TIME_OPTIONS);
16
+ try {
17
+ return formatter.formatRange(start, end);
18
+ } catch {
19
+ return `${formatter.format(start)} \u2013 ${formatter.format(end)}`;
20
+ }
21
+ }
22
+ export {
23
+ formatDateLabel,
24
+ formatDateRangeLabel,
25
+ formatTimeRangeLabel
26
+ };
27
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/modules/customers/lib/calendar/format.ts"],
4
+ "sourcesContent": ["const DATE_OPTIONS: Intl.DateTimeFormatOptions = { day: 'numeric', month: 'short', year: 'numeric' }\n\nconst TIME_OPTIONS: Intl.DateTimeFormatOptions = { hour: 'numeric', minute: '2-digit' }\n\nexport function formatDateLabel(locale: string, date: Date): string {\n return new Intl.DateTimeFormat(locale, DATE_OPTIONS).format(date)\n}\n\nexport function formatDateRangeLabel(locale: string, from: Date, to: Date): string {\n const formatter = new Intl.DateTimeFormat(locale, DATE_OPTIONS)\n try {\n return formatter.formatRange(from, to)\n } catch {\n return `${formatter.format(from)} \u2013 ${formatter.format(to)}`\n }\n}\n\nexport function formatTimeRangeLabel(locale: string, start: Date, end: Date): string {\n const formatter = new Intl.DateTimeFormat(locale, TIME_OPTIONS)\n try {\n return formatter.formatRange(start, end)\n } catch {\n return `${formatter.format(start)} \u2013 ${formatter.format(end)}`\n }\n}\n"],
5
+ "mappings": "AAAA,MAAM,eAA2C,EAAE,KAAK,WAAW,OAAO,SAAS,MAAM,UAAU;AAEnG,MAAM,eAA2C,EAAE,MAAM,WAAW,QAAQ,UAAU;AAE/E,SAAS,gBAAgB,QAAgB,MAAoB;AAClE,SAAO,IAAI,KAAK,eAAe,QAAQ,YAAY,EAAE,OAAO,IAAI;AAClE;AAEO,SAAS,qBAAqB,QAAgB,MAAY,IAAkB;AACjF,QAAM,YAAY,IAAI,KAAK,eAAe,QAAQ,YAAY;AAC9D,MAAI;AACF,WAAO,UAAU,YAAY,MAAM,EAAE;AAAA,EACvC,QAAQ;AACN,WAAO,GAAG,UAAU,OAAO,IAAI,CAAC,WAAM,UAAU,OAAO,EAAE,CAAC;AAAA,EAC5D;AACF;AAEO,SAAS,qBAAqB,QAAgB,OAAa,KAAmB;AACnF,QAAM,YAAY,IAAI,KAAK,eAAe,QAAQ,YAAY;AAC9D,MAAI;AACF,WAAO,UAAU,YAAY,OAAO,GAAG;AAAA,EACzC,QAAQ;AACN,WAAO,GAAG,UAAU,OAAO,KAAK,CAAC,WAAM,UAAU,OAAO,GAAG,CAAC;AAAA,EAC9D;AACF;",
6
+ "names": []
7
+ }
@@ -7,9 +7,11 @@ import { IconButton } from "@open-mercato/ui/primitives/icon-button";
7
7
  import { useT } from "@open-mercato/shared/lib/i18n/context";
8
8
  import { apiCall, apiCallOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
9
9
  import { flash } from "@open-mercato/ui/backend/FlashMessages";
10
+ import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
10
11
  import { ProjectColorDot } from "./ProjectColorDot.js";
11
12
  import { startTimerEntry } from "./startTimer.js";
12
13
  import { resolveTimerActionError } from "./timerErrors.js";
14
+ const TIMER_MUTATION_CONTEXT_ID = "staff-timesheets-timer-bar";
13
15
  function formatElapsed(seconds) {
14
16
  const h = Math.floor(seconds / 3600);
15
17
  const m = Math.floor(seconds % 3600 / 60);
@@ -25,6 +27,10 @@ function getToday() {
25
27
  }
26
28
  function TimerBar({ projects, staffMemberId, onTimerStopped }) {
27
29
  const t = useT();
30
+ const { runMutation, retryLastMutation } = useGuardedMutation({
31
+ contextId: TIMER_MUTATION_CONTEXT_ID,
32
+ blockedMessage: t("ui.forms.flash.saveBlocked", "Save blocked by validation")
33
+ });
28
34
  const [activeEntryId, setActiveEntryId] = useState(null);
29
35
  const [activeProjectId, setActiveProjectId] = useState(null);
30
36
  const [elapsedSeconds, setElapsedSeconds] = useState(0);
@@ -103,11 +109,23 @@ function TimerBar({ projects, staffMemberId, onTimerStopped }) {
103
109
  setIsStarting(true);
104
110
  try {
105
111
  const today = getToday();
106
- const { id: entryId } = await startTimerEntry({
112
+ const startPayload = {
107
113
  staffMemberId,
108
114
  timeProjectId: selectedProjectId,
109
115
  date: today,
110
116
  notes: description || null
117
+ };
118
+ const { id: entryId } = await runMutation({
119
+ operation: () => startTimerEntry(startPayload),
120
+ context: {
121
+ formId: TIMER_MUTATION_CONTEXT_ID,
122
+ resourceKind: "staff.timesheets.time_entry",
123
+ resourceId: staffMemberId,
124
+ staffMemberId,
125
+ action: "timer-start",
126
+ retryLastMutation
127
+ },
128
+ mutationPayload: startPayload
111
129
  });
112
130
  setActiveEntryId(entryId);
113
131
  setActiveProjectId(selectedProjectId);
@@ -125,10 +143,26 @@ function TimerBar({ projects, staffMemberId, onTimerStopped }) {
125
143
  if (!activeEntryId) return;
126
144
  setIsStopping(true);
127
145
  try {
128
- await apiCallOrThrow(
129
- `/api/staff/timesheets/time-entries/${activeEntryId}/timer-stop`,
130
- { method: "POST" }
131
- );
146
+ const stopPayload = {
147
+ id: activeEntryId,
148
+ action: "timer-stop",
149
+ staffMemberId
150
+ };
151
+ await runMutation({
152
+ operation: () => apiCallOrThrow(
153
+ `/api/staff/timesheets/time-entries/${activeEntryId}/timer-stop`,
154
+ { method: "POST" }
155
+ ),
156
+ context: {
157
+ formId: TIMER_MUTATION_CONTEXT_ID,
158
+ resourceKind: "staff.timesheets.time_entry",
159
+ resourceId: activeEntryId,
160
+ staffMemberId,
161
+ action: "timer-stop",
162
+ retryLastMutation
163
+ },
164
+ mutationPayload: stopPayload
165
+ });
132
166
  setActiveEntryId(null);
133
167
  setActiveProjectId(null);
134
168
  setDescription("");
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/staff/lib/timesheets-ui/TimerBar.tsx"],
4
- "sourcesContent": ["'use client'\n\nimport { useState, useEffect, useRef, useCallback } from 'react'\nimport { Play, Square } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { ProjectColorDot } from './ProjectColorDot'\nimport { startTimerEntry } from './startTimer'\nimport { resolveTimerActionError } from './timerErrors'\n\ntype ProjectOption = {\n id: string\n name: string\n code: string | null\n color?: string | null\n}\n\ntype TimerBarProps = {\n projects: ProjectOption[]\n staffMemberId: string | null\n onTimerStopped: () => void\n}\n\nfunction formatElapsed(seconds: number): string {\n const h = Math.floor(seconds / 3600)\n const m = Math.floor((seconds % 3600) / 60)\n const s = seconds % 60\n return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`\n}\n\nfunction getToday(): string {\n const now = new Date()\n const year = now.getFullYear()\n const month = String(now.getMonth() + 1).padStart(2, '0')\n const day = String(now.getDate()).padStart(2, '0')\n return `${year}-${month}-${day}`\n}\n\nexport function TimerBar({ projects, staffMemberId, onTimerStopped }: TimerBarProps) {\n const t = useT()\n\n const [activeEntryId, setActiveEntryId] = useState<string | null>(null)\n const [activeProjectId, setActiveProjectId] = useState<string | null>(null)\n const [elapsedSeconds, setElapsedSeconds] = useState(0)\n const [description, setDescription] = useState('')\n const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null)\n const [isStarting, setIsStarting] = useState(false)\n const [isStopping, setIsStopping] = useState(false)\n const [showProjectDropdown, setShowProjectDropdown] = useState(false)\n const [projectFilter, setProjectFilter] = useState('')\n\n const dropdownRef = useRef<HTMLDivElement>(null)\n const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)\n\n const isRunning = activeEntryId !== null\n\n const activeProject = projects.find((p) => p.id === activeProjectId)\n const selectedProject = projects.find((p) => p.id === selectedProjectId)\n\n const filteredProjects = projects.filter((p) =>\n p.name.toLowerCase().includes(projectFilter.toLowerCase()),\n )\n\n const startElapsedCounter = useCallback((startedAt: string) => {\n const startTime = new Date(startedAt).getTime()\n const now = Date.now()\n const initialElapsed = Math.max(0, Math.floor((now - startTime) / 1000))\n setElapsedSeconds(initialElapsed)\n\n if (intervalRef.current) clearInterval(intervalRef.current)\n intervalRef.current = setInterval(() => {\n setElapsedSeconds((prev) => prev + 1)\n }, 1000)\n }, [])\n\n const stopElapsedCounter = useCallback(() => {\n if (intervalRef.current) {\n clearInterval(intervalRef.current)\n intervalRef.current = null\n }\n setElapsedSeconds(0)\n }, [])\n\n useEffect(() => {\n if (!staffMemberId) return\n\n const today = getToday()\n const checkActiveTimer = async () => {\n const response = await apiCall(\n `/api/staff/timesheets/time-entries?staffMemberId=${staffMemberId}&from=${today}&to=${today}&pageSize=50`,\n )\n if (!response.ok) return\n\n const data = response.result as Record<string, unknown> | null\n const items = (data?.items ?? data?.data ?? []) as Array<Record<string, unknown>>\n\n const activeEntry = items.find(\n (entry: Record<string, unknown>) =>\n entry.started_at && !entry.ended_at,\n )\n\n if (activeEntry) {\n setActiveEntryId(String(activeEntry.id ?? ''))\n setActiveProjectId(String(activeEntry.time_project_id ?? ''))\n setDescription(String(activeEntry.notes ?? ''))\n startElapsedCounter(String(activeEntry.started_at ?? ''))\n }\n }\n\n checkActiveTimer()\n }, [staffMemberId, startElapsedCounter])\n\n useEffect(() => {\n return () => {\n if (intervalRef.current) clearInterval(intervalRef.current)\n }\n }, [])\n\n useEffect(() => {\n function handleClickOutside(event: MouseEvent) {\n if (\n dropdownRef.current &&\n !dropdownRef.current.contains(event.target as Node)\n ) {\n setShowProjectDropdown(false)\n setProjectFilter('')\n }\n }\n\n if (showProjectDropdown) {\n document.addEventListener('mousedown', handleClickOutside)\n }\n return () => document.removeEventListener('mousedown', handleClickOutside)\n }, [showProjectDropdown])\n\n const handleStart = async () => {\n if (!selectedProjectId || !staffMemberId) return\n\n setIsStarting(true)\n try {\n const today = getToday()\n const { id: entryId } = await startTimerEntry({\n staffMemberId,\n timeProjectId: selectedProjectId,\n date: today,\n notes: description || null,\n })\n\n setActiveEntryId(entryId)\n setActiveProjectId(selectedProjectId)\n startElapsedCounter(new Date().toISOString())\n } catch (err) {\n flash(\n resolveTimerActionError(err, t('staff.timesheets.my.timer.startError', 'Failed to start timer')),\n 'error',\n )\n } finally {\n setIsStarting(false)\n }\n }\n\n const handleStop = async () => {\n if (!activeEntryId) return\n\n setIsStopping(true)\n try {\n await apiCallOrThrow(\n `/api/staff/timesheets/time-entries/${activeEntryId}/timer-stop`,\n { method: 'POST' },\n )\n\n setActiveEntryId(null)\n setActiveProjectId(null)\n setDescription('')\n stopElapsedCounter()\n onTimerStopped()\n } catch (err) {\n flash(\n resolveTimerActionError(err, t('staff.timesheets.my.timer.stopError', 'Failed to stop timer')),\n 'error',\n )\n } finally {\n setIsStopping(false)\n }\n }\n\n return (\n <div className=\"flex items-center gap-3 rounded-lg border bg-card p-3 mb-4\">\n <input\n type=\"text\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n readOnly={isRunning}\n placeholder={t(\n 'staff.timesheets.my.timer.placeholder',\n 'What are you working on?',\n )}\n className=\"flex-1 bg-transparent border-0 outline-none text-sm placeholder:text-muted-foreground\"\n />\n\n <div className=\"relative\" ref={dropdownRef}>\n {isRunning ? (\n activeProject ? (\n <span className=\"inline-flex items-center gap-1.5 text-xs font-medium px-2 py-1 rounded border bg-muted\">\n <ProjectColorDot colorKey={activeProject.color} projectName={activeProject.name} size=\"xs\" />\n {activeProject.name}\n </span>\n ) : null\n ) : (\n <>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => {\n setShowProjectDropdown(!showProjectDropdown)\n setProjectFilter('')\n }}\n className=\"text-xs font-medium\"\n >\n {selectedProject\n ? (<><ProjectColorDot colorKey={selectedProject.color} projectName={selectedProject.name} size=\"xs\" /><span className=\"ml-1\">{selectedProject.name}</span></>)\n : t('staff.timesheets.my.timer.selectProject', 'Project')}\n </Button>\n\n {showProjectDropdown && (\n <div className=\"absolute right-0 top-full mt-1 z-50 min-w-[200px] rounded-md border bg-popover p-1 shadow-md\">\n <input\n type=\"text\"\n value={projectFilter}\n onChange={(e) => setProjectFilter(e.target.value)}\n placeholder={t(\n 'staff.timesheets.my.timer.searchProject',\n 'Search projects...',\n )}\n className=\"w-full bg-transparent border-b px-2 py-1.5 text-xs outline-none placeholder:text-muted-foreground mb-1\"\n autoFocus\n />\n <div className=\"max-h-[200px] overflow-y-auto\">\n {filteredProjects.length === 0 ? (\n <div className=\"px-2 py-1.5 text-xs text-muted-foreground\">\n {t(\n 'staff.timesheets.my.timer.noProjects',\n 'No projects found',\n )}\n </div>\n ) : (\n filteredProjects.map((project) => (\n <Button\n key={project.id}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start text-xs h-auto py-1.5\"\n onClick={() => {\n setSelectedProjectId(project.id)\n setShowProjectDropdown(false)\n setProjectFilter('')\n }}\n >\n <ProjectColorDot colorKey={project.color} projectName={project.name} size=\"xs\" />\n <span className=\"ml-1\">{project.name}</span>\n {project.code ? (\n <span className=\"ml-1 text-muted-foreground\">\n ({project.code})\n </span>\n ) : null}\n </Button>\n ))\n )}\n </div>\n </div>\n )}\n </>\n )}\n </div>\n\n <span className=\"font-mono text-sm tabular-nums min-w-[64px] text-right\">\n {formatElapsed(elapsedSeconds)}\n </span>\n\n {isRunning ? (\n <IconButton\n type=\"button\"\n variant=\"destructive\"\n size=\"default\"\n onClick={handleStop}\n disabled={isStopping}\n aria-label={t('staff.timesheets.my.timer.stop', 'Stop timer')}\n >\n <Square className=\"size-4\" />\n </IconButton>\n ) : (\n <IconButton\n type=\"button\"\n variant=\"primary\"\n size=\"default\"\n onClick={handleStart}\n disabled={isStarting || !selectedProjectId}\n aria-label={t('staff.timesheets.my.timer.start', 'Start timer')}\n >\n <Play className=\"size-4\" />\n </IconButton>\n )}\n </div>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["'use client'\n\nimport { useState, useEffect, useRef, useCallback } from 'react'\nimport { Play, Square } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { ProjectColorDot } from './ProjectColorDot'\nimport { startTimerEntry } from './startTimer'\nimport { resolveTimerActionError } from './timerErrors'\n\ntype ProjectOption = {\n id: string\n name: string\n code: string | null\n color?: string | null\n}\n\ntype TimerBarProps = {\n projects: ProjectOption[]\n staffMemberId: string | null\n onTimerStopped: () => void\n}\n\nconst TIMER_MUTATION_CONTEXT_ID = 'staff-timesheets-timer-bar'\n\ntype TimerMutationContext = {\n formId: string\n resourceKind: string\n resourceId: string\n staffMemberId: string | null\n action: 'timer-create' | 'timer-start' | 'timer-stop'\n retryLastMutation: () => Promise<boolean>\n}\n\nfunction formatElapsed(seconds: number): string {\n const h = Math.floor(seconds / 3600)\n const m = Math.floor((seconds % 3600) / 60)\n const s = seconds % 60\n return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`\n}\n\nfunction getToday(): string {\n const now = new Date()\n const year = now.getFullYear()\n const month = String(now.getMonth() + 1).padStart(2, '0')\n const day = String(now.getDate()).padStart(2, '0')\n return `${year}-${month}-${day}`\n}\n\nexport function TimerBar({ projects, staffMemberId, onTimerStopped }: TimerBarProps) {\n const t = useT()\n const { runMutation, retryLastMutation } = useGuardedMutation<TimerMutationContext>({\n contextId: TIMER_MUTATION_CONTEXT_ID,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n const [activeEntryId, setActiveEntryId] = useState<string | null>(null)\n const [activeProjectId, setActiveProjectId] = useState<string | null>(null)\n const [elapsedSeconds, setElapsedSeconds] = useState(0)\n const [description, setDescription] = useState('')\n const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null)\n const [isStarting, setIsStarting] = useState(false)\n const [isStopping, setIsStopping] = useState(false)\n const [showProjectDropdown, setShowProjectDropdown] = useState(false)\n const [projectFilter, setProjectFilter] = useState('')\n\n const dropdownRef = useRef<HTMLDivElement>(null)\n const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)\n\n const isRunning = activeEntryId !== null\n\n const activeProject = projects.find((p) => p.id === activeProjectId)\n const selectedProject = projects.find((p) => p.id === selectedProjectId)\n\n const filteredProjects = projects.filter((p) =>\n p.name.toLowerCase().includes(projectFilter.toLowerCase()),\n )\n\n const startElapsedCounter = useCallback((startedAt: string) => {\n const startTime = new Date(startedAt).getTime()\n const now = Date.now()\n const initialElapsed = Math.max(0, Math.floor((now - startTime) / 1000))\n setElapsedSeconds(initialElapsed)\n\n if (intervalRef.current) clearInterval(intervalRef.current)\n intervalRef.current = setInterval(() => {\n setElapsedSeconds((prev) => prev + 1)\n }, 1000)\n }, [])\n\n const stopElapsedCounter = useCallback(() => {\n if (intervalRef.current) {\n clearInterval(intervalRef.current)\n intervalRef.current = null\n }\n setElapsedSeconds(0)\n }, [])\n\n useEffect(() => {\n if (!staffMemberId) return\n\n const today = getToday()\n const checkActiveTimer = async () => {\n const response = await apiCall(\n `/api/staff/timesheets/time-entries?staffMemberId=${staffMemberId}&from=${today}&to=${today}&pageSize=50`,\n )\n if (!response.ok) return\n\n const data = response.result as Record<string, unknown> | null\n const items = (data?.items ?? data?.data ?? []) as Array<Record<string, unknown>>\n\n const activeEntry = items.find(\n (entry: Record<string, unknown>) =>\n entry.started_at && !entry.ended_at,\n )\n\n if (activeEntry) {\n setActiveEntryId(String(activeEntry.id ?? ''))\n setActiveProjectId(String(activeEntry.time_project_id ?? ''))\n setDescription(String(activeEntry.notes ?? ''))\n startElapsedCounter(String(activeEntry.started_at ?? ''))\n }\n }\n\n checkActiveTimer()\n }, [staffMemberId, startElapsedCounter])\n\n useEffect(() => {\n return () => {\n if (intervalRef.current) clearInterval(intervalRef.current)\n }\n }, [])\n\n useEffect(() => {\n function handleClickOutside(event: MouseEvent) {\n if (\n dropdownRef.current &&\n !dropdownRef.current.contains(event.target as Node)\n ) {\n setShowProjectDropdown(false)\n setProjectFilter('')\n }\n }\n\n if (showProjectDropdown) {\n document.addEventListener('mousedown', handleClickOutside)\n }\n return () => document.removeEventListener('mousedown', handleClickOutside)\n }, [showProjectDropdown])\n\n const handleStart = async () => {\n if (!selectedProjectId || !staffMemberId) return\n\n setIsStarting(true)\n try {\n const today = getToday()\n const startPayload = {\n staffMemberId,\n timeProjectId: selectedProjectId,\n date: today,\n notes: description || null,\n }\n const { id: entryId } = await runMutation({\n operation: () => startTimerEntry(startPayload),\n context: {\n formId: TIMER_MUTATION_CONTEXT_ID,\n resourceKind: 'staff.timesheets.time_entry',\n resourceId: staffMemberId,\n staffMemberId,\n action: 'timer-start',\n retryLastMutation,\n },\n mutationPayload: startPayload,\n })\n\n setActiveEntryId(entryId)\n setActiveProjectId(selectedProjectId)\n startElapsedCounter(new Date().toISOString())\n } catch (err) {\n flash(\n resolveTimerActionError(err, t('staff.timesheets.my.timer.startError', 'Failed to start timer')),\n 'error',\n )\n } finally {\n setIsStarting(false)\n }\n }\n\n const handleStop = async () => {\n if (!activeEntryId) return\n\n setIsStopping(true)\n try {\n const stopPayload = {\n id: activeEntryId,\n action: 'timer-stop',\n staffMemberId,\n }\n await runMutation({\n operation: () =>\n apiCallOrThrow(\n `/api/staff/timesheets/time-entries/${activeEntryId}/timer-stop`,\n { method: 'POST' },\n ),\n context: {\n formId: TIMER_MUTATION_CONTEXT_ID,\n resourceKind: 'staff.timesheets.time_entry',\n resourceId: activeEntryId,\n staffMemberId,\n action: 'timer-stop',\n retryLastMutation,\n },\n mutationPayload: stopPayload,\n })\n\n setActiveEntryId(null)\n setActiveProjectId(null)\n setDescription('')\n stopElapsedCounter()\n onTimerStopped()\n } catch (err) {\n flash(\n resolveTimerActionError(err, t('staff.timesheets.my.timer.stopError', 'Failed to stop timer')),\n 'error',\n )\n } finally {\n setIsStopping(false)\n }\n }\n\n return (\n <div className=\"flex items-center gap-3 rounded-lg border bg-card p-3 mb-4\">\n <input\n type=\"text\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n readOnly={isRunning}\n placeholder={t(\n 'staff.timesheets.my.timer.placeholder',\n 'What are you working on?',\n )}\n className=\"flex-1 bg-transparent border-0 outline-none text-sm placeholder:text-muted-foreground\"\n />\n\n <div className=\"relative\" ref={dropdownRef}>\n {isRunning ? (\n activeProject ? (\n <span className=\"inline-flex items-center gap-1.5 text-xs font-medium px-2 py-1 rounded border bg-muted\">\n <ProjectColorDot colorKey={activeProject.color} projectName={activeProject.name} size=\"xs\" />\n {activeProject.name}\n </span>\n ) : null\n ) : (\n <>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => {\n setShowProjectDropdown(!showProjectDropdown)\n setProjectFilter('')\n }}\n className=\"text-xs font-medium\"\n >\n {selectedProject\n ? (<><ProjectColorDot colorKey={selectedProject.color} projectName={selectedProject.name} size=\"xs\" /><span className=\"ml-1\">{selectedProject.name}</span></>)\n : t('staff.timesheets.my.timer.selectProject', 'Project')}\n </Button>\n\n {showProjectDropdown && (\n <div className=\"absolute right-0 top-full mt-1 z-50 min-w-[200px] rounded-md border bg-popover p-1 shadow-md\">\n <input\n type=\"text\"\n value={projectFilter}\n onChange={(e) => setProjectFilter(e.target.value)}\n placeholder={t(\n 'staff.timesheets.my.timer.searchProject',\n 'Search projects...',\n )}\n className=\"w-full bg-transparent border-b px-2 py-1.5 text-xs outline-none placeholder:text-muted-foreground mb-1\"\n autoFocus\n />\n <div className=\"max-h-[200px] overflow-y-auto\">\n {filteredProjects.length === 0 ? (\n <div className=\"px-2 py-1.5 text-xs text-muted-foreground\">\n {t(\n 'staff.timesheets.my.timer.noProjects',\n 'No projects found',\n )}\n </div>\n ) : (\n filteredProjects.map((project) => (\n <Button\n key={project.id}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start text-xs h-auto py-1.5\"\n onClick={() => {\n setSelectedProjectId(project.id)\n setShowProjectDropdown(false)\n setProjectFilter('')\n }}\n >\n <ProjectColorDot colorKey={project.color} projectName={project.name} size=\"xs\" />\n <span className=\"ml-1\">{project.name}</span>\n {project.code ? (\n <span className=\"ml-1 text-muted-foreground\">\n ({project.code})\n </span>\n ) : null}\n </Button>\n ))\n )}\n </div>\n </div>\n )}\n </>\n )}\n </div>\n\n <span className=\"font-mono text-sm tabular-nums min-w-[64px] text-right\">\n {formatElapsed(elapsedSeconds)}\n </span>\n\n {isRunning ? (\n <IconButton\n type=\"button\"\n variant=\"destructive\"\n size=\"default\"\n onClick={handleStop}\n disabled={isStopping}\n aria-label={t('staff.timesheets.my.timer.stop', 'Stop timer')}\n >\n <Square className=\"size-4\" />\n </IconButton>\n ) : (\n <IconButton\n type=\"button\"\n variant=\"primary\"\n size=\"default\"\n onClick={handleStart}\n disabled={isStarting || !selectedProjectId}\n aria-label={t('staff.timesheets.my.timer.start', 'Start timer')}\n >\n <Play className=\"size-4\" />\n </IconButton>\n )}\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -5,9 +5,11 @@ import { apiCall, readApiResultOrThrow } from "@open-mercato/ui/backend/utils/ap
5
5
  import { Button } from "@open-mercato/ui/primitives/button";
6
6
  import { Input } from "@open-mercato/ui/primitives/input";
7
7
  import { useT } from "@open-mercato/shared/lib/i18n/context";
8
+ import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
8
9
  import { startTimerEntry } from "../../../lib/timesheets-ui/startTimer.js";
9
10
  import { resolveTimerActionError } from "../../../lib/timesheets-ui/timerErrors.js";
10
11
  import { DEFAULT_SETTINGS, hydrateSettings } from "./config.js";
12
+ const TIMER_WIDGET_MUTATION_CONTEXT_ID = "staff-timesheets-time-reporting-widget";
11
13
  function formatElapsed(startedAt) {
12
14
  const elapsed = Math.max(0, Date.now() - new Date(startedAt).getTime());
13
15
  const totalSeconds = Math.floor(elapsed / 1e3);
@@ -24,6 +26,10 @@ const TimeReportingWidget = ({
24
26
  onRefreshStateChange
25
27
  }) => {
26
28
  const t = useT();
29
+ const { runMutation, retryLastMutation } = useGuardedMutation({
30
+ contextId: TIMER_WIDGET_MUTATION_CONTEXT_ID,
31
+ blockedMessage: t("ui.forms.flash.saveBlocked", "Save blocked by validation")
32
+ });
27
33
  const hydrated = React.useMemo(() => hydrateSettings(settings), [settings]);
28
34
  const [projects, setProjects] = React.useState([]);
29
35
  const [selectedProjectId, setSelectedProjectId] = React.useState(hydrated.lastProjectId);
@@ -116,11 +122,23 @@ const TimeReportingWidget = ({
116
122
  setActionLoading(true);
117
123
  try {
118
124
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
119
- await startTimerEntry({
125
+ const startPayload = {
120
126
  staffMemberId,
121
127
  timeProjectId: selectedProjectId,
122
128
  date: today,
123
129
  notes: notes.trim() || null
130
+ };
131
+ await runMutation({
132
+ operation: () => startTimerEntry(startPayload),
133
+ context: {
134
+ formId: TIMER_WIDGET_MUTATION_CONTEXT_ID,
135
+ resourceKind: "staff.timesheets.time_entry",
136
+ resourceId: staffMemberId,
137
+ staffMemberId,
138
+ action: "timer-start",
139
+ retryLastMutation
140
+ },
141
+ mutationPayload: startPayload
124
142
  });
125
143
  onSettingsChange({ ...hydrated, lastProjectId: selectedProjectId });
126
144
  await loadState();
@@ -130,12 +148,28 @@ const TimeReportingWidget = ({
130
148
  } finally {
131
149
  setActionLoading(false);
132
150
  }
133
- }, [selectedProjectId, staffMemberId, timer.running, notes, hydrated, onSettingsChange, loadState, t]);
151
+ }, [selectedProjectId, staffMemberId, timer.running, notes, hydrated, onSettingsChange, loadState, runMutation, retryLastMutation, t]);
134
152
  const handleStop = React.useCallback(async () => {
135
- if (!timer.entryId) return;
153
+ if (!timer.entryId || !staffMemberId) return;
136
154
  setActionLoading(true);
137
155
  try {
138
- await apiCall(`/api/staff/timesheets/time-entries/${timer.entryId}/timer-stop`, { method: "POST" });
156
+ const stopPayload = {
157
+ id: timer.entryId,
158
+ action: "timer-stop",
159
+ staffMemberId
160
+ };
161
+ await runMutation({
162
+ operation: () => apiCall(`/api/staff/timesheets/time-entries/${timer.entryId}/timer-stop`, { method: "POST" }),
163
+ context: {
164
+ formId: TIMER_WIDGET_MUTATION_CONTEXT_ID,
165
+ resourceKind: "staff.timesheets.time_entry",
166
+ resourceId: timer.entryId,
167
+ staffMemberId,
168
+ action: "timer-stop",
169
+ retryLastMutation
170
+ },
171
+ mutationPayload: stopPayload
172
+ });
139
173
  await loadState();
140
174
  } catch (err) {
141
175
  console.error("staff.timesheets.timeReporting.stop", err);
@@ -143,7 +177,7 @@ const TimeReportingWidget = ({
143
177
  } finally {
144
178
  setActionLoading(false);
145
179
  }
146
- }, [timer.entryId, loadState, t]);
180
+ }, [timer.entryId, staffMemberId, loadState, runMutation, retryLastMutation, t]);
147
181
  if (mode === "settings") {
148
182
  return /* @__PURE__ */ jsx("div", { className: "space-y-2 text-sm", children: /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: t("staff.timesheets.widgets.timeReporting.settings.description", "No additional settings. Select a project and start tracking from the widget.") }) });
149
183
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { startTimerEntry } from '../../../lib/timesheets-ui/startTimer'\nimport { resolveTimerActionError } from '../../../lib/timesheets-ui/timerErrors'\nimport { DEFAULT_SETTINGS, hydrateSettings, type TimeReportingSettings } from './config'\n\ntype ProjectOption = { id: string; name: string; code: string | null }\n\ntype TimerState = {\n entryId: string | null\n running: boolean\n startedAt: string | null\n projectId: string | null\n}\n\nfunction formatElapsed(startedAt: string): string {\n const elapsed = Math.max(0, Date.now() - new Date(startedAt).getTime())\n const totalSeconds = Math.floor(elapsed / 1000)\n const hours = Math.floor(totalSeconds / 3600)\n const minutes = Math.floor((totalSeconds % 3600) / 60)\n const seconds = totalSeconds % 60\n return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`\n}\n\nconst TimeReportingWidget: React.FC<DashboardWidgetComponentProps<TimeReportingSettings>> = ({\n mode,\n settings = DEFAULT_SETTINGS,\n onSettingsChange,\n refreshToken,\n onRefreshStateChange,\n}) => {\n const t = useT()\n const hydrated = React.useMemo(() => hydrateSettings(settings), [settings])\n\n const [projects, setProjects] = React.useState<ProjectOption[]>([])\n const [selectedProjectId, setSelectedProjectId] = React.useState<string | null>(hydrated.lastProjectId)\n const [notes, setNotes] = React.useState('')\n const [timer, setTimer] = React.useState<TimerState>({ entryId: null, running: false, startedAt: null, projectId: null })\n const [elapsed, setElapsed] = React.useState('00:00:00')\n const [staffMemberId, setStaffMemberId] = React.useState<string | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [actionLoading, setActionLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n\n const loadState = React.useCallback(async () => {\n onRefreshStateChange?.(true)\n setLoading(true)\n setError(null)\n try {\n // Load assigned projects\n const assignmentsRes = await readApiResultOrThrow<{ items?: Array<Record<string, unknown>> }>(\n '/api/staff/timesheets/my-projects?pageSize=100',\n undefined,\n { errorMessage: '', fallback: { items: [] } },\n )\n const assignmentItems = Array.isArray(assignmentsRes.items) ? assignmentsRes.items : []\n const projectIds = assignmentItems\n .map((item) => String(item.time_project_id ?? item.timeProjectId ?? ''))\n .filter((id) => id.length > 0)\n\n if (projectIds.length > 0) {\n const projectsRes = await readApiResultOrThrow<{ items?: Array<Record<string, unknown>> }>(\n `/api/staff/timesheets/time-projects?ids=${projectIds.join(',')}&pageSize=100`,\n undefined,\n { errorMessage: '', fallback: { items: [] } },\n )\n const items = Array.isArray(projectsRes.items) ? projectsRes.items : []\n setProjects(items.map((item) => ({\n id: String(item.id ?? ''),\n name: String(item.name ?? ''),\n code: typeof item.code === 'string' ? item.code : null,\n })))\n } else {\n setProjects([])\n }\n\n // Check for active timer \u2014 look for today's entries with startedAt set and endedAt null\n const selfRes = await readApiResultOrThrow<{ member?: { id: string } | null }>(\n '/api/staff/team-members/self',\n undefined,\n { errorMessage: '', fallback: { member: null } },\n )\n const memberId = selfRes.member?.id ?? null\n setStaffMemberId(memberId)\n if (memberId) {\n const today = new Date().toISOString().slice(0, 10)\n const entriesRes = await readApiResultOrThrow<{ items?: Array<Record<string, unknown>> }>(\n `/api/staff/timesheets/time-entries?staffMemberId=${memberId}&from=${today}&to=${today}&pageSize=100`,\n undefined,\n { errorMessage: '', fallback: { items: [] } },\n )\n const entries = Array.isArray(entriesRes.items) ? entriesRes.items : []\n const running = entries.find((e) => {\n const startedAt = e.started_at ?? e.startedAt\n const endedAt = e.ended_at ?? e.endedAt\n return startedAt != null && endedAt == null\n })\n if (running) {\n setTimer({\n entryId: String(running.id ?? ''),\n running: true,\n startedAt: String(running.started_at ?? running.startedAt ?? ''),\n projectId: String(running.time_project_id ?? running.timeProjectId ?? ''),\n })\n } else {\n setTimer({ entryId: null, running: false, startedAt: null, projectId: null })\n }\n }\n } catch (err) {\n console.error('staff.timesheets.timeReporting.load', err)\n setError(t('staff.timesheets.widgets.timeReporting.error', 'Failed to load timer state'))\n } finally {\n setLoading(false)\n onRefreshStateChange?.(false)\n }\n }, [onRefreshStateChange, t])\n\n React.useEffect(() => {\n void loadState()\n }, [loadState, refreshToken])\n\n // Tick elapsed time\n React.useEffect(() => {\n if (!timer.running || !timer.startedAt) return\n const tick = () => setElapsed(formatElapsed(timer.startedAt!))\n tick()\n const interval = setInterval(tick, 1000)\n return () => clearInterval(interval)\n }, [timer.running, timer.startedAt])\n\n const handleStart = React.useCallback(async () => {\n if (!selectedProjectId || !staffMemberId) return\n if (timer.running) return\n setActionLoading(true)\n try {\n const today = new Date().toISOString().slice(0, 10)\n // Single atomic create+start request (issue #3311): a partial failure can\n // no longer leave an orphaned, unstarted timer entry, and a rejected start\n // now surfaces here instead of being silently ignored.\n await startTimerEntry({\n staffMemberId,\n timeProjectId: selectedProjectId,\n date: today,\n notes: notes.trim() || null,\n })\n\n onSettingsChange({ ...hydrated, lastProjectId: selectedProjectId })\n await loadState()\n } catch (err) {\n console.error('staff.timesheets.timeReporting.start', err)\n setError(resolveTimerActionError(err, t('staff.timesheets.widgets.timeReporting.startError', 'Failed to start timer')))\n } finally {\n setActionLoading(false)\n }\n }, [selectedProjectId, staffMemberId, timer.running, notes, hydrated, onSettingsChange, loadState, t])\n\n const handleStop = React.useCallback(async () => {\n if (!timer.entryId) return\n setActionLoading(true)\n try {\n await apiCall(`/api/staff/timesheets/time-entries/${timer.entryId}/timer-stop`, { method: 'POST' })\n await loadState()\n } catch (err) {\n console.error('staff.timesheets.timeReporting.stop', err)\n setError(resolveTimerActionError(err, t('staff.timesheets.widgets.timeReporting.stopError', 'Failed to stop timer')))\n } finally {\n setActionLoading(false)\n }\n }, [timer.entryId, loadState, t])\n\n if (mode === 'settings') {\n return (\n <div className=\"space-y-2 text-sm\">\n <p className=\"text-muted-foreground\">\n {t('staff.timesheets.widgets.timeReporting.settings.description', 'No additional settings. Select a project and start tracking from the widget.')}\n </p>\n </div>\n )\n }\n\n if (loading) {\n return (\n <div className=\"flex h-full items-center justify-center py-8\">\n <p className=\"text-sm text-muted-foreground\">{t('staff.timesheets.widgets.timeReporting.loading', 'Loading...')}</p>\n </div>\n )\n }\n\n if (error) {\n return (\n <div className=\"flex h-full items-center justify-center py-8\">\n <p role=\"alert\" className=\"text-sm text-destructive\">{error}</p>\n </div>\n )\n }\n\n if (projects.length === 0) {\n return (\n <div className=\"flex h-full items-center justify-center py-8\">\n <p className=\"text-sm text-muted-foreground\">\n {t('staff.timesheets.widgets.timeReporting.noProjects', 'No projects assigned.')}\n </p>\n </div>\n )\n }\n\n // Timer is running\n if (timer.running) {\n const runningProject = projects.find((p) => p.id === timer.projectId)\n return (\n <div className=\"space-y-3\">\n <div className=\"text-sm text-muted-foreground\">\n {runningProject?.name ?? t('staff.timesheets.widgets.timeReporting.unknownProject', 'Unknown project')}\n </div>\n <div className=\"text-center\">\n <p className=\"font-mono text-3xl font-bold tabular-nums\">{elapsed}</p>\n <p className=\"mt-1 text-xs text-muted-foreground\">\n {t('staff.timesheets.widgets.timeReporting.running', 'Timer running')}\n </p>\n </div>\n <Button\n type=\"button\"\n variant=\"destructive\"\n className=\"w-full\"\n onClick={handleStop}\n disabled={actionLoading}\n >\n {t('staff.timesheets.widgets.timeReporting.stop', 'Stop Timer')}\n </Button>\n </div>\n )\n }\n\n // Timer not running \u2014 show start form\n return (\n <div className=\"space-y-3\">\n <div className=\"space-y-1.5\">\n <label className=\"text-xs font-medium text-muted-foreground\" htmlFor=\"timer-project\">\n {t('staff.timesheets.widgets.timeReporting.project', 'Project')}\n </label>\n <select\n id=\"timer-project\"\n className=\"w-full rounded-md border bg-background px-3 py-2 text-sm\"\n value={selectedProjectId ?? ''}\n onChange={(e) => setSelectedProjectId(e.target.value || null)}\n >\n <option value=\"\">{t('staff.timesheets.widgets.timeReporting.selectProject', 'Select project...')}</option>\n {projects.map((project) => (\n <option key={project.id} value={project.id}>\n {project.name}{project.code ? ` (${project.code})` : ''}\n </option>\n ))}\n </select>\n </div>\n <div className=\"space-y-1.5\">\n <label className=\"text-xs font-medium text-muted-foreground\" htmlFor=\"timer-notes\">\n {t('staff.timesheets.widgets.timeReporting.taskNote', 'Task / Note')}\n </label>\n <Input\n id=\"timer-notes\"\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n placeholder={t('staff.timesheets.widgets.timeReporting.notesPlaceholder', 'What are you working on?')}\n />\n </div>\n <div className=\"text-center\">\n <p className=\"text-sm text-muted-foreground\">\n {t('staff.timesheets.widgets.timeReporting.notRunning', 'Not running')}\n </p>\n </div>\n <Button\n type=\"button\"\n className=\"w-full\"\n onClick={handleStart}\n disabled={actionLoading || !selectedProjectId}\n >\n {t('staff.timesheets.widgets.timeReporting.start', 'Start Timer')}\n </Button>\n </div>\n )\n}\n\nexport default TimeReportingWidget\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { startTimerEntry } from '../../../lib/timesheets-ui/startTimer'\nimport { resolveTimerActionError } from '../../../lib/timesheets-ui/timerErrors'\nimport { DEFAULT_SETTINGS, hydrateSettings, type TimeReportingSettings } from './config'\n\ntype ProjectOption = { id: string; name: string; code: string | null }\n\ntype TimerState = {\n entryId: string | null\n running: boolean\n startedAt: string | null\n projectId: string | null\n}\n\nconst TIMER_WIDGET_MUTATION_CONTEXT_ID = 'staff-timesheets-time-reporting-widget'\n\ntype TimerWidgetMutationContext = {\n formId: string\n resourceKind: string\n resourceId: string\n staffMemberId: string\n action: 'timer-create' | 'timer-start' | 'timer-stop'\n retryLastMutation: () => Promise<boolean>\n}\n\nfunction formatElapsed(startedAt: string): string {\n const elapsed = Math.max(0, Date.now() - new Date(startedAt).getTime())\n const totalSeconds = Math.floor(elapsed / 1000)\n const hours = Math.floor(totalSeconds / 3600)\n const minutes = Math.floor((totalSeconds % 3600) / 60)\n const seconds = totalSeconds % 60\n return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`\n}\n\nconst TimeReportingWidget: React.FC<DashboardWidgetComponentProps<TimeReportingSettings>> = ({\n mode,\n settings = DEFAULT_SETTINGS,\n onSettingsChange,\n refreshToken,\n onRefreshStateChange,\n}) => {\n const t = useT()\n const { runMutation, retryLastMutation } = useGuardedMutation<TimerWidgetMutationContext>({\n contextId: TIMER_WIDGET_MUTATION_CONTEXT_ID,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n const hydrated = React.useMemo(() => hydrateSettings(settings), [settings])\n\n const [projects, setProjects] = React.useState<ProjectOption[]>([])\n const [selectedProjectId, setSelectedProjectId] = React.useState<string | null>(hydrated.lastProjectId)\n const [notes, setNotes] = React.useState('')\n const [timer, setTimer] = React.useState<TimerState>({ entryId: null, running: false, startedAt: null, projectId: null })\n const [elapsed, setElapsed] = React.useState('00:00:00')\n const [staffMemberId, setStaffMemberId] = React.useState<string | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [actionLoading, setActionLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n\n const loadState = React.useCallback(async () => {\n onRefreshStateChange?.(true)\n setLoading(true)\n setError(null)\n try {\n // Load assigned projects\n const assignmentsRes = await readApiResultOrThrow<{ items?: Array<Record<string, unknown>> }>(\n '/api/staff/timesheets/my-projects?pageSize=100',\n undefined,\n { errorMessage: '', fallback: { items: [] } },\n )\n const assignmentItems = Array.isArray(assignmentsRes.items) ? assignmentsRes.items : []\n const projectIds = assignmentItems\n .map((item) => String(item.time_project_id ?? item.timeProjectId ?? ''))\n .filter((id) => id.length > 0)\n\n if (projectIds.length > 0) {\n const projectsRes = await readApiResultOrThrow<{ items?: Array<Record<string, unknown>> }>(\n `/api/staff/timesheets/time-projects?ids=${projectIds.join(',')}&pageSize=100`,\n undefined,\n { errorMessage: '', fallback: { items: [] } },\n )\n const items = Array.isArray(projectsRes.items) ? projectsRes.items : []\n setProjects(items.map((item) => ({\n id: String(item.id ?? ''),\n name: String(item.name ?? ''),\n code: typeof item.code === 'string' ? item.code : null,\n })))\n } else {\n setProjects([])\n }\n\n // Check for active timer \u2014 look for today's entries with startedAt set and endedAt null\n const selfRes = await readApiResultOrThrow<{ member?: { id: string } | null }>(\n '/api/staff/team-members/self',\n undefined,\n { errorMessage: '', fallback: { member: null } },\n )\n const memberId = selfRes.member?.id ?? null\n setStaffMemberId(memberId)\n if (memberId) {\n const today = new Date().toISOString().slice(0, 10)\n const entriesRes = await readApiResultOrThrow<{ items?: Array<Record<string, unknown>> }>(\n `/api/staff/timesheets/time-entries?staffMemberId=${memberId}&from=${today}&to=${today}&pageSize=100`,\n undefined,\n { errorMessage: '', fallback: { items: [] } },\n )\n const entries = Array.isArray(entriesRes.items) ? entriesRes.items : []\n const running = entries.find((e) => {\n const startedAt = e.started_at ?? e.startedAt\n const endedAt = e.ended_at ?? e.endedAt\n return startedAt != null && endedAt == null\n })\n if (running) {\n setTimer({\n entryId: String(running.id ?? ''),\n running: true,\n startedAt: String(running.started_at ?? running.startedAt ?? ''),\n projectId: String(running.time_project_id ?? running.timeProjectId ?? ''),\n })\n } else {\n setTimer({ entryId: null, running: false, startedAt: null, projectId: null })\n }\n }\n } catch (err) {\n console.error('staff.timesheets.timeReporting.load', err)\n setError(t('staff.timesheets.widgets.timeReporting.error', 'Failed to load timer state'))\n } finally {\n setLoading(false)\n onRefreshStateChange?.(false)\n }\n }, [onRefreshStateChange, t])\n\n React.useEffect(() => {\n void loadState()\n }, [loadState, refreshToken])\n\n // Tick elapsed time\n React.useEffect(() => {\n if (!timer.running || !timer.startedAt) return\n const tick = () => setElapsed(formatElapsed(timer.startedAt!))\n tick()\n const interval = setInterval(tick, 1000)\n return () => clearInterval(interval)\n }, [timer.running, timer.startedAt])\n\n const handleStart = React.useCallback(async () => {\n if (!selectedProjectId || !staffMemberId) return\n if (timer.running) return\n setActionLoading(true)\n try {\n const today = new Date().toISOString().slice(0, 10)\n // Single atomic create+start request (issue #3311): a partial failure can\n // no longer leave an orphaned, unstarted timer entry, and a rejected start\n // now surfaces here instead of being silently ignored. The call is routed\n // through the mutation guard (issue #3308) so global injection modules can\n // run onBeforeSave/onAfterSave and surface conflicts consistently.\n const startPayload = {\n staffMemberId,\n timeProjectId: selectedProjectId,\n date: today,\n notes: notes.trim() || null,\n }\n await runMutation({\n operation: () => startTimerEntry(startPayload),\n context: {\n formId: TIMER_WIDGET_MUTATION_CONTEXT_ID,\n resourceKind: 'staff.timesheets.time_entry',\n resourceId: staffMemberId,\n staffMemberId,\n action: 'timer-start',\n retryLastMutation,\n },\n mutationPayload: startPayload,\n })\n\n onSettingsChange({ ...hydrated, lastProjectId: selectedProjectId })\n await loadState()\n } catch (err) {\n console.error('staff.timesheets.timeReporting.start', err)\n setError(resolveTimerActionError(err, t('staff.timesheets.widgets.timeReporting.startError', 'Failed to start timer')))\n } finally {\n setActionLoading(false)\n }\n }, [selectedProjectId, staffMemberId, timer.running, notes, hydrated, onSettingsChange, loadState, runMutation, retryLastMutation, t])\n\n const handleStop = React.useCallback(async () => {\n if (!timer.entryId || !staffMemberId) return\n setActionLoading(true)\n try {\n const stopPayload = {\n id: timer.entryId,\n action: 'timer-stop',\n staffMemberId,\n }\n await runMutation({\n operation: () =>\n apiCall(`/api/staff/timesheets/time-entries/${timer.entryId}/timer-stop`, { method: 'POST' }),\n context: {\n formId: TIMER_WIDGET_MUTATION_CONTEXT_ID,\n resourceKind: 'staff.timesheets.time_entry',\n resourceId: timer.entryId,\n staffMemberId,\n action: 'timer-stop',\n retryLastMutation,\n },\n mutationPayload: stopPayload,\n })\n await loadState()\n } catch (err) {\n console.error('staff.timesheets.timeReporting.stop', err)\n setError(resolveTimerActionError(err, t('staff.timesheets.widgets.timeReporting.stopError', 'Failed to stop timer')))\n } finally {\n setActionLoading(false)\n }\n }, [timer.entryId, staffMemberId, loadState, runMutation, retryLastMutation, t])\n\n if (mode === 'settings') {\n return (\n <div className=\"space-y-2 text-sm\">\n <p className=\"text-muted-foreground\">\n {t('staff.timesheets.widgets.timeReporting.settings.description', 'No additional settings. Select a project and start tracking from the widget.')}\n </p>\n </div>\n )\n }\n\n if (loading) {\n return (\n <div className=\"flex h-full items-center justify-center py-8\">\n <p className=\"text-sm text-muted-foreground\">{t('staff.timesheets.widgets.timeReporting.loading', 'Loading...')}</p>\n </div>\n )\n }\n\n if (error) {\n return (\n <div className=\"flex h-full items-center justify-center py-8\">\n <p role=\"alert\" className=\"text-sm text-destructive\">{error}</p>\n </div>\n )\n }\n\n if (projects.length === 0) {\n return (\n <div className=\"flex h-full items-center justify-center py-8\">\n <p className=\"text-sm text-muted-foreground\">\n {t('staff.timesheets.widgets.timeReporting.noProjects', 'No projects assigned.')}\n </p>\n </div>\n )\n }\n\n // Timer is running\n if (timer.running) {\n const runningProject = projects.find((p) => p.id === timer.projectId)\n return (\n <div className=\"space-y-3\">\n <div className=\"text-sm text-muted-foreground\">\n {runningProject?.name ?? t('staff.timesheets.widgets.timeReporting.unknownProject', 'Unknown project')}\n </div>\n <div className=\"text-center\">\n <p className=\"font-mono text-3xl font-bold tabular-nums\">{elapsed}</p>\n <p className=\"mt-1 text-xs text-muted-foreground\">\n {t('staff.timesheets.widgets.timeReporting.running', 'Timer running')}\n </p>\n </div>\n <Button\n type=\"button\"\n variant=\"destructive\"\n className=\"w-full\"\n onClick={handleStop}\n disabled={actionLoading}\n >\n {t('staff.timesheets.widgets.timeReporting.stop', 'Stop Timer')}\n </Button>\n </div>\n )\n }\n\n // Timer not running \u2014 show start form\n return (\n <div className=\"space-y-3\">\n <div className=\"space-y-1.5\">\n <label className=\"text-xs font-medium text-muted-foreground\" htmlFor=\"timer-project\">\n {t('staff.timesheets.widgets.timeReporting.project', 'Project')}\n </label>\n <select\n id=\"timer-project\"\n className=\"w-full rounded-md border bg-background px-3 py-2 text-sm\"\n value={selectedProjectId ?? ''}\n onChange={(e) => setSelectedProjectId(e.target.value || null)}\n >\n <option value=\"\">{t('staff.timesheets.widgets.timeReporting.selectProject', 'Select project...')}</option>\n {projects.map((project) => (\n <option key={project.id} value={project.id}>\n {project.name}{project.code ? ` (${project.code})` : ''}\n </option>\n ))}\n </select>\n </div>\n <div className=\"space-y-1.5\">\n <label className=\"text-xs font-medium text-muted-foreground\" htmlFor=\"timer-notes\">\n {t('staff.timesheets.widgets.timeReporting.taskNote', 'Task / Note')}\n </label>\n <Input\n id=\"timer-notes\"\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n placeholder={t('staff.timesheets.widgets.timeReporting.notesPlaceholder', 'What are you working on?')}\n />\n </div>\n <div className=\"text-center\">\n <p className=\"text-sm text-muted-foreground\">\n {t('staff.timesheets.widgets.timeReporting.notRunning', 'Not running')}\n </p>\n </div>\n <Button\n type=\"button\"\n className=\"w-full\"\n onClick={handleStart}\n disabled={actionLoading || !selectedProjectId}\n >\n {t('staff.timesheets.widgets.timeReporting.start', 'Start Timer')}\n </Button>\n </div>\n )\n}\n\nexport default TimeReportingWidget\n"],
5
+ "mappings": ";AAkOQ,cAyCA,YAzCA;AAhOR,YAAY,WAAW;AAEvB,SAAS,SAAS,4BAA4B;AAC9C,SAAS,cAAc;AACvB,SAAS,aAAa;AACtB,SAAS,YAAY;AACrB,SAAS,0BAA0B;AACnC,SAAS,uBAAuB;AAChC,SAAS,+BAA+B;AACxC,SAAS,kBAAkB,uBAAmD;AAW9E,MAAM,mCAAmC;AAWzC,SAAS,cAAc,WAA2B;AAChD,QAAM,UAAU,KAAK,IAAI,GAAG,KAAK,IAAI,IAAI,IAAI,KAAK,SAAS,EAAE,QAAQ,CAAC;AACtE,QAAM,eAAe,KAAK,MAAM,UAAU,GAAI;AAC9C,QAAM,QAAQ,KAAK,MAAM,eAAe,IAAI;AAC5C,QAAM,UAAU,KAAK,MAAO,eAAe,OAAQ,EAAE;AACrD,QAAM,UAAU,eAAe;AAC/B,SAAO,GAAG,OAAO,KAAK,EAAE,SAAS,GAAG,GAAG,CAAC,IAAI,OAAO,OAAO,EAAE,SAAS,GAAG,GAAG,CAAC,IAAI,OAAO,OAAO,EAAE,SAAS,GAAG,GAAG,CAAC;AAClH;AAEA,MAAM,sBAAsF,CAAC;AAAA,EAC3F;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,IAAI,KAAK;AACf,QAAM,EAAE,aAAa,kBAAkB,IAAI,mBAA+C;AAAA,IACxF,WAAW;AAAA,IACX,gBAAgB,EAAE,8BAA8B,4BAA4B;AAAA,EAC9E,CAAC;AACD,QAAM,WAAW,MAAM,QAAQ,MAAM,gBAAgB,QAAQ,GAAG,CAAC,QAAQ,CAAC;AAE1E,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAA0B,CAAC,CAAC;AAClE,QAAM,CAAC,mBAAmB,oBAAoB,IAAI,MAAM,SAAwB,SAAS,aAAa;AACtG,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAS,EAAE;AAC3C,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAqB,EAAE,SAAS,MAAM,SAAS,OAAO,WAAW,MAAM,WAAW,KAAK,CAAC;AACxH,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,UAAU;AACvD,QAAM,CAAC,eAAe,gBAAgB,IAAI,MAAM,SAAwB,IAAI;AAC5E,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,IAAI;AACjD,QAAM,CAAC,eAAe,gBAAgB,IAAI,MAAM,SAAS,KAAK;AAC9D,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAwB,IAAI;AAE5D,QAAM,YAAY,MAAM,YAAY,YAAY;AAC9C,2BAAuB,IAAI;AAC3B,eAAW,IAAI;AACf,aAAS,IAAI;AACb,QAAI;AAEF,YAAM,iBAAiB,MAAM;AAAA,QAC3B;AAAA,QACA;AAAA,QACA,EAAE,cAAc,IAAI,UAAU,EAAE,OAAO,CAAC,EAAE,EAAE;AAAA,MAC9C;AACA,YAAM,kBAAkB,MAAM,QAAQ,eAAe,KAAK,IAAI,eAAe,QAAQ,CAAC;AACtF,YAAM,aAAa,gBAChB,IAAI,CAAC,SAAS,OAAO,KAAK,mBAAmB,KAAK,iBAAiB,EAAE,CAAC,EACtE,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC;AAE/B,UAAI,WAAW,SAAS,GAAG;AACzB,cAAM,cAAc,MAAM;AAAA,UACxB,2CAA2C,WAAW,KAAK,GAAG,CAAC;AAAA,UAC/D;AAAA,UACA,EAAE,cAAc,IAAI,UAAU,EAAE,OAAO,CAAC,EAAE,EAAE;AAAA,QAC9C;AACA,cAAM,QAAQ,MAAM,QAAQ,YAAY,KAAK,IAAI,YAAY,QAAQ,CAAC;AACtE,oBAAY,MAAM,IAAI,CAAC,UAAU;AAAA,UAC/B,IAAI,OAAO,KAAK,MAAM,EAAE;AAAA,UACxB,MAAM,OAAO,KAAK,QAAQ,EAAE;AAAA,UAC5B,MAAM,OAAO,KAAK,SAAS,WAAW,KAAK,OAAO;AAAA,QACpD,EAAE,CAAC;AAAA,MACL,OAAO;AACL,oBAAY,CAAC,CAAC;AAAA,MAChB;AAGA,YAAM,UAAU,MAAM;AAAA,QACpB;AAAA,QACA;AAAA,QACA,EAAE,cAAc,IAAI,UAAU,EAAE,QAAQ,KAAK,EAAE;AAAA,MACjD;AACA,YAAM,WAAW,QAAQ,QAAQ,MAAM;AACvC,uBAAiB,QAAQ;AACzB,UAAI,UAAU;AACZ,cAAM,SAAQ,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AAClD,cAAM,aAAa,MAAM;AAAA,UACvB,oDAAoD,QAAQ,SAAS,KAAK,OAAO,KAAK;AAAA,UACtF;AAAA,UACA,EAAE,cAAc,IAAI,UAAU,EAAE,OAAO,CAAC,EAAE,EAAE;AAAA,QAC9C;AACA,cAAM,UAAU,MAAM,QAAQ,WAAW,KAAK,IAAI,WAAW,QAAQ,CAAC;AACtE,cAAM,UAAU,QAAQ,KAAK,CAAC,MAAM;AAClC,gBAAM,YAAY,EAAE,cAAc,EAAE;AACpC,gBAAM,UAAU,EAAE,YAAY,EAAE;AAChC,iBAAO,aAAa,QAAQ,WAAW;AAAA,QACzC,CAAC;AACD,YAAI,SAAS;AACX,mBAAS;AAAA,YACP,SAAS,OAAO,QAAQ,MAAM,EAAE;AAAA,YAChC,SAAS;AAAA,YACT,WAAW,OAAO,QAAQ,cAAc,QAAQ,aAAa,EAAE;AAAA,YAC/D,WAAW,OAAO,QAAQ,mBAAmB,QAAQ,iBAAiB,EAAE;AAAA,UAC1E,CAAC;AAAA,QACH,OAAO;AACL,mBAAS,EAAE,SAAS,MAAM,SAAS,OAAO,WAAW,MAAM,WAAW,KAAK,CAAC;AAAA,QAC9E;AAAA,MACF;AAAA,IACF,SAAS,KAAK;AACZ,cAAQ,MAAM,uCAAuC,GAAG;AACxD,eAAS,EAAE,gDAAgD,4BAA4B,CAAC;AAAA,IAC1F,UAAE;AACA,iBAAW,KAAK;AAChB,6BAAuB,KAAK;AAAA,IAC9B;AAAA,EACF,GAAG,CAAC,sBAAsB,CAAC,CAAC;AAE5B,QAAM,UAAU,MAAM;AACpB,SAAK,UAAU;AAAA,EACjB,GAAG,CAAC,WAAW,YAAY,CAAC;AAG5B,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,MAAM,WAAW,CAAC,MAAM,UAAW;AACxC,UAAM,OAAO,MAAM,WAAW,cAAc,MAAM,SAAU,CAAC;AAC7D,SAAK;AACL,UAAM,WAAW,YAAY,MAAM,GAAI;AACvC,WAAO,MAAM,cAAc,QAAQ;AAAA,EACrC,GAAG,CAAC,MAAM,SAAS,MAAM,SAAS,CAAC;AAEnC,QAAM,cAAc,MAAM,YAAY,YAAY;AAChD,QAAI,CAAC,qBAAqB,CAAC,cAAe;AAC1C,QAAI,MAAM,QAAS;AACnB,qBAAiB,IAAI;AACrB,QAAI;AACF,YAAM,SAAQ,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AAMlD,YAAM,eAAe;AAAA,QACnB;AAAA,QACA,eAAe;AAAA,QACf,MAAM;AAAA,QACN,OAAO,MAAM,KAAK,KAAK;AAAA,MACzB;AACA,YAAM,YAAY;AAAA,QAChB,WAAW,MAAM,gBAAgB,YAAY;AAAA,QAC7C,SAAS;AAAA,UACP,QAAQ;AAAA,UACR,cAAc;AAAA,UACd,YAAY;AAAA,UACZ;AAAA,UACA,QAAQ;AAAA,UACR;AAAA,QACF;AAAA,QACA,iBAAiB;AAAA,MACnB,CAAC;AAED,uBAAiB,EAAE,GAAG,UAAU,eAAe,kBAAkB,CAAC;AAClE,YAAM,UAAU;AAAA,IAClB,SAAS,KAAK;AACZ,cAAQ,MAAM,wCAAwC,GAAG;AACzD,eAAS,wBAAwB,KAAK,EAAE,qDAAqD,uBAAuB,CAAC,CAAC;AAAA,IACxH,UAAE;AACA,uBAAiB,KAAK;AAAA,IACxB;AAAA,EACF,GAAG,CAAC,mBAAmB,eAAe,MAAM,SAAS,OAAO,UAAU,kBAAkB,WAAW,aAAa,mBAAmB,CAAC,CAAC;AAErI,QAAM,aAAa,MAAM,YAAY,YAAY;AAC/C,QAAI,CAAC,MAAM,WAAW,CAAC,cAAe;AACtC,qBAAiB,IAAI;AACrB,QAAI;AACF,YAAM,cAAc;AAAA,QAClB,IAAI,MAAM;AAAA,QACV,QAAQ;AAAA,QACR;AAAA,MACF;AACA,YAAM,YAAY;AAAA,QAChB,WAAW,MACT,QAAQ,sCAAsC,MAAM,OAAO,eAAe,EAAE,QAAQ,OAAO,CAAC;AAAA,QAC9F,SAAS;AAAA,UACP,QAAQ;AAAA,UACR,cAAc;AAAA,UACd,YAAY,MAAM;AAAA,UAClB;AAAA,UACA,QAAQ;AAAA,UACR;AAAA,QACF;AAAA,QACA,iBAAiB;AAAA,MACnB,CAAC;AACD,YAAM,UAAU;AAAA,IAClB,SAAS,KAAK;AACZ,cAAQ,MAAM,uCAAuC,GAAG;AACxD,eAAS,wBAAwB,KAAK,EAAE,oDAAoD,sBAAsB,CAAC,CAAC;AAAA,IACtH,UAAE;AACA,uBAAiB,KAAK;AAAA,IACxB;AAAA,EACF,GAAG,CAAC,MAAM,SAAS,eAAe,WAAW,aAAa,mBAAmB,CAAC,CAAC;AAE/E,MAAI,SAAS,YAAY;AACvB,WACE,oBAAC,SAAI,WAAU,qBACb,8BAAC,OAAE,WAAU,yBACV,YAAE,+DAA+D,8EAA8E,GAClJ,GACF;AAAA,EAEJ;AAEA,MAAI,SAAS;AACX,WACE,oBAAC,SAAI,WAAU,gDACb,8BAAC,OAAE,WAAU,iCAAiC,YAAE,kDAAkD,YAAY,GAAE,GAClH;AAAA,EAEJ;AAEA,MAAI,OAAO;AACT,WACE,oBAAC,SAAI,WAAU,gDACb,8BAAC,OAAE,MAAK,SAAQ,WAAU,4BAA4B,iBAAM,GAC9D;AAAA,EAEJ;AAEA,MAAI,SAAS,WAAW,GAAG;AACzB,WACE,oBAAC,SAAI,WAAU,gDACb,8BAAC,OAAE,WAAU,iCACV,YAAE,qDAAqD,uBAAuB,GACjF,GACF;AAAA,EAEJ;AAGA,MAAI,MAAM,SAAS;AACjB,UAAM,iBAAiB,SAAS,KAAK,CAAC,MAAM,EAAE,OAAO,MAAM,SAAS;AACpE,WACE,qBAAC,SAAI,WAAU,aACb;AAAA,0BAAC,SAAI,WAAU,iCACZ,0BAAgB,QAAQ,EAAE,yDAAyD,iBAAiB,GACvG;AAAA,MACA,qBAAC,SAAI,WAAU,eACb;AAAA,4BAAC,OAAE,WAAU,6CAA6C,mBAAQ;AAAA,QAClE,oBAAC,OAAE,WAAU,sCACV,YAAE,kDAAkD,eAAe,GACtE;AAAA,SACF;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,WAAU;AAAA,UACV,SAAS;AAAA,UACT,UAAU;AAAA,UAET,YAAE,+CAA+C,YAAY;AAAA;AAAA,MAChE;AAAA,OACF;AAAA,EAEJ;AAGA,SACE,qBAAC,SAAI,WAAU,aACb;AAAA,yBAAC,SAAI,WAAU,eACb;AAAA,0BAAC,WAAM,WAAU,6CAA4C,SAAQ,iBAClE,YAAE,kDAAkD,SAAS,GAChE;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,IAAG;AAAA,UACH,WAAU;AAAA,UACV,OAAO,qBAAqB;AAAA,UAC5B,UAAU,CAAC,MAAM,qBAAqB,EAAE,OAAO,SAAS,IAAI;AAAA,UAE5D;AAAA,gCAAC,YAAO,OAAM,IAAI,YAAE,wDAAwD,mBAAmB,GAAE;AAAA,YAChG,SAAS,IAAI,CAAC,YACb,qBAAC,YAAwB,OAAO,QAAQ,IACrC;AAAA,sBAAQ;AAAA,cAAM,QAAQ,OAAO,KAAK,QAAQ,IAAI,MAAM;AAAA,iBAD1C,QAAQ,EAErB,CACD;AAAA;AAAA;AAAA,MACH;AAAA,OACF;AAAA,IACA,qBAAC,SAAI,WAAU,eACb;AAAA,0BAAC,WAAM,WAAU,6CAA4C,SAAQ,eAClE,YAAE,mDAAmD,aAAa,GACrE;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,IAAG;AAAA,UACH,OAAO;AAAA,UACP,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK;AAAA,UACxC,aAAa,EAAE,2DAA2D,0BAA0B;AAAA;AAAA,MACtG;AAAA,OACF;AAAA,IACA,oBAAC,SAAI,WAAU,eACb,8BAAC,OAAE,WAAU,iCACV,YAAE,qDAAqD,aAAa,GACvE,GACF;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,WAAU;AAAA,QACV,SAAS;AAAA,QACT,UAAU,iBAAiB,CAAC;AAAA,QAE3B,YAAE,gDAAgD,aAAa;AAAA;AAAA,IAClE;AAAA,KACF;AAEJ;AAEA,IAAO,wBAAQ;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.6-develop.6241.1.47c01ad4ed",
3
+ "version": "0.6.6-develop.6245.1.2be3b151f8",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -246,16 +246,16 @@
246
246
  "zod": "^4.4.3"
247
247
  },
248
248
  "peerDependencies": {
249
- "@open-mercato/ai-assistant": "0.6.6-develop.6241.1.47c01ad4ed",
250
- "@open-mercato/shared": "0.6.6-develop.6241.1.47c01ad4ed",
251
- "@open-mercato/ui": "0.6.6-develop.6241.1.47c01ad4ed",
249
+ "@open-mercato/ai-assistant": "0.6.6-develop.6245.1.2be3b151f8",
250
+ "@open-mercato/shared": "0.6.6-develop.6245.1.2be3b151f8",
251
+ "@open-mercato/ui": "0.6.6-develop.6245.1.2be3b151f8",
252
252
  "react": "^19.0.0",
253
253
  "react-dom": "^19.0.0"
254
254
  },
255
255
  "devDependencies": {
256
- "@open-mercato/ai-assistant": "0.6.6-develop.6241.1.47c01ad4ed",
257
- "@open-mercato/shared": "0.6.6-develop.6241.1.47c01ad4ed",
258
- "@open-mercato/ui": "0.6.6-develop.6241.1.47c01ad4ed",
256
+ "@open-mercato/ai-assistant": "0.6.6-develop.6245.1.2be3b151f8",
257
+ "@open-mercato/shared": "0.6.6-develop.6245.1.2be3b151f8",
258
+ "@open-mercato/ui": "0.6.6-develop.6245.1.2be3b151f8",
259
259
  "@testing-library/dom": "^10.4.1",
260
260
  "@testing-library/jest-dom": "^6.9.1",
261
261
  "@testing-library/react": "^16.3.1",
@@ -1,7 +1,6 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from 'react'
4
- import { format } from 'date-fns/format'
5
4
  import { CalendarRange, ListFilter, Settings } from 'lucide-react'
6
5
  import { Badge } from '@open-mercato/ui/primitives/badge'
7
6
  import { Button } from '@open-mercato/ui/primitives/button'
@@ -18,7 +17,8 @@ import {
18
17
  SelectTrigger,
19
18
  SelectValue,
20
19
  } from '@open-mercato/ui/primitives/select'
21
- import { useT } from '@open-mercato/shared/lib/i18n/context'
20
+ import { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'
21
+ import { formatDateRangeLabel } from '../../lib/calendar/format'
22
22
  import type {
23
23
  CalendarFiltersValue,
24
24
  CalendarRangePreset,
@@ -33,10 +33,6 @@ const ALL_OPTION = 'all'
33
33
 
34
34
  const EMPTY_FILTERS: CalendarFiltersValue = { types: [], status: null, ownerUserId: null }
35
35
 
36
- function formatRangeLabel(from: Date, to: Date): string {
37
- return `${format(from, 'MMM dd')} – ${format(to, 'MMM dd, yyyy')}`
38
- }
39
-
40
36
  export function CalendarToolbar(props: CalendarToolbarProps) {
41
37
  const {
42
38
  anchor,
@@ -54,6 +50,7 @@ export function CalendarToolbar(props: CalendarToolbarProps) {
54
50
  onOpenSettings,
55
51
  } = props
56
52
  const t = useT()
53
+ const locale = useLocale()
57
54
  const [rangeOpen, setRangeOpen] = React.useState(false)
58
55
  const [filtersOpen, setFiltersOpen] = React.useState(false)
59
56
  const [pendingFilters, setPendingFilters] = React.useState<CalendarFiltersValue>(filters)
@@ -128,7 +125,7 @@ export function CalendarToolbar(props: CalendarToolbarProps) {
128
125
  className="min-w-0 text-muted-foreground sm:-ml-px sm:rounded-l-none"
129
126
  >
130
127
  <CalendarRange aria-hidden="true" />
131
- <span className="truncate">{formatRangeLabel(range.from, range.to)}</span>
128
+ <span className="truncate">{formatDateRangeLabel(locale, range.from, range.to)}</span>
132
129
  </Button>
133
130
  </PopoverTrigger>
134
131
  <PopoverContent align="start" className="w-auto min-w-0 p-2">
@@ -6,6 +6,7 @@ import { cn } from '@open-mercato/shared/lib/utils'
6
6
  import { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'
7
7
  import { Avatar, AvatarStack } from '@open-mercato/ui/primitives/avatar'
8
8
  import { Button } from '@open-mercato/ui/primitives/button'
9
+ import { formatTimeRangeLabel } from '../../lib/calendar/format'
9
10
  import type { CalendarItem, CalendarPlatform } from './types'
10
11
 
11
12
  const SHOW_TIME_MIN_HEIGHT_PX = 44
@@ -63,12 +64,7 @@ export function resolveEventTone(item: CalendarItem, nowMs: number): EventTone {
63
64
  }
64
65
 
65
66
  export function formatTimeRange(locale: string, start: Date, end: Date): string {
66
- const formatter = new Intl.DateTimeFormat(locale, { hour: 'numeric', minute: '2-digit' })
67
- try {
68
- return formatter.formatRange(start, end)
69
- } catch {
70
- return `${formatter.format(start)} - ${formatter.format(end)}`
71
- }
67
+ return formatTimeRangeLabel(locale, start, end)
72
68
  }
73
69
 
74
70
  function participantLabel(participant: CalendarItem['participants'][number]): string {
@@ -2,7 +2,6 @@
2
2
 
3
3
  import * as React from 'react'
4
4
  import { differenceInCalendarDays } from 'date-fns/differenceInCalendarDays'
5
- import { format } from 'date-fns/format'
6
5
  import { AlertTriangle, ChevronDown, Clock } from 'lucide-react'
7
6
  import { Button } from '@open-mercato/ui/primitives/button'
8
7
  import { IconButton } from '@open-mercato/ui/primitives/icon-button'
@@ -13,12 +12,13 @@ import {
13
12
  PopoverContent,
14
13
  PopoverTrigger,
15
14
  } from '@open-mercato/ui/primitives/popover'
16
- import { useT } from '@open-mercato/shared/lib/i18n/context'
15
+ import { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'
16
+ import { formatDateLabel, formatTimeRangeLabel } from '../../lib/calendar/format'
17
17
  import type { CalendarItem, UpcomingCard, UpcomingCardsProps } from './types'
18
18
 
19
- function formatTimeRange(item: CalendarItem, allDayLabel: string): string {
19
+ function formatTimeRange(locale: string, item: CalendarItem, allDayLabel: string): string {
20
20
  if (item.allDay) return allDayLabel
21
- return `${format(item.start, 'h:mm a')} – ${format(item.end, 'h:mm a')}`
21
+ return formatTimeRangeLabel(locale, item.start, item.end)
22
22
  }
23
23
 
24
24
  function canJoin(item: CalendarItem): boolean {
@@ -37,6 +37,7 @@ function UpcomingCardStatus({
37
37
  onSeeConflict: UpcomingCardsProps['onSeeConflict']
38
38
  }) {
39
39
  const t = useT()
40
+ const locale = useLocale()
40
41
  const { item, kind, conflictCount } = card
41
42
 
42
43
  if (kind === 'today') {
@@ -91,7 +92,7 @@ function UpcomingCardStatus({
91
92
  {t('customers.calendar.cards.cancelled', 'Cancelled')}
92
93
  </span>
93
94
  <span className="shrink-0 text-xs text-muted-foreground">
94
- {format(item.start, 'MMM dd, yyyy')}
95
+ {formatDateLabel(locale, item.start)}
95
96
  </span>
96
97
  </div>
97
98
  )
@@ -107,7 +108,7 @@ function UpcomingCardStatus({
107
108
  : t('customers.calendar.cards.daysLater', '{days} days later', { days: daysLater })}
108
109
  </span>
109
110
  <span className="shrink-0 text-xs text-muted-foreground">
110
- {format(item.start, 'MMM dd, yyyy')}
111
+ {formatDateLabel(locale, item.start)}
111
112
  </span>
112
113
  </div>
113
114
  )
@@ -123,6 +124,7 @@ function UpcomingCardItem({
123
124
  onCancel,
124
125
  }: { card: UpcomingCard } & CardCallbacks) {
125
126
  const t = useT()
127
+ const locale = useLocale()
126
128
  const { item } = card
127
129
 
128
130
  return (
@@ -131,7 +133,7 @@ function UpcomingCardItem({
131
133
  <div className="flex min-w-0 flex-1 flex-col gap-1">
132
134
  <p className="truncate text-sm font-medium text-foreground">{item.title}</p>
133
135
  <p className="truncate text-xs text-muted-foreground">
134
- {formatTimeRange(item, t('customers.calendar.cards.allDay', 'All day'))}
136
+ {formatTimeRange(locale, item, t('customers.calendar.cards.allDay', 'All day'))}
135
137
  </p>
136
138
  </div>
137
139
  <Popover>
@@ -0,0 +1,25 @@
1
+ const DATE_OPTIONS: Intl.DateTimeFormatOptions = { day: 'numeric', month: 'short', year: 'numeric' }
2
+
3
+ const TIME_OPTIONS: Intl.DateTimeFormatOptions = { hour: 'numeric', minute: '2-digit' }
4
+
5
+ export function formatDateLabel(locale: string, date: Date): string {
6
+ return new Intl.DateTimeFormat(locale, DATE_OPTIONS).format(date)
7
+ }
8
+
9
+ export function formatDateRangeLabel(locale: string, from: Date, to: Date): string {
10
+ const formatter = new Intl.DateTimeFormat(locale, DATE_OPTIONS)
11
+ try {
12
+ return formatter.formatRange(from, to)
13
+ } catch {
14
+ return `${formatter.format(from)} – ${formatter.format(to)}`
15
+ }
16
+ }
17
+
18
+ export function formatTimeRangeLabel(locale: string, start: Date, end: Date): string {
19
+ const formatter = new Intl.DateTimeFormat(locale, TIME_OPTIONS)
20
+ try {
21
+ return formatter.formatRange(start, end)
22
+ } catch {
23
+ return `${formatter.format(start)} – ${formatter.format(end)}`
24
+ }
25
+ }
@@ -7,6 +7,7 @@ import { IconButton } from '@open-mercato/ui/primitives/icon-button'
7
7
  import { useT } from '@open-mercato/shared/lib/i18n/context'
8
8
  import { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
9
9
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
10
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
10
11
  import { ProjectColorDot } from './ProjectColorDot'
11
12
  import { startTimerEntry } from './startTimer'
12
13
  import { resolveTimerActionError } from './timerErrors'
@@ -24,6 +25,17 @@ type TimerBarProps = {
24
25
  onTimerStopped: () => void
25
26
  }
26
27
 
28
+ const TIMER_MUTATION_CONTEXT_ID = 'staff-timesheets-timer-bar'
29
+
30
+ type TimerMutationContext = {
31
+ formId: string
32
+ resourceKind: string
33
+ resourceId: string
34
+ staffMemberId: string | null
35
+ action: 'timer-create' | 'timer-start' | 'timer-stop'
36
+ retryLastMutation: () => Promise<boolean>
37
+ }
38
+
27
39
  function formatElapsed(seconds: number): string {
28
40
  const h = Math.floor(seconds / 3600)
29
41
  const m = Math.floor((seconds % 3600) / 60)
@@ -41,6 +53,10 @@ function getToday(): string {
41
53
 
42
54
  export function TimerBar({ projects, staffMemberId, onTimerStopped }: TimerBarProps) {
43
55
  const t = useT()
56
+ const { runMutation, retryLastMutation } = useGuardedMutation<TimerMutationContext>({
57
+ contextId: TIMER_MUTATION_CONTEXT_ID,
58
+ blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
59
+ })
44
60
 
45
61
  const [activeEntryId, setActiveEntryId] = useState<string | null>(null)
46
62
  const [activeProjectId, setActiveProjectId] = useState<string | null>(null)
@@ -142,11 +158,23 @@ export function TimerBar({ projects, staffMemberId, onTimerStopped }: TimerBarPr
142
158
  setIsStarting(true)
143
159
  try {
144
160
  const today = getToday()
145
- const { id: entryId } = await startTimerEntry({
161
+ const startPayload = {
146
162
  staffMemberId,
147
163
  timeProjectId: selectedProjectId,
148
164
  date: today,
149
165
  notes: description || null,
166
+ }
167
+ const { id: entryId } = await runMutation({
168
+ operation: () => startTimerEntry(startPayload),
169
+ context: {
170
+ formId: TIMER_MUTATION_CONTEXT_ID,
171
+ resourceKind: 'staff.timesheets.time_entry',
172
+ resourceId: staffMemberId,
173
+ staffMemberId,
174
+ action: 'timer-start',
175
+ retryLastMutation,
176
+ },
177
+ mutationPayload: startPayload,
150
178
  })
151
179
 
152
180
  setActiveEntryId(entryId)
@@ -167,10 +195,27 @@ export function TimerBar({ projects, staffMemberId, onTimerStopped }: TimerBarPr
167
195
 
168
196
  setIsStopping(true)
169
197
  try {
170
- await apiCallOrThrow(
171
- `/api/staff/timesheets/time-entries/${activeEntryId}/timer-stop`,
172
- { method: 'POST' },
173
- )
198
+ const stopPayload = {
199
+ id: activeEntryId,
200
+ action: 'timer-stop',
201
+ staffMemberId,
202
+ }
203
+ await runMutation({
204
+ operation: () =>
205
+ apiCallOrThrow(
206
+ `/api/staff/timesheets/time-entries/${activeEntryId}/timer-stop`,
207
+ { method: 'POST' },
208
+ ),
209
+ context: {
210
+ formId: TIMER_MUTATION_CONTEXT_ID,
211
+ resourceKind: 'staff.timesheets.time_entry',
212
+ resourceId: activeEntryId,
213
+ staffMemberId,
214
+ action: 'timer-stop',
215
+ retryLastMutation,
216
+ },
217
+ mutationPayload: stopPayload,
218
+ })
174
219
 
175
220
  setActiveEntryId(null)
176
221
  setActiveProjectId(null)
@@ -6,6 +6,7 @@ import { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/ap
6
6
  import { Button } from '@open-mercato/ui/primitives/button'
7
7
  import { Input } from '@open-mercato/ui/primitives/input'
8
8
  import { useT } from '@open-mercato/shared/lib/i18n/context'
9
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
9
10
  import { startTimerEntry } from '../../../lib/timesheets-ui/startTimer'
10
11
  import { resolveTimerActionError } from '../../../lib/timesheets-ui/timerErrors'
11
12
  import { DEFAULT_SETTINGS, hydrateSettings, type TimeReportingSettings } from './config'
@@ -19,6 +20,17 @@ type TimerState = {
19
20
  projectId: string | null
20
21
  }
21
22
 
23
+ const TIMER_WIDGET_MUTATION_CONTEXT_ID = 'staff-timesheets-time-reporting-widget'
24
+
25
+ type TimerWidgetMutationContext = {
26
+ formId: string
27
+ resourceKind: string
28
+ resourceId: string
29
+ staffMemberId: string
30
+ action: 'timer-create' | 'timer-start' | 'timer-stop'
31
+ retryLastMutation: () => Promise<boolean>
32
+ }
33
+
22
34
  function formatElapsed(startedAt: string): string {
23
35
  const elapsed = Math.max(0, Date.now() - new Date(startedAt).getTime())
24
36
  const totalSeconds = Math.floor(elapsed / 1000)
@@ -36,6 +48,10 @@ const TimeReportingWidget: React.FC<DashboardWidgetComponentProps<TimeReportingS
36
48
  onRefreshStateChange,
37
49
  }) => {
38
50
  const t = useT()
51
+ const { runMutation, retryLastMutation } = useGuardedMutation<TimerWidgetMutationContext>({
52
+ contextId: TIMER_WIDGET_MUTATION_CONTEXT_ID,
53
+ blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
54
+ })
39
55
  const hydrated = React.useMemo(() => hydrateSettings(settings), [settings])
40
56
 
41
57
  const [projects, setProjects] = React.useState<ProjectOption[]>([])
@@ -142,12 +158,26 @@ const TimeReportingWidget: React.FC<DashboardWidgetComponentProps<TimeReportingS
142
158
  const today = new Date().toISOString().slice(0, 10)
143
159
  // Single atomic create+start request (issue #3311): a partial failure can
144
160
  // no longer leave an orphaned, unstarted timer entry, and a rejected start
145
- // now surfaces here instead of being silently ignored.
146
- await startTimerEntry({
161
+ // now surfaces here instead of being silently ignored. The call is routed
162
+ // through the mutation guard (issue #3308) so global injection modules can
163
+ // run onBeforeSave/onAfterSave and surface conflicts consistently.
164
+ const startPayload = {
147
165
  staffMemberId,
148
166
  timeProjectId: selectedProjectId,
149
167
  date: today,
150
168
  notes: notes.trim() || null,
169
+ }
170
+ await runMutation({
171
+ operation: () => startTimerEntry(startPayload),
172
+ context: {
173
+ formId: TIMER_WIDGET_MUTATION_CONTEXT_ID,
174
+ resourceKind: 'staff.timesheets.time_entry',
175
+ resourceId: staffMemberId,
176
+ staffMemberId,
177
+ action: 'timer-start',
178
+ retryLastMutation,
179
+ },
180
+ mutationPayload: startPayload,
151
181
  })
152
182
 
153
183
  onSettingsChange({ ...hydrated, lastProjectId: selectedProjectId })
@@ -158,13 +188,30 @@ const TimeReportingWidget: React.FC<DashboardWidgetComponentProps<TimeReportingS
158
188
  } finally {
159
189
  setActionLoading(false)
160
190
  }
161
- }, [selectedProjectId, staffMemberId, timer.running, notes, hydrated, onSettingsChange, loadState, t])
191
+ }, [selectedProjectId, staffMemberId, timer.running, notes, hydrated, onSettingsChange, loadState, runMutation, retryLastMutation, t])
162
192
 
163
193
  const handleStop = React.useCallback(async () => {
164
- if (!timer.entryId) return
194
+ if (!timer.entryId || !staffMemberId) return
165
195
  setActionLoading(true)
166
196
  try {
167
- await apiCall(`/api/staff/timesheets/time-entries/${timer.entryId}/timer-stop`, { method: 'POST' })
197
+ const stopPayload = {
198
+ id: timer.entryId,
199
+ action: 'timer-stop',
200
+ staffMemberId,
201
+ }
202
+ await runMutation({
203
+ operation: () =>
204
+ apiCall(`/api/staff/timesheets/time-entries/${timer.entryId}/timer-stop`, { method: 'POST' }),
205
+ context: {
206
+ formId: TIMER_WIDGET_MUTATION_CONTEXT_ID,
207
+ resourceKind: 'staff.timesheets.time_entry',
208
+ resourceId: timer.entryId,
209
+ staffMemberId,
210
+ action: 'timer-stop',
211
+ retryLastMutation,
212
+ },
213
+ mutationPayload: stopPayload,
214
+ })
168
215
  await loadState()
169
216
  } catch (err) {
170
217
  console.error('staff.timesheets.timeReporting.stop', err)
@@ -172,7 +219,7 @@ const TimeReportingWidget: React.FC<DashboardWidgetComponentProps<TimeReportingS
172
219
  } finally {
173
220
  setActionLoading(false)
174
221
  }
175
- }, [timer.entryId, loadState, t])
222
+ }, [timer.entryId, staffMemberId, loadState, runMutation, retryLastMutation, t])
176
223
 
177
224
  if (mode === 'settings') {
178
225
  return (