@open-mercato/core 0.6.7-develop.6834.1.e76f4b8cbc → 0.6.7-develop.6842.1.8cd8a8883c

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.
Files changed (51) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/customers/components/detail/ActivitiesDayStrip.js +2 -3
  3. package/dist/modules/customers/components/detail/ActivitiesDayStrip.js.map +2 -2
  4. package/dist/modules/dashboards/lib/aggregations.js +20 -6
  5. package/dist/modules/dashboards/lib/aggregations.js.map +2 -2
  6. package/package.json +8 -8
  7. package/src/modules/api_keys/i18n/ko.json +42 -0
  8. package/src/modules/attachments/i18n/ko.json +172 -0
  9. package/src/modules/audit_logs/i18n/ko.json +66 -0
  10. package/src/modules/auth/i18n/ko.json +255 -0
  11. package/src/modules/business_rules/i18n/ko.json +396 -0
  12. package/src/modules/catalog/i18n/ko.json +723 -0
  13. package/src/modules/communication_channels/i18n/ko.json +138 -0
  14. package/src/modules/configs/i18n/ko.json +197 -0
  15. package/src/modules/currencies/i18n/de.json +1 -0
  16. package/src/modules/currencies/i18n/en.json +1 -0
  17. package/src/modules/currencies/i18n/es.json +1 -0
  18. package/src/modules/currencies/i18n/ko.json +160 -0
  19. package/src/modules/currencies/i18n/pl.json +1 -0
  20. package/src/modules/customer_accounts/i18n/ko.json +354 -0
  21. package/src/modules/customers/components/detail/ActivitiesDayStrip.tsx +2 -3
  22. package/src/modules/customers/i18n/de.json +10 -0
  23. package/src/modules/customers/i18n/en.json +10 -0
  24. package/src/modules/customers/i18n/es.json +10 -0
  25. package/src/modules/customers/i18n/ko.json +2855 -0
  26. package/src/modules/customers/i18n/pl.json +10 -0
  27. package/src/modules/dashboards/i18n/ko.json +118 -0
  28. package/src/modules/dashboards/lib/aggregations.ts +42 -6
  29. package/src/modules/data_sync/i18n/ko.json +125 -0
  30. package/src/modules/design_system/i18n/ko.json +58 -0
  31. package/src/modules/dictionaries/i18n/ko.json +130 -0
  32. package/src/modules/directory/i18n/ko.json +105 -0
  33. package/src/modules/entities/i18n/ko.json +260 -0
  34. package/src/modules/feature_toggles/i18n/ko.json +96 -0
  35. package/src/modules/inbox_ops/i18n/ko.json +196 -0
  36. package/src/modules/integrations/i18n/ko.json +129 -0
  37. package/src/modules/messages/i18n/ko.json +263 -0
  38. package/src/modules/notifications/i18n/ko.json +55 -0
  39. package/src/modules/payment_gateways/i18n/ko.json +87 -0
  40. package/src/modules/planner/i18n/ko.json +565 -0
  41. package/src/modules/portal/i18n/ko.json +143 -0
  42. package/src/modules/progress/i18n/ko.json +20 -0
  43. package/src/modules/query_index/i18n/ko.json +69 -0
  44. package/src/modules/resources/i18n/ko.json +770 -0
  45. package/src/modules/sales/i18n/ko.json +1442 -0
  46. package/src/modules/shipping_carriers/i18n/ko.json +85 -0
  47. package/src/modules/staff/i18n/ko.json +1162 -0
  48. package/src/modules/sync_excel/i18n/ko.json +107 -0
  49. package/src/modules/translations/i18n/ko.json +45 -0
  50. package/src/modules/wms/i18n/ko.json +1053 -0
  51. package/src/modules/workflows/i18n/ko.json +1203 -0
@@ -1,4 +1,4 @@
1
- [build:core] found 3783 entry points
1
+ [build:core] found 3785 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 197 entry points
4
4
  [build:core:generated] built successfully
@@ -180,9 +180,8 @@ function ActivitiesDayStrip({ entityId, selectedDate, onSelectDate, refreshKey =
180
180
  );
181
181
  setFetchedEvents(Array.isArray(payload?.items) ? payload.items : []);
182
182
  } catch (err) {
183
- if (err?.name !== "AbortError") {
184
- logger.warn("failed to load interactions", { component: "ActivitiesDayStrip", err });
185
- }
183
+ if (err?.name === "AbortError") return;
184
+ logger.warn("failed to load interactions", { component: "ActivitiesDayStrip", err });
186
185
  setFetchedEvents([]);
187
186
  }
188
187
  })();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/customers/components/detail/ActivitiesDayStrip.tsx"],
