@open-mercato/core 0.6.6-develop.6094.1.28b081ea16 → 0.6.6-develop.6126.1.0b9392fdae
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/generated/entities/catalog_product/index.js +52 -0
- package/dist/generated/entities/catalog_product/index.js.map +2 -2
- package/dist/generated/entities/catalog_product_variant/index.js +4 -0
- package/dist/generated/entities/catalog_product_variant/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +28 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/catalog/api/products/route.js +103 -33
- package/dist/modules/catalog/api/products/route.js.map +2 -2
- package/dist/modules/catalog/api/variants/route.js +4 -0
- package/dist/modules/catalog/api/variants/route.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js +46 -2
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js +4 -0
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/create/page.js +2 -0
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/create/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/create/page.js +42 -2
- package/dist/modules/catalog/backend/catalog/products/create/page.js.map +2 -2
- package/dist/modules/catalog/commands/products.js +251 -14
- package/dist/modules/catalog/commands/products.js.map +2 -2
- package/dist/modules/catalog/commands/variants.js +72 -1
- package/dist/modules/catalog/commands/variants.js.map +2 -2
- package/dist/modules/catalog/components/products/ProductComplianceSection.js +500 -0
- package/dist/modules/catalog/components/products/ProductComplianceSection.js.map +7 -0
- package/dist/modules/catalog/components/products/VariantBuilder.js +39 -1
- package/dist/modules/catalog/components/products/VariantBuilder.js.map +2 -2
- package/dist/modules/catalog/components/products/productForm.js +164 -2
- package/dist/modules/catalog/components/products/productForm.js.map +2 -2
- package/dist/modules/catalog/components/products/variantForm.js +2 -0
- package/dist/modules/catalog/components/products/variantForm.js.map +2 -2
- package/dist/modules/catalog/data/entities.js +93 -0
- package/dist/modules/catalog/data/entities.js.map +2 -2
- package/dist/modules/catalog/data/types.js +28 -0
- package/dist/modules/catalog/data/types.js.map +2 -2
- package/dist/modules/catalog/data/validators.js +100 -5
- package/dist/modules/catalog/data/validators.js.map +2 -2
- package/dist/modules/catalog/lib/gtin.js +61 -0
- package/dist/modules/catalog/lib/gtin.js.map +7 -0
- package/dist/modules/catalog/migrations/Migration20260611090000.js +17 -0
- package/dist/modules/catalog/migrations/Migration20260611090000.js.map +7 -0
- package/dist/modules/catalog/translations.js +1 -1
- package/dist/modules/catalog/translations.js.map +2 -2
- package/dist/modules/configs/api/cache/route.js +60 -10
- package/dist/modules/configs/api/cache/route.js.map +3 -3
- package/dist/modules/configs/components/CachePanel.js +52 -29
- package/dist/modules/configs/components/CachePanel.js.map +2 -2
- package/dist/modules/currencies/api/currencies/options/route.js +18 -4
- package/dist/modules/currencies/api/currencies/options/route.js.map +2 -2
- package/dist/modules/currencies/services/rateFetchingService.js +35 -18
- package/dist/modules/currencies/services/rateFetchingService.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/page.js +16 -16
- package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/page.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js +1 -1
- package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js.map +2 -2
- package/dist/modules/customers/backend/calendar/page.js +10 -0
- package/dist/modules/customers/backend/calendar/page.js.map +7 -0
- package/dist/modules/customers/backend/calendar/page.meta.js +25 -0
- package/dist/modules/customers/backend/calendar/page.meta.js.map +7 -0
- package/dist/modules/customers/cli.js +1 -0
- package/dist/modules/customers/cli.js.map +2 -2
- package/dist/modules/customers/components/calendar/AgendaList.js +182 -0
- package/dist/modules/customers/components/calendar/AgendaList.js.map +7 -0
- package/dist/modules/customers/components/calendar/CalendarEventEditor.js +331 -0
- package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +7 -0
- package/dist/modules/customers/components/calendar/CalendarFooter.js +48 -0
- package/dist/modules/customers/components/calendar/CalendarFooter.js.map +7 -0
- package/dist/modules/customers/components/calendar/CalendarHeader.js +36 -0
- package/dist/modules/customers/components/calendar/CalendarHeader.js.map +7 -0
- package/dist/modules/customers/components/calendar/CalendarScreen.js +516 -0
- package/dist/modules/customers/components/calendar/CalendarScreen.js.map +7 -0
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +163 -0
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +7 -0
- package/dist/modules/customers/components/calendar/CalendarTabs.js +60 -0
- package/dist/modules/customers/components/calendar/CalendarTabs.js.map +7 -0
- package/dist/modules/customers/components/calendar/CalendarToolbar.js +262 -0
- package/dist/modules/customers/components/calendar/CalendarToolbar.js.map +7 -0
- package/dist/modules/customers/components/calendar/EventBlock.js +149 -0
- package/dist/modules/customers/components/calendar/EventBlock.js.map +7 -0
- package/dist/modules/customers/components/calendar/EventPeekPopover.js +72 -0
- package/dist/modules/customers/components/calendar/EventPeekPopover.js.map +7 -0
- package/dist/modules/customers/components/calendar/MonthGrid.js +214 -0
- package/dist/modules/customers/components/calendar/MonthGrid.js.map +7 -0
- package/dist/modules/customers/components/calendar/ShortcutsDialog.js +44 -0
- package/dist/modules/customers/components/calendar/ShortcutsDialog.js.map +7 -0
- package/dist/modules/customers/components/calendar/TimeGrid.js +410 -0
- package/dist/modules/customers/components/calendar/TimeGrid.js.map +7 -0
- package/dist/modules/customers/components/calendar/UpcomingCards.js +175 -0
- package/dist/modules/customers/components/calendar/UpcomingCards.js.map +7 -0
- package/dist/modules/customers/components/calendar/editor/CategoryField.js +75 -0
- package/dist/modules/customers/components/calendar/editor/CategoryField.js.map +7 -0
- package/dist/modules/customers/components/calendar/editor/LocationField.js +30 -0
- package/dist/modules/customers/components/calendar/editor/LocationField.js.map +7 -0
- package/dist/modules/customers/components/calendar/editor/PeopleField.js +135 -0
- package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +7 -0
- package/dist/modules/customers/components/calendar/editor/RelatedToField.js +218 -0
- package/dist/modules/customers/components/calendar/editor/RelatedToField.js.map +7 -0
- package/dist/modules/customers/components/calendar/editor/RepeatField.js +119 -0
- package/dist/modules/customers/components/calendar/editor/RepeatField.js.map +7 -0
- package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +83 -0
- package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +7 -0
- package/dist/modules/customers/components/calendar/editor/SegmentGroup.js +44 -0
- package/dist/modules/customers/components/calendar/editor/SegmentGroup.js.map +7 -0
- package/dist/modules/customers/components/calendar/editor/hooks.js +132 -0
- package/dist/modules/customers/components/calendar/editor/hooks.js.map +7 -0
- package/dist/modules/customers/components/calendar/editor/inputs.js +160 -0
- package/dist/modules/customers/components/calendar/editor/inputs.js.map +7 -0
- package/dist/modules/customers/components/calendar/editor/lookups.js +107 -0
- package/dist/modules/customers/components/calendar/editor/lookups.js.map +7 -0
- package/dist/modules/customers/components/calendar/types.js +30 -0
- package/dist/modules/customers/components/calendar/types.js.map +7 -0
- package/dist/modules/customers/components/calendar/useCalendarItems.js +131 -0
- package/dist/modules/customers/components/calendar/useCalendarItems.js.map +7 -0
- package/dist/modules/customers/components/calendar/useCalendarPreferences.js +41 -0
- package/dist/modules/customers/components/calendar/useCalendarPreferences.js.map +7 -0
- package/dist/modules/customers/lib/calendar/categories.js +28 -0
- package/dist/modules/customers/lib/calendar/categories.js.map +7 -0
- package/dist/modules/customers/lib/calendar/conflicts.js +72 -0
- package/dist/modules/customers/lib/calendar/conflicts.js.map +7 -0
- package/dist/modules/customers/lib/calendar/editorPayload.js +271 -0
- package/dist/modules/customers/lib/calendar/editorPayload.js.map +7 -0
- package/dist/modules/customers/lib/calendar/grid.js +48 -0
- package/dist/modules/customers/lib/calendar/grid.js.map +7 -0
- package/dist/modules/customers/lib/calendar/layout.js +39 -0
- package/dist/modules/customers/lib/calendar/layout.js.map +7 -0
- package/dist/modules/customers/lib/calendar/mapItem.js +79 -0
- package/dist/modules/customers/lib/calendar/mapItem.js.map +7 -0
- package/dist/modules/customers/lib/calendar/preferences.js +71 -0
- package/dist/modules/customers/lib/calendar/preferences.js.map +7 -0
- package/dist/modules/customers/lib/calendar/range.js +46 -0
- package/dist/modules/customers/lib/calendar/range.js.map +7 -0
- package/dist/modules/customers/lib/calendar/recurrence.js +113 -0
- package/dist/modules/customers/lib/calendar/recurrence.js.map +7 -0
- package/dist/modules/dashboards/services/widgetDataService.js +5 -5
- package/dist/modules/dashboards/services/widgetDataService.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js +61 -1
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js +33 -1
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/route.js +61 -1
- package/dist/modules/dictionaries/api/[dictionaryId]/route.js.map +2 -2
- package/dist/modules/dictionaries/api/route.js +33 -1
- package/dist/modules/dictionaries/api/route.js.map +2 -2
- package/dist/modules/feature_toggles/lib/queries.js +36 -52
- package/dist/modules/feature_toggles/lib/queries.js.map +2 -2
- package/dist/modules/perspectives/api/[tableId]/[perspectiveId]/route.js +32 -1
- package/dist/modules/perspectives/api/[tableId]/[perspectiveId]/route.js.map +2 -2
- package/dist/modules/perspectives/api/[tableId]/roles/[roleId]/route.js +32 -1
- package/dist/modules/perspectives/api/[tableId]/roles/[roleId]/route.js.map +2 -2
- package/dist/modules/perspectives/api/[tableId]/route.js +73 -2
- package/dist/modules/perspectives/api/[tableId]/route.js.map +2 -2
- package/dist/modules/perspectives/services/perspectiveService.js +5 -3
- package/dist/modules/perspectives/services/perspectiveService.js.map +2 -2
- package/dist/modules/planner/api/access.js +9 -0
- package/dist/modules/planner/api/access.js.map +2 -2
- package/dist/modules/planner/api/availability-date-specific.js +22 -1
- package/dist/modules/planner/api/availability-date-specific.js.map +2 -2
- package/dist/modules/planner/api/availability-weekly.js +22 -1
- package/dist/modules/planner/api/availability-weekly.js.map +2 -2
- package/dist/modules/resources/api/resources/tags/assign/route.js +39 -0
- package/dist/modules/resources/api/resources/tags/assign/route.js.map +2 -2
- package/dist/modules/resources/api/resources/tags/unassign/route.js +39 -0
- package/dist/modules/resources/api/resources/tags/unassign/route.js.map +2 -2
- package/dist/modules/sales/api/settings/document-numbers/route.js +33 -1
- package/dist/modules/sales/api/settings/document-numbers/route.js.map +2 -2
- package/dist/modules/sales/api/settings/order-editing/route.js +35 -3
- package/dist/modules/sales/api/settings/order-editing/route.js.map +2 -2
- package/dist/modules/staff/api/leave-requests/accept/route.js +41 -0
- package/dist/modules/staff/api/leave-requests/accept/route.js.map +2 -2
- package/dist/modules/staff/api/leave-requests/reject/route.js +41 -0
- package/dist/modules/staff/api/leave-requests/reject/route.js.map +2 -2
- package/dist/modules/staff/api/team-members/self/route.js +40 -0
- package/dist/modules/staff/api/team-members/self/route.js.map +2 -2
- package/dist/modules/staff/api/team-members/tags/assign/route.js +41 -0
- package/dist/modules/staff/api/team-members/tags/assign/route.js.map +2 -2
- package/dist/modules/staff/api/team-members/tags/unassign/route.js +41 -0
- package/dist/modules/staff/api/team-members/tags/unassign/route.js.map +2 -2
- package/dist/modules/translations/api/[entityType]/[entityId]/route.js +61 -1
- package/dist/modules/translations/api/[entityType]/[entityId]/route.js.map +2 -2
- package/dist/modules/translations/api/context.js +7 -0
- package/dist/modules/translations/api/context.js.map +2 -2
- package/dist/modules/translations/api/put/locales.js +33 -1
- package/dist/modules/translations/api/put/locales.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js +6 -7
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
- package/dist/modules/workflows/backend/instances/[id]/page.js.map +1 -1
- package/dist/modules/workflows/components/EdgeEditDialog.js.map +1 -1
- package/dist/modules/workflows/components/EdgeEditDialogCrudForm.js.map +1 -1
- package/dist/modules/workflows/components/NodeEditDialog.js.map +1 -1
- package/dist/modules/workflows/components/NodeEditDialogCrudForm.js.map +1 -1
- package/dist/modules/workflows/components/WorkflowGraph.js.map +1 -1
- package/dist/modules/workflows/components/WorkflowGraphImpl.js +17 -9
- package/dist/modules/workflows/components/WorkflowGraphImpl.js.map +2 -2
- package/dist/modules/workflows/components/mobile/MobileVisualEditor.js.map +1 -1
- package/dist/modules/workflows/frontend/checkout-demo/deriveCurrentStep.js +8 -0
- package/dist/modules/workflows/frontend/checkout-demo/deriveCurrentStep.js.map +7 -0
- package/dist/modules/workflows/frontend/checkout-demo/page.js +2 -20
- package/dist/modules/workflows/frontend/checkout-demo/page.js.map +2 -2
- package/dist/modules/workflows/lib/graph-utils.js +8 -0
- package/dist/modules/workflows/lib/graph-utils.js.map +2 -2
- package/generated/entities/catalog_product/index.ts +26 -0
- package/generated/entities/catalog_product_variant/index.ts +2 -0
- package/generated/entity-fields-registry.ts +28 -0
- package/package.json +7 -7
- package/src/modules/catalog/api/products/route.ts +123 -39
- package/src/modules/catalog/api/variants/route.ts +4 -0
- package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +43 -0
- package/src/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.tsx +14 -0
- package/src/modules/catalog/backend/catalog/products/[productId]/variants/create/page.tsx +2 -0
- package/src/modules/catalog/backend/catalog/products/create/page.tsx +41 -0
- package/src/modules/catalog/commands/products.ts +307 -2
- package/src/modules/catalog/commands/variants.ts +90 -1
- package/src/modules/catalog/components/products/ProductComplianceSection.tsx +497 -0
- package/src/modules/catalog/components/products/VariantBuilder.tsx +43 -0
- package/src/modules/catalog/components/products/productForm.ts +262 -0
- package/src/modules/catalog/components/products/variantForm.ts +4 -0
- package/src/modules/catalog/data/entities.ts +97 -0
- package/src/modules/catalog/data/types.ts +36 -0
- package/src/modules/catalog/data/validators.ts +159 -3
- package/src/modules/catalog/i18n/de.json +64 -0
- package/src/modules/catalog/i18n/en.json +64 -0
- package/src/modules/catalog/i18n/es.json +64 -0
- package/src/modules/catalog/i18n/pl.json +64 -0
- package/src/modules/catalog/lib/gtin.ts +66 -0
- package/src/modules/catalog/migrations/.snapshot-open-mercato.json +457 -0
- package/src/modules/catalog/migrations/Migration20260611090000.ts +17 -0
- package/src/modules/catalog/translations.ts +1 -1
- package/src/modules/configs/api/cache/route.ts +72 -10
- package/src/modules/configs/components/CachePanel.tsx +59 -29
- package/src/modules/currencies/api/currencies/options/route.ts +22 -4
- package/src/modules/currencies/services/rateFetchingService.ts +63 -26
- package/src/modules/customer_accounts/backend/customer_accounts/users/[id]/page.tsx +18 -18
- package/src/modules/customer_accounts/backend/customer_accounts/users/page.tsx +1 -1
- package/src/modules/customer_accounts/i18n/de.json +1 -1
- package/src/modules/customer_accounts/i18n/en.json +1 -1
- package/src/modules/customer_accounts/i18n/es.json +1 -1
- package/src/modules/customer_accounts/i18n/pl.json +1 -1
- package/src/modules/customers/backend/calendar/page.meta.ts +23 -0
- package/src/modules/customers/backend/calendar/page.tsx +12 -0
- package/src/modules/customers/cli.ts +1 -0
- package/src/modules/customers/components/calendar/AgendaList.tsx +219 -0
- package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +386 -0
- package/src/modules/customers/components/calendar/CalendarFooter.tsx +61 -0
- package/src/modules/customers/components/calendar/CalendarHeader.tsx +40 -0
- package/src/modules/customers/components/calendar/CalendarScreen.tsx +590 -0
- package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +207 -0
- package/src/modules/customers/components/calendar/CalendarTabs.tsx +63 -0
- package/src/modules/customers/components/calendar/CalendarToolbar.tsx +295 -0
- package/src/modules/customers/components/calendar/EventBlock.tsx +205 -0
- package/src/modules/customers/components/calendar/EventPeekPopover.tsx +100 -0
- package/src/modules/customers/components/calendar/MonthGrid.tsx +236 -0
- package/src/modules/customers/components/calendar/ShortcutsDialog.tsx +57 -0
- package/src/modules/customers/components/calendar/TimeGrid.tsx +510 -0
- package/src/modules/customers/components/calendar/UpcomingCards.tsx +220 -0
- package/src/modules/customers/components/calendar/editor/CategoryField.tsx +79 -0
- package/src/modules/customers/components/calendar/editor/LocationField.tsx +37 -0
- package/src/modules/customers/components/calendar/editor/PeopleField.tsx +148 -0
- package/src/modules/customers/components/calendar/editor/RelatedToField.tsx +242 -0
- package/src/modules/customers/components/calendar/editor/RepeatField.tsx +132 -0
- package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +110 -0
- package/src/modules/customers/components/calendar/editor/SegmentGroup.tsx +50 -0
- package/src/modules/customers/components/calendar/editor/hooks.ts +162 -0
- package/src/modules/customers/components/calendar/editor/inputs.tsx +158 -0
- package/src/modules/customers/components/calendar/editor/lookups.ts +160 -0
- package/src/modules/customers/components/calendar/types.ts +163 -0
- package/src/modules/customers/components/calendar/useCalendarItems.ts +162 -0
- package/src/modules/customers/components/calendar/useCalendarPreferences.ts +52 -0
- package/src/modules/customers/i18n/de.json +195 -0
- package/src/modules/customers/i18n/en.json +195 -0
- package/src/modules/customers/i18n/es.json +195 -0
- package/src/modules/customers/i18n/pl.json +195 -0
- package/src/modules/customers/lib/calendar/categories.ts +26 -0
- package/src/modules/customers/lib/calendar/conflicts.ts +117 -0
- package/src/modules/customers/lib/calendar/editorPayload.ts +388 -0
- package/src/modules/customers/lib/calendar/grid.ts +50 -0
- package/src/modules/customers/lib/calendar/layout.ts +47 -0
- package/src/modules/customers/lib/calendar/mapItem.ts +94 -0
- package/src/modules/customers/lib/calendar/preferences.ts +95 -0
- package/src/modules/customers/lib/calendar/range.ts +45 -0
- package/src/modules/customers/lib/calendar/recurrence.ts +137 -0
- package/src/modules/dashboards/services/widgetDataService.ts +8 -6
- package/src/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.ts +61 -1
- package/src/modules/dictionaries/api/[dictionaryId]/entries/route.ts +33 -1
- package/src/modules/dictionaries/api/[dictionaryId]/route.ts +65 -1
- package/src/modules/dictionaries/api/route.ts +36 -1
- package/src/modules/feature_toggles/lib/queries.ts +41 -60
- package/src/modules/perspectives/api/[tableId]/[perspectiveId]/route.ts +34 -1
- package/src/modules/perspectives/api/[tableId]/roles/[roleId]/route.ts +34 -1
- package/src/modules/perspectives/api/[tableId]/route.ts +107 -32
- package/src/modules/perspectives/services/perspectiveService.ts +9 -5
- package/src/modules/planner/api/access.ts +9 -0
- package/src/modules/planner/api/availability-date-specific.ts +22 -1
- package/src/modules/planner/api/availability-weekly.ts +22 -1
- package/src/modules/resources/api/resources/tags/assign/route.ts +42 -0
- package/src/modules/resources/api/resources/tags/unassign/route.ts +42 -0
- package/src/modules/sales/api/settings/document-numbers/route.ts +35 -1
- package/src/modules/sales/api/settings/order-editing/route.ts +37 -3
- package/src/modules/staff/api/leave-requests/accept/route.ts +45 -0
- package/src/modules/staff/api/leave-requests/reject/route.ts +45 -0
- package/src/modules/staff/api/team-members/self/route.ts +43 -0
- package/src/modules/staff/api/team-members/tags/assign/route.ts +45 -0
- package/src/modules/staff/api/team-members/tags/unassign/route.ts +45 -0
- package/src/modules/translations/api/[entityType]/[entityId]/route.ts +65 -1
- package/src/modules/translations/api/context.ts +7 -0
- package/src/modules/translations/api/put/locales.ts +35 -1
- package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +11 -9
- package/src/modules/workflows/backend/instances/[id]/page.tsx +1 -1
- package/src/modules/workflows/components/EdgeEditDialog.tsx +1 -1
- package/src/modules/workflows/components/EdgeEditDialogCrudForm.tsx +1 -1
- package/src/modules/workflows/components/NodeEditDialog.tsx +1 -1
- package/src/modules/workflows/components/NodeEditDialogCrudForm.tsx +1 -1
- package/src/modules/workflows/components/WorkflowGraph.tsx +2 -2
- package/src/modules/workflows/components/WorkflowGraphImpl.tsx +27 -13
- package/src/modules/workflows/components/mobile/MobileVisualEditor.tsx +3 -3
- package/src/modules/workflows/frontend/checkout-demo/deriveCurrentStep.ts +12 -0
- package/src/modules/workflows/frontend/checkout-demo/page.tsx +3 -23
- package/src/modules/workflows/lib/graph-utils.ts +21 -1
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import type { CalendarItem } from '../../components/calendar/types'
|
|
2
|
+
import { parseRecurrenceRule } from './recurrence'
|
|
3
|
+
|
|
4
|
+
export type EditorKind = 'meeting' | 'call' | 'email' | 'note' | 'event' | 'task'
|
|
5
|
+
|
|
6
|
+
export const EDITOR_KINDS: EditorKind[] = ['meeting', 'call', 'email', 'note', 'event', 'task']
|
|
7
|
+
|
|
8
|
+
export type EditorDateLabel = 'starts' | 'when' | 'sent' | 'logged' | 'due'
|
|
9
|
+
|
|
10
|
+
export type EditorPeopleMode = 'attendees' | 'participants' | 'to' | 'assignee'
|
|
11
|
+
|
|
12
|
+
export type EditorKindConfig = {
|
|
13
|
+
dateLabel: EditorDateLabel
|
|
14
|
+
hasEnd: boolean
|
|
15
|
+
hasAllDay: boolean
|
|
16
|
+
hasRepeat: boolean
|
|
17
|
+
location: 'location' | 'phoneLink' | null
|
|
18
|
+
people: EditorPeopleMode | null
|
|
19
|
+
hasPriority: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const KIND_CONFIG: Record<EditorKind, EditorKindConfig> = {
|
|
23
|
+
meeting: { dateLabel: 'starts', hasEnd: true, hasAllDay: true, hasRepeat: true, location: 'location', people: 'attendees', hasPriority: false },
|
|
24
|
+
call: { dateLabel: 'when', hasEnd: false, hasAllDay: true, hasRepeat: true, location: 'phoneLink', people: 'participants', hasPriority: false },
|
|
25
|
+
email: { dateLabel: 'sent', hasEnd: false, hasAllDay: true, hasRepeat: true, location: null, people: 'to', hasPriority: false },
|
|
26
|
+
note: { dateLabel: 'logged', hasEnd: false, hasAllDay: false, hasRepeat: false, location: null, people: null, hasPriority: false },
|
|
27
|
+
event: { dateLabel: 'starts', hasEnd: true, hasAllDay: true, hasRepeat: true, location: 'location', people: 'attendees', hasPriority: false },
|
|
28
|
+
task: { dateLabel: 'due', hasEnd: false, hasAllDay: true, hasRepeat: true, location: null, people: 'assignee', hasPriority: true },
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const KIND_BY_INTERACTION_TYPE: Record<string, EditorKind> = {
|
|
32
|
+
meeting: 'meeting',
|
|
33
|
+
call: 'call',
|
|
34
|
+
'video-call': 'call',
|
|
35
|
+
email: 'email',
|
|
36
|
+
note: 'note',
|
|
37
|
+
event: 'event',
|
|
38
|
+
webinar: 'event',
|
|
39
|
+
task: 'task',
|
|
40
|
+
todo: 'task',
|
|
41
|
+
deadline: 'task',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function editorKindOfInteractionType(interactionType: string): EditorKind {
|
|
45
|
+
return KIND_BY_INTERACTION_TYPE[interactionType] ?? 'meeting'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type EditorCategoryOption = { value: string; label: string }
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Builds the editor's Category quick-pick options from the tenant dictionary and
|
|
52
|
+
* the user's calendar preferences:
|
|
53
|
+
* - `surfacedTypes` (settings "Activity Types") filters which dictionary types are
|
|
54
|
+
* offered — a non-empty list keeps only matching labels; empty means "show all".
|
|
55
|
+
* - `eventCategories` (settings "Event Categories") add custom quick-pick labels.
|
|
56
|
+
* Dictionary entries keep their canonical key as the value (so category→tab/tint
|
|
57
|
+
* mapping is preserved); custom labels use the label as the value.
|
|
58
|
+
*/
|
|
59
|
+
export function buildEditorCategoryOptions(params: {
|
|
60
|
+
typeLabels: Record<string, string>
|
|
61
|
+
surfacedTypes: string[]
|
|
62
|
+
eventCategories: string[]
|
|
63
|
+
selectedValue: string
|
|
64
|
+
selectedFallbackLabel: string
|
|
65
|
+
}): EditorCategoryOption[] {
|
|
66
|
+
const { typeLabels, surfacedTypes, eventCategories, selectedValue, selectedFallbackLabel } = params
|
|
67
|
+
const dictionaryOptions: EditorCategoryOption[] = Object.entries(typeLabels).map(([value, label]) => ({ value, label }))
|
|
68
|
+
const surfacedSet = new Set(surfacedTypes)
|
|
69
|
+
const filtered =
|
|
70
|
+
surfacedTypes.length > 0
|
|
71
|
+
? dictionaryOptions.filter((option) => surfacedSet.has(option.label))
|
|
72
|
+
: dictionaryOptions
|
|
73
|
+
const options = [...filtered]
|
|
74
|
+
const knownLabels = new Set(options.map((option) => option.label))
|
|
75
|
+
const knownValues = new Set(options.map((option) => option.value))
|
|
76
|
+
const customLabels: string[] = []
|
|
77
|
+
for (const label of [...eventCategories, ...surfacedTypes]) {
|
|
78
|
+
const trimmed = label.trim()
|
|
79
|
+
if (!trimmed || knownLabels.has(trimmed) || customLabels.includes(trimmed)) continue
|
|
80
|
+
customLabels.push(trimmed)
|
|
81
|
+
}
|
|
82
|
+
for (const label of customLabels) {
|
|
83
|
+
options.push({ value: label, label })
|
|
84
|
+
knownValues.add(label)
|
|
85
|
+
}
|
|
86
|
+
if (!knownValues.has(selectedValue)) {
|
|
87
|
+
options.unshift({ value: selectedValue, label: typeLabels[selectedValue] ?? selectedFallbackLabel })
|
|
88
|
+
}
|
|
89
|
+
return options
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type EditorRepeatFreq = 'none' | 'daily' | 'weekly'
|
|
93
|
+
|
|
94
|
+
export type EditorRepeatEndType = 'never' | 'date' | 'count'
|
|
95
|
+
|
|
96
|
+
export type EditorPriority = 'low' | 'medium' | 'high'
|
|
97
|
+
|
|
98
|
+
export const PRIORITY_NUMBER: Record<EditorPriority, number> = { low: 10, medium: 50, high: 90 }
|
|
99
|
+
|
|
100
|
+
export function priorityFromNumber(value: number | null | undefined): EditorPriority {
|
|
101
|
+
if (typeof value !== 'number' || Number.isNaN(value)) return 'medium'
|
|
102
|
+
if (value <= 33) return 'low'
|
|
103
|
+
if (value <= 66) return 'medium'
|
|
104
|
+
return 'high'
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type EditorRelatedTo = {
|
|
108
|
+
id: string
|
|
109
|
+
kind: 'person' | 'company' | 'unknown'
|
|
110
|
+
label: string
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export type EditorParticipant = {
|
|
114
|
+
userId: string
|
|
115
|
+
name: string
|
|
116
|
+
email?: string
|
|
117
|
+
isCustomer: boolean
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export type EditorFormState = {
|
|
121
|
+
kind: EditorKind
|
|
122
|
+
title: string
|
|
123
|
+
relatedTo: EditorRelatedTo | null
|
|
124
|
+
dealId: string | null
|
|
125
|
+
dealLabel: string | null
|
|
126
|
+
allDay: boolean
|
|
127
|
+
date: string
|
|
128
|
+
startTime: string
|
|
129
|
+
endDate: string
|
|
130
|
+
endTime: string
|
|
131
|
+
repeatFreq: EditorRepeatFreq
|
|
132
|
+
repeatDays: boolean[]
|
|
133
|
+
repeatEndType: EditorRepeatEndType
|
|
134
|
+
repeatCount: number
|
|
135
|
+
repeatUntilDate: string
|
|
136
|
+
category: string | null
|
|
137
|
+
location: string
|
|
138
|
+
participants: EditorParticipant[]
|
|
139
|
+
assigneeUserId: string | null
|
|
140
|
+
assigneeName: string | null
|
|
141
|
+
priority: EditorPriority
|
|
142
|
+
description: string
|
|
143
|
+
status: 'planned' | 'done' | 'canceled'
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const MO_FIRST_DAY_TOKENS = ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'] as const
|
|
147
|
+
|
|
148
|
+
function padDatePart(value: number): string {
|
|
149
|
+
return String(value).padStart(2, '0')
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function formatLocalDateInput(date: Date): string {
|
|
153
|
+
return `${date.getFullYear()}-${padDatePart(date.getMonth() + 1)}-${padDatePart(date.getDate())}`
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function formatLocalTimeInput(date: Date): string {
|
|
157
|
+
return `${padDatePart(date.getHours())}:${padDatePart(date.getMinutes())}`
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function moFirstIndexOfDate(date: Date): number {
|
|
161
|
+
return (date.getDay() + 6) % 7
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function defaultRepeatDaysForDateInput(dateInput: string): boolean[] {
|
|
165
|
+
const parsed = new Date(`${dateInput}T00:00:00`)
|
|
166
|
+
return defaultRepeatDays(Number.isNaN(parsed.getTime()) ? new Date() : parsed)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function defaultRepeatDays(start: Date): boolean[] {
|
|
170
|
+
const days = [false, false, false, false, false, false, false]
|
|
171
|
+
days[moFirstIndexOfDate(start)] = true
|
|
172
|
+
return days
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Resolves the `ownerUserId` an interaction will actually be SAVED with, so the
|
|
177
|
+
* editor's conflict probe keys on the same owner the grid sees after save (see
|
|
178
|
+
* buildInteractionPayload + the create/update command):
|
|
179
|
+
* - Task (people === 'assignee'): owner = the assignee.
|
|
180
|
+
* - Other kinds: the payload omits owner, so edit preserves the existing owner
|
|
181
|
+
* and create is ownerless (null) — conflicts then come from participants only.
|
|
182
|
+
*/
|
|
183
|
+
export function resolveSavedOwnerUserId(
|
|
184
|
+
config: EditorKindConfig,
|
|
185
|
+
form: EditorFormState,
|
|
186
|
+
isEdit: boolean,
|
|
187
|
+
existingOwnerUserId: string | null,
|
|
188
|
+
): string | null {
|
|
189
|
+
if (config.people === 'assignee') return form.assigneeUserId ?? null
|
|
190
|
+
return isEdit ? existingOwnerUserId : null
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function createDefaultFormState(
|
|
194
|
+
defaultDate?: Date | null,
|
|
195
|
+
now: Date = new Date(),
|
|
196
|
+
range?: { start: Date; end: Date } | null,
|
|
197
|
+
): EditorFormState {
|
|
198
|
+
let base: Date
|
|
199
|
+
let end: Date
|
|
200
|
+
if (range) {
|
|
201
|
+
base = new Date(range.start)
|
|
202
|
+
end = new Date(range.end)
|
|
203
|
+
} else {
|
|
204
|
+
base = defaultDate ? new Date(defaultDate) : new Date(now)
|
|
205
|
+
base.setHours(now.getHours(), 0, 0, 0)
|
|
206
|
+
base.setHours(base.getHours() + 1)
|
|
207
|
+
end = new Date(base.getTime() + 90 * 60_000)
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
kind: 'meeting',
|
|
211
|
+
title: '',
|
|
212
|
+
relatedTo: null,
|
|
213
|
+
dealId: null,
|
|
214
|
+
dealLabel: null,
|
|
215
|
+
allDay: false,
|
|
216
|
+
date: formatLocalDateInput(base),
|
|
217
|
+
startTime: formatLocalTimeInput(base),
|
|
218
|
+
endDate: formatLocalDateInput(end),
|
|
219
|
+
endTime: formatLocalTimeInput(end),
|
|
220
|
+
repeatFreq: 'none',
|
|
221
|
+
repeatDays: defaultRepeatDays(base),
|
|
222
|
+
repeatEndType: 'never',
|
|
223
|
+
repeatCount: 8,
|
|
224
|
+
repeatUntilDate: '',
|
|
225
|
+
category: null,
|
|
226
|
+
location: '',
|
|
227
|
+
participants: [],
|
|
228
|
+
assigneeUserId: null,
|
|
229
|
+
assigneeName: null,
|
|
230
|
+
priority: 'medium',
|
|
231
|
+
description: '',
|
|
232
|
+
status: 'planned',
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function buildRecurrenceRule(state: EditorFormState): string | null {
|
|
237
|
+
if (state.repeatFreq === 'none') return null
|
|
238
|
+
let rule: string
|
|
239
|
+
if (state.repeatFreq === 'daily') {
|
|
240
|
+
rule = 'FREQ=DAILY'
|
|
241
|
+
} else {
|
|
242
|
+
const selected = MO_FIRST_DAY_TOKENS.filter((_, index) => state.repeatDays[index])
|
|
243
|
+
const tokens = selected.length > 0
|
|
244
|
+
? selected
|
|
245
|
+
: [MO_FIRST_DAY_TOKENS[moFirstIndexOfDate(new Date(`${state.date}T00:00:00`))]]
|
|
246
|
+
rule = `FREQ=WEEKLY;BYDAY=${tokens.join(',')}`
|
|
247
|
+
}
|
|
248
|
+
if (state.repeatEndType === 'count') rule += `;COUNT=${state.repeatCount}`
|
|
249
|
+
if (state.repeatEndType === 'date' && state.repeatUntilDate) {
|
|
250
|
+
rule += `;UNTIL=${state.repeatUntilDate.replace(/-/g, '')}T235959Z`
|
|
251
|
+
}
|
|
252
|
+
return rule
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function computeDurationMinutes(state: EditorFormState): number | null {
|
|
256
|
+
const start = new Date(`${state.date}T${state.startTime}:00`)
|
|
257
|
+
const end = new Date(`${state.endDate}T${state.endTime}:00`)
|
|
258
|
+
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) return null
|
|
259
|
+
const minutes = Math.round((end.getTime() - start.getTime()) / 60_000)
|
|
260
|
+
return minutes > 0 ? minutes : null
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export type BuildPayloadOptions = { mode: 'create' | 'edit'; id?: string }
|
|
264
|
+
|
|
265
|
+
export function buildInteractionPayload(state: EditorFormState, options: BuildPayloadOptions): Record<string, unknown> {
|
|
266
|
+
const config = KIND_CONFIG[state.kind]
|
|
267
|
+
const time = state.allDay && config.hasAllDay ? '00:00' : state.startTime
|
|
268
|
+
const scheduledAt = new Date(`${state.date}T${time}:00`).toISOString()
|
|
269
|
+
const recurrenceRule = config.hasRepeat ? buildRecurrenceRule(state) : null
|
|
270
|
+
const payload: Record<string, unknown> = {
|
|
271
|
+
...(options.mode === 'edit' && options.id ? { id: options.id } : {}),
|
|
272
|
+
entityId: state.relatedTo?.id ?? null,
|
|
273
|
+
dealId: state.dealId ?? null,
|
|
274
|
+
interactionType: state.category ?? state.kind,
|
|
275
|
+
title: state.title.trim(),
|
|
276
|
+
body: state.description.trim() || null,
|
|
277
|
+
status: options.mode === 'create' ? 'planned' : state.status,
|
|
278
|
+
date: state.date,
|
|
279
|
+
time,
|
|
280
|
+
scheduledAt,
|
|
281
|
+
durationMinutes: config.hasEnd && !(state.allDay && config.hasAllDay) ? computeDurationMinutes(state) : null,
|
|
282
|
+
allDay: config.hasAllDay ? state.allDay : null,
|
|
283
|
+
location: config.location ? state.location.trim() || null : null,
|
|
284
|
+
recurrenceRule,
|
|
285
|
+
recurrenceEnd:
|
|
286
|
+
recurrenceRule && state.repeatEndType === 'date' && state.repeatUntilDate
|
|
287
|
+
? new Date(state.repeatUntilDate).toISOString()
|
|
288
|
+
: null,
|
|
289
|
+
participants:
|
|
290
|
+
config.people && config.people !== 'assignee' && state.participants.length > 0
|
|
291
|
+
? state.participants.map((participant) => ({
|
|
292
|
+
userId: participant.userId,
|
|
293
|
+
name: participant.name,
|
|
294
|
+
email: participant.email,
|
|
295
|
+
status: participant.isCustomer ? 'customer' : 'pending',
|
|
296
|
+
}))
|
|
297
|
+
: null,
|
|
298
|
+
}
|
|
299
|
+
if (config.people === 'assignee') payload.ownerUserId = state.assigneeUserId ?? null
|
|
300
|
+
if (config.hasPriority) payload.priority = PRIORITY_NUMBER[state.priority]
|
|
301
|
+
return payload
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function readUnknownString(value: unknown): string | null {
|
|
305
|
+
return typeof value === 'string' && value.length > 0 ? value : null
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function readUnknownNumber(value: unknown): number | null {
|
|
309
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
type ParsedRepeat = Pick<EditorFormState, 'repeatFreq' | 'repeatDays' | 'repeatEndType' | 'repeatCount' | 'repeatUntilDate'>
|
|
313
|
+
|
|
314
|
+
function parseRepeatFromRule(rawRule: unknown, start: Date): ParsedRepeat {
|
|
315
|
+
const fallback: ParsedRepeat = {
|
|
316
|
+
repeatFreq: 'none',
|
|
317
|
+
repeatDays: defaultRepeatDays(start),
|
|
318
|
+
repeatEndType: 'never',
|
|
319
|
+
repeatCount: 8,
|
|
320
|
+
repeatUntilDate: '',
|
|
321
|
+
}
|
|
322
|
+
const ruleText = readUnknownString(rawRule)
|
|
323
|
+
if (!ruleText) return fallback
|
|
324
|
+
const parsed = parseRecurrenceRule(ruleText)
|
|
325
|
+
if (!parsed) return fallback
|
|
326
|
+
const repeatDays = defaultRepeatDays(start)
|
|
327
|
+
if (parsed.byDay) {
|
|
328
|
+
repeatDays.fill(false)
|
|
329
|
+
for (const jsWeekday of parsed.byDay) repeatDays[(jsWeekday + 6) % 7] = true
|
|
330
|
+
}
|
|
331
|
+
let repeatEndType: EditorRepeatEndType = 'never'
|
|
332
|
+
let repeatCount = 8
|
|
333
|
+
let repeatUntilDate = ''
|
|
334
|
+
if (parsed.count !== null) {
|
|
335
|
+
repeatEndType = 'count'
|
|
336
|
+
repeatCount = parsed.count
|
|
337
|
+
} else if (parsed.until) {
|
|
338
|
+
repeatEndType = 'date'
|
|
339
|
+
repeatUntilDate = `${parsed.until.getUTCFullYear()}-${padDatePart(parsed.until.getUTCMonth() + 1)}-${padDatePart(parsed.until.getUTCDate())}`
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
repeatFreq: parsed.freq === 'DAILY' ? 'daily' : 'weekly',
|
|
343
|
+
repeatDays,
|
|
344
|
+
repeatEndType,
|
|
345
|
+
repeatCount,
|
|
346
|
+
repeatUntilDate,
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function parseParticipants(item: CalendarItem): EditorParticipant[] {
|
|
351
|
+
const rawParticipants = Array.isArray(item.raw.participants) ? item.raw.participants : []
|
|
352
|
+
const statusByUserId = new Map<string, string | null>()
|
|
353
|
+
for (const raw of rawParticipants) {
|
|
354
|
+
statusByUserId.set(raw.userId, readUnknownString((raw as Record<string, unknown>).status))
|
|
355
|
+
}
|
|
356
|
+
return item.participants.map((participant) => ({
|
|
357
|
+
userId: participant.userId,
|
|
358
|
+
name: participant.name ?? participant.email ?? participant.userId,
|
|
359
|
+
email: participant.email,
|
|
360
|
+
isCustomer: statusByUserId.get(participant.userId) === 'customer',
|
|
361
|
+
}))
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function parseItemToFormState(item: CalendarItem): EditorFormState {
|
|
365
|
+
const kind = editorKindOfInteractionType(item.interactionType)
|
|
366
|
+
const raw = item.raw as Record<string, unknown>
|
|
367
|
+
return {
|
|
368
|
+
kind,
|
|
369
|
+
title: item.title,
|
|
370
|
+
relatedTo: item.entityId ? { id: item.entityId, kind: 'unknown', label: '' } : null,
|
|
371
|
+
dealId: item.dealId,
|
|
372
|
+
dealLabel: null,
|
|
373
|
+
allDay: item.allDay,
|
|
374
|
+
date: formatLocalDateInput(item.start),
|
|
375
|
+
startTime: formatLocalTimeInput(item.start),
|
|
376
|
+
endDate: formatLocalDateInput(item.end),
|
|
377
|
+
endTime: formatLocalTimeInput(item.end),
|
|
378
|
+
...parseRepeatFromRule(item.raw.recurrenceRule, item.start),
|
|
379
|
+
category: item.interactionType,
|
|
380
|
+
location: item.location ?? '',
|
|
381
|
+
participants: parseParticipants(item),
|
|
382
|
+
assigneeUserId: item.ownerUserId,
|
|
383
|
+
assigneeName: null,
|
|
384
|
+
priority: priorityFromNumber(readUnknownNumber(raw.priority)),
|
|
385
|
+
description: readUnknownString(raw.body) ?? '',
|
|
386
|
+
status: item.status,
|
|
387
|
+
}
|
|
388
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export const DRAG_SNAP_MINUTES = 15
|
|
2
|
+
export const MIN_DRAG_DURATION_MINUTES = 30
|
|
3
|
+
const MINUTES_PER_DAY = 24 * 60
|
|
4
|
+
|
|
5
|
+
export function isWeekendDay(date: Date): boolean {
|
|
6
|
+
const weekday = date.getDay()
|
|
7
|
+
return weekday === 0 || weekday === 6
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function applyWeekendVisibility(days: Date[], showWeekends: boolean): Date[] {
|
|
11
|
+
if (showWeekends) return days
|
|
12
|
+
const workingDays = days.filter((day) => !isWeekendDay(day))
|
|
13
|
+
return workingDays.length > 0 ? workingDays : days
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function clampMinutes(value: number): number {
|
|
17
|
+
if (value < 0) return 0
|
|
18
|
+
if (value > MINUTES_PER_DAY) return MINUTES_PER_DAY
|
|
19
|
+
return value
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function offsetYToMinutes(
|
|
23
|
+
offsetY: number,
|
|
24
|
+
hourHeightPx: number,
|
|
25
|
+
snapMinutes: number = DRAG_SNAP_MINUTES,
|
|
26
|
+
): number {
|
|
27
|
+
if (hourHeightPx <= 0) return 0
|
|
28
|
+
const rawMinutes = (offsetY / hourHeightPx) * 60
|
|
29
|
+
const snapped = Math.round(rawMinutes / snapMinutes) * snapMinutes
|
|
30
|
+
return clampMinutes(snapped)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type DragRange = { start: Date; end: Date }
|
|
34
|
+
|
|
35
|
+
export function buildDragRange(dayStart: Date, startMinutes: number, endMinutes: number): DragRange {
|
|
36
|
+
let lower = clampMinutes(Math.min(startMinutes, endMinutes))
|
|
37
|
+
let upper = clampMinutes(Math.max(startMinutes, endMinutes))
|
|
38
|
+
if (upper - lower < MIN_DRAG_DURATION_MINUTES) upper = lower + MIN_DRAG_DURATION_MINUTES
|
|
39
|
+
if (upper > MINUTES_PER_DAY) {
|
|
40
|
+
upper = MINUTES_PER_DAY
|
|
41
|
+
lower = Math.max(0, upper - MIN_DRAG_DURATION_MINUTES)
|
|
42
|
+
}
|
|
43
|
+
const start = new Date(dayStart)
|
|
44
|
+
start.setHours(0, 0, 0, 0)
|
|
45
|
+
start.setMinutes(lower)
|
|
46
|
+
const end = new Date(dayStart)
|
|
47
|
+
end.setHours(0, 0, 0, 0)
|
|
48
|
+
end.setMinutes(upper)
|
|
49
|
+
return { start, end }
|
|
50
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { CalendarItem } from '../../components/calendar/types'
|
|
2
|
+
|
|
3
|
+
export type PackedCalendarItem = {
|
|
4
|
+
item: CalendarItem
|
|
5
|
+
column: number
|
|
6
|
+
columns: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function packOverlaps(dayItems: CalendarItem[]): PackedCalendarItem[] {
|
|
10
|
+
const sorted = [...dayItems].sort((first, second) => {
|
|
11
|
+
const startDelta = first.start.getTime() - second.start.getTime()
|
|
12
|
+
if (startDelta !== 0) return startDelta
|
|
13
|
+
return second.end.getTime() - first.end.getTime()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const result: PackedCalendarItem[] = []
|
|
17
|
+
let cluster: Array<{ item: CalendarItem; column: number }> = []
|
|
18
|
+
let columnEnds: number[] = []
|
|
19
|
+
let clusterEnd = Number.NEGATIVE_INFINITY
|
|
20
|
+
|
|
21
|
+
const flushCluster = () => {
|
|
22
|
+
const columns = columnEnds.length
|
|
23
|
+
for (const entry of cluster) {
|
|
24
|
+
result.push({ item: entry.item, column: entry.column, columns })
|
|
25
|
+
}
|
|
26
|
+
cluster = []
|
|
27
|
+
columnEnds = []
|
|
28
|
+
clusterEnd = Number.NEGATIVE_INFINITY
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const item of sorted) {
|
|
32
|
+
const startTime = item.start.getTime()
|
|
33
|
+
if (cluster.length > 0 && startTime >= clusterEnd) flushCluster()
|
|
34
|
+
let column = columnEnds.findIndex((columnEnd) => columnEnd <= startTime)
|
|
35
|
+
if (column === -1) {
|
|
36
|
+
column = columnEnds.length
|
|
37
|
+
columnEnds.push(item.end.getTime())
|
|
38
|
+
} else {
|
|
39
|
+
columnEnds[column] = item.end.getTime()
|
|
40
|
+
}
|
|
41
|
+
cluster.push({ item, column })
|
|
42
|
+
clusterEnd = Math.max(clusterEnd, item.end.getTime())
|
|
43
|
+
}
|
|
44
|
+
flushCluster()
|
|
45
|
+
|
|
46
|
+
return result
|
|
47
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { addMinutes } from 'date-fns/addMinutes'
|
|
2
|
+
import { endOfDay } from 'date-fns/endOfDay'
|
|
3
|
+
import { startOfDay } from 'date-fns/startOfDay'
|
|
4
|
+
import type {
|
|
5
|
+
CalendarInteractionPayload,
|
|
6
|
+
CalendarItem,
|
|
7
|
+
CalendarItemStatus,
|
|
8
|
+
CalendarLocationKind,
|
|
9
|
+
CalendarParticipant,
|
|
10
|
+
CalendarPlatform,
|
|
11
|
+
} from '../../components/calendar/types'
|
|
12
|
+
import { categoryOf } from './categories'
|
|
13
|
+
|
|
14
|
+
const DEFAULT_DURATION_MINUTES = 30
|
|
15
|
+
|
|
16
|
+
function narrowStatus(status: string): CalendarItemStatus {
|
|
17
|
+
if (status === 'done') return 'done'
|
|
18
|
+
if (status === 'canceled') return 'canceled'
|
|
19
|
+
return 'planned'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function detectPlatform(location: string | null): CalendarPlatform | null {
|
|
23
|
+
if (!location) return null
|
|
24
|
+
const normalized = location.toLowerCase()
|
|
25
|
+
if (normalized.includes('zoom.us') || normalized.includes('zoom')) return 'zoom'
|
|
26
|
+
if (normalized.includes('meet.google') || normalized.includes('on meet')) return 'meet'
|
|
27
|
+
if (normalized.includes('slack')) return 'slack'
|
|
28
|
+
if (normalized.includes('teams')) return 'teams'
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function detectLocationKind(location: string | null, platform: CalendarPlatform | null): CalendarLocationKind | null {
|
|
33
|
+
if (!location) return null
|
|
34
|
+
const normalized = location.trim().toLowerCase()
|
|
35
|
+
if (normalized.startsWith('http') || normalized.startsWith('www')) return 'url'
|
|
36
|
+
if (platform) return 'platform'
|
|
37
|
+
return 'venue'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function mapParticipants(payload: CalendarInteractionPayload): CalendarParticipant[] {
|
|
41
|
+
const participants = payload.participants
|
|
42
|
+
if (!Array.isArray(participants)) return []
|
|
43
|
+
const seen = new Set<string>()
|
|
44
|
+
const mapped: CalendarParticipant[] = []
|
|
45
|
+
for (const participant of participants) {
|
|
46
|
+
if (seen.has(participant.userId)) continue
|
|
47
|
+
seen.add(participant.userId)
|
|
48
|
+
const entry: CalendarParticipant = { userId: participant.userId }
|
|
49
|
+
if (typeof participant.name === 'string') entry.name = participant.name
|
|
50
|
+
if (typeof participant.email === 'string') entry.email = participant.email
|
|
51
|
+
mapped.push(entry)
|
|
52
|
+
}
|
|
53
|
+
return mapped
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function mapInteractionToCalendarItem(
|
|
57
|
+
payload: CalendarInteractionPayload,
|
|
58
|
+
typeColorByType: Record<string, string | null>,
|
|
59
|
+
): CalendarItem | null {
|
|
60
|
+
const effectiveStartRaw = payload.occurredAt ?? payload.scheduledAt ?? null
|
|
61
|
+
if (!effectiveStartRaw) return null
|
|
62
|
+
const parsedStart = new Date(effectiveStartRaw)
|
|
63
|
+
if (Number.isNaN(parsedStart.getTime())) return null
|
|
64
|
+
|
|
65
|
+
const allDay = payload.allDay === true
|
|
66
|
+
const durationMinutes = payload.durationMinutes ?? DEFAULT_DURATION_MINUTES
|
|
67
|
+
const start = allDay ? startOfDay(parsedStart) : parsedStart
|
|
68
|
+
const end = allDay ? endOfDay(parsedStart) : addMinutes(parsedStart, durationMinutes)
|
|
69
|
+
|
|
70
|
+
const location = payload.location ?? null
|
|
71
|
+
const platform = detectPlatform(location)
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
id: payload.id,
|
|
75
|
+
title: payload.title ?? '',
|
|
76
|
+
interactionType: payload.interactionType,
|
|
77
|
+
category: categoryOf(payload.interactionType),
|
|
78
|
+
status: narrowStatus(payload.status),
|
|
79
|
+
start,
|
|
80
|
+
end,
|
|
81
|
+
allDay,
|
|
82
|
+
location,
|
|
83
|
+
platform,
|
|
84
|
+
locationKind: detectLocationKind(location, platform),
|
|
85
|
+
participants: mapParticipants(payload),
|
|
86
|
+
ownerUserId: payload.ownerUserId ?? null,
|
|
87
|
+
entityId: payload.entityId ?? null,
|
|
88
|
+
dealId: payload.dealId ?? null,
|
|
89
|
+
color: payload.appearanceColor ?? typeColorByType[payload.interactionType] ?? null,
|
|
90
|
+
isRecurringOccurrence: false,
|
|
91
|
+
updatedAt: payload.updatedAt ?? null,
|
|
92
|
+
raw: payload,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Whose overlaps the calendar flags as conflicts:
|
|
2
|
+
// - 'mine': only when the current user is an actor (owner/participant) in BOTH
|
|
3
|
+
// overlapping events — i.e. the current user is personally double-booked.
|
|
4
|
+
// - 'all': any two visible org events that share an actor, even when the current
|
|
5
|
+
// user is in neither (team-wide scheduling awareness).
|
|
6
|
+
export type ConflictScope = 'mine' | 'all'
|
|
7
|
+
|
|
8
|
+
export const CONFLICT_SCOPES: ConflictScope[] = ['mine', 'all']
|
|
9
|
+
|
|
10
|
+
export type CalendarPreferences = {
|
|
11
|
+
showWeekends: boolean
|
|
12
|
+
conflictWarnings: boolean
|
|
13
|
+
conflictScope: ConflictScope
|
|
14
|
+
showCrmActivities: boolean
|
|
15
|
+
aiSummaries: boolean
|
|
16
|
+
eventCategories: string[]
|
|
17
|
+
activityTypes: string[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const MAX_EVENT_CATEGORIES = 8
|
|
21
|
+
export const MAX_ACTIVITY_TYPES = 6
|
|
22
|
+
|
|
23
|
+
export const DEFAULT_CALENDAR_PREFERENCES: CalendarPreferences = {
|
|
24
|
+
showWeekends: false,
|
|
25
|
+
conflictWarnings: true,
|
|
26
|
+
conflictScope: 'mine',
|
|
27
|
+
showCrmActivities: true,
|
|
28
|
+
aiSummaries: true,
|
|
29
|
+
eventCategories: [],
|
|
30
|
+
activityTypes: [],
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const CALENDAR_PREFERENCES_STORAGE_PREFIX = 'om.customers.calendar.preferences.v1'
|
|
34
|
+
|
|
35
|
+
export function calendarPreferencesStorageKey(scopeKey: string): string {
|
|
36
|
+
return `${CALENDAR_PREFERENCES_STORAGE_PREFIX}:${scopeKey}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function normalizeCalendarTagList(value: unknown, max: number): string[] {
|
|
40
|
+
if (!Array.isArray(value)) return []
|
|
41
|
+
const seen = new Set<string>()
|
|
42
|
+
const result: string[] = []
|
|
43
|
+
for (const entry of value) {
|
|
44
|
+
if (typeof entry !== 'string') continue
|
|
45
|
+
const trimmed = entry.trim()
|
|
46
|
+
if (!trimmed || seen.has(trimmed)) continue
|
|
47
|
+
seen.add(trimmed)
|
|
48
|
+
result.push(trimmed)
|
|
49
|
+
if (result.length >= max) break
|
|
50
|
+
}
|
|
51
|
+
return result
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readConflictScope(value: unknown, fallback: ConflictScope): ConflictScope {
|
|
55
|
+
return value === 'mine' || value === 'all' ? value : fallback
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function mergeCalendarPreferences(stored: unknown): CalendarPreferences {
|
|
59
|
+
if (!stored || typeof stored !== 'object') return { ...DEFAULT_CALENDAR_PREFERENCES }
|
|
60
|
+
const record = stored as Record<string, unknown>
|
|
61
|
+
const readBoolean = (key: keyof CalendarPreferences, fallback: boolean): boolean =>
|
|
62
|
+
typeof record[key] === 'boolean' ? (record[key] as boolean) : fallback
|
|
63
|
+
return {
|
|
64
|
+
showWeekends: readBoolean('showWeekends', DEFAULT_CALENDAR_PREFERENCES.showWeekends),
|
|
65
|
+
conflictWarnings: readBoolean('conflictWarnings', DEFAULT_CALENDAR_PREFERENCES.conflictWarnings),
|
|
66
|
+
conflictScope: readConflictScope(record.conflictScope, DEFAULT_CALENDAR_PREFERENCES.conflictScope),
|
|
67
|
+
showCrmActivities: readBoolean('showCrmActivities', DEFAULT_CALENDAR_PREFERENCES.showCrmActivities),
|
|
68
|
+
aiSummaries: readBoolean('aiSummaries', DEFAULT_CALENDAR_PREFERENCES.aiSummaries),
|
|
69
|
+
eventCategories: normalizeCalendarTagList(record.eventCategories, MAX_EVENT_CATEGORIES),
|
|
70
|
+
activityTypes: normalizeCalendarTagList(record.activityTypes, MAX_ACTIVITY_TYPES),
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function parseStoredCalendarPreferences(raw: string | null): CalendarPreferences {
|
|
75
|
+
if (!raw) return { ...DEFAULT_CALENDAR_PREFERENCES }
|
|
76
|
+
try {
|
|
77
|
+
return mergeCalendarPreferences(JSON.parse(raw))
|
|
78
|
+
} catch {
|
|
79
|
+
return { ...DEFAULT_CALENDAR_PREFERENCES }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function calendarPreferencesEqual(first: CalendarPreferences, second: CalendarPreferences): boolean {
|
|
84
|
+
return (
|
|
85
|
+
first.showWeekends === second.showWeekends &&
|
|
86
|
+
first.conflictWarnings === second.conflictWarnings &&
|
|
87
|
+
first.conflictScope === second.conflictScope &&
|
|
88
|
+
first.showCrmActivities === second.showCrmActivities &&
|
|
89
|
+
first.aiSummaries === second.aiSummaries &&
|
|
90
|
+
first.eventCategories.length === second.eventCategories.length &&
|
|
91
|
+
first.eventCategories.every((value, index) => value === second.eventCategories[index]) &&
|
|
92
|
+
first.activityTypes.length === second.activityTypes.length &&
|
|
93
|
+
first.activityTypes.every((value, index) => value === second.activityTypes[index])
|
|
94
|
+
)
|
|
95
|
+
}
|