@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/customers/components/calendar/CalendarToolbar.js +4 -6
- package/dist/modules/customers/components/calendar/CalendarToolbar.js.map +2 -2
- package/dist/modules/customers/components/calendar/EventBlock.js +2 -6
- package/dist/modules/customers/components/calendar/EventBlock.js.map +2 -2
- package/dist/modules/customers/components/calendar/UpcomingCards.js +9 -7
- package/dist/modules/customers/components/calendar/UpcomingCards.js.map +2 -2
- package/dist/modules/customers/lib/calendar/format.js +27 -0
- package/dist/modules/customers/lib/calendar/format.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-ui/TimerBar.js +39 -5
- package/dist/modules/staff/lib/timesheets-ui/TimerBar.js.map +2 -2
- package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js +39 -5
- package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customers/components/calendar/CalendarToolbar.tsx +4 -7
- package/src/modules/customers/components/calendar/EventBlock.tsx +2 -6
- package/src/modules/customers/components/calendar/UpcomingCards.tsx +9 -7
- package/src/modules/customers/lib/calendar/format.ts +25 -0
- package/src/modules/staff/lib/timesheets-ui/TimerBar.tsx +50 -5
- package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.tsx +53 -6
package/.turbo/turbo-build.log
CHANGED
|
@@ -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:
|
|
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": ";
|
|
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": ";AA+FQ,cAIE,YAJF;AA7FR,YAAY,WAAW;AACvB,SAAS,eAAe,YAAY,gBAAgB;AACpD,SAAS,aAAa;AACtB,SAAS,cAAc;AACvB,SAAS,gBAAgB;AACzB,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAC3B,SAAS,WAAW;AACpB,SAAS,SAAS,gBAAgB,sBAAsB;AACxD,SAAS,mBAAmB;AAC5B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,WAAW,YAAY;AAChC,SAAS,4BAA4B;AAOrC,MAAM,gBAAuC,CAAC,YAAY,SAAS,aAAa,QAAQ;AAExF,MAAM,iBAAiB,CAAC,WAAW,QAAQ,UAAU;AAErD,MAAM,aAAa;AAEnB,MAAM,gBAAsC,EAAE,OAAO,CAAC,GAAG,QAAQ,MAAM,aAAa,KAAK;AAElF,SAAS,gBAAgB,OAA6B;AAC3D,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AACJ,QAAM,IAAI,KAAK;AACf,QAAM,SAAS,UAAU;AACzB,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAS,KAAK;AACtD,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAS,KAAK;AAC1D,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,MAAM,SAA+B,OAAO;AAExF,QAAM,eAAoD;AAAA,IACxD,UAAU,EAAE,+CAA+C,WAAW;AAAA,IACtE,OAAO,EAAE,4CAA4C,aAAa;AAAA,IAClE,WAAW,EAAE,gDAAgD,YAAY;AAAA,IACzE,QAAQ,EAAE,6CAA6C,cAAc;AAAA,EACvE;AAEA,QAAM,oBACJ,QAAQ,MAAM,UAAU,QAAQ,SAAS,IAAI,MAAM,QAAQ,cAAc,IAAI;AAE/E,QAAM,0BAA0B,CAAC,SAAkB;AACjD,QAAI,KAAM,mBAAkB,OAAO;AACnC,mBAAe,IAAI;AAAA,EACrB;AAEA,QAAM,eAAe,MAAM;AACzB,oBAAgB,cAAc;AAC9B,mBAAe,KAAK;AAAA,EACtB;AAEA,QAAM,eAAe,MAAM;AACzB,sBAAkB,aAAa;AAC/B,oBAAgB,aAAa;AAC7B,mBAAe,KAAK;AAAA,EACtB;AAEA,QAAM,oBAAoB,CAAC,OAAe,YAAqB;AAC7D,sBAAkB,CAAC,aAAa;AAAA,MAC9B,GAAG;AAAA,MACH,OAAO,UACH,CAAC,GAAG,QAAQ,OAAO,KAAK,IACxB,QAAQ,MAAM,OAAO,CAAC,UAAU,UAAU,KAAK;AAAA,IACrD,EAAE;AAAA,EACJ;AAEA,SACE,qBAAC,SAAI,WAAU,8CACb;AAAA,yBAAC,SAAI,WAAU,sDACb;AAAA,0BAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,SAAS,SAC9C,YAAE,oCAAoC,OAAO,GAChD;AAAA,MACA,qBAAC,SAAI,WAAU,6BACb;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO,UAAU;AAAA,YACjB,eAAe,CAAC,UAAU,eAAe,KAA4B;AAAA,YAErE;AAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,WAAU;AAAA,kBACV,cAAY,EAAE,0CAA0C,mBAAmB;AAAA,kBAE3E;AAAA,oBAAC;AAAA;AAAA,sBACC,aAAa,EAAE,gDAAgD,cAAc;AAAA;AAAA,kBAC/E;AAAA;AAAA,cACF;AAAA,cACA,oBAAC,iBACE,wBAAc,IAAI,CAAC,UAClB,oBAAC,cAAuB,OACrB,uBAAa,KAAK,KADJ,KAEjB,CACD,GACH;AAAA;AAAA;AAAA,QACF;AAAA,QACA,qBAAC,WAAQ,MAAM,WAAW,cAAc,cACtC;AAAA,8BAAC,kBAAe,SAAO,MACrB;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,WAAU;AAAA,cAEV;AAAA,oCAAC,iBAAc,eAAY,QAAO;AAAA,gBAClC,oBAAC,UAAK,WAAU,YAAY,+BAAqB,QAAQ,MAAM,MAAM,MAAM,EAAE,GAAE;AAAA;AAAA;AAAA,UACjF,GACF;AAAA,UACA,oBAAC,kBAAe,OAAM,SAAQ,WAAU,sBACtC;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,UAAU;AAAA,cACV,cAAc;AAAA,cACd,UAAU,CAAC,SAAS;AAClB,oBAAI,CAAC,KAAM;AACX,+BAAe,IAAI;AACnB,6BAAa,KAAK;AAAA,cACpB;AAAA;AAAA,UACF,GACF;AAAA,WACF;AAAA,SACF;AAAA,OACF;AAAA,IACA,qBAAC,SAAI,WAAU,qFACb;AAAA,2BAAC,SAAI,WAAU,gDACb;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO;AAAA,YACP,UAAU;AAAA,YACV,aAAa,EAAE,gDAAgD,cAAS;AAAA,YACxE,cAAY,EAAE,gDAAgD,cAAS;AAAA,YACvE,wBAAqB;AAAA;AAAA,QACvB;AAAA,QACC,OAAO,WAAW,IACjB,oBAAC,OAAI,WAAU,uFAAsF,eAAC,IACpG;AAAA,SACN;AAAA,MACA,qBAAC,WAAQ,MAAM,aAAa,cAAc,yBACxC;AAAA,4BAAC,kBAAe,SAAO,MACrB;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,cAAY,EAAE,4CAA4C,QAAQ;AAAA,YAElE;AAAA,kCAAC,cAAW,eAAY,QAAO;AAAA,cAC/B,oBAAC,UAAK,WAAU,oBACb,YAAE,4CAA4C,QAAQ,GACzD;AAAA,cACC,oBAAoB,IACnB,oBAAC,SAAM,SAAQ,aAAY,MAAK,MAC7B,6BACH,IACE;AAAA;AAAA;AAAA,QACN,GACF;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,OAAM;AAAA,YACN,WAAU;AAAA,YACV,WAAW,CAAC,UAAU;AACpB,kBAAI,MAAM,QAAQ,YAAY,MAAM,WAAW,MAAM,UAAU;AAC7D,sBAAM,eAAe;AACrB,6BAAa;AAAA,cACf;AAAA,YACF;AAAA,YAEA,+BAAC,SAAI,WAAU,uBACZ;AAAA,0BAAY,SAAS,IACpB,qBAAC,cAAS,WAAU,uBAClB;AAAA,oCAAC,YAAO,WAAU,oFACf,YAAE,4CAA4C,MAAM,GACvD;AAAA,gBACC,YAAY,IAAI,CAAC,WAChB;AAAA,kBAAC;AAAA;AAAA,oBAEC,OAAO,OAAO;AAAA,oBACd,MAAK;AAAA,oBACL,SAAS,eAAe,MAAM,SAAS,OAAO,KAAK;AAAA,oBACnD,iBAAiB,CAAC,YAChB,kBAAkB,OAAO,OAAO,YAAY,IAAI;AAAA;AAAA,kBAL7C,OAAO;AAAA,gBAOd,CACD;AAAA,iBACH,IACE;AAAA,cACJ,qBAAC,SAAI,WAAU,yBACb;AAAA,oCAAC,UAAK,WAAU,+EACb,YAAE,6CAA6C,QAAQ,GAC1D;AAAA,gBACA;AAAA,kBAAC;AAAA;AAAA,oBACC,OAAO,eAAe,UAAU;AAAA,oBAChC,eAAe,CAAC,UACd,kBAAkB,CAAC,aAAa;AAAA,sBAC9B,GAAG;AAAA,sBACH,QAAQ,UAAU,aAAa,OAAO;AAAA,oBACxC,EAAE;AAAA,oBAGJ;AAAA;AAAA,wBAAC;AAAA;AAAA,0BACC,MAAK;AAAA,0BACL,cAAY,EAAE,6CAA6C,QAAQ;AAAA,0BAEnE,8BAAC,eAAY;AAAA;AAAA,sBACf;AAAA,sBACA,qBAAC,iBACC;AAAA,4CAAC,cAAW,OAAO,YAChB,YAAE,kDAAkD,cAAc,GACrE;AAAA,wBACC,eAAe,IAAI,CAAC,WACnB,oBAAC,cAAwB,OAAO,QAC7B,YAAE,+CAA+C,MAAM,IAAI,MAAM,KADnD,MAEjB,CACD;AAAA,yBACH;AAAA;AAAA;AAAA,gBACF;AAAA,iBACF;AAAA,cACC,aAAa,SAAS,IACrB,qBAAC,SAAI,WAAU,yBACb;AAAA,oCAAC,UAAK,WAAU,+EACb,YAAE,4CAA4C,OAAO,GACxD;AAAA,gBACA;AAAA,kBAAC;AAAA;AAAA,oBACC,OAAO,eAAe,eAAe;AAAA,oBACrC,eAAe,CAAC,UACd,kBAAkB,CAAC,aAAa;AAAA,sBAC9B,GAAG;AAAA,sBACH,aAAa,UAAU,aAAa,OAAO;AAAA,oBAC7C,EAAE;AAAA,oBAGJ;AAAA;AAAA,wBAAC;AAAA;AAAA,0BACC,MAAK;AAAA,0BACL,cAAY,EAAE,4CAA4C,OAAO;AAAA,0BAEjE,8BAAC,eAAY;AAAA;AAAA,sBACf;AAAA,sBACA,qBAAC,iBACC;AAAA,4CAAC,cAAW,OAAO,YAChB,YAAE,gDAAgD,YAAY,GACjE;AAAA,wBACC,aAAa,IAAI,CAAC,WACjB,oBAAC,cAA8B,OAAO,OAAO,OAC1C,iBAAO,SADO,OAAO,KAExB,CACD;AAAA,yBACH;AAAA;AAAA;AAAA,gBACF;AAAA,iBACF,IACE;AAAA,cACJ,qBAAC,SAAI,WAAU,qDACb;AAAA,oCAAC,UAAO,MAAK,UAAS,SAAQ,SAAQ,MAAK,MAAK,SAAS,cACtD,YAAE,4CAA4C,OAAO,GACxD;AAAA,gBACA,oBAAC,UAAO,MAAK,UAAS,MAAK,MAAK,SAAS,cACtC,YAAE,4CAA4C,OAAO,GACxD;AAAA,iBACF;AAAA,eACF;AAAA;AAAA,QACF;AAAA,SACF;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,cAAY,EAAE,uCAAuC,mBAAmB;AAAA,UACxE,SAAS;AAAA,UAET,8BAAC,YAAS,eAAW,MAAC;AAAA;AAAA,MACxB;AAAA,OACF;AAAA,KACF;AAEJ;",
|
|
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
|
-
|
|
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
|
|
5
|
-
"mappings": ";
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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 {
|
|
5
|
-
"mappings": ";
|
|
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
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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": ";
|
|
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
|
-
|
|
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
|
-
|
|
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": ";
|
|
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": "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",
|
|
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.
|
|
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.
|
|
250
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
251
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
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.
|
|
257
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
258
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
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">{
|
|
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
|
-
|
|
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
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|