4
- "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { ChevronLeft, ChevronRight } from 'lucide-react'\nimport { toZonedTime } from 'date-fns-tz'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport type { InteractionSummary } from './types'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\n\ninterface ActivitiesDayStripProps {\n entityId: string\n selectedDate: Date\n onSelectDate: (date: Date) => void\n refreshKey?: number\n /**\n * Optional pre-fetched events. When provided, the day strip skips its own fetch\n * and uses the supplied list, ensuring its busyness count agrees with the\n * activity list rendered alongside it (issue #1809 \u2014 E1 status filter alignment).\n */\n events?: InteractionSummary[]\n}\n\nconst USER_TIMEZONE = (() => {\n try {\n return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'\n } catch {\n return 'UTC'\n }\n})()\n\n// Project a UTC ISO timestamp to the user's local timezone before comparing\n// \"same day\" (issue #1809 \u2014 E3). The browser's `new Date(iso)` treats the\n// instant correctly, but `getDate()/getMonth()/getFullYear()` reflect the\n// user's local day, so for activities scheduled at e.g. 23:30 local on a UTC\n// boundary the day-strip and list now agree.\nfunction toLocalZonedDate(value: string | Date): Date {\n return toZonedTime(value, USER_TIMEZONE)\n}\n\nconst VISIBLE_DAYS = 5\nconst BUSYNESS_SLOTS = 10\nconst SLOT_START_HOUR = 7\nconst SLOT_END_HOUR = 22\n\nconst DAY_LABEL_KEYS: Array<[number, string, string]> = [\n [0, 'customers.calendar.day.sun', 'SUN'],\n [1, 'customers.calendar.day.mon', 'MON'],\n [2, 'customers.calendar.day.tue', 'TUE'],\n [3, 'customers.calendar.day.wed', 'WED'],\n [4, 'customers.calendar.day.thu', 'THU'],\n [5, 'customers.calendar.day.fri', 'FRI'],\n [6, 'customers.calendar.day.sat', 'SAT'],\n]\n\nconst MONTH_KEYS: Array<[number, string, string]> = [\n [0, 'customers.calendar.month.january', 'January'],\n [1, 'customers.calendar.month.february', 'February'],\n [2, 'customers.calendar.month.march', 'March'],\n [3, 'customers.calendar.month.april', 'April'],\n [4, 'customers.calendar.month.may', 'May'],\n [5, 'customers.calendar.month.june', 'June'],\n [6, 'customers.calendar.month.july', 'July'],\n [7, 'customers.calendar.month.august', 'August'],\n [8, 'customers.calendar.month.september', 'September'],\n [9, 'customers.calendar.month.october', 'October'],\n [10, 'customers.calendar.month.november', 'November'],\n [11, 'customers.calendar.month.december', 'December'],\n]\n\nfunction startOfDay(date: Date): Date {\n const next = new Date(date)\n next.setHours(0, 0, 0, 0)\n return next\n}\n\nfunction endOfDay(date: Date): Date {\n const next = new Date(date)\n next.setHours(23, 59, 59, 999)\n return next\n}\n\nfunction isSameDay(a: Date, b: Date): boolean {\n return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()\n}\n\nfunction isWeekend(date: Date): boolean {\n const day = date.getDay()\n return day === 0 || day === 6\n}\n\nfunction addDays(date: Date, delta: number): Date {\n const next = new Date(date)\n next.setDate(date.getDate() + delta)\n return next\n}\n\nfunction buildVisibleDays(anchor: Date): Date[] {\n const start = startOfDay(anchor)\n return Array.from({ length: VISIBLE_DAYS }, (_, index) => addDays(start, index))\n}\n\n// Anchor the visible window so that the given focal date lands at the center slot\n// (position 2 out of 5). Matches Figma 784:809 where the selected day is centered.\nfunction anchorCenteredOn(focalDate: Date): Date {\n return startOfDay(addDays(focalDate, -Math.floor(VISIBLE_DAYS / 2)))\n}\n\ntype SlotState = 'empty' | 'partial' | 'full' | 'conflict'\n\ntype DayBusyness = {\n totalMinutes: number\n eventCount: number\n slots: SlotState[]\n}\n\nfunction emptyBusyness(): DayBusyness {\n return {\n totalMinutes: 0,\n eventCount: 0,\n slots: Array<SlotState>(BUSYNESS_SLOTS).fill('empty'),\n }\n}\n\nfunction computeDayBusyness(events: InteractionSummary[], day: Date): DayBusyness {\n if (events.length === 0) return emptyBusyness()\n const dayStart = startOfDay(day).getTime()\n const slotMs = ((SLOT_END_HOUR - SLOT_START_HOUR) * 60 * 60 * 1000) / BUSYNESS_SLOTS\n const slotMinutes = slotMs / 60000\n const slotCounts: number[] = Array(BUSYNESS_SLOTS).fill(0)\n const slotMinutesUsed: number[] = Array(BUSYNESS_SLOTS).fill(0)\n let totalMinutes = 0\n let eventCount = 0\n\n for (const event of events) {\n const startIso = event.scheduledAt ?? event.occurredAt ?? event.createdAt\n if (!startIso) continue\n const start = new Date(startIso)\n if (Number.isNaN(start.getTime())) continue\n // Compare in the user's local timezone so an activity at 23:30 local time\n // doesn't bleed into the next UTC day's chip (issue #1809 \u2014 E3).\n const localStart = toLocalZonedDate(startIso)\n if (!isSameDay(localStart, day)) continue\n eventCount += 1\n const durationMinutes = typeof event.duration === 'number' && event.duration > 0 ? event.duration : 30\n totalMinutes += durationMinutes\n const eventStartMs = start.getTime()\n const eventEndMs = eventStartMs + durationMinutes * 60000\n const slotsStartMs = dayStart + SLOT_START_HOUR * 60 * 60 * 1000\n for (let slot = 0; slot < BUSYNESS_SLOTS; slot += 1) {\n const slotStart = slotsStartMs + slot * slotMs\n const slotEnd = slotStart + slotMs\n const overlapStart = Math.max(slotStart, eventStartMs)\n const overlapEnd = Math.min(slotEnd, eventEndMs)\n const overlapMinutes = Math.max(0, (overlapEnd - overlapStart) / 60000)\n if (overlapMinutes <= 0) continue\n slotCounts[slot] += 1\n slotMinutesUsed[slot] += overlapMinutes\n }\n }\n\n const slots: SlotState[] = slotCounts.map((count, index) => {\n if (count === 0) return 'empty'\n if (count > 1) return 'conflict'\n const used = slotMinutesUsed[index]\n if (used >= slotMinutes * 0.5) return 'full'\n return 'partial'\n })\n\n return { totalMinutes, eventCount, slots }\n}\n\nfunction formatBusyLabel(busy: DayBusyness, t: TranslateFn): string {\n if (busy.eventCount === 0) return ''\n // Match Figma 784:809 label format: \"Xm\" when under an hour, \"Xh\" otherwise.\n // Mixed \"Xh Ym\" overflows the 101px card and is not part of the visual spec.\n const durationLabel = busy.totalMinutes < 60\n ? t('customers.activities.calendar.minutesShort', '{minutes}m', { minutes: Math.max(Math.round(busy.totalMinutes), 1) })\n : t('customers.activities.calendar.hoursShort', '{hours}h', { hours: Math.floor(busy.totalMinutes / 60) })\n return t('customers.activities.calendar.eventsSummary', '{count} {countLabel} \u00B7 {duration}', {\n count: busy.eventCount,\n countLabel: busy.eventCount === 1\n ? t('customers.activities.calendar.eventSingular', 'event')\n : t('customers.activities.calendar.eventPlural', 'events'),\n duration: durationLabel,\n })\n}\n\nfunction formatMonthLabel(date: Date, t: TranslateFn): string {\n const monthEntry = MONTH_KEYS.find(([index]) => index === date.getMonth())\n const monthName = monthEntry ? t(monthEntry[1], monthEntry[2]) : ''\n return t('customers.activities.calendar.monthYear', '{month} {year}', { month: monthName, year: date.getFullYear() })\n}\n\nfunction formatDayLabel(date: Date, t: TranslateFn): string {\n const entry = DAY_LABEL_KEYS.find(([index]) => index === date.getDay())\n return entry ? t(entry[1], entry[2]) : ''\n}\n\nexport function ActivitiesDayStrip({ entityId, selectedDate, onSelectDate, refreshKey = 0, events: providedEvents }: ActivitiesDayStripProps) {\n const t = useT()\n const [anchor, setAnchor] = React.useState<Date>(() => anchorCenteredOn(selectedDate))\n const [fetchedEvents, setFetchedEvents] = React.useState<InteractionSummary[]>([])\n // When the parent supplies `events` (preferred path \u2014 keeps day strip and\n // the list in lockstep, fixes #1809 E1), skip the local fetch entirely.\n const useProvidedEvents = providedEvents !== undefined\n const events = useProvidedEvents ? providedEvents : fetchedEvents\n\n React.useEffect(() => {\n setAnchor((current) => {\n const days = buildVisibleDays(current)\n const visible = days.some((day) => isSameDay(day, selectedDate))\n if (visible) return current\n return anchorCenteredOn(selectedDate)\n })\n }, [selectedDate])\n\n const visibleDays = React.useMemo(() => buildVisibleDays(anchor), [anchor])\n const headerLabel = React.useMemo(() => formatMonthLabel(visibleDays[0], t), [visibleDays, t])\n\n React.useEffect(() => {\n if (useProvidedEvents) return\n if (!entityId || visibleDays.length === 0) return\n const controller = new AbortController()\n const fromIso = startOfDay(visibleDays[0]).toISOString()\n const toIso = endOfDay(visibleDays[visibleDays.length - 1]).toISOString()\n const params = new URLSearchParams({\n entityId,\n from: fromIso,\n to: toIso,\n limit: '100',\n sortField: 'scheduledAt',\n sortDir: 'asc',\n excludeInteractionType: 'task',\n })\n void (async () => {\n try {\n const payload = await readApiResultOrThrow<{ items?: InteractionSummary[] }>(\n `/api/customers/interactions?${params.toString()}`,\n { signal: controller.signal },\n )\n setFetchedEvents(Array.isArray(payload?.items) ? payload.items : [])\n } catch (err) {\n if ((err as { name?: string } | null)?.name !== 'AbortError') {\n logger.warn('failed to load interactions', { component: 'ActivitiesDayStrip', err })\n }\n setFetchedEvents([])\n }\n })()\n return () => controller.abort()\n }, [entityId, visibleDays, refreshKey, useProvidedEvents])\n\n const todayDate = React.useMemo(() => startOfDay(new Date()), [])\n\n const handlePrev = React.useCallback(() => {\n setAnchor((current) => addDays(current, -VISIBLE_DAYS))\n }, [])\n const handleNext = React.useCallback(() => {\n setAnchor((current) => addDays(current, VISIBLE_DAYS))\n }, [])\n const handleHeaderPrev = React.useCallback(() => {\n setAnchor((current) => {\n const next = new Date(current)\n next.setMonth(current.getMonth() - 1)\n return startOfDay(next)\n })\n }, [])\n const handleHeaderNext = React.useCallback(() => {\n setAnchor((current) => {\n const next = new Date(current)\n next.setMonth(current.getMonth() + 1)\n return startOfDay(next)\n })\n }, [])\n\n return (\n <div className=\"flex flex-col gap-2.5 rounded-md px-3.5 py-3 w-full\">\n <div className=\"flex items-center justify-center gap-1.5 rounded-md bg-muted px-1.5 py-1.5\">\n <button\n type=\"button\"\n onClick={handleHeaderPrev}\n aria-label={t('customers.activities.calendar.prevMonth', 'Previous month')}\n className=\"flex size-6 items-center justify-center rounded-md border border-border bg-card shadow-xs hover:bg-accent/40\"\n >\n <ChevronLeft className=\"size-4 text-foreground\" />\n </button>\n <span className=\"flex-1 text-center text-sm font-medium leading-5 text-foreground\">{headerLabel}</span>\n <button\n type=\"button\"\n onClick={handleHeaderNext}\n aria-label={t('customers.activities.calendar.nextMonth', 'Next month')}\n className=\"flex size-6 items-center justify-center rounded-md border border-border bg-card shadow-xs hover:bg-accent/40\"\n >\n <ChevronRight className=\"size-4 text-foreground\" />\n </button>\n </div>\n <div className=\"flex w-full items-center gap-2\">\n <button\n type=\"button\"\n onClick={handlePrev}\n aria-label={t('customers.activities.calendar.prevWindow', 'Previous days')}\n className=\"flex size-6 shrink-0 items-center justify-center rounded-md border border-border bg-card shadow-xs hover:bg-accent/40\"\n >\n <ChevronLeft className=\"size-4 text-foreground\" />\n </button>\n <div className=\"flex flex-1 items-stretch justify-center gap-1\">\n {visibleDays.map((day) => {\n const busy = computeDayBusyness(events, day)\n const isSelected = isSameDay(day, selectedDate)\n const isToday = isSameDay(day, todayDate)\n const weekend = isWeekend(day)\n const busyLabel = busy.eventCount > 0\n ? formatBusyLabel(busy, t)\n : weekend\n ? t('customers.activities.calendar.weekend', 'Weekend')\n : ''\n return (\n <DayCard\n key={day.toISOString()}\n day={day}\n isActive={isSelected}\n isToday={isToday}\n busyness={busy}\n label={busyLabel}\n dayName={formatDayLabel(day, t)}\n onSelect={() => onSelectDate(day)}\n />\n )\n })}\n </div>\n <button\n type=\"button\"\n onClick={handleNext}\n aria-label={t('customers.activities.calendar.nextWindow', 'Next days')}\n className=\"flex size-6 shrink-0 items-center justify-center rounded-md border border-border bg-card shadow-xs hover:bg-accent/40\"\n >\n <ChevronRight className=\"size-4 text-foreground\" />\n </button>\n </div>\n </div>\n )\n}\n\ninterface DayCardProps {\n day: Date\n isActive: boolean\n isToday: boolean\n busyness: DayBusyness\n label: string\n dayName: string\n onSelect: () => void\n}\n\nfunction DayCard({ day, isActive, isToday, busyness, label, dayName, onSelect }: DayCardProps) {\n const dayNumber = String(day.getDate()).padStart(2, '0')\n return (\n <button\n type=\"button\"\n onClick={onSelect}\n aria-pressed={isActive}\n aria-label={`${dayName} ${dayNumber}`}\n className={cn(\n 'flex h-[104px] w-[101px] flex-col items-center gap-[6px] overflow-hidden rounded-[10px] border p-[12px] transition-colors',\n isActive\n ? 'border-transparent bg-foreground'\n : 'border-border bg-card hover:border-foreground/40',\n )}\n >\n <span className=\"text-[11px] font-medium leading-none tracking-[0.44px] text-muted-foreground\">\n {dayName}\n </span>\n <div className=\"flex items-center gap-[5px]\">\n <span\n className={cn(\n 'text-2xl font-semibold leading-7',\n isActive ? 'text-background' : 'text-foreground',\n )}\n >\n {dayNumber}\n </span>\n {isToday ? (\n <span\n className=\"inline-block size-1.5 rounded-full bg-status-info-icon\"\n aria-hidden\n />\n ) : null}\n </div>\n <div className=\"flex h-4 w-[82px] items-end gap-[1.5px]\">\n {busyness.slots.map((state, index) => (\n <BusySlot key={index} state={state} active={isActive} />\n ))}\n </div>\n <span className=\"text-[11px] leading-[14px] font-normal whitespace-nowrap text-muted-foreground\">\n {label}\n </span>\n </button>\n )\n}\n\nfunction BusySlot({ state, active }: { state: SlotState; active: boolean }) {\n const heightClass = state === 'empty'\n ? 'h-0.5'\n : state === 'partial'\n ? 'h-2'\n : 'h-3.5'\n let bgClass: string\n if (state === 'conflict') {\n bgClass = 'bg-status-error-icon'\n } else if (active) {\n if (state === 'empty') bgClass = 'bg-background/30'\n else if (state === 'partial') bgClass = 'bg-background/60'\n else bgClass = 'bg-background'\n } else {\n if (state === 'empty') bgClass = 'bg-border'\n else if (state === 'partial') bgClass = 'bg-muted-foreground'\n else bgClass = 'bg-foreground'\n }\n return <div className={cn('w-[7px] shrink-0 rounded-[1.5px]', heightClass, bgClass)} aria-hidden />\n}\n\nexport default ActivitiesDayStrip\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { ChevronLeft, ChevronRight } from 'lucide-react'\nimport { toZonedTime } from 'date-fns-tz'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport type { InteractionSummary } from './types'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\n\ninterface ActivitiesDayStripProps {\n entityId: string\n selectedDate: Date\n onSelectDate: (date: Date) => void\n refreshKey?: number\n /**\n * Optional pre-fetched events. When provided, the day strip skips its own fetch\n * and uses the supplied list, ensuring its busyness count agrees with the\n * activity list rendered alongside it (issue #1809 \u2014 E1 status filter alignment).\n */\n events?: InteractionSummary[]\n}\n\nconst USER_TIMEZONE = (() => {\n try {\n return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'\n } catch {\n return 'UTC'\n }\n})()\n\n// Project a UTC ISO timestamp to the user's local timezone before comparing\n// \"same day\" (issue #1809 \u2014 E3). The browser's `new Date(iso)` treats the\n// instant correctly, but `getDate()/getMonth()/getFullYear()` reflect the\n// user's local day, so for activities scheduled at e.g. 23:30 local on a UTC\n// boundary the day-strip and list now agree.\nfunction toLocalZonedDate(value: string | Date): Date {\n return toZonedTime(value, USER_TIMEZONE)\n}\n\nconst VISIBLE_DAYS = 5\nconst BUSYNESS_SLOTS = 10\nconst SLOT_START_HOUR = 7\nconst SLOT_END_HOUR = 22\n\nconst DAY_LABEL_KEYS: Array<[number, string, string]> = [\n [0, 'customers.calendar.day.sun', 'SUN'],\n [1, 'customers.calendar.day.mon', 'MON'],\n [2, 'customers.calendar.day.tue', 'TUE'],\n [3, 'customers.calendar.day.wed', 'WED'],\n [4, 'customers.calendar.day.thu', 'THU'],\n [5, 'customers.calendar.day.fri', 'FRI'],\n [6, 'customers.calendar.day.sat', 'SAT'],\n]\n\nconst MONTH_KEYS: Array<[number, string, string]> = [\n [0, 'customers.calendar.month.january', 'January'],\n [1, 'customers.calendar.month.february', 'February'],\n [2, 'customers.calendar.month.march', 'March'],\n [3, 'customers.calendar.month.april', 'April'],\n [4, 'customers.calendar.month.may', 'May'],\n [5, 'customers.calendar.month.june', 'June'],\n [6, 'customers.calendar.month.july', 'July'],\n [7, 'customers.calendar.month.august', 'August'],\n [8, 'customers.calendar.month.september', 'September'],\n [9, 'customers.calendar.month.october', 'October'],\n [10, 'customers.calendar.month.november', 'November'],\n [11, 'customers.calendar.month.december', 'December'],\n]\n\nfunction startOfDay(date: Date): Date {\n const next = new Date(date)\n next.setHours(0, 0, 0, 0)\n return next\n}\n\nfunction endOfDay(date: Date): Date {\n const next = new Date(date)\n next.setHours(23, 59, 59, 999)\n return next\n}\n\nfunction isSameDay(a: Date, b: Date): boolean {\n return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()\n}\n\nfunction isWeekend(date: Date): boolean {\n const day = date.getDay()\n return day === 0 || day === 6\n}\n\nfunction addDays(date: Date, delta: number): Date {\n const next = new Date(date)\n next.setDate(date.getDate() + delta)\n return next\n}\n\nfunction buildVisibleDays(anchor: Date): Date[] {\n const start = startOfDay(anchor)\n return Array.from({ length: VISIBLE_DAYS }, (_, index) => addDays(start, index))\n}\n\n// Anchor the visible window so that the given focal date lands at the center slot\n// (position 2 out of 5). Matches Figma 784:809 where the selected day is centered.\nfunction anchorCenteredOn(focalDate: Date): Date {\n return startOfDay(addDays(focalDate, -Math.floor(VISIBLE_DAYS / 2)))\n}\n\ntype SlotState = 'empty' | 'partial' | 'full' | 'conflict'\n\ntype DayBusyness = {\n totalMinutes: number\n eventCount: number\n slots: SlotState[]\n}\n\nfunction emptyBusyness(): DayBusyness {\n return {\n totalMinutes: 0,\n eventCount: 0,\n slots: Array<SlotState>(BUSYNESS_SLOTS).fill('empty'),\n }\n}\n\nfunction computeDayBusyness(events: InteractionSummary[], day: Date): DayBusyness {\n if (events.length === 0) return emptyBusyness()\n const dayStart = startOfDay(day).getTime()\n const slotMs = ((SLOT_END_HOUR - SLOT_START_HOUR) * 60 * 60 * 1000) / BUSYNESS_SLOTS\n const slotMinutes = slotMs / 60000\n const slotCounts: number[] = Array(BUSYNESS_SLOTS).fill(0)\n const slotMinutesUsed: number[] = Array(BUSYNESS_SLOTS).fill(0)\n let totalMinutes = 0\n let eventCount = 0\n\n for (const event of events) {\n const startIso = event.scheduledAt ?? event.occurredAt ?? event.createdAt\n if (!startIso) continue\n const start = new Date(startIso)\n if (Number.isNaN(start.getTime())) continue\n // Compare in the user's local timezone so an activity at 23:30 local time\n // doesn't bleed into the next UTC day's chip (issue #1809 \u2014 E3).\n const localStart = toLocalZonedDate(startIso)\n if (!isSameDay(localStart, day)) continue\n eventCount += 1\n const durationMinutes = typeof event.duration === 'number' && event.duration > 0 ? event.duration : 30\n totalMinutes += durationMinutes\n const eventStartMs = start.getTime()\n const eventEndMs = eventStartMs + durationMinutes * 60000\n const slotsStartMs = dayStart + SLOT_START_HOUR * 60 * 60 * 1000\n for (let slot = 0; slot < BUSYNESS_SLOTS; slot += 1) {\n const slotStart = slotsStartMs + slot * slotMs\n const slotEnd = slotStart + slotMs\n const overlapStart = Math.max(slotStart, eventStartMs)\n const overlapEnd = Math.min(slotEnd, eventEndMs)\n const overlapMinutes = Math.max(0, (overlapEnd - overlapStart) / 60000)\n if (overlapMinutes <= 0) continue\n slotCounts[slot] += 1\n slotMinutesUsed[slot] += overlapMinutes\n }\n }\n\n const slots: SlotState[] = slotCounts.map((count, index) => {\n if (count === 0) return 'empty'\n if (count > 1) return 'conflict'\n const used = slotMinutesUsed[index]\n if (used >= slotMinutes * 0.5) return 'full'\n return 'partial'\n })\n\n return { totalMinutes, eventCount, slots }\n}\n\nfunction formatBusyLabel(busy: DayBusyness, t: TranslateFn): string {\n if (busy.eventCount === 0) return ''\n // Match Figma 784:809 label format: \"Xm\" when under an hour, \"Xh\" otherwise.\n // Mixed \"Xh Ym\" overflows the 101px card and is not part of the visual spec.\n const durationLabel = busy.totalMinutes < 60\n ? t('customers.activities.calendar.minutesShort', '{minutes}m', { minutes: Math.max(Math.round(busy.totalMinutes), 1) })\n : t('customers.activities.calendar.hoursShort', '{hours}h', { hours: Math.floor(busy.totalMinutes / 60) })\n return t('customers.activities.calendar.eventsSummary', '{count} {countLabel} \u00B7 {duration}', {\n count: busy.eventCount,\n countLabel: busy.eventCount === 1\n ? t('customers.activities.calendar.eventSingular', 'event')\n : t('customers.activities.calendar.eventPlural', 'events'),\n duration: durationLabel,\n })\n}\n\nfunction formatMonthLabel(date: Date, t: TranslateFn): string {\n const monthEntry = MONTH_KEYS.find(([index]) => index === date.getMonth())\n const monthName = monthEntry ? t(monthEntry[1], monthEntry[2]) : ''\n return t('customers.activities.calendar.monthYear', '{month} {year}', { month: monthName, year: date.getFullYear() })\n}\n\nfunction formatDayLabel(date: Date, t: TranslateFn): string {\n const entry = DAY_LABEL_KEYS.find(([index]) => index === date.getDay())\n return entry ? t(entry[1], entry[2]) : ''\n}\n\nexport function ActivitiesDayStrip({ entityId, selectedDate, onSelectDate, refreshKey = 0, events: providedEvents }: ActivitiesDayStripProps) {\n const t = useT()\n const [anchor, setAnchor] = React.useState<Date>(() => anchorCenteredOn(selectedDate))\n const [fetchedEvents, setFetchedEvents] = React.useState<InteractionSummary[]>([])\n // When the parent supplies `events` (preferred path \u2014 keeps day strip and\n // the list in lockstep, fixes #1809 E1), skip the local fetch entirely.\n const useProvidedEvents = providedEvents !== undefined\n const events = useProvidedEvents ? providedEvents : fetchedEvents\n\n React.useEffect(() => {\n setAnchor((current) => {\n const days = buildVisibleDays(current)\n const visible = days.some((day) => isSameDay(day, selectedDate))\n if (visible) return current\n return anchorCenteredOn(selectedDate)\n })\n }, [selectedDate])\n\n const visibleDays = React.useMemo(() => buildVisibleDays(anchor), [anchor])\n const headerLabel = React.useMemo(() => formatMonthLabel(visibleDays[0], t), [visibleDays, t])\n\n React.useEffect(() => {\n if (useProvidedEvents) return\n if (!entityId || visibleDays.length === 0) return\n const controller = new AbortController()\n const fromIso = startOfDay(visibleDays[0]).toISOString()\n const toIso = endOfDay(visibleDays[visibleDays.length - 1]).toISOString()\n const params = new URLSearchParams({\n entityId,\n from: fromIso,\n to: toIso,\n limit: '100',\n sortField: 'scheduledAt',\n sortDir: 'asc',\n excludeInteractionType: 'task',\n })\n void (async () => {\n try {\n const payload = await readApiResultOrThrow<{ items?: InteractionSummary[] }>(\n `/api/customers/interactions?${params.toString()}`,\n { signal: controller.signal },\n )\n setFetchedEvents(Array.isArray(payload?.items) ? payload.items : [])\n } catch (err) {\n if ((err as { name?: string } | null)?.name === 'AbortError') return\n logger.warn('failed to load interactions', { component: 'ActivitiesDayStrip', err })\n setFetchedEvents([])\n }\n })()\n return () => controller.abort()\n }, [entityId, visibleDays, refreshKey, useProvidedEvents])\n\n const todayDate = React.useMemo(() => startOfDay(new Date()), [])\n\n const handlePrev = React.useCallback(() => {\n setAnchor((current) => addDays(current, -VISIBLE_DAYS))\n }, [])\n const handleNext = React.useCallback(() => {\n setAnchor((current) => addDays(current, VISIBLE_DAYS))\n }, [])\n const handleHeaderPrev = React.useCallback(() => {\n setAnchor((current) => {\n const next = new Date(current)\n next.setMonth(current.getMonth() - 1)\n return startOfDay(next)\n })\n }, [])\n const handleHeaderNext = React.useCallback(() => {\n setAnchor((current) => {\n const next = new Date(current)\n next.setMonth(current.getMonth() + 1)\n return startOfDay(next)\n })\n }, [])\n\n return (\n <div className=\"flex flex-col gap-2.5 rounded-md px-3.5 py-3 w-full\">\n <div className=\"flex items-center justify-center gap-1.5 rounded-md bg-muted px-1.5 py-1.5\">\n <button\n type=\"button\"\n onClick={handleHeaderPrev}\n aria-label={t('customers.activities.calendar.prevMonth', 'Previous month')}\n className=\"flex size-6 items-center justify-center rounded-md border border-border bg-card shadow-xs hover:bg-accent/40\"\n >\n <ChevronLeft className=\"size-4 text-foreground\" />\n </button>\n <span className=\"flex-1 text-center text-sm font-medium leading-5 text-foreground\">{headerLabel}</span>\n <button\n type=\"button\"\n onClick={handleHeaderNext}\n aria-label={t('customers.activities.calendar.nextMonth', 'Next month')}\n className=\"flex size-6 items-center justify-center rounded-md border border-border bg-card shadow-xs hover:bg-accent/40\"\n >\n <ChevronRight className=\"size-4 text-foreground\" />\n </button>\n </div>\n <div className=\"flex w-full items-center gap-2\">\n <button\n type=\"button\"\n onClick={handlePrev}\n aria-label={t('customers.activities.calendar.prevWindow', 'Previous days')}\n className=\"flex size-6 shrink-0 items-center justify-center rounded-md border border-border bg-card shadow-xs hover:bg-accent/40\"\n >\n <ChevronLeft className=\"size-4 text-foreground\" />\n </button>\n <div className=\"flex flex-1 items-stretch justify-center gap-1\">\n {visibleDays.map((day) => {\n const busy = computeDayBusyness(events, day)\n const isSelected = isSameDay(day, selectedDate)\n const isToday = isSameDay(day, todayDate)\n const weekend = isWeekend(day)\n const busyLabel = busy.eventCount > 0\n ? formatBusyLabel(busy, t)\n : weekend\n ? t('customers.activities.calendar.weekend', 'Weekend')\n : ''\n return (\n <DayCard\n key={day.toISOString()}\n day={day}\n isActive={isSelected}\n isToday={isToday}\n busyness={busy}\n label={busyLabel}\n dayName={formatDayLabel(day, t)}\n onSelect={() => onSelectDate(day)}\n />\n )\n })}\n </div>\n <button\n type=\"button\"\n onClick={handleNext}\n aria-label={t('customers.activities.calendar.nextWindow', 'Next days')}\n className=\"flex size-6 shrink-0 items-center justify-center rounded-md border border-border bg-card shadow-xs hover:bg-accent/40\"\n >\n <ChevronRight className=\"size-4 text-foreground\" />\n </button>\n </div>\n </div>\n )\n}\n\ninterface DayCardProps {\n day: Date\n isActive: boolean\n isToday: boolean\n busyness: DayBusyness\n label: string\n dayName: string\n onSelect: () => void\n}\n\nfunction DayCard({ day, isActive, isToday, busyness, label, dayName, onSelect }: DayCardProps) {\n const dayNumber = String(day.getDate()).padStart(2, '0')\n return (\n <button\n type=\"button\"\n onClick={onSelect}\n aria-pressed={isActive}\n aria-label={`${dayName} ${dayNumber}`}\n className={cn(\n 'flex h-[104px] w-[101px] flex-col items-center gap-[6px] overflow-hidden rounded-[10px] border p-[12px] transition-colors',\n isActive\n ? 'border-transparent bg-foreground'\n : 'border-border bg-card hover:border-foreground/40',\n )}\n >\n <span className=\"text-[11px] font-medium leading-none tracking-[0.44px] text-muted-foreground\">\n {dayName}\n </span>\n <div className=\"flex items-center gap-[5px]\">\n <span\n className={cn(\n 'text-2xl font-semibold leading-7',\n isActive ? 'text-background' : 'text-foreground',\n )}\n >\n {dayNumber}\n </span>\n {isToday ? (\n <span\n className=\"inline-block size-1.5 rounded-full bg-status-info-icon\"\n aria-hidden\n />\n ) : null}\n </div>\n <div className=\"flex h-4 w-[82px] items-end gap-[1.5px]\">\n {busyness.slots.map((state, index) => (\n <BusySlot key={index} state={state} active={isActive} />\n ))}\n </div>\n <span className=\"text-[11px] leading-[14px] font-normal whitespace-nowrap text-muted-foreground\">\n {label}\n </span>\n </button>\n )\n}\n\nfunction BusySlot({ state, active }: { state: SlotState; active: boolean }) {\n const heightClass = state === 'empty'\n ? 'h-0.5'\n : state === 'partial'\n ? 'h-2'\n : 'h-3.5'\n let bgClass: string\n if (state === 'conflict') {\n bgClass = 'bg-status-error-icon'\n } else if (active) {\n if (state === 'empty') bgClass = 'bg-background/30'\n else if (state === 'partial') bgClass = 'bg-background/60'\n else bgClass = 'bg-background'\n } else {\n if (state === 'empty') bgClass = 'bg-border'\n else if (state === 'partial') bgClass = 'bg-muted-foreground'\n else bgClass = 'bg-foreground'\n }\n return <div className={cn('w-[7px] shrink-0 rounded-[1.5px]', heightClass, bgClass)} aria-hidden />\n}\n\nexport default ActivitiesDayStrip\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -69,6 +69,15 @@ function resolveGroupExpression(registry, entityType, groupBy) {
69
69
  }
70
70
  return { expression: groupMapping.dbColumn, dbColumn: groupMapping.dbColumn, jsonPath: null };
71
71
  }
72
+ function normalizeSetFilterValues(value) {
73
+ const values = Array.isArray(value) ? value : value === void 0 ? [] : [value];
74
+ if (values.some((member) => member === null || member === void 0)) {
75
+ throw new Error(
76
+ "[internal] Set filter members must not be null or undefined \u2014 use the is_null / is_not_null operators to filter on nullness"
77
+ );
78
+ }
79
+ return values;
80
+ }
72
81
  function buildWhereClause(options) {
73
82
  const { registry } = options;
74
83
  const params = [];
@@ -119,13 +128,18 @@ function buildWhereClause(options) {
119
128
  params.push(filter.value);
120
129
  break;
121
130
  case "in":
122
- whereClauses.push(`${filterMapping.dbColumn} = ANY(?)`);
123
- params.push(filter.value);
124
- break;
125
- case "not_in":
126
- whereClauses.push(`${filterMapping.dbColumn} != ALL(?)`);
127
- params.push(filter.value);
131
+ case "not_in": {
132
+ const values = normalizeSetFilterValues(filter.value);
133
+ if (values.length === 0) {
134
+ whereClauses.push(filter.operator === "in" ? "FALSE" : "TRUE");
135
+ break;
136
+ }
137
+ const keyword = filter.operator === "in" ? "IN" : "NOT IN";
138
+ const placeholders = values.map(() => "?").join(", ");
139
+ whereClauses.push(`${filterMapping.dbColumn} ${keyword} (${placeholders})`);
140
+ params.push(...values);
128
141
  break;
142
+ }
129
143
  case "is_null":
130
144
  whereClauses.push(`${filterMapping.dbColumn} IS NULL`);
131
145
  break;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/dashboards/lib/aggregations.ts"],
4
- "sourcesContent": ["import type { AnalyticsRegistry } from '../services/analyticsRegistry'\nimport type { AnalyticsEntityTypeConfig, AnalyticsFieldMapping } from '@open-mercato/shared/modules/analytics'\n\nexport type AggregateFunction = 'count' | 'sum' | 'avg' | 'min' | 'max'\nexport type DateGranularity = 'day' | 'week' | 'month' | 'quarter' | 'year'\n\nconst VALID_GRANULARITIES: readonly DateGranularity[] = ['day', 'week', 'month', 'quarter', 'year']\nconst VALID_AGGREGATES: readonly AggregateFunction[] = ['count', 'sum', 'avg', 'min', 'max']\nconst SAFE_IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/\n\nexport function isValidGranularity(value: unknown): value is DateGranularity {\n return typeof value === 'string' && VALID_GRANULARITIES.includes(value as DateGranularity)\n}\n\nexport function isValidAggregate(value: unknown): value is AggregateFunction {\n return typeof value === 'string' && VALID_AGGREGATES.includes(value as AggregateFunction)\n}\n\nfunction isSafeIdentifier(value: string): boolean {\n return SAFE_IDENTIFIER_PATTERN.test(value)\n}\n\n// Re-export types from shared module for convenience\nexport type EntityTypeConfig = AnalyticsEntityTypeConfig\nexport type FieldMapping = AnalyticsFieldMapping\n\nexport function buildAggregateExpression(aggregate: AggregateFunction, column: string): string {\n switch (aggregate) {\n case 'count':\n return column === 'id' ? 'COUNT(*)' : `COUNT(${column})`\n case 'sum':\n return `COALESCE(SUM(${column}::numeric), 0)`\n case 'avg':\n return `COALESCE(AVG(${column}::numeric), 0)`\n case 'min':\n return `MIN(${column}::numeric)`\n case 'max':\n return `MAX(${column}::numeric)`\n default:\n return `COUNT(*)`\n }\n}\n\nexport function buildDateTruncExpression(column: string, granularity: DateGranularity): string {\n if (!isValidGranularity(granularity)) {\n throw new Error(`Invalid granularity: ${granularity}`)\n }\n return `DATE_TRUNC('${granularity}', ${column})`\n}\n\nexport function buildJsonbFieldExpression(column: string, path: string): string {\n const parts = path.split('.')\n for (const part of parts) {\n if (!isSafeIdentifier(part)) {\n throw new Error(`Invalid JSONB path part: ${part}`)\n }\n }\n if (parts.length === 1) {\n return `${column}->>'${parts[0]}'`\n }\n const intermediate = parts.slice(0, -1).map((p) => `'${p}'`).join('->')\n const lastPart = parts[parts.length - 1]\n return `${column}->${intermediate}->>'${lastPart}'`\n}\n\nexport type AggregationQuery = {\n sql: string\n params: unknown[]\n}\n\nexport type BuildAggregationQueryOptions = {\n entityType: string\n metric: {\n field: string\n aggregate: AggregateFunction\n }\n groupBy?: {\n field: string\n granularity?: DateGranularity\n limit?: number\n }\n dateRange?: {\n field: string\n start: Date\n end: Date\n }\n filters?: Array<{\n field: string\n operator: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'not_in' | 'is_null' | 'is_not_null'\n value?: unknown\n }>\n scope: {\n tenantId: string\n organizationIds?: string[]\n }\n /** Analytics registry for resolving entity and field configurations */\n registry: AnalyticsRegistry\n}\n\nexport type ResolvedGroupExpression = {\n expression: string\n /** Column the group expression reads from \u2014 the encryption map is keyed by this. */\n dbColumn: string\n /** JSONB path below `dbColumn`, when the groupBy field used path notation. */\n jsonPath: string | null\n}\n\n/**\n * Resolves the SQL expression a groupBy field maps to, together with the underlying column so\n * callers can decide whether the source is encrypted at rest before grouping over it (#4622).\n */\nexport function resolveGroupExpression(\n registry: AnalyticsRegistry,\n entityType: string,\n groupBy: { field: string; granularity?: DateGranularity },\n): ResolvedGroupExpression | null {\n const groupMapping = registry.getFieldMapping(entityType, groupBy.field)\n\n // Handle JSONB path notation (e.g., shippingAddressSnapshot.region)\n if (!groupMapping && groupBy.field.includes('.')) {\n const [baseField, ...pathParts] = groupBy.field.split('.')\n const baseMapping = registry.getFieldMapping(entityType, baseField)\n if (baseMapping?.type !== 'jsonb') return null\n const jsonPath = pathParts.join('.')\n return {\n expression: buildJsonbFieldExpression(baseMapping.dbColumn, jsonPath),\n dbColumn: baseMapping.dbColumn,\n jsonPath,\n }\n }\n\n if (!groupMapping) return null\n\n if (groupMapping.type === 'timestamp' && groupBy.granularity) {\n return {\n expression: buildDateTruncExpression(groupMapping.dbColumn, groupBy.granularity),\n dbColumn: groupMapping.dbColumn,\n jsonPath: null,\n }\n }\n\n return { expression: groupMapping.dbColumn, dbColumn: groupMapping.dbColumn, jsonPath: null }\n}\n\nfunction buildWhereClause(\n options: Pick<BuildAggregationQueryOptions, 'entityType' | 'dateRange' | 'filters' | 'scope' | 'registry'>,\n): { clause: string; params: unknown[] } {\n const { registry } = options\n const params: unknown[] = []\n const whereClauses: string[] = []\n\n whereClauses.push(`tenant_id = ?`)\n params.push(options.scope.tenantId)\n\n if (options.scope.organizationIds && options.scope.organizationIds.length > 0) {\n whereClauses.push(`organization_id = ANY(?::uuid[])`)\n params.push(`{${options.scope.organizationIds.join(',')}}`)\n }\n\n whereClauses.push(`deleted_at IS NULL`)\n\n if (options.dateRange) {\n const dateMapping = registry.getFieldMapping(options.entityType, options.dateRange.field)\n if (dateMapping) {\n whereClauses.push(`${dateMapping.dbColumn} >= ?`)\n params.push(options.dateRange.start)\n whereClauses.push(`${dateMapping.dbColumn} <= ?`)\n params.push(options.dateRange.end)\n }\n }\n\n if (options.filters) {\n for (const filter of options.filters) {\n const filterMapping = registry.getFieldMapping(options.entityType, filter.field)\n if (!filterMapping) continue\n\n switch (filter.operator) {\n case 'eq':\n whereClauses.push(`${filterMapping.dbColumn} = ?`)\n params.push(filter.value)\n break\n case 'neq':\n whereClauses.push(`${filterMapping.dbColumn} != ?`)\n params.push(filter.value)\n break\n case 'gt':\n whereClauses.push(`${filterMapping.dbColumn} > ?`)\n params.push(filter.value)\n break\n case 'gte':\n whereClauses.push(`${filterMapping.dbColumn} >= ?`)\n params.push(filter.value)\n break\n case 'lt':\n whereClauses.push(`${filterMapping.dbColumn} < ?`)\n params.push(filter.value)\n break\n case 'lte':\n whereClauses.push(`${filterMapping.dbColumn} <= ?`)\n params.push(filter.value)\n break\n case 'in':\n whereClauses.push(`${filterMapping.dbColumn} = ANY(?)`)\n params.push(filter.value)\n break\n case 'not_in':\n whereClauses.push(`${filterMapping.dbColumn} != ALL(?)`)\n params.push(filter.value)\n break\n case 'is_null':\n whereClauses.push(`${filterMapping.dbColumn} IS NULL`)\n break\n case 'is_not_null':\n whereClauses.push(`${filterMapping.dbColumn} IS NOT NULL`)\n break\n }\n }\n }\n\n return { clause: whereClauses.length > 0 ? `WHERE ${whereClauses.join(' AND ')}` : '', params }\n}\n\nfunction resolveTableName(config: EntityTypeConfig): string {\n return config.schema ? `\"${config.schema}\".\"${config.tableName}\"` : `\"${config.tableName}\"`\n}\n\nexport type BuildDistinctCurrencyQueryOptions = Pick<\n BuildAggregationQueryOptions,\n 'entityType' | 'dateRange' | 'filters' | 'scope' | 'registry'\n>\n\n/**\n * Builds the query that reads the distinct per-row currencies of the rows an aggregation\n * would sum, over exactly the same tenant/organization scope, date range and filters.\n * Returns `null` when the entity declares no `currencyField`, which means its amounts are\n * not per-row denominated and there is nothing to verify (#4676).\n */\nexport function buildDistinctCurrencyQuery(\n options: BuildDistinctCurrencyQueryOptions,\n): AggregationQuery | null {\n const { registry } = options\n const config = registry.getEntityTypeConfig(options.entityType)\n if (!config?.currencyField) return null\n\n const currencyMapping = registry.getFieldMapping(options.entityType, config.currencyField)\n if (!currencyMapping || !isSafeIdentifier(currencyMapping.dbColumn)) return null\n\n const where = buildWhereClause(options)\n const normalizedCurrencyExpression = `UPPER(NULLIF(BTRIM(${currencyMapping.dbColumn}), ''))`\n const sql = [\n `SELECT DISTINCT ${normalizedCurrencyExpression} AS code`,\n `FROM ${resolveTableName(config)}`,\n where.clause,\n 'LIMIT 2',\n ]\n .filter(Boolean)\n .join(' ')\n\n return { sql, params: where.params }\n}\n\nexport function buildAggregationQuery(options: BuildAggregationQueryOptions): AggregationQuery | null {\n const { registry } = options\n const config = registry.getEntityTypeConfig(options.entityType)\n if (!config) return null\n\n const metricMapping = registry.getFieldMapping(options.entityType, options.metric.field)\n if (!metricMapping) return null\n\n const tableName = resolveTableName(config)\n const aggregateExpr = buildAggregateExpression(options.metric.aggregate, metricMapping.dbColumn)\n\n let selectClause = `SELECT ${aggregateExpr} AS value`\n let groupByClause = ''\n let orderByClause = ''\n let limitClause = ''\n\n if (options.groupBy) {\n const resolved = resolveGroupExpression(registry, options.entityType, options.groupBy)\n\n if (resolved) {\n selectClause = `SELECT ${resolved.expression} AS group_key, ${aggregateExpr} AS value`\n groupByClause = `GROUP BY ${resolved.expression}`\n // NULLS LAST is stated explicitly (PostgreSQL defaults DESC to NULLS FIRST) so a group limit\n // keeps the highest-value buckets instead of the empty ones, and so the application-side\n // encrypted path can mirror the same ordering (#4622).\n orderByClause = `ORDER BY value DESC NULLS LAST`\n\n if (options.groupBy.limit && options.groupBy.limit > 0) {\n limitClause = `LIMIT ${Math.min(options.groupBy.limit, 100)}`\n }\n }\n }\n\n const where = buildWhereClause(options)\n\n const sql = [selectClause, `FROM ${tableName}`, where.clause, groupByClause, orderByClause, limitClause]\n .filter(Boolean)\n .join(' ')\n\n return { sql, params: where.params }\n}\n\nexport type BuildGroupSourceRowsQueryOptions = Omit<BuildAggregationQueryOptions, 'groupBy'> & {\n /** Column holding the (encrypted) group source, resolved via `resolveGroupExpression`. */\n groupColumn: string\n /** Hard cap on scanned rows; callers fetch `rowLimit + 1` to detect overflow. */\n rowLimit: number\n}\n\n/**\n * Builds a per-row query for group sources that cannot be grouped in SQL because the column is\n * encrypted at rest. Returns the raw group source alongside the metric value so the caller can\n * decrypt, then aggregate in application code (#4622).\n */\nexport function buildGroupSourceRowsQuery(options: BuildGroupSourceRowsQueryOptions): AggregationQuery | null {\n const { registry } = options\n const config = registry.getEntityTypeConfig(options.entityType)\n if (!config) return null\n\n const metricMapping = registry.getFieldMapping(options.entityType, options.metric.field)\n if (!metricMapping) return null\n\n if (!isSafeIdentifier(options.groupColumn)) {\n throw new Error(`Invalid group column: ${options.groupColumn}`)\n }\n\n const where = buildWhereClause(options)\n const sql = [\n `SELECT ${options.groupColumn} AS group_source, ${metricMapping.dbColumn} AS metric_value`,\n `FROM ${resolveTableName(config)}`,\n where.clause,\n `LIMIT ${Math.max(1, Math.floor(options.rowLimit)) + 1}`,\n ]\n .filter(Boolean)\n .join(' ')\n\n return { sql, params: where.params }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { AnalyticsRegistry } from '../services/analyticsRegistry'\nimport type { AnalyticsEntityTypeConfig, AnalyticsFieldMapping } from '@open-mercato/shared/modules/analytics'\n\nexport type AggregateFunction = 'count' | 'sum' | 'avg' | 'min' | 'max'\nexport type DateGranularity = 'day' | 'week' | 'month' | 'quarter' | 'year'\n\nconst VALID_GRANULARITIES: readonly DateGranularity[] = ['day', 'week', 'month', 'quarter', 'year']\nconst VALID_AGGREGATES: readonly AggregateFunction[] = ['count', 'sum', 'avg', 'min', 'max']\nconst SAFE_IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/\n\nexport function isValidGranularity(value: unknown): value is DateGranularity {\n return typeof value === 'string' && VALID_GRANULARITIES.includes(value as DateGranularity)\n}\n\nexport function isValidAggregate(value: unknown): value is AggregateFunction {\n return typeof value === 'string' && VALID_AGGREGATES.includes(value as AggregateFunction)\n}\n\nfunction isSafeIdentifier(value: string): boolean {\n return SAFE_IDENTIFIER_PATTERN.test(value)\n}\n\n// Re-export types from shared module for convenience\nexport type EntityTypeConfig = AnalyticsEntityTypeConfig\nexport type FieldMapping = AnalyticsFieldMapping\n\nexport function buildAggregateExpression(aggregate: AggregateFunction, column: string): string {\n switch (aggregate) {\n case 'count':\n return column === 'id' ? 'COUNT(*)' : `COUNT(${column})`\n case 'sum':\n return `COALESCE(SUM(${column}::numeric), 0)`\n case 'avg':\n return `COALESCE(AVG(${column}::numeric), 0)`\n case 'min':\n return `MIN(${column}::numeric)`\n case 'max':\n return `MAX(${column}::numeric)`\n default:\n return `COUNT(*)`\n }\n}\n\nexport function buildDateTruncExpression(column: string, granularity: DateGranularity): string {\n if (!isValidGranularity(granularity)) {\n throw new Error(`Invalid granularity: ${granularity}`)\n }\n return `DATE_TRUNC('${granularity}', ${column})`\n}\n\nexport function buildJsonbFieldExpression(column: string, path: string): string {\n const parts = path.split('.')\n for (const part of parts) {\n if (!isSafeIdentifier(part)) {\n throw new Error(`Invalid JSONB path part: ${part}`)\n }\n }\n if (parts.length === 1) {\n return `${column}->>'${parts[0]}'`\n }\n const intermediate = parts.slice(0, -1).map((p) => `'${p}'`).join('->')\n const lastPart = parts[parts.length - 1]\n return `${column}->${intermediate}->>'${lastPart}'`\n}\n\nexport type AggregationQuery = {\n sql: string\n params: unknown[]\n}\n\nexport type BuildAggregationQueryOptions = {\n entityType: string\n metric: {\n field: string\n aggregate: AggregateFunction\n }\n groupBy?: {\n field: string\n granularity?: DateGranularity\n limit?: number\n }\n dateRange?: {\n field: string\n start: Date\n end: Date\n }\n filters?: Array<{\n field: string\n operator: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'not_in' | 'is_null' | 'is_not_null'\n value?: unknown\n }>\n scope: {\n tenantId: string\n organizationIds?: string[]\n }\n /** Analytics registry for resolving entity and field configurations */\n registry: AnalyticsRegistry\n}\n\nexport type ResolvedGroupExpression = {\n expression: string\n /** Column the group expression reads from \u2014 the encryption map is keyed by this. */\n dbColumn: string\n /** JSONB path below `dbColumn`, when the groupBy field used path notation. */\n jsonPath: string | null\n}\n\n/**\n * Resolves the SQL expression a groupBy field maps to, together with the underlying column so\n * callers can decide whether the source is encrypted at rest before grouping over it (#4622).\n */\nexport function resolveGroupExpression(\n registry: AnalyticsRegistry,\n entityType: string,\n groupBy: { field: string; granularity?: DateGranularity },\n): ResolvedGroupExpression | null {\n const groupMapping = registry.getFieldMapping(entityType, groupBy.field)\n\n // Handle JSONB path notation (e.g., shippingAddressSnapshot.region)\n if (!groupMapping && groupBy.field.includes('.')) {\n const [baseField, ...pathParts] = groupBy.field.split('.')\n const baseMapping = registry.getFieldMapping(entityType, baseField)\n if (baseMapping?.type !== 'jsonb') return null\n const jsonPath = pathParts.join('.')\n return {\n expression: buildJsonbFieldExpression(baseMapping.dbColumn, jsonPath),\n dbColumn: baseMapping.dbColumn,\n jsonPath,\n }\n }\n\n if (!groupMapping) return null\n\n if (groupMapping.type === 'timestamp' && groupBy.granularity) {\n return {\n expression: buildDateTruncExpression(groupMapping.dbColumn, groupBy.granularity),\n dbColumn: groupMapping.dbColumn,\n jsonPath: null,\n }\n }\n\n return { expression: groupMapping.dbColumn, dbColumn: groupMapping.dbColumn, jsonPath: null }\n}\n\n/**\n * Normalizes the value of a set filter (`in` / `not_in`) into the list of members it selects.\n *\n * The list is rendered as one placeholder per member rather than as a single array parameter:\n * MikroORM does not bind parameters at the driver level \u2014 `AbstractSqlConnection.execute` calls\n * `platform.formatQuery`, which interpolates each value through `BasePostgreSqlPlatform.escape`,\n * and that renders a JavaScript array as a bare comma-separated list. The previous\n * `= ANY(?)` / `!= ALL(?)` form therefore produced `= ANY('a', 'b')`, which PostgreSQL rejects\n * for every shape of value (#4669).\n *\n * A missing value yields an empty set rather than a dropped filter, so an `in` filter the caller\n * failed to populate selects nothing instead of silently widening the result.\n *\n * A null or undefined *member* is refused instead of rendered. `column IN (NULL)` matches no row\n * and `column NOT IN ('a', NULL)` is NULL for every row, so such a member would return an empty\n * aggregation with no error \u2014 a silent zero on a reporting surface, which is worse than a loud\n * failure. Requests are already rejected at the API boundary by `widgetDataFilterSchema` in\n * `api/widgets/data/schema.ts`; this throw keeps in-process callers, whose filter values are\n * typed as `unknown`, from reaching the same silent path. Nullness is queried with the dedicated\n * `is_null` / `is_not_null` operators.\n */\nfunction normalizeSetFilterValues(value: unknown): unknown[] {\n const values = Array.isArray(value) ? value : value === undefined ? [] : [value]\n if (values.some((member) => member === null || member === undefined)) {\n throw new Error(\n '[internal] Set filter members must not be null or undefined \u2014 use the is_null / is_not_null operators to filter on nullness',\n )\n }\n return values\n}\n\nfunction buildWhereClause(\n options: Pick<BuildAggregationQueryOptions, 'entityType' | 'dateRange' | 'filters' | 'scope' | 'registry'>,\n): { clause: string; params: unknown[] } {\n const { registry } = options\n const params: unknown[] = []\n const whereClauses: string[] = []\n\n whereClauses.push(`tenant_id = ?`)\n params.push(options.scope.tenantId)\n\n if (options.scope.organizationIds && options.scope.organizationIds.length > 0) {\n whereClauses.push(`organization_id = ANY(?::uuid[])`)\n params.push(`{${options.scope.organizationIds.join(',')}}`)\n }\n\n whereClauses.push(`deleted_at IS NULL`)\n\n if (options.dateRange) {\n const dateMapping = registry.getFieldMapping(options.entityType, options.dateRange.field)\n if (dateMapping) {\n whereClauses.push(`${dateMapping.dbColumn} >= ?`)\n params.push(options.dateRange.start)\n whereClauses.push(`${dateMapping.dbColumn} <= ?`)\n params.push(options.dateRange.end)\n }\n }\n\n if (options.filters) {\n for (const filter of options.filters) {\n const filterMapping = registry.getFieldMapping(options.entityType, filter.field)\n if (!filterMapping) continue\n\n switch (filter.operator) {\n case 'eq':\n whereClauses.push(`${filterMapping.dbColumn} = ?`)\n params.push(filter.value)\n break\n case 'neq':\n whereClauses.push(`${filterMapping.dbColumn} != ?`)\n params.push(filter.value)\n break\n case 'gt':\n whereClauses.push(`${filterMapping.dbColumn} > ?`)\n params.push(filter.value)\n break\n case 'gte':\n whereClauses.push(`${filterMapping.dbColumn} >= ?`)\n params.push(filter.value)\n break\n case 'lt':\n whereClauses.push(`${filterMapping.dbColumn} < ?`)\n params.push(filter.value)\n break\n case 'lte':\n whereClauses.push(`${filterMapping.dbColumn} <= ?`)\n params.push(filter.value)\n break\n case 'in':\n case 'not_in': {\n const values = normalizeSetFilterValues(filter.value)\n if (values.length === 0) {\n whereClauses.push(filter.operator === 'in' ? 'FALSE' : 'TRUE')\n break\n }\n const keyword = filter.operator === 'in' ? 'IN' : 'NOT IN'\n const placeholders = values.map(() => '?').join(', ')\n whereClauses.push(`${filterMapping.dbColumn} ${keyword} (${placeholders})`)\n params.push(...values)\n break\n }\n case 'is_null':\n whereClauses.push(`${filterMapping.dbColumn} IS NULL`)\n break\n case 'is_not_null':\n whereClauses.push(`${filterMapping.dbColumn} IS NOT NULL`)\n break\n }\n }\n }\n\n return { clause: whereClauses.length > 0 ? `WHERE ${whereClauses.join(' AND ')}` : '', params }\n}\n\nfunction resolveTableName(config: EntityTypeConfig): string {\n return config.schema ? `\"${config.schema}\".\"${config.tableName}\"` : `\"${config.tableName}\"`\n}\n\nexport type BuildDistinctCurrencyQueryOptions = Pick<\n BuildAggregationQueryOptions,\n 'entityType' | 'dateRange' | 'filters' | 'scope' | 'registry'\n>\n\n/**\n * Builds the query that reads the distinct per-row currencies of the rows an aggregation\n * would sum, over exactly the same tenant/organization scope, date range and filters.\n * Returns `null` when the entity declares no `currencyField`, which means its amounts are\n * not per-row denominated and there is nothing to verify (#4676).\n */\nexport function buildDistinctCurrencyQuery(\n options: BuildDistinctCurrencyQueryOptions,\n): AggregationQuery | null {\n const { registry } = options\n const config = registry.getEntityTypeConfig(options.entityType)\n if (!config?.currencyField) return null\n\n const currencyMapping = registry.getFieldMapping(options.entityType, config.currencyField)\n if (!currencyMapping || !isSafeIdentifier(currencyMapping.dbColumn)) return null\n\n const where = buildWhereClause(options)\n const normalizedCurrencyExpression = `UPPER(NULLIF(BTRIM(${currencyMapping.dbColumn}), ''))`\n const sql = [\n `SELECT DISTINCT ${normalizedCurrencyExpression} AS code`,\n `FROM ${resolveTableName(config)}`,\n where.clause,\n 'LIMIT 2',\n ]\n .filter(Boolean)\n .join(' ')\n\n return { sql, params: where.params }\n}\n\nexport function buildAggregationQuery(options: BuildAggregationQueryOptions): AggregationQuery | null {\n const { registry } = options\n const config = registry.getEntityTypeConfig(options.entityType)\n if (!config) return null\n\n const metricMapping = registry.getFieldMapping(options.entityType, options.metric.field)\n if (!metricMapping) return null\n\n const tableName = resolveTableName(config)\n const aggregateExpr = buildAggregateExpression(options.metric.aggregate, metricMapping.dbColumn)\n\n let selectClause = `SELECT ${aggregateExpr} AS value`\n let groupByClause = ''\n let orderByClause = ''\n let limitClause = ''\n\n if (options.groupBy) {\n const resolved = resolveGroupExpression(registry, options.entityType, options.groupBy)\n\n if (resolved) {\n selectClause = `SELECT ${resolved.expression} AS group_key, ${aggregateExpr} AS value`\n groupByClause = `GROUP BY ${resolved.expression}`\n // NULLS LAST is stated explicitly (PostgreSQL defaults DESC to NULLS FIRST) so a group limit\n // keeps the highest-value buckets instead of the empty ones, and so the application-side\n // encrypted path can mirror the same ordering (#4622).\n orderByClause = `ORDER BY value DESC NULLS LAST`\n\n if (options.groupBy.limit && options.groupBy.limit > 0) {\n limitClause = `LIMIT ${Math.min(options.groupBy.limit, 100)}`\n }\n }\n }\n\n const where = buildWhereClause(options)\n\n const sql = [selectClause, `FROM ${tableName}`, where.clause, groupByClause, orderByClause, limitClause]\n .filter(Boolean)\n .join(' ')\n\n return { sql, params: where.params }\n}\n\nexport type BuildGroupSourceRowsQueryOptions = Omit<BuildAggregationQueryOptions, 'groupBy'> & {\n /** Column holding the (encrypted) group source, resolved via `resolveGroupExpression`. */\n groupColumn: string\n /** Hard cap on scanned rows; callers fetch `rowLimit + 1` to detect overflow. */\n rowLimit: number\n}\n\n/**\n * Builds a per-row query for group sources that cannot be grouped in SQL because the column is\n * encrypted at rest. Returns the raw group source alongside the metric value so the caller can\n * decrypt, then aggregate in application code (#4622).\n */\nexport function buildGroupSourceRowsQuery(options: BuildGroupSourceRowsQueryOptions): AggregationQuery | null {\n const { registry } = options\n const config = registry.getEntityTypeConfig(options.entityType)\n if (!config) return null\n\n const metricMapping = registry.getFieldMapping(options.entityType, options.metric.field)\n if (!metricMapping) return null\n\n if (!isSafeIdentifier(options.groupColumn)) {\n throw new Error(`Invalid group column: ${options.groupColumn}`)\n }\n\n const where = buildWhereClause(options)\n const sql = [\n `SELECT ${options.groupColumn} AS group_source, ${metricMapping.dbColumn} AS metric_value`,\n `FROM ${resolveTableName(config)}`,\n where.clause,\n `LIMIT ${Math.max(1, Math.floor(options.rowLimit)) + 1}`,\n ]\n .filter(Boolean)\n .join(' ')\n\n return { sql, params: where.params }\n}\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.7-develop.6834.1.e76f4b8cbc",
3
+ "version": "0.6.7-develop.6842.1.8cd8a8883c",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -254,18 +254,18 @@
254
254
  "zod": "^4.4.3"
255
255
  },
256
256
  "peerDependencies": {
257
- "@open-mercato/ai-assistant": "0.6.7-develop.6834.1.e76f4b8cbc",
258
- "@open-mercato/shared": "0.6.7-develop.6834.1.e76f4b8cbc",
259
- "@open-mercato/ui": "0.6.7-develop.6834.1.e76f4b8cbc",
257
+ "@open-mercato/ai-assistant": "0.6.7-develop.6842.1.8cd8a8883c",
258
+ "@open-mercato/shared": "0.6.7-develop.6842.1.8cd8a8883c",
259
+ "@open-mercato/ui": "0.6.7-develop.6842.1.8cd8a8883c",
260
260
  "react": "^19.0.0",
261
261
  "react-dom": "^19.0.0"
262
262
  },
263
263
  "devDependencies": {
264
- "@open-mercato/ai-assistant": "0.6.7-develop.6834.1.e76f4b8cbc",
265
- "@open-mercato/shared": "0.6.7-develop.6834.1.e76f4b8cbc",
266
- "@open-mercato/ui": "0.6.7-develop.6834.1.e76f4b8cbc",
264
+ "@open-mercato/ai-assistant": "0.6.7-develop.6842.1.8cd8a8883c",
265
+ "@open-mercato/shared": "0.6.7-develop.6842.1.8cd8a8883c",
266
+ "@open-mercato/ui": "0.6.7-develop.6842.1.8cd8a8883c",
267
267
  "@testing-library/dom": "^10.4.1",
268
- "@testing-library/jest-dom": "^6.9.1",
268
+ "@testing-library/jest-dom": "^7.0.0",
269
269
  "@testing-library/react": "^16.3.1",
270
270
  "@types/chance": "^1.1.8",
271
271
  "@types/jest": "^30.0.0",
@@ -0,0 +1,42 @@
1
+ {
2
+ "api_keys.copy.prefix": "접두사: {prefix}",
3
+ "api_keys.copy.subtitle": "이 비밀 값을 안전하게 보관하세요. 이 화면을 닫으면 다시 볼 수 없습니다.",
4
+ "api_keys.copy.title": "API 키 복사",
5
+ "api_keys.copy.warning": "이 키를 안전하게 보관하세요 — 나중에 다시 확인할 수 없습니다.",
6
+ "api_keys.errors.forbidden": "접근이 거부되었습니다",
7
+ "api_keys.errors.notFound": "찾을 수 없습니다",
8
+ "api_keys.errors.organizationOutOfScope": "조직이 범위를 벗어났습니다",
9
+ "api_keys.errors.roleNotFound": "역할 {identifier}을(를) 찾을 수 없습니다",
10
+ "api_keys.errors.roleWrongTenant": "역할 {role}은(는) 다른 테넌트에 속합니다",
11
+ "api_keys.errors.tenantRequired": "테넌트 컨텍스트가 필요합니다",
12
+ "api_keys.form.description": "설명",
13
+ "api_keys.form.descriptionHint": "팀원이 이 키가 사용되는 위치를 이해하는 데 도움이 되는 선택적 메모입니다.",
14
+ "api_keys.form.details": "상세 정보",
15
+ "api_keys.form.error.createFailed": "API 키 생성에 실패했습니다",
16
+ "api_keys.form.error.secretMissing": "API 키가 생성되었지만 비밀 값이 반환되지 않았습니다",
17
+ "api_keys.form.expiresAt": "만료일",
18
+ "api_keys.form.expiresHint": "만료일이 없으면 비워두세요.",
19
+ "api_keys.form.name": "이름",
20
+ "api_keys.form.organization": "조직",
21
+ "api_keys.form.organizationPlaceholder": "선택한 범위에서 상속",
22
+ "api_keys.form.roles": "역할",
23
+ "api_keys.form.rolesHint": "이 키로 인증된 요청은 이 역할을 대신 수행합니다.",
24
+ "api_keys.form.success": "API 키가 생성되었습니다. 지금 바로 비밀 값을 복사하세요 — 다시 표시되지 않습니다.",
25
+ "api_keys.form.title": "API 키 생성",
26
+ "api_keys.list.actions.create": "생성",
27
+ "api_keys.list.columns.expires": "만료일",
28
+ "api_keys.list.columns.key": "키",
29
+ "api_keys.list.columns.lastUsed": "마지막 사용",
30
+ "api_keys.list.columns.name": "이름",
31
+ "api_keys.list.columns.organization": "조직",
32
+ "api_keys.list.columns.roles": "역할",
33
+ "api_keys.list.confirmDelete": "API 키 \"{name}\"을(를) 삭제하시겠습니까? 즉시 비밀 값이 무효화됩니다.",
34
+ "api_keys.list.error.deleteFailed": "API 키 삭제에 실패했습니다",
35
+ "api_keys.list.error.loadFailed": "API 키를 불러오지 못했습니다",
36
+ "api_keys.list.noDate": "—",
37
+ "api_keys.list.noRoles": "없음",
38
+ "api_keys.list.success.deleted": "API 키가 삭제되었습니다",
39
+ "api_keys.list.title": "API 키",
40
+ "api_keys.nav.apiKeys": "API 키",
41
+ "api_keys.nav.apiKeys.create": "API 키 생성"
42
+ }
@@ -0,0 +1,172 @@
1
+ {
2
+ "attachments.errors.activeContentBlocked": "활성 콘텐츠 업로드는 허용되지 않습니다.",
3
+ "attachments.errors.dangerousExecutable": "실행 파일 형식은 첨부 파일로 허용되지 않습니다.",
4
+ "attachments.errors.maxUploadSize": "첨부 파일이 최대 업로드 크기를 초과합니다.",
5
+ "attachments.errors.publicPartitionBlocked": "이 업로드에는 공개 저장소 파티션을 명시적으로 선택할 수 없습니다.",
6
+ "attachments.errors.quotaExceeded": "이 테넌트의 첨부 파일 저장소 할당량을 초과했습니다.",
7
+ "attachments.errors.selectOrganization": "첨부파일을 업로드하기 전에 특정 조직을 선택하세요.",
8
+ "attachments.library.actions.copied": "링크가 복사되었습니다.",
9
+ "attachments.library.actions.copyError": "링크를 복사할 수 없습니다.",
10
+ "attachments.library.actions.copyUrl": "URL 복사",
11
+ "attachments.library.actions.delete": "삭제",
12
+ "attachments.library.actions.edit": "메타데이터 편집",
13
+ "attachments.library.actions.open": "열기",
14
+ "attachments.library.actions.refresh": "새로고침",
15
+ "attachments.library.actions.upload": "업로드",
16
+ "attachments.library.confirm.delete": "첨부 파일 \"{{name}}\"을(를) 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.",
17
+ "attachments.library.delete": "첨부 파일 삭제",
18
+ "attachments.library.description": "이 워크스페이스에 저장된 모든 파일을 찾아보고, 태그를 지정하고, 관리하세요.",
19
+ "attachments.library.errors.delete": "첨부 파일 삭제에 실패했습니다.",
20
+ "attachments.library.errors.load": "첨부 파일을 불러오지 못했습니다.",
21
+ "attachments.library.filters.partition": "파티션",
22
+ "attachments.library.filters.tags": "태그",
23
+ "attachments.library.filters.tagsPlaceholder": "태그로 필터링",
24
+ "attachments.library.loadMore": "더 보기",
25
+ "attachments.library.loading": "첨부 파일을 불러오는 중…",
26
+ "attachments.library.messages.deleted": "첨부 파일이 제거되었습니다.",
27
+ "attachments.library.metadata.assignments.add": "할당 추가",
28
+ "attachments.library.metadata.assignments.description": "이 첨부 파일이 속한 레코드를 선택적 링크와 함께 추가하세요.",
29
+ "attachments.library.metadata.assignments.href": "링크",
30
+ "attachments.library.metadata.assignments.id": "레코드 ID",
31
+ "attachments.library.metadata.assignments.label": "라벨",
32
+ "attachments.library.metadata.assignments.remove": "제거",
33
+ "attachments.library.metadata.assignments.title": "할당",
34
+ "attachments.library.metadata.assignments.type": "유형",
35
+ "attachments.library.metadata.cancel": "취소",
36
+ "attachments.library.metadata.details": "상세 정보",
37
+ "attachments.library.metadata.download": "다운로드",
38
+ "attachments.library.metadata.error": "메타데이터 업데이트에 실패했습니다.",
39
+ "attachments.library.metadata.extractedTitle": "추출된 텍스트",
40
+ "attachments.library.metadata.loadError": "첨부 파일 메타데이터를 불러오지 못했습니다.",
41
+ "attachments.library.metadata.loading": "메타데이터를 불러오는 중…",
42
+ "attachments.library.metadata.noContent": "추출된 텍스트 없음",
43
+ "attachments.library.metadata.noSelection": "편집할 첨부 파일을 선택하세요.",
44
+ "attachments.library.metadata.preview": "미리보기",
45
+ "attachments.library.metadata.previewUnavailable": "이 파일 형식은 미리보기를 사용할 수 없습니다.",
46
+ "attachments.library.metadata.resizeTool.copied": "이미지 URL이 복사되었습니다.",
47
+ "attachments.library.metadata.resizeTool.copy": "URL 복사",
48
+ "attachments.library.metadata.resizeTool.copyError": "URL을 복사할 수 없습니다.",
49
+ "attachments.library.metadata.resizeTool.height": "높이 (px)",
50
+ "attachments.library.metadata.resizeTool.missing": "URL을 생성하려면 너비 또는 높이를 입력하세요.",
51
+ "attachments.library.metadata.resizeTool.title": "크기 조정된 URL 생성",
52
+ "attachments.library.metadata.resizeTool.width": "너비 (px)",
53
+ "attachments.library.metadata.save": "저장",
54
+ "attachments.library.metadata.saving": "저장 중…",
55
+ "attachments.library.metadata.showLess": "간략히 보기",
56
+ "attachments.library.metadata.showMore": "더 보기",
57
+ "attachments.library.metadata.source": "소스",
58
+ "attachments.library.metadata.success": "첨부 파일이 업데이트되었습니다.",
59
+ "attachments.library.metadata.tagsPlaceholder": "태그 추가",
60
+ "attachments.library.metadata.title": "첨부 파일 메타데이터 편집",
61
+ "attachments.library.open": "첨부 파일 상세 정보 열기",
62
+ "attachments.library.openMetadata": "{name}의 첨부 파일 상세 정보 열기",
63
+ "attachments.library.table.assignments": "할당",
64
+ "attachments.library.table.created": "생성됨",
65
+ "attachments.library.table.download": "다운로드",
66
+ "attachments.library.table.empty": "첨부 파일을 찾을 수 없습니다.",
67
+ "attachments.library.table.file": "파일",
68
+ "attachments.library.table.partition": "파티션",
69
+ "attachments.library.table.search": "파일 검색…",
70
+ "attachments.library.table.tags": "태그",
71
+ "attachments.library.title": "첨부 파일",
72
+ "attachments.library.upload.assignments.add": "할당 추가",
73
+ "attachments.library.upload.assignments.description": "지금 이 파일을 기존 레코드에 연결하거나 나중에 추가할 수 있습니다.",
74
+ "attachments.library.upload.assignments.href": "링크",
75
+ "attachments.library.upload.assignments.id": "레코드 ID",
76
+ "attachments.library.upload.assignments.label": "라벨",
77
+ "attachments.library.upload.assignments.remove": "제거",
78
+ "attachments.library.upload.assignments.title": "할당",
79
+ "attachments.library.upload.assignments.type": "유형",
80
+ "attachments.library.upload.cancel": "취소",
81
+ "attachments.library.upload.choose": "파일 선택",
82
+ "attachments.library.upload.dropHint": "여기에 파일을 끌어다 놓거나 클릭하여 업로드하세요.",
83
+ "attachments.library.upload.failed": "업로드에 실패했습니다.",
84
+ "attachments.library.upload.file": "파일",
85
+ "attachments.library.upload.fileRequired": "업로드할 파일을 선택하세요.",
86
+ "attachments.library.upload.noFiles": "아직 선택된 파일이 없습니다.",
87
+ "attachments.library.upload.partition": "파티션",
88
+ "attachments.library.upload.partitionDefault": "기본값 (비공개)",
89
+ "attachments.library.upload.progressLabel": "파일 업로드 중",
90
+ "attachments.library.upload.saveFirst": "파일을 업로드하기 전에 레코드를 저장하세요.",
91
+ "attachments.library.upload.submit": "업로드",
92
+ "attachments.library.upload.submitting": "업로드 중…",
93
+ "attachments.library.upload.success": "첨부 파일이 업로드되었습니다.",
94
+ "attachments.library.upload.tagsPlaceholder": "태그 추가",
95
+ "attachments.library.upload.title": "첨부 파일 업로드",
96
+ "attachments.nav.group": "메디아",
97
+ "attachments.partitions.actions.add": "파티션 추가",
98
+ "attachments.partitions.actions.cancel": "취소",
99
+ "attachments.partitions.actions.create": "파티션 생성",
100
+ "attachments.partitions.actions.delete": "삭제",
101
+ "attachments.partitions.actions.edit": "편집",
102
+ "attachments.partitions.actions.refresh": "새로고침",
103
+ "attachments.partitions.actions.save": "변경 사항 저장",
104
+ "attachments.partitions.confirm.delete": "파티션 \"{{code}}\"을(를) 삭제하시겠습니까?",
105
+ "attachments.partitions.description": "업로드를 위한 저장소 파티션과 공개 범위를 정의합니다.",
106
+ "attachments.partitions.dialog.createDescription": "첨부 파일용 저장소 파티션을 정의합니다.",
107
+ "attachments.partitions.dialog.createTitle": "파티션 생성",
108
+ "attachments.partitions.dialog.editDescription": "파티션 메타데이터와 공개 범위를 업데이트합니다.",
109
+ "attachments.partitions.dialog.editTitle": "파티션 편집",
110
+ "attachments.partitions.errors.delete": "파티션 삭제에 실패했습니다.",
111
+ "attachments.partitions.errors.load": "파티션을 불러오지 못했습니다.",
112
+ "attachments.partitions.errors.required": "코드와 제목은 필수입니다.",
113
+ "attachments.partitions.errors.s3BucketRequired": "별도의 자격 증명 소스를 사용하는 경우 버킷이 필요합니다.",
114
+ "attachments.partitions.errors.s3Disabled": "이 파티션은 S3를 사용하지만 S3는 비활성화되어 있습니다. OM_ENABLE_STORAGE_S3=true로 설정하고 앱을 다시 시작하세요.",
115
+ "attachments.partitions.errors.s3EnvPrefixRequired": "별도의 자격 증명 소스를 사용하는 경우 자격 증명 env 접두사가 필요합니다.",
116
+ "attachments.partitions.errors.save": "파티션 저장에 실패했습니다.",
117
+ "attachments.partitions.form.codeLabel": "코드",
118
+ "attachments.partitions.form.codePlaceholder": "예: marketingAssets",
119
+ "attachments.partitions.form.descriptionLabel": "설명",
120
+ "attachments.partitions.form.descriptionPlaceholder": "이 파티션이 어떻게 사용되는지 설명하세요.",
121
+ "attachments.partitions.form.envKeyHelp": "저장소 경로를 재정의하려면 이 환경 변수를 설정하세요:",
122
+ "attachments.partitions.form.ocrLabel": "OCR/텍스트 추출 필요",
123
+ "attachments.partitions.form.ocrModelHelp": "이미지 및 PDF OCR에 사용할 LLM 모델을 선택하세요. OCR_MODEL 환경 변수 또는 gpt-4o로 대체됩니다. LLM OCR을 사용할 수 없으면 PDF는 순수 JS 텍스트 추출(pdfjs-dist)로, DOCX 파일은 순수 JS 추출(mammoth)로 대체됩니다.",
124
+ "attachments.partitions.form.ocrModelLabel": "OCR 모델",
125
+ "attachments.partitions.form.ocrModelOptions.default": "기본값 (환경에서)",
126
+ "attachments.partitions.form.ocrModelOptions.gpt-4o": "GPT-4o (권장)",
127
+ "attachments.partitions.form.ocrModelOptions.gpt-4o-mini": "GPT-4o Mini (더 빠르고 저렴함)",
128
+ "attachments.partitions.form.publicLabel": "공개 접근 가능",
129
+ "attachments.partitions.form.s3BucketLabel": "버킷",
130
+ "attachments.partitions.form.s3BucketOverrideLabel": "버킷 재정의 (선택 사항)",
131
+ "attachments.partitions.form.s3BucketOverridePlaceholder": "통합 버킷을 사용하려면 비워 두세요.",
132
+ "attachments.partitions.form.s3ConfigTitle": "S3 구성",
133
+ "attachments.partitions.form.s3CredsPrefixHelp": "환경 변수에서 {PREFIX}_ACCESS_KEY_ID 및 {PREFIX}_SECRET_ACCESS_KEY를 읽습니다.",
134
+ "attachments.partitions.form.s3CredsPrefixLabel": "자격 증명 환경 변수 접두사",
135
+ "attachments.partitions.form.s3CredsSourceEnv": "별도의 환경 기반 자격 증명 사용",
136
+ "attachments.partitions.form.s3CredsSourceEnvHelp": "이 파티션은 환경 변수에서 읽은 자체 자격 증명을 사용합니다. 통합 자격 증명은 무시됩니다. 버킷 및 자격 증명 접두사가 필요합니다.",
137
+ "attachments.partitions.form.s3CredsSourceIntegration": "Integration Marketplace 자격 증명 사용",
138
+ "attachments.partitions.form.s3CredsSourceIntegrationHelp": "이 파티션은 S3 통합에 구성된 자격 증명, 버킷, 지역 및 엔드포인트를 재사용합니다. 필요한 경우에만 아래의 개별 필드를 재정의하세요.",
139
+ "attachments.partitions.form.s3CredsSourceLabel": "자격 증명 소스",
140
+ "attachments.partitions.form.s3EndpointHelp": "AWS S3의 경우 비워두세요. MinIO, DigitalOcean Spaces 등에는 필수입니다.",
141
+ "attachments.partitions.form.s3EndpointLabel": "커스텀 엔드포인트",
142
+ "attachments.partitions.form.s3EndpointOverrideLabel": "커스텀 엔드포인트 재정의 (선택 사항)",
143
+ "attachments.partitions.form.s3EndpointOverridePlaceholder": "통합 엔드포인트를 사용하려면 비워 두세요.",
144
+ "attachments.partitions.form.s3ForcePathStyleLabel": "경로 스타일 강제 (MinIO에 필요)",
145
+ "attachments.partitions.form.s3RegionLabel": "리전",
146
+ "attachments.partitions.form.s3RegionOverrideLabel": "리전 재정의 (선택 사항)",
147
+ "attachments.partitions.form.s3RegionOverridePlaceholder": "통합 지역을 사용하려면 비워두세요.",
148
+ "attachments.partitions.form.storageDriverLabel": "저장소 드라이버",
149
+ "attachments.partitions.form.storageDriverS3DisabledHelp": "S3가 비활성화되었습니다. OM_ENABLE_STORAGE_S3=true로 설정하고 다시 시작하여 S3 파티션을 활성화합니다.",
150
+ "attachments.partitions.form.titleLabel": "제목",
151
+ "attachments.partitions.form.titlePlaceholder": "예: 마케팅 자산",
152
+ "attachments.partitions.locked.description": "데모 또는 온보딩 모드가 활성화된 동안 첨부 파일 파티션은 환경 기본값으로 관리됩니다.",
153
+ "attachments.partitions.locked.hint": "앱 내 구성을 다시 활성화하려면 아래 환경 변수를 업데이트하세요.",
154
+ "attachments.partitions.locked.title": "파티션 설정이 잠겨 있습니다",
155
+ "attachments.partitions.messages.created": "파티션이 생성되었습니다.",
156
+ "attachments.partitions.messages.deleted": "파티션이 제거되었습니다.",
157
+ "attachments.partitions.messages.updated": "파티션이 업데이트되었습니다.",
158
+ "attachments.partitions.nav.title": "첨부 파일",
159
+ "attachments.partitions.table.code": "코드",
160
+ "attachments.partitions.table.description": "설명",
161
+ "attachments.partitions.table.empty": "구성된 파티션이 없습니다.",
162
+ "attachments.partitions.table.envKey": "환경 변수",
163
+ "attachments.partitions.table.ocr": "OCR",
164
+ "attachments.partitions.table.private": "비공개",
165
+ "attachments.partitions.table.public": "공개",
166
+ "attachments.partitions.table.search": "파티션 검색…",
167
+ "attachments.partitions.table.storageDriver": "저장소",
168
+ "attachments.partitions.table.title": "제목",
169
+ "attachments.partitions.table.visibility": "공개 범위",
170
+ "attachments.partitions.title": "첨부 파일 파티션",
171
+ "attachments.storage.nav.title": "저장소"
172
+ }