@open-mercato/core 0.5.1-develop.2860.07af3a6a9d → 0.5.1-develop.2874.77704bccbd
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/dist/modules/api_docs/frontend/docs/api/Explorer.js +18 -18
- package/dist/modules/api_docs/frontend/docs/api/Explorer.js.map +2 -2
- package/dist/modules/api_keys/backend/api-keys/create/page.js +1 -1
- package/dist/modules/api_keys/backend/api-keys/create/page.js.map +1 -1
- package/dist/modules/attachments/components/AttachmentLibrary.js +2 -2
- package/dist/modules/attachments/components/AttachmentLibrary.js.map +2 -2
- package/dist/modules/attachments/components/AttachmentPartitionSettings.js +1 -1
- package/dist/modules/attachments/components/AttachmentPartitionSettings.js.map +1 -1
- package/dist/modules/attachments/fields/attachment.js +1 -1
- package/dist/modules/attachments/fields/attachment.js.map +1 -1
- package/dist/modules/audit_logs/components/ActionLogDetailsDialog.js +1 -1
- package/dist/modules/audit_logs/components/ActionLogDetailsDialog.js.map +2 -2
- package/dist/modules/audit_logs/lib/display-helpers.js +1 -1
- package/dist/modules/audit_logs/lib/display-helpers.js.map +1 -1
- package/dist/modules/auth/backend/users/create/page.js +1 -1
- package/dist/modules/auth/backend/users/create/page.js.map +1 -1
- package/dist/modules/business_rules/backend/rules/page.js +6 -6
- package/dist/modules/business_rules/backend/rules/page.js.map +2 -2
- package/dist/modules/business_rules/backend/sets/page.js +2 -2
- package/dist/modules/business_rules/backend/sets/page.js.map +2 -2
- package/dist/modules/business_rules/components/ActionBuilder.js +5 -5
- package/dist/modules/business_rules/components/ActionBuilder.js.map +2 -2
- package/dist/modules/business_rules/components/ActionRow.js +8 -8
- package/dist/modules/business_rules/components/ActionRow.js.map +1 -1
- package/dist/modules/business_rules/components/ConditionBuilder.js +5 -5
- package/dist/modules/business_rules/components/ConditionBuilder.js.map +2 -2
- package/dist/modules/business_rules/components/ConditionGroup.js +2 -2
- package/dist/modules/business_rules/components/ConditionGroup.js.map +1 -1
- package/dist/modules/business_rules/components/ConditionRow.js +3 -3
- package/dist/modules/business_rules/components/ConditionRow.js.map +2 -2
- package/dist/modules/business_rules/components/RuleSetMembers.js +8 -8
- package/dist/modules/business_rules/components/RuleSetMembers.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js +2 -2
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +1 -1
- package/dist/modules/catalog/backend/catalog/products/create/page.js +5 -5
- package/dist/modules/catalog/backend/catalog/products/create/page.js.map +1 -1
- package/dist/modules/catalog/components/products/MetadataEditor.js +1 -1
- package/dist/modules/catalog/components/products/MetadataEditor.js.map +1 -1
- package/dist/modules/catalog/components/products/ProductImageCell.js +1 -1
- package/dist/modules/catalog/components/products/ProductImageCell.js.map +1 -1
- package/dist/modules/catalog/components/products/VariantBuilder.js +1 -1
- package/dist/modules/catalog/components/products/VariantBuilder.js.map +1 -1
- package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js +1 -1
- package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js.map +2 -2
- package/dist/modules/currencies/components/CurrencyFetchingConfig.js +1 -1
- package/dist/modules/currencies/components/CurrencyFetchingConfig.js.map +1 -1
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/page.js +9 -9
- 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 +7 -7
- package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +2 -2
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +1 -1
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js +3 -3
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js.map +1 -1
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +1 -1
- package/dist/modules/customers/components/AddressTiles.js +1 -1
- package/dist/modules/customers/components/AddressTiles.js.map +1 -1
- package/dist/modules/customers/components/detail/ActivityForm.js +3 -3
- package/dist/modules/customers/components/detail/ActivityForm.js.map +1 -1
- package/dist/modules/customers/components/detail/AnnualRevenueField.js +2 -2
- package/dist/modules/customers/components/detail/AnnualRevenueField.js.map +1 -1
- package/dist/modules/customers/components/detail/CustomFieldValuesList.js +1 -1
- package/dist/modules/customers/components/detail/CustomFieldValuesList.js.map +1 -1
- package/dist/modules/customers/components/detail/DealForm.js +1 -1
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/DealsSection.js +1 -1
- package/dist/modules/customers/components/detail/DealsSection.js.map +1 -1
- package/dist/modules/customers/components/detail/DetailFieldsSection.js +1 -1
- package/dist/modules/customers/components/detail/DetailFieldsSection.js.map +1 -1
- package/dist/modules/customers/components/detail/InlineEditors.js +5 -5
- package/dist/modules/customers/components/detail/InlineEditors.js.map +2 -2
- package/dist/modules/customers/components/detail/TasksSection.js +1 -1
- package/dist/modules/customers/components/detail/TasksSection.js.map +1 -1
- package/dist/modules/customers/components/detail/TimelineItemHeader.js +1 -1
- package/dist/modules/customers/components/detail/TimelineItemHeader.js.map +1 -1
- package/dist/modules/customers/components/formConfig.js +2 -2
- package/dist/modules/customers/components/formConfig.js.map +1 -1
- package/dist/modules/customers/widgets/dashboard/customer-todos/widget.client.js +1 -1
- package/dist/modules/customers/widgets/dashboard/customer-todos/widget.client.js.map +1 -1
- package/dist/modules/customers/widgets/dashboard/new-customers/widget.client.js +2 -2
- package/dist/modules/customers/widgets/dashboard/new-customers/widget.client.js.map +1 -1
- package/dist/modules/customers/widgets/dashboard/new-deals/widget.client.js +1 -1
- package/dist/modules/customers/widgets/dashboard/new-deals/widget.client.js.map +1 -1
- package/dist/modules/customers/widgets/dashboard/next-interactions/widget.client.js +1 -1
- package/dist/modules/customers/widgets/dashboard/next-interactions/widget.client.js.map +1 -1
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js +1 -1
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js.map +1 -1
- package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js +1 -1
- package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js.map +1 -1
- package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.js +1 -1
- package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.js.map +1 -1
- package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js +1 -1
- package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js.map +1 -1
- package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.js +1 -1
- package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.js.map +1 -1
- package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js +1 -1
- package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js.map +1 -1
- package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js +2 -2
- package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js.map +1 -1
- package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js +1 -1
- package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js.map +1 -1
- package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js +1 -1
- package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js.map +1 -1
- package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js +2 -2
- package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js.map +1 -1
- package/dist/modules/data_sync/backend/data-sync/page.js +4 -4
- package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +2 -2
- package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +1 -1
- package/dist/modules/dictionaries/components/AppearanceSelector.js +3 -3
- package/dist/modules/dictionaries/components/AppearanceSelector.js.map +2 -2
- package/dist/modules/dictionaries/components/DictionariesManager.js +4 -4
- package/dist/modules/dictionaries/components/DictionariesManager.js.map +2 -2
- package/dist/modules/dictionaries/components/DictionaryEntriesEditor.js +2 -2
- package/dist/modules/dictionaries/components/DictionaryEntriesEditor.js.map +2 -2
- package/dist/modules/dictionaries/components/DictionaryEntrySelect.js +3 -3
- package/dist/modules/dictionaries/components/DictionaryEntrySelect.js.map +1 -1
- package/dist/modules/dictionaries/fields/dictionary.js +4 -4
- package/dist/modules/dictionaries/fields/dictionary.js.map +1 -1
- package/dist/modules/entities/components/EncryptionManager.js +3 -3
- package/dist/modules/entities/components/EncryptionManager.js.map +2 -2
- package/dist/modules/entities/components/UserEntitiesTable.js +1 -1
- package/dist/modules/entities/components/UserEntitiesTable.js.map +2 -2
- package/dist/modules/feature_toggles/components/formConfig.js +1 -1
- package/dist/modules/feature_toggles/components/formConfig.js.map +1 -1
- package/dist/modules/feature_toggles/components/overrideFormConfig.js +2 -2
- package/dist/modules/feature_toggles/components/overrideFormConfig.js.map +1 -1
- package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.js +12 -12
- package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.js.map +2 -2
- package/dist/modules/inbox_ops/components/messages/InboxEmailPreview.js +1 -1
- package/dist/modules/inbox_ops/components/messages/InboxEmailPreview.js.map +1 -1
- package/dist/modules/inbox_ops/components/proposals/ActionCard.js +12 -12
- package/dist/modules/inbox_ops/components/proposals/ActionCard.js.map +2 -2
- package/dist/modules/inbox_ops/widgets/notifications/ProposalCreatedRenderer.js +3 -3
- package/dist/modules/inbox_ops/widgets/notifications/ProposalCreatedRenderer.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/[id]/page.js +6 -6
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/messages/components/MessagesInboxPageClient.js +1 -1
- package/dist/modules/messages/components/MessagesInboxPageClient.js.map +1 -1
- package/dist/modules/messages/components/defaults/DefaultMessageListItem.js +1 -1
- package/dist/modules/messages/components/defaults/DefaultMessageListItem.js.map +1 -1
- package/dist/modules/messages/components/defaults/MessageRecordObjectDetail.js +1 -1
- package/dist/modules/messages/components/defaults/MessageRecordObjectDetail.js.map +1 -1
- package/dist/modules/messages/components/defaults/MessageRecordObjectPreview.js +1 -1
- package/dist/modules/messages/components/defaults/MessageRecordObjectPreview.js.map +1 -1
- package/dist/modules/messages/components/message-detail/panels/MessageListComponent.js +1 -1
- package/dist/modules/messages/components/message-detail/panels/MessageListComponent.js.map +1 -1
- package/dist/modules/messages/components/message-detail/panels/attachments-panel.js +1 -1
- package/dist/modules/messages/components/message-detail/panels/attachments-panel.js.map +1 -1
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js +11 -11
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js.map +2 -2
- package/dist/modules/planner/components/AvailabilityRulesEditor.js +2 -2
- package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js +3 -3
- package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/page.js +3 -3
- package/dist/modules/portal/frontend/[orgSlug]/portal/page.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/profile/page.js +4 -4
- package/dist/modules/portal/frontend/[orgSlug]/portal/profile/page.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/signup/page.js +4 -4
- package/dist/modules/portal/frontend/[orgSlug]/portal/signup/page.js.map +2 -2
- package/dist/modules/resources/backend/resources/resources/[id]/page.js +1 -1
- package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +1 -1
- package/dist/modules/sales/backend/sales/documents/[id]/page.js +4 -4
- package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +1 -1
- package/dist/modules/sales/components/DocumentNumberSettings.js +1 -1
- package/dist/modules/sales/components/DocumentNumberSettings.js.map +1 -1
- package/dist/modules/sales/components/channels/ChannelOfferForm.js +1 -1
- package/dist/modules/sales/components/channels/ChannelOfferForm.js.map +1 -1
- package/dist/modules/sales/components/documents/AdjustmentDialog.js +1 -1
- package/dist/modules/sales/components/documents/AdjustmentDialog.js.map +1 -1
- package/dist/modules/sales/components/documents/DocumentTotals.js +3 -3
- package/dist/modules/sales/components/documents/DocumentTotals.js.map +1 -1
- package/dist/modules/sales/components/documents/PaymentDialog.js +1 -1
- package/dist/modules/sales/components/documents/PaymentDialog.js.map +1 -1
- package/dist/modules/sales/components/documents/SalesDocumentForm.js +2 -2
- package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
- package/dist/modules/sales/widgets/dashboard/new-orders/widget.client.js +4 -4
- package/dist/modules/sales/widgets/dashboard/new-orders/widget.client.js.map +1 -1
- package/dist/modules/sales/widgets/dashboard/new-quotes/widget.client.js +4 -4
- package/dist/modules/sales/widgets/dashboard/new-quotes/widget.client.js.map +1 -1
- package/dist/modules/sales/widgets/injection/document-history/widget.client.js +2 -2
- package/dist/modules/sales/widgets/injection/document-history/widget.client.js.map +1 -1
- package/dist/modules/sales/widgets/messages/SalesDocumentMessageDetail.js +1 -1
- package/dist/modules/sales/widgets/messages/SalesDocumentMessageDetail.js.map +1 -1
- package/dist/modules/sales/widgets/messages/SalesDocumentMessagePreview.js +1 -1
- package/dist/modules/sales/widgets/messages/SalesDocumentMessagePreview.js.map +1 -1
- package/dist/modules/shipping_carriers/lib/shipment-wizard/components/PackageEditor.js +1 -1
- package/dist/modules/shipping_carriers/lib/shipment-wizard/components/PackageEditor.js.map +1 -1
- package/dist/modules/staff/backend/staff/team-members/[id]/page.js +1 -1
- package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +1 -1
- package/dist/modules/translations/components/TranslationDrawerAction.js +2 -2
- package/dist/modules/translations/components/TranslationDrawerAction.js.map +1 -1
- package/dist/modules/translations/components/TranslationManager.js +3 -3
- package/dist/modules/translations/components/TranslationManager.js.map +1 -1
- package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js +2 -2
- package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js.map +1 -1
- package/dist/modules/workflows/backend/definitions/[id]/page.js +5 -5
- package/dist/modules/workflows/backend/definitions/[id]/page.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js +2 -2
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
- package/dist/modules/workflows/backend/events/[id]/page.js +4 -4
- package/dist/modules/workflows/backend/events/[id]/page.js.map +1 -1
- package/dist/modules/workflows/backend/instances/[id]/page.js +2 -2
- package/dist/modules/workflows/backend/instances/[id]/page.js.map +1 -1
- package/dist/modules/workflows/backend/tasks/[id]/page.js +20 -20
- package/dist/modules/workflows/backend/tasks/[id]/page.js.map +2 -2
- package/dist/modules/workflows/components/DefinitionTriggersEditor.js +1 -1
- package/dist/modules/workflows/components/DefinitionTriggersEditor.js.map +1 -1
- package/dist/modules/workflows/components/EdgeEditDialog.js +12 -12
- package/dist/modules/workflows/components/EdgeEditDialog.js.map +1 -1
- package/dist/modules/workflows/components/NodeEditDialog.js +26 -26
- package/dist/modules/workflows/components/NodeEditDialog.js.map +1 -1
- package/dist/modules/workflows/components/fields/FormFieldArrayEditor.js +1 -1
- package/dist/modules/workflows/components/fields/FormFieldArrayEditor.js.map +1 -1
- package/dist/modules/workflows/components/mobile/MobileDefinitionDetail.js +2 -2
- package/dist/modules/workflows/components/mobile/MobileDefinitionDetail.js.map +2 -2
- package/dist/modules/workflows/components/mobile/MobileInstanceOverview.js +7 -7
- package/dist/modules/workflows/components/mobile/MobileInstanceOverview.js.map +2 -2
- package/dist/modules/workflows/components/mobile/MobileTaskForm.js +11 -11
- package/dist/modules/workflows/components/mobile/MobileTaskForm.js.map +2 -2
- package/dist/modules/workflows/components/mobile/MobileVisualEditor.js +1 -1
- package/dist/modules/workflows/components/mobile/MobileVisualEditor.js.map +1 -1
- package/dist/modules/workflows/components/mobile/MobileWorkflowTimeline.js +23 -23
- package/dist/modules/workflows/components/mobile/MobileWorkflowTimeline.js.map +2 -2
- package/dist/modules/workflows/frontend/checkout-demo/page.js +6 -6
- package/dist/modules/workflows/frontend/checkout-demo/page.js.map +1 -1
- package/package.json +3 -3
- package/src/modules/api_docs/frontend/docs/api/Explorer.tsx +18 -18
- package/src/modules/api_keys/backend/api-keys/create/page.tsx +1 -1
- package/src/modules/attachments/components/AttachmentLibrary.tsx +3 -3
- package/src/modules/attachments/components/AttachmentPartitionSettings.tsx +1 -1
- package/src/modules/attachments/fields/attachment.tsx +1 -1
- package/src/modules/audit_logs/components/ActionLogDetailsDialog.tsx +1 -1
- package/src/modules/audit_logs/lib/display-helpers.tsx +1 -1
- package/src/modules/auth/backend/users/create/page.tsx +1 -1
- package/src/modules/business_rules/backend/rules/page.tsx +7 -7
- package/src/modules/business_rules/backend/sets/page.tsx +3 -3
- package/src/modules/business_rules/components/ActionBuilder.tsx +6 -6
- package/src/modules/business_rules/components/ActionRow.tsx +8 -8
- package/src/modules/business_rules/components/ConditionBuilder.tsx +6 -6
- package/src/modules/business_rules/components/ConditionGroup.tsx +2 -2
- package/src/modules/business_rules/components/ConditionRow.tsx +3 -3
- package/src/modules/business_rules/components/RuleSetMembers.tsx +9 -9
- package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +2 -2
- package/src/modules/catalog/backend/catalog/products/create/page.tsx +5 -5
- package/src/modules/catalog/components/products/MetadataEditor.tsx +1 -1
- package/src/modules/catalog/components/products/ProductImageCell.tsx +1 -1
- package/src/modules/catalog/components/products/VariantBuilder.tsx +1 -1
- package/src/modules/catalog/widgets/injection/product-seo/widget.client.tsx +1 -1
- package/src/modules/currencies/components/CurrencyFetchingConfig.tsx +1 -1
- package/src/modules/customer_accounts/backend/customer_accounts/roles/page.tsx +2 -2
- package/src/modules/customer_accounts/backend/customer_accounts/users/[id]/page.tsx +10 -10
- package/src/modules/customer_accounts/backend/customer_accounts/users/page.tsx +9 -9
- package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +2 -2
- package/src/modules/customers/backend/config/customers/pipeline-stages/page.tsx +3 -3
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +2 -2
- package/src/modules/customers/components/AddressTiles.tsx +1 -1
- package/src/modules/customers/components/detail/ActivityForm.tsx +3 -3
- package/src/modules/customers/components/detail/AnnualRevenueField.tsx +2 -2
- package/src/modules/customers/components/detail/CustomFieldValuesList.tsx +1 -1
- package/src/modules/customers/components/detail/DealForm.tsx +1 -1
- package/src/modules/customers/components/detail/DealsSection.tsx +1 -1
- package/src/modules/customers/components/detail/DetailFieldsSection.tsx +1 -1
- package/src/modules/customers/components/detail/InlineEditors.tsx +5 -5
- package/src/modules/customers/components/detail/TasksSection.tsx +1 -1
- package/src/modules/customers/components/detail/TimelineItemHeader.tsx +1 -1
- package/src/modules/customers/components/formConfig.tsx +2 -2
- package/src/modules/customers/widgets/dashboard/customer-todos/widget.client.tsx +1 -1
- package/src/modules/customers/widgets/dashboard/new-customers/widget.client.tsx +2 -2
- package/src/modules/customers/widgets/dashboard/new-deals/widget.client.tsx +1 -1
- package/src/modules/customers/widgets/dashboard/next-interactions/widget.client.tsx +1 -1
- package/src/modules/dashboards/components/WidgetVisibilityEditor.tsx +1 -1
- package/src/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.tsx +1 -1
- package/src/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.tsx +1 -1
- package/src/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.tsx +1 -1
- package/src/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.tsx +1 -1
- package/src/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.tsx +1 -1
- package/src/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.tsx +2 -2
- package/src/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.tsx +1 -1
- package/src/modules/dashboards/widgets/dashboard/top-customers/widget.client.tsx +1 -1
- package/src/modules/dashboards/widgets/dashboard/top-products/widget.client.tsx +2 -2
- package/src/modules/data_sync/backend/data-sync/page.tsx +4 -4
- package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +2 -2
- package/src/modules/dictionaries/components/AppearanceSelector.tsx +3 -3
- package/src/modules/dictionaries/components/DictionariesManager.tsx +4 -4
- package/src/modules/dictionaries/components/DictionaryEntriesEditor.tsx +2 -2
- package/src/modules/dictionaries/components/DictionaryEntrySelect.tsx +3 -3
- package/src/modules/dictionaries/fields/dictionary.tsx +4 -4
- package/src/modules/entities/components/EncryptionManager.tsx +3 -3
- package/src/modules/entities/components/UserEntitiesTable.tsx +1 -1
- package/src/modules/feature_toggles/components/formConfig.tsx +1 -1
- package/src/modules/feature_toggles/components/overrideFormConfig.tsx +2 -2
- package/src/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.tsx +12 -12
- package/src/modules/inbox_ops/components/messages/InboxEmailPreview.tsx +1 -1
- package/src/modules/inbox_ops/components/proposals/ActionCard.tsx +12 -12
- package/src/modules/inbox_ops/widgets/notifications/ProposalCreatedRenderer.tsx +4 -4
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +6 -6
- package/src/modules/messages/components/MessagesInboxPageClient.tsx +1 -1
- package/src/modules/messages/components/defaults/DefaultMessageListItem.tsx +1 -1
- package/src/modules/messages/components/defaults/MessageRecordObjectDetail.tsx +1 -1
- package/src/modules/messages/components/defaults/MessageRecordObjectPreview.tsx +1 -1
- package/src/modules/messages/components/message-detail/panels/MessageListComponent.tsx +1 -1
- package/src/modules/messages/components/message-detail/panels/attachments-panel.tsx +1 -1
- package/src/modules/payment_gateways/backend/payment-gateways/page.tsx +11 -11
- package/src/modules/planner/components/AvailabilityRulesEditor.tsx +2 -2
- package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/page.tsx +2 -2
- package/src/modules/portal/frontend/[orgSlug]/portal/login/page.tsx +3 -3
- package/src/modules/portal/frontend/[orgSlug]/portal/page.tsx +3 -3
- package/src/modules/portal/frontend/[orgSlug]/portal/profile/page.tsx +4 -4
- package/src/modules/portal/frontend/[orgSlug]/portal/signup/page.tsx +4 -4
- package/src/modules/resources/backend/resources/resources/[id]/page.tsx +1 -1
- package/src/modules/sales/backend/sales/documents/[id]/page.tsx +4 -4
- package/src/modules/sales/components/DocumentNumberSettings.tsx +1 -1
- package/src/modules/sales/components/channels/ChannelOfferForm.tsx +1 -1
- package/src/modules/sales/components/documents/AdjustmentDialog.tsx +1 -1
- package/src/modules/sales/components/documents/DocumentTotals.tsx +3 -3
- package/src/modules/sales/components/documents/PaymentDialog.tsx +1 -1
- package/src/modules/sales/components/documents/SalesDocumentForm.tsx +2 -2
- package/src/modules/sales/widgets/dashboard/new-orders/widget.client.tsx +4 -4
- package/src/modules/sales/widgets/dashboard/new-quotes/widget.client.tsx +4 -4
- package/src/modules/sales/widgets/injection/document-history/widget.client.tsx +2 -2
- package/src/modules/sales/widgets/messages/SalesDocumentMessageDetail.tsx +1 -1
- package/src/modules/sales/widgets/messages/SalesDocumentMessagePreview.tsx +1 -1
- package/src/modules/shipping_carriers/lib/shipment-wizard/components/PackageEditor.tsx +1 -1
- package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +1 -1
- package/src/modules/translations/components/TranslationDrawerAction.tsx +2 -2
- package/src/modules/translations/components/TranslationManager.tsx +3 -3
- package/src/modules/translations/widgets/injection/translation-manager/widget.client.tsx +2 -2
- package/src/modules/workflows/backend/definitions/[id]/page.tsx +5 -5
- package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +2 -2
- package/src/modules/workflows/backend/events/[id]/page.tsx +4 -4
- package/src/modules/workflows/backend/instances/[id]/page.tsx +2 -2
- package/src/modules/workflows/backend/tasks/[id]/page.tsx +23 -23
- package/src/modules/workflows/components/DefinitionTriggersEditor.tsx +1 -1
- package/src/modules/workflows/components/EdgeEditDialog.tsx +12 -12
- package/src/modules/workflows/components/NodeEditDialog.tsx +26 -26
- package/src/modules/workflows/components/fields/FormFieldArrayEditor.tsx +1 -1
- package/src/modules/workflows/components/mobile/MobileDefinitionDetail.tsx +2 -2
- package/src/modules/workflows/components/mobile/MobileInstanceOverview.tsx +7 -7
- package/src/modules/workflows/components/mobile/MobileTaskForm.tsx +14 -14
- package/src/modules/workflows/components/mobile/MobileVisualEditor.tsx +1 -1
- package/src/modules/workflows/components/mobile/MobileWorkflowTimeline.tsx +23 -23
- package/src/modules/workflows/frontend/checkout-demo/page.tsx +6 -6
package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport React from 'react'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\n\ninterface AccountStatusData {\n id: string\n email: string\n isActive: boolean\n emailVerified: boolean\n lastLoginAt: string | null\n}\n\ninterface AccountStatusProps {\n context?: {\n entityId?: string\n recordId?: string\n }\n}\n\ninterface RoleOption {\n id: string\n name: string\n}\n\ninterface PersonData {\n person?: {\n primaryEmail?: string | null\n displayName?: string | null\n }\n profile?: {\n firstName?: string | null\n lastName?: string | null\n } | null\n}\n\nfunction InviteForm({ personEntityId, onSuccess }: { personEntityId: string; onSuccess: () => void }) {\n const t = useT()\n const [isLoadingPerson, setIsLoadingPerson] = React.useState(true)\n const [email, setEmail] = React.useState('')\n const [displayName, setDisplayName] = React.useState('')\n const [selectedRoleIds, setSelectedRoleIds] = React.useState<string[]>([])\n const [availableRoles, setAvailableRoles] = React.useState<RoleOption[]>([])\n const [isLoadingRoles, setIsLoadingRoles] = React.useState(true)\n const [isSubmitting, setIsSubmitting] = React.useState(false)\n\n React.useEffect(() => {\n let cancelled = false\n async function loadPerson() {\n try {\n const call = await apiCall<PersonData>(\n `/api/customers/people/${encodeURIComponent(personEntityId)}`,\n )\n if (cancelled) return\n if (call.ok && call.result) {\n const person = call.result.person\n const profile = call.result.profile\n if (person?.primaryEmail) {\n setEmail(person.primaryEmail)\n }\n const nameParts = [profile?.firstName, profile?.lastName].filter(Boolean)\n if (nameParts.length > 0) {\n setDisplayName(nameParts.join(' '))\n } else if (person?.displayName) {\n setDisplayName(person.displayName)\n }\n }\n } catch {\n /* ignore - fields will remain empty for manual entry */\n } finally {\n if (!cancelled) setIsLoadingPerson(false)\n }\n }\n loadPerson()\n return () => { cancelled = true }\n }, [personEntityId])\n\n React.useEffect(() => {\n let cancelled = false\n async function loadRoles() {\n try {\n const call = await apiCall<{ items?: RoleOption[] }>(\n '/api/customer_accounts/admin/roles?pageSize=100',\n )\n if (cancelled) return\n if (call.ok && call.result) {\n const items = Array.isArray(call.result.items) ? call.result.items : []\n setAvailableRoles(items.map((role) => ({ id: role.id, name: role.name })))\n }\n } catch {\n /* ignore */\n } finally {\n if (!cancelled) setIsLoadingRoles(false)\n }\n }\n loadRoles()\n return () => { cancelled = true }\n }, [])\n\n function toggleRole(roleId: string) {\n setSelectedRoleIds((prev) =>\n prev.includes(roleId) ? prev.filter((id) => id !== roleId) : [...prev, roleId],\n )\n }\n\n async function handleSubmit(event: React.FormEvent) {\n event.preventDefault()\n\n const trimmedEmail = email.trim()\n if (!trimmedEmail) {\n flash(t('customer_accounts.widgets.invite.error.emailRequired', 'Email is required'), 'error')\n return\n }\n if (selectedRoleIds.length === 0) {\n flash(t('customer_accounts.widgets.invite.error.roleRequired', 'At least one role must be selected'), 'error')\n return\n }\n\n setIsSubmitting(true)\n try {\n const call = await apiCall<{ ok: boolean; error?: string }>(\n '/api/customer_accounts/admin/users-invite',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n email: trimmedEmail,\n roleIds: selectedRoleIds,\n displayName: displayName.trim() || undefined,\n customerEntityId: personEntityId,\n }),\n },\n )\n\n if (!call.ok) {\n const errorMessage = (call.result as Record<string, unknown> | null)?.error as string | undefined\n flash(errorMessage || t('customer_accounts.widgets.invite.error.failed', 'Failed to send invitation'), 'error')\n return\n }\n\n flash(t('customer_accounts.widgets.invite.success', 'Invitation sent successfully'), 'success')\n onSuccess()\n } catch {\n flash(t('customer_accounts.widgets.invite.error.failed', 'Failed to send invitation'), 'error')\n } finally {\n setIsSubmitting(false)\n }\n }\n\n const isLoading = isLoadingPerson || isLoadingRoles\n\n if (isLoading) {\n return (\n <div className=\"text-sm text-muted-foreground py-2\">\n {t('common.loading', 'Loading...')}\n </div>\n )\n }\n\n return (\n <form onSubmit={handleSubmit} className=\"space-y-3 mt-2\">\n <div>\n <label htmlFor=\"invite-email\" className=\"block text-xs font-medium text-muted-foreground mb-1\">\n {t('common.email', 'Email')}\n </label>\n <input\n id=\"invite-email\"\n type=\"email\"\n value={email}\n onChange={(event) => setEmail(event.target.value)}\n className=\"w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-ring\"\n required\n disabled={isSubmitting}\n />\n </div>\n\n <div>\n <label htmlFor=\"invite-display-name\" className=\"block text-xs font-medium text-muted-foreground mb-1\">\n {t('customer_accounts.widgets.invite.displayName', 'Display Name')}\n </label>\n <input\n id=\"invite-display-name\"\n type=\"text\"\n value={displayName}\n onChange={(event) => setDisplayName(event.target.value)}\n className=\"w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-ring\"\n disabled={isSubmitting}\n />\n </div>\n\n <div>\n <div className=\"text-xs font-medium text-muted-foreground mb-1.5\">\n {t('customer_accounts.widgets.invite.roles', 'Roles')}\n </div>\n {availableRoles.length === 0 ? (\n <div className=\"text-xs text-muted-foreground\">\n {t('customer_accounts.widgets.invite.noRoles', 'No roles available')}\n </div>\n ) : (\n <div className=\"flex flex-wrap gap-1.5\">\n {availableRoles.map((role) => (\n <Button\n key={role.id}\n type=\"button\"\n variant={selectedRoleIds.includes(role.id) ? 'default' : 'outline'}\n size=\"sm\"\n onClick={() => toggleRole(role.id)}\n disabled={isSubmitting}\n className=\"text-xs h-7\"\n >\n {role.name}\n </Button>\n ))}\n </div>\n )}\n </div>\n\n <div className=\"flex justify-end gap-2 pt-1\">\n <Button\n type=\"submit\"\n size=\"sm\"\n disabled={isSubmitting || !email.trim() || selectedRoleIds.length === 0}\n >\n {isSubmitting\n ? t('common.loading', 'Loading...')\n : t('customer_accounts.widgets.invite.submit', 'Send Invitation')}\n </Button>\n </div>\n </form>\n )\n}\n\nexport default function AccountStatusWidget({ context }: AccountStatusProps) {\n const t = useT()\n const queryClient = useQueryClient()\n const personEntityId = context?.recordId\n const [showInviteForm, setShowInviteForm] = React.useState(false)\n\n const { data, isLoading } = useQuery({\n queryKey: ['customer-account-status', personEntityId],\n queryFn: async (): Promise<AccountStatusData | null> => {\n if (!personEntityId) return null\n const result = await apiCall(`/api/customer_accounts/admin/users?personEntityId=${personEntityId}&pageSize=1`)\n if (!result.ok) return null\n const json = result.result as Record<string, unknown> | null\n const items = json?.items as AccountStatusData[] | undefined\n return items?.[0] || null\n },\n enabled: !!personEntityId,\n })\n\n function handleInviteSuccess() {\n setShowInviteForm(false)\n queryClient.invalidateQueries({ queryKey: ['customer-account-status', personEntityId] })\n }\n\n if (isLoading) {\n return <div className=\"text-sm text-muted-foreground\">{t('common.loading', 'Loading...')}</div>\n }\n\n if (!data) {\n return (\n <div className=\"rounded-md border p-3\">\n <div className=\"text-sm font-medium mb-1\">{t('customer_accounts.widgets.accountStatus', 'Portal Account')}</div>\n <div className=\"text-sm text-muted-foreground\">{t('customer_accounts.widgets.noAccount', 'No portal account linked')}</div>\n {!showInviteForm && personEntityId && (\n <div className=\"mt-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setShowInviteForm(true)}\n >\n {t('customer_accounts.widgets.invite.button', 'Invite to Portal')}\n </Button>\n </div>\n )}\n {showInviteForm && personEntityId && (\n <InviteForm personEntityId={personEntityId} onSuccess={handleInviteSuccess} />\n )}\n </div>\n )\n }\n\n return (\n <div className=\"rounded-md border p-3\">\n <div className=\"text-sm font-medium mb-2\">{t('customer_accounts.widgets.accountStatus', 'Portal Account')}</div>\n <div className=\"space-y-1 text-sm\">\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">{t('common.status', 'Status')}</span>\n <span className={data.isActive ? 'text-green-600' : 'text-red-600'}>\n {data.isActive ? t('common.active', 'Active') : t('common.inactive', 'Inactive')}\n </span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">{t('common.email', 'Email')}</span>\n <span>{data.email}</span>\n </div>\n {data.emailVerified !== undefined && (\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">{t('customer_accounts.widgets.emailVerified', 'Email Verified')}</span>\n <span>{data.emailVerified ? '\u2713' : '\u2717'}</span>\n </div>\n )}\n {data.lastLoginAt && (\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">{t('customer_accounts.widgets.lastLogin', 'Last Login')}</span>\n <span>{new Date(data.lastLoginAt).toLocaleDateString()}</span>\n </div>\n )}\n </div>\n <div className=\"mt-2\">\n <a\n href={`/backend/customer_accounts/users/${data.id}`}\n className=\"text-xs text-primary hover:underline\"\n >\n {t('customer_accounts.widgets.viewAccount', 'View account details \u2192')}\n </a>\n </div>\n </div>\n )\n}\n"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport React from 'react'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\n\ninterface AccountStatusData {\n id: string\n email: string\n isActive: boolean\n emailVerified: boolean\n lastLoginAt: string | null\n}\n\ninterface AccountStatusProps {\n context?: {\n entityId?: string\n recordId?: string\n }\n}\n\ninterface RoleOption {\n id: string\n name: string\n}\n\ninterface PersonData {\n person?: {\n primaryEmail?: string | null\n displayName?: string | null\n }\n profile?: {\n firstName?: string | null\n lastName?: string | null\n } | null\n}\n\nfunction InviteForm({ personEntityId, onSuccess }: { personEntityId: string; onSuccess: () => void }) {\n const t = useT()\n const [isLoadingPerson, setIsLoadingPerson] = React.useState(true)\n const [email, setEmail] = React.useState('')\n const [displayName, setDisplayName] = React.useState('')\n const [selectedRoleIds, setSelectedRoleIds] = React.useState<string[]>([])\n const [availableRoles, setAvailableRoles] = React.useState<RoleOption[]>([])\n const [isLoadingRoles, setIsLoadingRoles] = React.useState(true)\n const [isSubmitting, setIsSubmitting] = React.useState(false)\n\n React.useEffect(() => {\n let cancelled = false\n async function loadPerson() {\n try {\n const call = await apiCall<PersonData>(\n `/api/customers/people/${encodeURIComponent(personEntityId)}`,\n )\n if (cancelled) return\n if (call.ok && call.result) {\n const person = call.result.person\n const profile = call.result.profile\n if (person?.primaryEmail) {\n setEmail(person.primaryEmail)\n }\n const nameParts = [profile?.firstName, profile?.lastName].filter(Boolean)\n if (nameParts.length > 0) {\n setDisplayName(nameParts.join(' '))\n } else if (person?.displayName) {\n setDisplayName(person.displayName)\n }\n }\n } catch {\n /* ignore - fields will remain empty for manual entry */\n } finally {\n if (!cancelled) setIsLoadingPerson(false)\n }\n }\n loadPerson()\n return () => { cancelled = true }\n }, [personEntityId])\n\n React.useEffect(() => {\n let cancelled = false\n async function loadRoles() {\n try {\n const call = await apiCall<{ items?: RoleOption[] }>(\n '/api/customer_accounts/admin/roles?pageSize=100',\n )\n if (cancelled) return\n if (call.ok && call.result) {\n const items = Array.isArray(call.result.items) ? call.result.items : []\n setAvailableRoles(items.map((role) => ({ id: role.id, name: role.name })))\n }\n } catch {\n /* ignore */\n } finally {\n if (!cancelled) setIsLoadingRoles(false)\n }\n }\n loadRoles()\n return () => { cancelled = true }\n }, [])\n\n function toggleRole(roleId: string) {\n setSelectedRoleIds((prev) =>\n prev.includes(roleId) ? prev.filter((id) => id !== roleId) : [...prev, roleId],\n )\n }\n\n async function handleSubmit(event: React.FormEvent) {\n event.preventDefault()\n\n const trimmedEmail = email.trim()\n if (!trimmedEmail) {\n flash(t('customer_accounts.widgets.invite.error.emailRequired', 'Email is required'), 'error')\n return\n }\n if (selectedRoleIds.length === 0) {\n flash(t('customer_accounts.widgets.invite.error.roleRequired', 'At least one role must be selected'), 'error')\n return\n }\n\n setIsSubmitting(true)\n try {\n const call = await apiCall<{ ok: boolean; error?: string }>(\n '/api/customer_accounts/admin/users-invite',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n email: trimmedEmail,\n roleIds: selectedRoleIds,\n displayName: displayName.trim() || undefined,\n customerEntityId: personEntityId,\n }),\n },\n )\n\n if (!call.ok) {\n const errorMessage = (call.result as Record<string, unknown> | null)?.error as string | undefined\n flash(errorMessage || t('customer_accounts.widgets.invite.error.failed', 'Failed to send invitation'), 'error')\n return\n }\n\n flash(t('customer_accounts.widgets.invite.success', 'Invitation sent successfully'), 'success')\n onSuccess()\n } catch {\n flash(t('customer_accounts.widgets.invite.error.failed', 'Failed to send invitation'), 'error')\n } finally {\n setIsSubmitting(false)\n }\n }\n\n const isLoading = isLoadingPerson || isLoadingRoles\n\n if (isLoading) {\n return (\n <div className=\"text-sm text-muted-foreground py-2\">\n {t('common.loading', 'Loading...')}\n </div>\n )\n }\n\n return (\n <form onSubmit={handleSubmit} className=\"space-y-3 mt-2\">\n <div>\n <label htmlFor=\"invite-email\" className=\"block text-xs font-medium text-muted-foreground mb-1\">\n {t('common.email', 'Email')}\n </label>\n <input\n id=\"invite-email\"\n type=\"email\"\n value={email}\n onChange={(event) => setEmail(event.target.value)}\n className=\"w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring\"\n required\n disabled={isSubmitting}\n />\n </div>\n\n <div>\n <label htmlFor=\"invite-display-name\" className=\"block text-xs font-medium text-muted-foreground mb-1\">\n {t('customer_accounts.widgets.invite.displayName', 'Display Name')}\n </label>\n <input\n id=\"invite-display-name\"\n type=\"text\"\n value={displayName}\n onChange={(event) => setDisplayName(event.target.value)}\n className=\"w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring\"\n disabled={isSubmitting}\n />\n </div>\n\n <div>\n <div className=\"text-xs font-medium text-muted-foreground mb-1.5\">\n {t('customer_accounts.widgets.invite.roles', 'Roles')}\n </div>\n {availableRoles.length === 0 ? (\n <div className=\"text-xs text-muted-foreground\">\n {t('customer_accounts.widgets.invite.noRoles', 'No roles available')}\n </div>\n ) : (\n <div className=\"flex flex-wrap gap-1.5\">\n {availableRoles.map((role) => (\n <Button\n key={role.id}\n type=\"button\"\n variant={selectedRoleIds.includes(role.id) ? 'default' : 'outline'}\n size=\"sm\"\n onClick={() => toggleRole(role.id)}\n disabled={isSubmitting}\n className=\"text-xs h-7\"\n >\n {role.name}\n </Button>\n ))}\n </div>\n )}\n </div>\n\n <div className=\"flex justify-end gap-2 pt-1\">\n <Button\n type=\"submit\"\n size=\"sm\"\n disabled={isSubmitting || !email.trim() || selectedRoleIds.length === 0}\n >\n {isSubmitting\n ? t('common.loading', 'Loading...')\n : t('customer_accounts.widgets.invite.submit', 'Send Invitation')}\n </Button>\n </div>\n </form>\n )\n}\n\nexport default function AccountStatusWidget({ context }: AccountStatusProps) {\n const t = useT()\n const queryClient = useQueryClient()\n const personEntityId = context?.recordId\n const [showInviteForm, setShowInviteForm] = React.useState(false)\n\n const { data, isLoading } = useQuery({\n queryKey: ['customer-account-status', personEntityId],\n queryFn: async (): Promise<AccountStatusData | null> => {\n if (!personEntityId) return null\n const result = await apiCall(`/api/customer_accounts/admin/users?personEntityId=${personEntityId}&pageSize=1`)\n if (!result.ok) return null\n const json = result.result as Record<string, unknown> | null\n const items = json?.items as AccountStatusData[] | undefined\n return items?.[0] || null\n },\n enabled: !!personEntityId,\n })\n\n function handleInviteSuccess() {\n setShowInviteForm(false)\n queryClient.invalidateQueries({ queryKey: ['customer-account-status', personEntityId] })\n }\n\n if (isLoading) {\n return <div className=\"text-sm text-muted-foreground\">{t('common.loading', 'Loading...')}</div>\n }\n\n if (!data) {\n return (\n <div className=\"rounded-md border p-3\">\n <div className=\"text-sm font-medium mb-1\">{t('customer_accounts.widgets.accountStatus', 'Portal Account')}</div>\n <div className=\"text-sm text-muted-foreground\">{t('customer_accounts.widgets.noAccount', 'No portal account linked')}</div>\n {!showInviteForm && personEntityId && (\n <div className=\"mt-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setShowInviteForm(true)}\n >\n {t('customer_accounts.widgets.invite.button', 'Invite to Portal')}\n </Button>\n </div>\n )}\n {showInviteForm && personEntityId && (\n <InviteForm personEntityId={personEntityId} onSuccess={handleInviteSuccess} />\n )}\n </div>\n )\n }\n\n return (\n <div className=\"rounded-md border p-3\">\n <div className=\"text-sm font-medium mb-2\">{t('customer_accounts.widgets.accountStatus', 'Portal Account')}</div>\n <div className=\"space-y-1 text-sm\">\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">{t('common.status', 'Status')}</span>\n <span className={data.isActive ? 'text-green-600' : 'text-red-600'}>\n {data.isActive ? t('common.active', 'Active') : t('common.inactive', 'Inactive')}\n </span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">{t('common.email', 'Email')}</span>\n <span>{data.email}</span>\n </div>\n {data.emailVerified !== undefined && (\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">{t('customer_accounts.widgets.emailVerified', 'Email Verified')}</span>\n <span>{data.emailVerified ? '\u2713' : '\u2717'}</span>\n </div>\n )}\n {data.lastLoginAt && (\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">{t('customer_accounts.widgets.lastLogin', 'Last Login')}</span>\n <span>{new Date(data.lastLoginAt).toLocaleDateString()}</span>\n </div>\n )}\n </div>\n <div className=\"mt-2\">\n <a\n href={`/backend/customer_accounts/users/${data.id}`}\n className=\"text-xs text-primary hover:underline\"\n >\n {t('customer_accounts.widgets.viewAccount', 'View account details \u2192')}\n </a>\n </div>\n </div>\n )\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -270,7 +270,7 @@ function PipelineStagesPage() {
|
|
|
270
270
|
/* @__PURE__ */ jsxs(
|
|
271
271
|
"select",
|
|
272
272
|
{
|
|
273
|
-
className: "flex h-9 w-full max-w-xs rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-ring",
|
|
273
|
+
className: "flex h-9 w-full max-w-xs rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
274
274
|
value: selectedPipelineId ?? "",
|
|
275
275
|
onChange: (e) => setSelectedPipelineId(e.target.value || null),
|
|
276
276
|
children: [
|
|
@@ -311,7 +311,7 @@ function PipelineStagesPage() {
|
|
|
311
311
|
"button",
|
|
312
312
|
{
|
|
313
313
|
type: "button",
|
|
314
|
-
className: "text-muted-foreground hover:text-foreground disabled:opacity-
|
|
314
|
+
className: "text-muted-foreground hover:text-foreground disabled:opacity-50",
|
|
315
315
|
onClick: () => moveStage(index, "up"),
|
|
316
316
|
disabled: index === 0,
|
|
317
317
|
"aria-label": t("customers.config.pipelineStages.moveUp", "Move up"),
|
|
@@ -322,7 +322,7 @@ function PipelineStagesPage() {
|
|
|
322
322
|
"button",
|
|
323
323
|
{
|
|
324
324
|
type: "button",
|
|
325
|
-
className: "text-muted-foreground hover:text-foreground disabled:opacity-
|
|
325
|
+
className: "text-muted-foreground hover:text-foreground disabled:opacity-50",
|
|
326
326
|
onClick: () => moveStage(index, "down"),
|
|
327
327
|
disabled: index === stages.length - 1,
|
|
328
328
|
"aria-label": t("customers.config.pipelineStages.moveDown", "Move down"),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/customers/backend/config/customers/pipeline-stages/page.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@open-mercato/ui/primitives/dialog'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { AppearanceSelector, type AppearanceSelectorLabels } from '@open-mercato/core/modules/dictionaries/components/AppearanceSelector'\nimport { renderDictionaryColor, renderDictionaryIcon, ICON_SUGGESTIONS } from '@open-mercato/core/modules/dictionaries/components/dictionaryAppearance'\n\ntype Pipeline = {\n id: string\n name: string\n isDefault: boolean\n}\n\ntype PipelineStage = {\n id: string\n pipelineId: string\n label: string\n order: number\n color: string | null\n icon: string | null\n}\n\ntype PipelineDialogState =\n | { mode: 'create' }\n | { mode: 'edit'; pipeline: Pipeline }\n | null\n\ntype StageDialogState =\n | { mode: 'create' }\n | { mode: 'edit'; stage: PipelineStage }\n | null\n\nexport default function PipelineStagesPage() {\n const t = useT()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n\n const [pipelines, setPipelines] = React.useState<Pipeline[]>([])\n const [selectedPipelineId, setSelectedPipelineId] = React.useState<string | null>(null)\n const [stages, setStages] = React.useState<PipelineStage[]>([])\n const [loadingPipelines, setLoadingPipelines] = React.useState(true)\n const [loadingStages, setLoadingStages] = React.useState(false)\n const [pipelineDialog, setPipelineDialog] = React.useState<PipelineDialogState>(null)\n const [stageDialog, setStageDialog] = React.useState<StageDialogState>(null)\n const [pipelineName, setPipelineName] = React.useState('')\n const [pipelineIsDefault, setPipelineIsDefault] = React.useState(false)\n const [stageName, setStageName] = React.useState('')\n const [stageColor, setStageColor] = React.useState<string | null>(null)\n const [stageIcon, setStageIcon] = React.useState<string | null>(null)\n const [saving, setSaving] = React.useState(false)\n\n const selectedPipeline = React.useMemo(\n () => pipelines.find((p) => p.id === selectedPipelineId) ?? null,\n [pipelines, selectedPipelineId],\n )\n\n const loadPipelines = React.useCallback(async () => {\n setLoadingPipelines(true)\n try {\n const result = await apiCall<{ items: Pipeline[] }>('/api/customers/pipelines')\n if (result.ok && result.result?.items) {\n const items = result.result.items\n setPipelines(items)\n if (!selectedPipelineId && items.length > 0) {\n const defaultPipeline = items.find((p) => p.isDefault) ?? items[0]\n setSelectedPipelineId(defaultPipeline.id)\n }\n }\n } catch {\n flash(t('customers.config.pipelineStages.errorLoadPipelines', 'Failed to load pipelines'), 'error')\n } finally {\n setLoadingPipelines(false)\n }\n }, [selectedPipelineId, t])\n\n const loadStages = React.useCallback(async (pipelineId: string) => {\n setLoadingStages(true)\n try {\n const result = await apiCall<{ items: PipelineStage[] }>(\n `/api/customers/pipeline-stages?pipelineId=${encodeURIComponent(pipelineId)}`\n )\n if (result.ok && result.result?.items) {\n setStages(result.result.items)\n }\n } catch {\n flash(t('customers.config.pipelineStages.errorLoadStages', 'Failed to load pipeline stages'), 'error')\n } finally {\n setLoadingStages(false)\n }\n }, [t])\n\n React.useEffect(() => {\n void loadPipelines()\n }, [loadPipelines])\n\n React.useEffect(() => {\n if (selectedPipelineId) {\n void loadStages(selectedPipelineId)\n } else {\n setStages([])\n }\n }, [selectedPipelineId, loadStages])\n\n function openCreatePipeline() {\n setPipelineName('')\n setPipelineIsDefault(false)\n setPipelineDialog({ mode: 'create' })\n }\n\n function openEditPipeline(pipeline: Pipeline) {\n setPipelineName(pipeline.name)\n setPipelineIsDefault(pipeline.isDefault)\n setPipelineDialog({ mode: 'edit', pipeline })\n }\n\n async function savePipeline() {\n if (!pipelineName.trim()) return\n setSaving(true)\n try {\n if (pipelineDialog?.mode === 'create') {\n const result = await apiCall<{ id: string }>('/api/customers/pipelines', {\n method: 'POST',\n body: JSON.stringify({ name: pipelineName.trim(), isDefault: pipelineIsDefault }),\n headers: { 'Content-Type': 'application/json' },\n })\n if (!result.ok) {\n flash(t('customers.config.pipelineStages.errorCreatePipeline', 'Failed to create pipeline'), 'error')\n return\n }\n flash(t('customers.config.pipelineStages.createdPipeline', 'Pipeline created'), 'success')\n const newId = result.result?.id ?? null\n await loadPipelines()\n if (newId) setSelectedPipelineId(newId)\n } else if (pipelineDialog?.mode === 'edit') {\n const result = await apiCall('/api/customers/pipelines', {\n method: 'PUT',\n body: JSON.stringify({ id: pipelineDialog.pipeline.id, name: pipelineName.trim(), isDefault: pipelineIsDefault }),\n headers: { 'Content-Type': 'application/json' },\n })\n if (!result.ok) {\n flash(t('customers.config.pipelineStages.errorUpdatePipeline', 'Failed to update pipeline'), 'error')\n return\n }\n flash(t('customers.config.pipelineStages.updatedPipeline', 'Pipeline updated'), 'success')\n await loadPipelines()\n }\n setPipelineDialog(null)\n } finally {\n setSaving(false)\n }\n }\n\n async function deletePipeline(pipeline: Pipeline) {\n const confirmed = await confirm({\n title: t('customers.config.pipelineStages.deletePipelineTitle', 'Delete pipeline?'),\n text: t(\n 'customers.config.pipelineStages.deletePipelineDesc',\n 'This pipeline and all its stages will be permanently removed. Deals assigned to it will lose their pipeline assignment.',\n ),\n confirmText: t('customers.config.pipelineStages.deletePipelineConfirm', 'Delete'),\n variant: 'destructive',\n })\n if (!confirmed) return\n const result = await apiCall('/api/customers/pipelines', {\n method: 'DELETE',\n body: JSON.stringify({ id: pipeline.id }),\n headers: { 'Content-Type': 'application/json' },\n })\n if (!result.ok) {\n const error = (result.result as { error?: string })?.error\n flash(error ?? t('customers.config.pipelineStages.errorDeletePipeline', 'Failed to delete pipeline'), 'error')\n return\n }\n flash(t('customers.config.pipelineStages.deletedPipeline', 'Pipeline deleted'), 'success')\n if (selectedPipelineId === pipeline.id) setSelectedPipelineId(null)\n await loadPipelines()\n }\n\n function openCreateStage() {\n setStageName('')\n setStageColor(null)\n setStageIcon(null)\n setStageDialog({ mode: 'create' })\n }\n\n function openEditStage(stage: PipelineStage) {\n setStageName(stage.label)\n setStageColor(stage.color)\n setStageIcon(stage.icon)\n setStageDialog({ mode: 'edit', stage })\n }\n\n async function saveStage() {\n if (!stageName.trim() || !selectedPipelineId) return\n setSaving(true)\n try {\n if (stageDialog?.mode === 'create') {\n const result = await apiCall('/api/customers/pipeline-stages', {\n method: 'POST',\n body: JSON.stringify({ pipelineId: selectedPipelineId, label: stageName.trim(), color: stageColor, icon: stageIcon }),\n headers: { 'Content-Type': 'application/json' },\n })\n if (!result.ok) {\n flash(t('customers.config.pipelineStages.errorCreateStage', 'Failed to create stage'), 'error')\n return\n }\n flash(t('customers.config.pipelineStages.createdStage', 'Stage created'), 'success')\n } else if (stageDialog?.mode === 'edit') {\n const result = await apiCall('/api/customers/pipeline-stages', {\n method: 'PUT',\n body: JSON.stringify({ id: stageDialog.stage.id, label: stageName.trim(), color: stageColor, icon: stageIcon }),\n headers: { 'Content-Type': 'application/json' },\n })\n if (!result.ok) {\n flash(t('customers.config.pipelineStages.errorUpdateStage', 'Failed to update stage'), 'error')\n return\n }\n flash(t('customers.config.pipelineStages.updatedStage', 'Stage updated'), 'success')\n }\n setStageDialog(null)\n await loadStages(selectedPipelineId)\n } finally {\n setSaving(false)\n }\n }\n\n async function deleteStage(stage: PipelineStage) {\n const confirmed = await confirm({\n title: t('customers.config.pipelineStages.deleteStagTitle', 'Delete stage?'),\n text: t(\n 'customers.config.pipelineStages.deleteStageDesc',\n 'This stage will be permanently removed. Deals assigned to it will lose their stage assignment.',\n ),\n confirmText: t('customers.config.pipelineStages.deleteStageConfirm', 'Delete'),\n variant: 'destructive',\n })\n if (!confirmed) return\n const result = await apiCall('/api/customers/pipeline-stages', {\n method: 'DELETE',\n body: JSON.stringify({ id: stage.id }),\n headers: { 'Content-Type': 'application/json' },\n })\n if (!result.ok) {\n const error = (result.result as { error?: string })?.error\n flash(error ?? t('customers.config.pipelineStages.errorDeleteStage', 'Failed to delete stage'), 'error')\n return\n }\n flash(t('customers.config.pipelineStages.deletedStage', 'Stage deleted'), 'success')\n if (selectedPipelineId) await loadStages(selectedPipelineId)\n }\n\n async function moveStage(index: number, direction: 'up' | 'down') {\n const nextIndex = direction === 'up' ? index - 1 : index + 1\n if (nextIndex < 0 || nextIndex >= stages.length) return\n\n const reordered = [...stages]\n const [moved] = reordered.splice(index, 1)\n reordered.splice(nextIndex, 0, moved)\n\n const updated = reordered.map((stage, i) => ({ ...stage, order: i }))\n setStages(updated)\n\n const result = await apiCall('/api/customers/pipeline-stages/reorder', {\n method: 'POST',\n body: JSON.stringify({ stages: updated.map((s) => ({ id: s.id, order: s.order })) }),\n headers: { 'Content-Type': 'application/json' },\n })\n if (!result.ok) {\n flash(t('customers.config.pipelineStages.errorReorder', 'Failed to reorder stages'), 'error')\n if (selectedPipelineId) await loadStages(selectedPipelineId)\n }\n }\n\n const appearanceLabels = React.useMemo<AppearanceSelectorLabels>(() => ({\n colorLabel: t('customers.config.pipelineStages.colorLabel', 'Color'),\n colorHelp: t('customers.config.pipelineStages.colorHelp', 'Pick a highlight color for this entry.'),\n colorClearLabel: t('customers.config.pipelineStages.colorClear', 'Remove color'),\n iconLabel: t('customers.config.pipelineStages.iconLabel', 'Icon'),\n iconPlaceholder: t('customers.config.pipelineStages.iconPlaceholder', 'Type an emoji or pick one of the suggestions.'),\n iconPickerTriggerLabel: t('customers.config.pipelineStages.iconBrowse', 'Browse icons and emojis'),\n iconSearchPlaceholder: t('customers.config.pipelineStages.iconSearchPlaceholder', 'Search icons or emojis\u2026'),\n iconSearchEmptyLabel: t('customers.config.pipelineStages.iconSearchEmpty', 'No icons match your search.'),\n iconSuggestionsLabel: t('customers.config.pipelineStages.iconSuggestions', 'Suggestions'),\n iconClearLabel: t('customers.config.pipelineStages.iconClear', 'Remove icon'),\n previewEmptyLabel: t('customers.config.pipelineStages.previewEmpty', 'None'),\n }), [t])\n\n return (\n <Page>\n <PageBody>\n <div className=\"space-y-6 max-w-2xl\">\n <div className=\"flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold\">\n {t('customers.config.pipelineStages.title', 'Pipeline stages')}\n </h2>\n <a\n href=\"/backend/customers/deals/pipeline\"\n className=\"text-sm text-muted-foreground hover:underline\"\n >\n {t('customers.config.pipelineStages.viewBoard', 'View pipeline board')} \u2192\n </a>\n </div>\n\n {loadingPipelines ? (\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <Spinner size=\"sm\" />\n {t('customers.config.pipelineStages.loadingPipelines', 'Loading pipelines\u2026')}\n </div>\n ) : (\n <>\n <div className=\"flex items-center gap-3\">\n <select\n className=\"flex h-9 w-full max-w-xs rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-ring\"\n value={selectedPipelineId ?? ''}\n onChange={(e) => setSelectedPipelineId(e.target.value || null)}\n >\n {pipelines.length === 0 && (\n <option value=\"\">\n {t('customers.config.pipelineStages.noPipelines', 'No pipelines yet')}\n </option>\n )}\n {pipelines.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}{p.isDefault ? ` (${t('customers.config.pipelineStages.default', 'default')})` : ''}\n </option>\n ))}\n </select>\n {selectedPipeline && (\n <>\n <Button variant=\"outline\" size=\"sm\" onClick={() => openEditPipeline(selectedPipeline)}>\n {t('customers.config.pipelineStages.editPipeline', 'Edit')}\n </Button>\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"text-destructive\"\n onClick={() => deletePipeline(selectedPipeline)}\n >\n {t('customers.config.pipelineStages.deletePipeline', 'Delete')}\n </Button>\n </>\n )}\n <Button variant=\"outline\" size=\"sm\" onClick={openCreatePipeline}>\n {t('customers.config.pipelineStages.addPipeline', '+ Add pipeline')}\n </Button>\n </div>\n\n {selectedPipelineId && (\n <div className=\"space-y-3\">\n <div className=\"flex items-center justify-between\">\n <h3 className=\"text-sm font-medium text-muted-foreground\">\n {t('customers.config.pipelineStages.stagesTitle', 'Stages')}\n </h3>\n <Button size=\"sm\" onClick={openCreateStage}>\n {t('customers.config.pipelineStages.addStage', '+ Add stage')}\n </Button>\n </div>\n\n {loadingStages ? (\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <Spinner size=\"sm\" />\n {t('customers.config.pipelineStages.loadingStages', 'Loading stages\u2026')}\n </div>\n ) : stages.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n {t('customers.config.pipelineStages.noStages', 'No stages yet. Add your first stage.')}\n </p>\n ) : (\n <div className=\"divide-y rounded-md border\">\n {stages.map((stage, index) => (\n <div key={stage.id} className=\"flex items-center gap-3 px-4 py-3\">\n <div className=\"flex flex-col gap-1\">\n <button\n type=\"button\"\n className=\"text-muted-foreground hover:text-foreground disabled:opacity-30\"\n onClick={() => moveStage(index, 'up')}\n disabled={index === 0}\n aria-label={t('customers.config.pipelineStages.moveUp', 'Move up')}\n >\n \u2191\n </button>\n <button\n type=\"button\"\n className=\"text-muted-foreground hover:text-foreground disabled:opacity-30\"\n onClick={() => moveStage(index, 'down')}\n disabled={index === stages.length - 1}\n aria-label={t('customers.config.pipelineStages.moveDown', 'Move down')}\n >\n \u2193\n </button>\n </div>\n <span className=\"flex-1 text-sm flex items-center gap-2\">\n {stage.color ? renderDictionaryColor(stage.color) : null}\n {stage.icon ? renderDictionaryIcon(stage.icon) : null}\n {stage.label}\n </span>\n <Button variant=\"ghost\" size=\"sm\" onClick={() => openEditStage(stage)}>\n {t('customers.config.pipelineStages.editStage', 'Edit')}\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"text-destructive\"\n onClick={() => deleteStage(stage)}\n >\n {t('customers.config.pipelineStages.deleteStage', 'Delete')}\n </Button>\n </div>\n ))}\n </div>\n )}\n </div>\n )}\n </>\n )}\n </div>\n\n <Dialog open={pipelineDialog !== null} onOpenChange={(open) => { if (!open) setPipelineDialog(null) }}>\n <DialogContent>\n <DialogHeader>\n <DialogTitle>\n {pipelineDialog?.mode === 'create'\n ? t('customers.config.pipelineStages.createPipelineTitle', 'Create pipeline')\n : t('customers.config.pipelineStages.editPipelineTitle', 'Edit pipeline')}\n </DialogTitle>\n </DialogHeader>\n <div className=\"space-y-4 py-2\">\n <div className=\"space-y-1\">\n <label className=\"text-sm font-medium\">\n {t('customers.config.pipelineStages.pipelineName', 'Name')}\n </label>\n <Input\n value={pipelineName}\n onChange={(e) => setPipelineName(e.target.value)}\n placeholder={t('customers.config.pipelineStages.pipelineNamePlaceholder', 'e.g. Sales Pipeline')}\n onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); void savePipeline() } }}\n autoFocus\n />\n </div>\n <label className=\"flex items-center gap-2 text-sm cursor-pointer\">\n <input\n type=\"checkbox\"\n checked={pipelineIsDefault}\n onChange={(e) => setPipelineIsDefault(e.target.checked)}\n />\n {t('customers.config.pipelineStages.setAsDefault', 'Set as default pipeline')}\n </label>\n </div>\n <DialogFooter>\n <Button variant=\"outline\" onClick={() => setPipelineDialog(null)} disabled={saving}>\n {t('customers.config.pipelineStages.cancel', 'Cancel')}\n </Button>\n <Button onClick={() => void savePipeline()} disabled={saving || !pipelineName.trim()}>\n {saving ? <Spinner size=\"sm\" /> : t('customers.config.pipelineStages.save', 'Save')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n\n <Dialog open={stageDialog !== null} onOpenChange={(open) => { if (!open) setStageDialog(null) }}>\n <DialogContent>\n <DialogHeader>\n <DialogTitle>\n {stageDialog?.mode === 'create'\n ? t('customers.config.pipelineStages.createStageTitle', 'Create stage')\n : t('customers.config.pipelineStages.editStageTitle', 'Edit stage')}\n </DialogTitle>\n </DialogHeader>\n <div className=\"space-y-4 py-2\">\n <div className=\"space-y-1\">\n <label className=\"text-sm font-medium\">\n {t('customers.config.pipelineStages.stageName', 'Stage name')}\n </label>\n <Input\n value={stageName}\n onChange={(e) => setStageName(e.target.value)}\n placeholder={t('customers.config.pipelineStages.stageNamePlaceholder', 'e.g. Qualification')}\n onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); void saveStage() } }}\n autoFocus\n />\n </div>\n <AppearanceSelector\n color={stageColor}\n icon={stageIcon}\n onColorChange={setStageColor}\n onIconChange={setStageIcon}\n labels={appearanceLabels}\n iconSuggestions={ICON_SUGGESTIONS}\n />\n </div>\n <DialogFooter>\n <Button variant=\"outline\" onClick={() => setStageDialog(null)} disabled={saving}>\n {t('customers.config.pipelineStages.cancel', 'Cancel')}\n </Button>\n <Button onClick={() => void saveStage()} disabled={saving || !stageName.trim()}>\n {saving ? <Spinner size=\"sm\" /> : t('customers.config.pipelineStages.save', 'Save')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n {ConfirmDialogElement}\n </PageBody>\n </Page>\n )\n}\n"],
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@open-mercato/ui/primitives/dialog'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { AppearanceSelector, type AppearanceSelectorLabels } from '@open-mercato/core/modules/dictionaries/components/AppearanceSelector'\nimport { renderDictionaryColor, renderDictionaryIcon, ICON_SUGGESTIONS } from '@open-mercato/core/modules/dictionaries/components/dictionaryAppearance'\n\ntype Pipeline = {\n id: string\n name: string\n isDefault: boolean\n}\n\ntype PipelineStage = {\n id: string\n pipelineId: string\n label: string\n order: number\n color: string | null\n icon: string | null\n}\n\ntype PipelineDialogState =\n | { mode: 'create' }\n | { mode: 'edit'; pipeline: Pipeline }\n | null\n\ntype StageDialogState =\n | { mode: 'create' }\n | { mode: 'edit'; stage: PipelineStage }\n | null\n\nexport default function PipelineStagesPage() {\n const t = useT()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n\n const [pipelines, setPipelines] = React.useState<Pipeline[]>([])\n const [selectedPipelineId, setSelectedPipelineId] = React.useState<string | null>(null)\n const [stages, setStages] = React.useState<PipelineStage[]>([])\n const [loadingPipelines, setLoadingPipelines] = React.useState(true)\n const [loadingStages, setLoadingStages] = React.useState(false)\n const [pipelineDialog, setPipelineDialog] = React.useState<PipelineDialogState>(null)\n const [stageDialog, setStageDialog] = React.useState<StageDialogState>(null)\n const [pipelineName, setPipelineName] = React.useState('')\n const [pipelineIsDefault, setPipelineIsDefault] = React.useState(false)\n const [stageName, setStageName] = React.useState('')\n const [stageColor, setStageColor] = React.useState<string | null>(null)\n const [stageIcon, setStageIcon] = React.useState<string | null>(null)\n const [saving, setSaving] = React.useState(false)\n\n const selectedPipeline = React.useMemo(\n () => pipelines.find((p) => p.id === selectedPipelineId) ?? null,\n [pipelines, selectedPipelineId],\n )\n\n const loadPipelines = React.useCallback(async () => {\n setLoadingPipelines(true)\n try {\n const result = await apiCall<{ items: Pipeline[] }>('/api/customers/pipelines')\n if (result.ok && result.result?.items) {\n const items = result.result.items\n setPipelines(items)\n if (!selectedPipelineId && items.length > 0) {\n const defaultPipeline = items.find((p) => p.isDefault) ?? items[0]\n setSelectedPipelineId(defaultPipeline.id)\n }\n }\n } catch {\n flash(t('customers.config.pipelineStages.errorLoadPipelines', 'Failed to load pipelines'), 'error')\n } finally {\n setLoadingPipelines(false)\n }\n }, [selectedPipelineId, t])\n\n const loadStages = React.useCallback(async (pipelineId: string) => {\n setLoadingStages(true)\n try {\n const result = await apiCall<{ items: PipelineStage[] }>(\n `/api/customers/pipeline-stages?pipelineId=${encodeURIComponent(pipelineId)}`\n )\n if (result.ok && result.result?.items) {\n setStages(result.result.items)\n }\n } catch {\n flash(t('customers.config.pipelineStages.errorLoadStages', 'Failed to load pipeline stages'), 'error')\n } finally {\n setLoadingStages(false)\n }\n }, [t])\n\n React.useEffect(() => {\n void loadPipelines()\n }, [loadPipelines])\n\n React.useEffect(() => {\n if (selectedPipelineId) {\n void loadStages(selectedPipelineId)\n } else {\n setStages([])\n }\n }, [selectedPipelineId, loadStages])\n\n function openCreatePipeline() {\n setPipelineName('')\n setPipelineIsDefault(false)\n setPipelineDialog({ mode: 'create' })\n }\n\n function openEditPipeline(pipeline: Pipeline) {\n setPipelineName(pipeline.name)\n setPipelineIsDefault(pipeline.isDefault)\n setPipelineDialog({ mode: 'edit', pipeline })\n }\n\n async function savePipeline() {\n if (!pipelineName.trim()) return\n setSaving(true)\n try {\n if (pipelineDialog?.mode === 'create') {\n const result = await apiCall<{ id: string }>('/api/customers/pipelines', {\n method: 'POST',\n body: JSON.stringify({ name: pipelineName.trim(), isDefault: pipelineIsDefault }),\n headers: { 'Content-Type': 'application/json' },\n })\n if (!result.ok) {\n flash(t('customers.config.pipelineStages.errorCreatePipeline', 'Failed to create pipeline'), 'error')\n return\n }\n flash(t('customers.config.pipelineStages.createdPipeline', 'Pipeline created'), 'success')\n const newId = result.result?.id ?? null\n await loadPipelines()\n if (newId) setSelectedPipelineId(newId)\n } else if (pipelineDialog?.mode === 'edit') {\n const result = await apiCall('/api/customers/pipelines', {\n method: 'PUT',\n body: JSON.stringify({ id: pipelineDialog.pipeline.id, name: pipelineName.trim(), isDefault: pipelineIsDefault }),\n headers: { 'Content-Type': 'application/json' },\n })\n if (!result.ok) {\n flash(t('customers.config.pipelineStages.errorUpdatePipeline', 'Failed to update pipeline'), 'error')\n return\n }\n flash(t('customers.config.pipelineStages.updatedPipeline', 'Pipeline updated'), 'success')\n await loadPipelines()\n }\n setPipelineDialog(null)\n } finally {\n setSaving(false)\n }\n }\n\n async function deletePipeline(pipeline: Pipeline) {\n const confirmed = await confirm({\n title: t('customers.config.pipelineStages.deletePipelineTitle', 'Delete pipeline?'),\n text: t(\n 'customers.config.pipelineStages.deletePipelineDesc',\n 'This pipeline and all its stages will be permanently removed. Deals assigned to it will lose their pipeline assignment.',\n ),\n confirmText: t('customers.config.pipelineStages.deletePipelineConfirm', 'Delete'),\n variant: 'destructive',\n })\n if (!confirmed) return\n const result = await apiCall('/api/customers/pipelines', {\n method: 'DELETE',\n body: JSON.stringify({ id: pipeline.id }),\n headers: { 'Content-Type': 'application/json' },\n })\n if (!result.ok) {\n const error = (result.result as { error?: string })?.error\n flash(error ?? t('customers.config.pipelineStages.errorDeletePipeline', 'Failed to delete pipeline'), 'error')\n return\n }\n flash(t('customers.config.pipelineStages.deletedPipeline', 'Pipeline deleted'), 'success')\n if (selectedPipelineId === pipeline.id) setSelectedPipelineId(null)\n await loadPipelines()\n }\n\n function openCreateStage() {\n setStageName('')\n setStageColor(null)\n setStageIcon(null)\n setStageDialog({ mode: 'create' })\n }\n\n function openEditStage(stage: PipelineStage) {\n setStageName(stage.label)\n setStageColor(stage.color)\n setStageIcon(stage.icon)\n setStageDialog({ mode: 'edit', stage })\n }\n\n async function saveStage() {\n if (!stageName.trim() || !selectedPipelineId) return\n setSaving(true)\n try {\n if (stageDialog?.mode === 'create') {\n const result = await apiCall('/api/customers/pipeline-stages', {\n method: 'POST',\n body: JSON.stringify({ pipelineId: selectedPipelineId, label: stageName.trim(), color: stageColor, icon: stageIcon }),\n headers: { 'Content-Type': 'application/json' },\n })\n if (!result.ok) {\n flash(t('customers.config.pipelineStages.errorCreateStage', 'Failed to create stage'), 'error')\n return\n }\n flash(t('customers.config.pipelineStages.createdStage', 'Stage created'), 'success')\n } else if (stageDialog?.mode === 'edit') {\n const result = await apiCall('/api/customers/pipeline-stages', {\n method: 'PUT',\n body: JSON.stringify({ id: stageDialog.stage.id, label: stageName.trim(), color: stageColor, icon: stageIcon }),\n headers: { 'Content-Type': 'application/json' },\n })\n if (!result.ok) {\n flash(t('customers.config.pipelineStages.errorUpdateStage', 'Failed to update stage'), 'error')\n return\n }\n flash(t('customers.config.pipelineStages.updatedStage', 'Stage updated'), 'success')\n }\n setStageDialog(null)\n await loadStages(selectedPipelineId)\n } finally {\n setSaving(false)\n }\n }\n\n async function deleteStage(stage: PipelineStage) {\n const confirmed = await confirm({\n title: t('customers.config.pipelineStages.deleteStagTitle', 'Delete stage?'),\n text: t(\n 'customers.config.pipelineStages.deleteStageDesc',\n 'This stage will be permanently removed. Deals assigned to it will lose their stage assignment.',\n ),\n confirmText: t('customers.config.pipelineStages.deleteStageConfirm', 'Delete'),\n variant: 'destructive',\n })\n if (!confirmed) return\n const result = await apiCall('/api/customers/pipeline-stages', {\n method: 'DELETE',\n body: JSON.stringify({ id: stage.id }),\n headers: { 'Content-Type': 'application/json' },\n })\n if (!result.ok) {\n const error = (result.result as { error?: string })?.error\n flash(error ?? t('customers.config.pipelineStages.errorDeleteStage', 'Failed to delete stage'), 'error')\n return\n }\n flash(t('customers.config.pipelineStages.deletedStage', 'Stage deleted'), 'success')\n if (selectedPipelineId) await loadStages(selectedPipelineId)\n }\n\n async function moveStage(index: number, direction: 'up' | 'down') {\n const nextIndex = direction === 'up' ? index - 1 : index + 1\n if (nextIndex < 0 || nextIndex >= stages.length) return\n\n const reordered = [...stages]\n const [moved] = reordered.splice(index, 1)\n reordered.splice(nextIndex, 0, moved)\n\n const updated = reordered.map((stage, i) => ({ ...stage, order: i }))\n setStages(updated)\n\n const result = await apiCall('/api/customers/pipeline-stages/reorder', {\n method: 'POST',\n body: JSON.stringify({ stages: updated.map((s) => ({ id: s.id, order: s.order })) }),\n headers: { 'Content-Type': 'application/json' },\n })\n if (!result.ok) {\n flash(t('customers.config.pipelineStages.errorReorder', 'Failed to reorder stages'), 'error')\n if (selectedPipelineId) await loadStages(selectedPipelineId)\n }\n }\n\n const appearanceLabels = React.useMemo<AppearanceSelectorLabels>(() => ({\n colorLabel: t('customers.config.pipelineStages.colorLabel', 'Color'),\n colorHelp: t('customers.config.pipelineStages.colorHelp', 'Pick a highlight color for this entry.'),\n colorClearLabel: t('customers.config.pipelineStages.colorClear', 'Remove color'),\n iconLabel: t('customers.config.pipelineStages.iconLabel', 'Icon'),\n iconPlaceholder: t('customers.config.pipelineStages.iconPlaceholder', 'Type an emoji or pick one of the suggestions.'),\n iconPickerTriggerLabel: t('customers.config.pipelineStages.iconBrowse', 'Browse icons and emojis'),\n iconSearchPlaceholder: t('customers.config.pipelineStages.iconSearchPlaceholder', 'Search icons or emojis\u2026'),\n iconSearchEmptyLabel: t('customers.config.pipelineStages.iconSearchEmpty', 'No icons match your search.'),\n iconSuggestionsLabel: t('customers.config.pipelineStages.iconSuggestions', 'Suggestions'),\n iconClearLabel: t('customers.config.pipelineStages.iconClear', 'Remove icon'),\n previewEmptyLabel: t('customers.config.pipelineStages.previewEmpty', 'None'),\n }), [t])\n\n return (\n <Page>\n <PageBody>\n <div className=\"space-y-6 max-w-2xl\">\n <div className=\"flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold\">\n {t('customers.config.pipelineStages.title', 'Pipeline stages')}\n </h2>\n <a\n href=\"/backend/customers/deals/pipeline\"\n className=\"text-sm text-muted-foreground hover:underline\"\n >\n {t('customers.config.pipelineStages.viewBoard', 'View pipeline board')} \u2192\n </a>\n </div>\n\n {loadingPipelines ? (\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <Spinner size=\"sm\" />\n {t('customers.config.pipelineStages.loadingPipelines', 'Loading pipelines\u2026')}\n </div>\n ) : (\n <>\n <div className=\"flex items-center gap-3\">\n <select\n className=\"flex h-9 w-full max-w-xs rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring\"\n value={selectedPipelineId ?? ''}\n onChange={(e) => setSelectedPipelineId(e.target.value || null)}\n >\n {pipelines.length === 0 && (\n <option value=\"\">\n {t('customers.config.pipelineStages.noPipelines', 'No pipelines yet')}\n </option>\n )}\n {pipelines.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}{p.isDefault ? ` (${t('customers.config.pipelineStages.default', 'default')})` : ''}\n </option>\n ))}\n </select>\n {selectedPipeline && (\n <>\n <Button variant=\"outline\" size=\"sm\" onClick={() => openEditPipeline(selectedPipeline)}>\n {t('customers.config.pipelineStages.editPipeline', 'Edit')}\n </Button>\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"text-destructive\"\n onClick={() => deletePipeline(selectedPipeline)}\n >\n {t('customers.config.pipelineStages.deletePipeline', 'Delete')}\n </Button>\n </>\n )}\n <Button variant=\"outline\" size=\"sm\" onClick={openCreatePipeline}>\n {t('customers.config.pipelineStages.addPipeline', '+ Add pipeline')}\n </Button>\n </div>\n\n {selectedPipelineId && (\n <div className=\"space-y-3\">\n <div className=\"flex items-center justify-between\">\n <h3 className=\"text-sm font-medium text-muted-foreground\">\n {t('customers.config.pipelineStages.stagesTitle', 'Stages')}\n </h3>\n <Button size=\"sm\" onClick={openCreateStage}>\n {t('customers.config.pipelineStages.addStage', '+ Add stage')}\n </Button>\n </div>\n\n {loadingStages ? (\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <Spinner size=\"sm\" />\n {t('customers.config.pipelineStages.loadingStages', 'Loading stages\u2026')}\n </div>\n ) : stages.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n {t('customers.config.pipelineStages.noStages', 'No stages yet. Add your first stage.')}\n </p>\n ) : (\n <div className=\"divide-y rounded-md border\">\n {stages.map((stage, index) => (\n <div key={stage.id} className=\"flex items-center gap-3 px-4 py-3\">\n <div className=\"flex flex-col gap-1\">\n <button\n type=\"button\"\n className=\"text-muted-foreground hover:text-foreground disabled:opacity-50\"\n onClick={() => moveStage(index, 'up')}\n disabled={index === 0}\n aria-label={t('customers.config.pipelineStages.moveUp', 'Move up')}\n >\n \u2191\n </button>\n <button\n type=\"button\"\n className=\"text-muted-foreground hover:text-foreground disabled:opacity-50\"\n onClick={() => moveStage(index, 'down')}\n disabled={index === stages.length - 1}\n aria-label={t('customers.config.pipelineStages.moveDown', 'Move down')}\n >\n \u2193\n </button>\n </div>\n <span className=\"flex-1 text-sm flex items-center gap-2\">\n {stage.color ? renderDictionaryColor(stage.color) : null}\n {stage.icon ? renderDictionaryIcon(stage.icon) : null}\n {stage.label}\n </span>\n <Button variant=\"ghost\" size=\"sm\" onClick={() => openEditStage(stage)}>\n {t('customers.config.pipelineStages.editStage', 'Edit')}\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"text-destructive\"\n onClick={() => deleteStage(stage)}\n >\n {t('customers.config.pipelineStages.deleteStage', 'Delete')}\n </Button>\n </div>\n ))}\n </div>\n )}\n </div>\n )}\n </>\n )}\n </div>\n\n <Dialog open={pipelineDialog !== null} onOpenChange={(open) => { if (!open) setPipelineDialog(null) }}>\n <DialogContent>\n <DialogHeader>\n <DialogTitle>\n {pipelineDialog?.mode === 'create'\n ? t('customers.config.pipelineStages.createPipelineTitle', 'Create pipeline')\n : t('customers.config.pipelineStages.editPipelineTitle', 'Edit pipeline')}\n </DialogTitle>\n </DialogHeader>\n <div className=\"space-y-4 py-2\">\n <div className=\"space-y-1\">\n <label className=\"text-sm font-medium\">\n {t('customers.config.pipelineStages.pipelineName', 'Name')}\n </label>\n <Input\n value={pipelineName}\n onChange={(e) => setPipelineName(e.target.value)}\n placeholder={t('customers.config.pipelineStages.pipelineNamePlaceholder', 'e.g. Sales Pipeline')}\n onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); void savePipeline() } }}\n autoFocus\n />\n </div>\n <label className=\"flex items-center gap-2 text-sm cursor-pointer\">\n <input\n type=\"checkbox\"\n checked={pipelineIsDefault}\n onChange={(e) => setPipelineIsDefault(e.target.checked)}\n />\n {t('customers.config.pipelineStages.setAsDefault', 'Set as default pipeline')}\n </label>\n </div>\n <DialogFooter>\n <Button variant=\"outline\" onClick={() => setPipelineDialog(null)} disabled={saving}>\n {t('customers.config.pipelineStages.cancel', 'Cancel')}\n </Button>\n <Button onClick={() => void savePipeline()} disabled={saving || !pipelineName.trim()}>\n {saving ? <Spinner size=\"sm\" /> : t('customers.config.pipelineStages.save', 'Save')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n\n <Dialog open={stageDialog !== null} onOpenChange={(open) => { if (!open) setStageDialog(null) }}>\n <DialogContent>\n <DialogHeader>\n <DialogTitle>\n {stageDialog?.mode === 'create'\n ? t('customers.config.pipelineStages.createStageTitle', 'Create stage')\n : t('customers.config.pipelineStages.editStageTitle', 'Edit stage')}\n </DialogTitle>\n </DialogHeader>\n <div className=\"space-y-4 py-2\">\n <div className=\"space-y-1\">\n <label className=\"text-sm font-medium\">\n {t('customers.config.pipelineStages.stageName', 'Stage name')}\n </label>\n <Input\n value={stageName}\n onChange={(e) => setStageName(e.target.value)}\n placeholder={t('customers.config.pipelineStages.stageNamePlaceholder', 'e.g. Qualification')}\n onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); void saveStage() } }}\n autoFocus\n />\n </div>\n <AppearanceSelector\n color={stageColor}\n icon={stageIcon}\n onColorChange={setStageColor}\n onIconChange={setStageIcon}\n labels={appearanceLabels}\n iconSuggestions={ICON_SUGGESTIONS}\n />\n </div>\n <DialogFooter>\n <Button variant=\"outline\" onClick={() => setStageDialog(null)} disabled={saving}>\n {t('customers.config.pipelineStages.cancel', 'Cancel')}\n </Button>\n <Button onClick={() => void saveStage()} disabled={saving || !stageName.trim()}>\n {saving ? <Spinner size=\"sm\" /> : t('customers.config.pipelineStages.save', 'Save')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n {ConfirmDialogElement}\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -407,7 +407,7 @@ function SalesPipelinePage() {
|
|
|
407
407
|
"Showing the first {count} deals. Refine your filters to see more.",
|
|
408
408
|
{ count: deals.length }
|
|
409
409
|
) }) : null,
|
|
410
|
-
/* @__PURE__ */ jsx("div", { className: "flex flex-col gap-4 pb-6 md:flex-row md:overflow-x-auto", children: stages.length === 0 ? /* @__PURE__ */ jsx("div", { className: "flex h-[50vh] w-full items-center justify-center rounded-lg border border-dashed border-border bg-muted/
|
|
410
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-col gap-4 pb-6 md:flex-row md:overflow-x-auto", children: stages.length === 0 ? /* @__PURE__ */ jsx("div", { className: "flex h-[50vh] w-full items-center justify-center rounded-lg border border-dashed border-border bg-muted/30", children: /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: translate("customers.deals.pipeline.noStages", "Define pipeline stages to start tracking deals.") }) }) : stages.map((stage) => {
|
|
411
411
|
const stageKey = stage.value ?? null;
|
|
412
412
|
const laneDeals = groupedDeals.get(stageKey) ?? [];
|
|
413
413
|
const sortedLaneDeals = sortDeals(laneDeals, sortBy);
|
|
@@ -420,7 +420,7 @@ function SalesPipelinePage() {
|
|
|
420
420
|
onDrop: handleDrop(stage),
|
|
421
421
|
children: [
|
|
422
422
|
renderLaneHeader(stage, laneDeals.length),
|
|
423
|
-
/* @__PURE__ */ jsx("div", { className: "flex flex-1 flex-col gap-3 overflow-y-auto px-4 py-3", children: sortedLaneDeals.length === 0 ? /* @__PURE__ */ jsx("div", { className: "rounded-md border border-dashed border-border bg-muted/
|
|
423
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-1 flex-col gap-3 overflow-y-auto px-4 py-3", children: sortedLaneDeals.length === 0 ? /* @__PURE__ */ jsx("div", { className: "rounded-md border border-dashed border-border bg-muted/30 p-4 text-center text-xs text-muted-foreground", children: translate("customers.deals.pipeline.emptyLane", "No deals in this stage yet.") }) : sortedLaneDeals.map((deal) => {
|
|
424
424
|
const isDragging = draggingId === deal.id || pendingDealId === deal.id && updateStageMutation.isPending;
|
|
425
425
|
const valueLabel = formatCurrency(
|
|
426
426
|
deal.valueAmount,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/customers/backend/customers/deals/pipeline/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { ErrorNotice } from '@open-mercato/ui/primitives/ErrorNotice'\nimport { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\n\ntype DealAssociation = { id: string; label: string }\n\ntype DealRecord = {\n id: string\n title: string\n status: string | null\n pipelineStage: string | null\n pipelineId: string | null\n pipelineStageId: string | null\n valueAmount: number | null\n valueCurrency: string | null\n probability: number | null\n expectedCloseAt: string | null\n expectedCloseAtTs: number | null\n createdAt: string | null\n createdAtTs: number | null\n updatedAt: string | null\n people: DealAssociation[]\n companies: DealAssociation[]\n}\n\ntype DealsQueryData = {\n deals: DealRecord[]\n total: number\n}\n\ntype StageDefinition = {\n id: string\n value: string | null\n label: string\n color: string | null\n icon: string | null\n}\n\ntype SortOption = 'probability' | 'createdAt' | 'expectedCloseAt'\n\ntype PipelineRecord = { id: string; name: string; isDefault: boolean }\ntype PipelineStageRecord = { id: string; label: string; order: number; pipelineId: string }\n\nconst DEALS_QUERY_LIMIT = 100\n\nconst dealsQueryKey = (scopeVersion: number, pipelineId: string | null) =>\n ['customers', 'deals', 'pipeline', `scope:${scopeVersion}`, `pipeline:${pipelineId ?? 'none'}`] as const\n\nconst sortOptions: SortOption[] = ['probability', 'createdAt', 'expectedCloseAt']\n\nfunction normalizeAmount(value: unknown): number | null {\n if (typeof value === 'number' && Number.isFinite(value)) return value\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (!trimmed.length) return null\n const parsed = Number(trimmed)\n return Number.isFinite(parsed) ? parsed : null\n }\n return null\n}\n\nfunction normalizeProbability(value: unknown): number | null {\n const parsed = normalizeAmount(value)\n if (parsed === null) return null\n if (parsed < 0) return 0\n if (parsed > 100) return 100\n return Math.round(parsed)\n}\n\nfunction normalizeTimestamp(value: unknown): { iso: string | null; ts: number | null } {\n if (typeof value !== 'string' || !value.trim().length) return { iso: null, ts: null }\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return { iso: null, ts: null }\n return { iso: date.toISOString(), ts: date.getTime() }\n}\n\nfunction buildStageDefinitionsFromPipelineStages(\n pipelineStages: PipelineStageRecord[],\n deals: DealRecord[],\n t: ReturnType<typeof useT>,\n): StageDefinition[] {\n const result: StageDefinition[] = pipelineStages\n .slice()\n .sort((a, b) => a.order - b.order)\n .map((stage) => ({\n id: stage.id,\n value: stage.id,\n label: stage.label,\n color: null,\n icon: null,\n }))\n\n const knownIds = new Set(pipelineStages.map((s) => s.id))\n const hasUnassigned = deals.some((deal) => !deal.pipelineStageId || !knownIds.has(deal.pipelineStageId))\n if (hasUnassigned) {\n result.push({\n id: 'stage:__unassigned',\n value: null,\n label: translateWithFallback(t, 'customers.deals.pipeline.unassigned', 'No stage'),\n color: null,\n icon: null,\n })\n }\n\n return result\n}\n\nfunction createDealMap(deals: DealRecord[]): Map<string, DealRecord> {\n return deals.reduce<Map<string, DealRecord>>((acc, deal) => acc.set(deal.id, deal), new Map())\n}\n\nfunction groupDealsByStageId(deals: DealRecord[]): Map<string | null, DealRecord[]> {\n const byStage = new Map<string | null, DealRecord[]>()\n deals.forEach((deal) => {\n const stageKey = deal.pipelineStageId ?? null\n const bucket = byStage.get(stageKey) ?? []\n bucket.push(deal)\n byStage.set(stageKey, bucket)\n })\n return byStage\n}\n\nfunction sortDeals(deals: DealRecord[], option: SortOption): DealRecord[] {\n const sorted = [...deals]\n sorted.sort((a, b) => {\n if (option === 'probability') {\n const ap = typeof a.probability === 'number' ? a.probability : -1\n const bp = typeof b.probability === 'number' ? b.probability : -1\n if (ap !== bp) return bp - ap\n }\n if (option === 'expectedCloseAt') {\n const at = typeof a.expectedCloseAtTs === 'number' ? a.expectedCloseAtTs : Number.POSITIVE_INFINITY\n const bt = typeof b.expectedCloseAtTs === 'number' ? b.expectedCloseAtTs : Number.POSITIVE_INFINITY\n if (at !== bt) return at - bt\n }\n const at = typeof a.createdAtTs === 'number' ? a.createdAtTs : Number.NEGATIVE_INFINITY\n const bt = typeof b.createdAtTs === 'number' ? b.createdAtTs : Number.NEGATIVE_INFINITY\n if (option === 'createdAt') {\n if (at !== bt) return bt - at\n } else if (option === 'expectedCloseAt' || option === 'probability') {\n if (at !== bt) return bt - at\n }\n return a.title.localeCompare(b.title, undefined, { sensitivity: 'base' })\n })\n return sorted\n}\n\nfunction formatCurrency(amount: number | null, currency: string | null, fallback: string): string {\n if (amount === null || Number.isNaN(amount)) return fallback\n const code = currency && currency.length === 3 ? currency.toUpperCase() : 'USD'\n try {\n return new Intl.NumberFormat(undefined, {\n style: 'currency',\n currency: code,\n maximumFractionDigits: 2,\n }).format(amount)\n } catch {\n return `${code} ${amount.toFixed(2)}`\n }\n}\n\nfunction formatProbability(probability: number | null, fallback: string): string {\n if (typeof probability !== 'number' || Number.isNaN(probability)) return fallback\n return `${probability}%`\n}\n\nexport default function SalesPipelinePage(): React.ReactElement {\n const t = useT()\n const translate = React.useCallback(\n (key: string, fallback: string, params?: Record<string, string | number>) => {\n const value = translateWithFallback(t, key, fallback, params)\n if (value === fallback && params) {\n return fallback.replace(/\\{\\{(\\w+)\\}\\}|\\{(\\w+)\\}/g, (match, doubleToken, singleToken) => {\n const token = (doubleToken ?? singleToken) as string | undefined\n if (!token) return match\n const replacement = params[token]\n if (replacement === undefined) {\n return doubleToken ? `{{${token}}}` : `{${token}}`\n }\n return String(replacement)\n })\n }\n return value\n },\n [t],\n )\n const scopeVersion = useOrganizationScopeVersion()\n const queryClient = useQueryClient()\n const [sortBy, setSortBy] = React.useState<SortOption>('probability')\n const [pendingDealId, setPendingDealId] = React.useState<string | null>(null)\n const [selectedPipelineId, setSelectedPipelineId] = React.useState<string | null>(null)\n\n const pipelinesQuery = useQuery<PipelineRecord[]>({\n queryKey: ['customers', 'pipelines', `scope:${scopeVersion}`],\n staleTime: 60_000,\n queryFn: async () => {\n const payload = await readApiResultOrThrow<{ items: PipelineRecord[] }>(\n '/api/customers/pipelines',\n undefined,\n { errorMessage: translate('customers.deals.pipeline.loadError', 'Failed to load pipelines.') },\n )\n return payload?.items ?? []\n },\n })\n\n React.useEffect(() => {\n if (selectedPipelineId) return\n const pipelines = pipelinesQuery.data\n if (!pipelines || !pipelines.length) return\n const defaultPipeline = pipelines.find((p) => p.isDefault) ?? pipelines[0]\n if (defaultPipeline) setSelectedPipelineId(defaultPipeline.id)\n }, [pipelinesQuery.data, selectedPipelineId])\n\n const stagesQuery = useQuery<PipelineStageRecord[]>({\n queryKey: ['customers', 'pipeline-stages', `scope:${scopeVersion}`, `pipeline:${selectedPipelineId}`],\n enabled: !!selectedPipelineId,\n staleTime: 30_000,\n queryFn: async () => {\n const payload = await readApiResultOrThrow<{ items: PipelineStageRecord[] }>(\n `/api/customers/pipeline-stages?pipelineId=${encodeURIComponent(selectedPipelineId!)}`,\n undefined,\n { errorMessage: translate('customers.deals.pipeline.loadError', 'Failed to load stages.') },\n )\n return payload?.items ?? []\n },\n })\n\n const dealsKey = React.useMemo(() => dealsQueryKey(scopeVersion, selectedPipelineId), [scopeVersion, selectedPipelineId])\n\n const dealsQuery = useQuery<DealsQueryData>({\n queryKey: dealsKey,\n enabled: !!selectedPipelineId,\n staleTime: 30_000,\n queryFn: async () => {\n const search = new URLSearchParams()\n search.set('page', '1')\n search.set('pageSize', String(DEALS_QUERY_LIMIT))\n search.set('sortField', 'createdAt')\n search.set('sortDir', 'desc')\n if (selectedPipelineId) search.set('pipelineId', selectedPipelineId)\n const payload = await readApiResultOrThrow<Record<string, unknown>>(\n `/api/customers/deals?${search.toString()}`,\n undefined,\n { errorMessage: translate('customers.deals.pipeline.loadError', 'Failed to load deals.') },\n )\n const items = Array.isArray(payload?.items) ? payload.items : []\n const deals: DealRecord[] = []\n items.forEach((item) => {\n if (!item || typeof item !== 'object') return\n const data = item as Record<string, unknown>\n const id = typeof data.id === 'string' ? data.id : null\n if (!id) return\n const title =\n typeof data.title === 'string' && data.title.trim().length\n ? data.title.trim()\n : translate('customers.deals.pipeline.untitled', 'Untitled deal')\n const status =\n typeof data.status === 'string' && data.status.trim().length ? data.status.trim() : null\n const stage =\n typeof data.pipeline_stage === 'string' && data.pipeline_stage.trim().length\n ? data.pipeline_stage.trim()\n : null\n const amount = normalizeAmount(data.value_amount)\n const currency =\n typeof data.value_currency === 'string' && data.value_currency.trim().length\n ? data.value_currency.trim().toUpperCase()\n : null\n const probability = normalizeProbability(data.probability)\n const expected = normalizeTimestamp(data.expected_close_at)\n const created = normalizeTimestamp(data.created_at)\n const updated = normalizeTimestamp(data.updated_at)\n const rawPeople = Array.isArray(data.people) ? data.people : []\n const people: DealAssociation[] = rawPeople\n .map((entry) => {\n if (!entry || typeof entry !== 'object') return null\n const ref = entry as Record<string, unknown>\n const personId = typeof ref.id === 'string' ? ref.id : null\n if (!personId) return null\n const label =\n typeof ref.label === 'string' && ref.label.trim().length\n ? ref.label.trim()\n : personId\n return { id: personId, label }\n })\n .filter((entry): entry is DealAssociation => !!entry)\n const rawCompanies = Array.isArray(data.companies) ? data.companies : []\n const companies: DealAssociation[] = rawCompanies\n .map((entry) => {\n if (!entry || typeof entry !== 'object') return null\n const ref = entry as Record<string, unknown>\n const companyId = typeof ref.id === 'string' ? ref.id : null\n if (!companyId) return null\n const label =\n typeof ref.label === 'string' && ref.label.trim().length\n ? ref.label.trim()\n : companyId\n return { id: companyId, label }\n })\n .filter((entry): entry is DealAssociation => !!entry)\n deals.push({\n id,\n title,\n status,\n pipelineStage: stage,\n pipelineId: typeof data.pipeline_id === 'string' ? data.pipeline_id : null,\n pipelineStageId: typeof data.pipeline_stage_id === 'string' ? data.pipeline_stage_id : null,\n valueAmount: amount,\n valueCurrency: currency,\n probability,\n expectedCloseAt: expected.iso,\n expectedCloseAtTs: expected.ts,\n createdAt: created.iso,\n createdAtTs: created.ts,\n updatedAt: updated.iso,\n people,\n companies,\n })\n })\n\n const total = typeof payload?.total === 'number' ? payload.total : deals.length\n return { deals, total }\n },\n })\n\n const deals = dealsQuery.data?.deals ?? []\n const total = dealsQuery.data?.total ?? deals.length\n const dealMap = React.useMemo(() => createDealMap(deals), [deals])\n const groupedDeals = React.useMemo(() => groupDealsByStageId(deals), [deals])\n const stages = React.useMemo(\n () => buildStageDefinitionsFromPipelineStages(stagesQuery.data ?? [], deals, t),\n [stagesQuery.data, deals, t],\n )\n\n const dateFormatter = React.useMemo(\n () =>\n new Intl.DateTimeFormat(undefined, {\n dateStyle: 'medium',\n }),\n [],\n )\n\n const updateStageMutation = useMutation({\n mutationFn: async ({ id, pipelineStageId }: { id: string; pipelineStageId: string }) => {\n await apiCallOrThrow(\n '/api/customers/deals',\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id, pipelineStageId }),\n },\n { errorMessage: translate('customers.deals.pipeline.moveError', 'Failed to update deal stage.') },\n )\n return { id, pipelineStageId }\n },\n onMutate: async ({ id, pipelineStageId }) => {\n setPendingDealId(id)\n await queryClient.cancelQueries({ queryKey: dealsKey })\n const previous = queryClient.getQueryData<DealsQueryData>(dealsKey)\n if (previous) {\n const nextDeals = previous.deals.map((deal) =>\n deal.id === id ? { ...deal, pipelineStageId } : deal,\n )\n queryClient.setQueryData<DealsQueryData>(dealsKey, { ...previous, deals: nextDeals })\n }\n return { previous }\n },\n onError: (error, _variables, context) => {\n if (context?.previous) {\n queryClient.setQueryData<DealsQueryData>(dealsKey, context.previous)\n }\n const message =\n error instanceof Error && error.message\n ? error.message\n : translate('customers.deals.pipeline.moveError', 'Failed to update deal stage.')\n flash(message, 'error')\n },\n onSuccess: () => {\n flash(translate('customers.deals.pipeline.moveSuccess', 'Deal updated.'), 'success')\n },\n onSettled: () => {\n setPendingDealId(null)\n queryClient.invalidateQueries({ queryKey: dealsKey }).catch(() => {})\n },\n })\n\n const [draggingId, setDraggingId] = React.useState<string | null>(null)\n const [activeLane, setActiveLane] = React.useState<string | null>(null)\n const handleActionClick = React.useCallback((event: React.MouseEvent) => {\n event.stopPropagation()\n }, [])\n\n const handleSortChange = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => {\n const value = event.target.value as SortOption\n if (sortOptions.includes(value)) setSortBy(value)\n }, [])\n\n const handleDragStart = React.useCallback((dealId: string) => {\n setDraggingId(dealId)\n }, [])\n\n const handleDragEnd = React.useCallback(() => {\n setDraggingId(null)\n setActiveLane(null)\n }, [])\n\n const handleDrop = React.useCallback(\n (stage: StageDefinition) => async (event: React.DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n setActiveLane(null)\n const dealId = event.dataTransfer.getData('text/plain') || draggingId\n if (!dealId) return\n const deal = dealMap.get(dealId)\n if (!deal) return\n if (stage.value === null) {\n flash(\n translate('customers.deals.pipeline.unassignedDisabled', 'Moving to \"No stage\" is not supported.'),\n 'info',\n )\n return\n }\n if (deal.pipelineStageId === stage.value) return\n updateStageMutation.mutate({ id: dealId, pipelineStageId: stage.value })\n },\n [dealMap, draggingId, translate, updateStageMutation],\n )\n\n const handleDragOver = React.useCallback(\n (stageId: string) => (event: React.DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n event.dataTransfer.dropEffect = 'move'\n if (activeLane !== stageId) setActiveLane(stageId)\n },\n [activeLane],\n )\n\n const renderLaneHeader = (stage: StageDefinition, count: number) => {\n return (\n <div className=\"flex items-center justify-between gap-3 border-b border-border px-4 py-3\">\n <div className=\"flex items-center gap-2\">\n <div className=\"flex flex-col\">\n <span className=\"text-sm font-medium\">{stage.label}</span>\n <span className=\"text-xs text-muted-foreground\">\n {translate('customers.deals.pipeline.countLabel', 'Deals: {count}', { count })}\n </span>\n </div>\n </div>\n </div>\n )\n }\n\n return (\n <Page>\n <PageBody>\n <div className=\"flex flex-col gap-4\">\n <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex flex-col\">\n <h1 className=\"text-xl font-semibold text-foreground\">\n {translate('customers.deals.pipeline.title', 'Sales Pipeline')}\n </h1>\n <p className=\"text-sm text-muted-foreground\">\n {translate(\n 'customers.deals.pipeline.subtitle',\n 'Track deals by pipeline stage and drag them between lanes to update progress.',\n )}\n </p>\n </div>\n <div className=\"flex items-center gap-4\">\n {pipelinesQuery.data && pipelinesQuery.data.length > 0 ? (\n <label className=\"flex items-center gap-2 text-sm font-medium text-muted-foreground\">\n <span>{translate('customers.deals.pipeline.switch.label', 'Pipeline')}</span>\n <select\n className=\"h-9 rounded-md border border-border bg-background px-3 text-sm text-foreground shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40\"\n value={selectedPipelineId ?? ''}\n onChange={(e) => setSelectedPipelineId(e.target.value || null)}\n >\n {pipelinesQuery.data.map((p) => (\n <option key={p.id} value={p.id}>{p.name}</option>\n ))}\n </select>\n </label>\n ) : null}\n <Link\n href=\"/backend/config/customers/pipeline-stages\"\n className=\"text-sm font-medium text-primary hover:underline\"\n >\n {translate('customers.deals.pipeline.manageStages', 'Manage stages')}\n </Link>\n <label className=\"flex items-center gap-2 text-sm font-medium text-muted-foreground\">\n <span>{translate('customers.deals.pipeline.sort.label', 'Sort by')}</span>\n <select\n className=\"h-9 rounded-md border border-border bg-background px-3 text-sm text-foreground shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40\"\n value={sortBy}\n onChange={handleSortChange}\n >\n <option value=\"probability\">\n {translate('customers.deals.pipeline.sort.probability', 'Probability (high to low)')}\n </option>\n <option value=\"createdAt\">\n {translate('customers.deals.pipeline.sort.createdAt', 'Created (newest first)')}\n </option>\n <option value=\"expectedCloseAt\">\n {translate('customers.deals.pipeline.sort.expectedCloseAt', 'Expected close (soonest first)')}\n </option>\n </select>\n </label>\n </div>\n </div>\n\n {!selectedPipelineId ? (\n <div className=\"flex h-[50vh] items-center justify-center\">\n <span className=\"text-sm text-muted-foreground\">\n {translate('customers.deals.pipeline.noPipeline', 'No pipeline selected. Create a pipeline in settings.')}\n </span>\n </div>\n ) : dealsQuery.isLoading ? (\n <div className=\"flex h-[50vh] items-center justify-center\">\n <Spinner />\n </div>\n ) : dealsQuery.isError ? (\n <div className=\"max-w-xl\">\n <ErrorNotice\n message={\n dealsQuery.error instanceof Error\n ? dealsQuery.error.message\n : translate('customers.deals.pipeline.loadError', 'Failed to load deals.')\n }\n />\n </div>\n ) : (\n <div className=\"flex flex-col gap-3\">\n {total > deals.length ? (\n <div className=\"rounded-md border border-border bg-muted/30 px-4 py-2 text-sm text-muted-foreground\">\n {translate(\n 'customers.deals.pipeline.limitNotice',\n 'Showing the first {count} deals. Refine your filters to see more.',\n { count: deals.length },\n )}\n </div>\n ) : null}\n\n <div className=\"flex flex-col gap-4 pb-6 md:flex-row md:overflow-x-auto\">\n {stages.length === 0 ? (\n <div className=\"flex h-[50vh] w-full items-center justify-center rounded-lg border border-dashed border-border bg-muted/20\">\n <span className=\"text-sm text-muted-foreground\">\n {translate('customers.deals.pipeline.noStages', 'Define pipeline stages to start tracking deals.')}\n </span>\n </div>\n ) : (\n stages.map((stage) => {\n const stageKey = stage.value ?? null\n const laneDeals = groupedDeals.get(stageKey) ?? []\n const sortedLaneDeals = sortDeals(laneDeals, sortBy)\n const isActive = activeLane === stage.id\n return (\n <div\n key={stage.id}\n className={`flex min-h-[60vh] w-full flex-1 flex-col overflow-hidden rounded-lg border border-border bg-card shadow-sm transition-all md:w-72 md:flex-none ${\n isActive ? 'ring-2 ring-ring/40' : ''\n }`}\n onDragOver={handleDragOver(stage.id)}\n onDrop={handleDrop(stage)}\n >\n {renderLaneHeader(stage, laneDeals.length)}\n <div className=\"flex flex-1 flex-col gap-3 overflow-y-auto px-4 py-3\">\n {sortedLaneDeals.length === 0 ? (\n <div className=\"rounded-md border border-dashed border-border bg-muted/10 p-4 text-center text-xs text-muted-foreground\">\n {translate('customers.deals.pipeline.emptyLane', 'No deals in this stage yet.')}\n </div>\n ) : (\n sortedLaneDeals.map((deal) => {\n const isDragging = draggingId === deal.id\n || (pendingDealId === deal.id && updateStageMutation.isPending)\n const valueLabel = formatCurrency(\n deal.valueAmount,\n deal.valueCurrency,\n translate('customers.deals.list.noValue', 'No value assigned'),\n )\n const probabilityLabel = formatProbability(\n deal.probability,\n translate('customers.deals.pipeline.noProbability', 'N/A'),\n )\n const expectedLabel = deal.expectedCloseAt\n ? dateFormatter.format(new Date(deal.expectedCloseAt))\n : translate('customers.deals.pipeline.noExpectedClose', 'No date')\n return (\n <div\n key={deal.id}\n className={`group flex cursor-grab flex-col gap-2 rounded-md border border-border bg-background p-4 shadow-xs transition ${\n isDragging ? 'opacity-50' : 'hover:shadow-sm'\n }`}\n draggable\n onDragStart={(event) => {\n event.dataTransfer.effectAllowed = 'move'\n event.dataTransfer.setData('text/plain', deal.id)\n handleDragStart(deal.id)\n }}\n onDragEnd={handleDragEnd}\n >\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"flex flex-col\">\n <span className=\"line-clamp-2 text-sm font-medium text-foreground\">\n {deal.title}\n </span>\n {deal.status ? (\n <span className=\"text-xs uppercase tracking-wide text-muted-foreground\">\n {deal.status}\n </span>\n ) : null}\n </div>\n {pendingDealId === deal.id && updateStageMutation.isPending ? (\n <Spinner className=\"size-4\" />\n ) : null}\n </div>\n <div className=\"flex flex-col gap-1 text-xs text-muted-foreground\">\n <div className=\"flex items-center justify-between gap-2\">\n <span>{translate('customers.deals.pipeline.card.value', 'Value')}</span>\n <span className=\"font-medium text-foreground\">{valueLabel}</span>\n </div>\n <div className=\"flex items-center justify-between gap-2\">\n <span>{translate('customers.deals.pipeline.card.probability', 'Probability')}</span>\n <span className=\"font-medium text-foreground\">{probabilityLabel}</span>\n </div>\n <div className=\"flex items-center justify-between gap-2\">\n <span>{translate('customers.deals.pipeline.card.expectedClose', 'Expected close')}</span>\n <span className=\"font-medium text-foreground\">{expectedLabel}</span>\n </div>\n </div>\n <div className=\"mt-1 flex flex-wrap gap-2 text-xs\">\n <Link\n href={`/backend/customers/deals/${deal.id}`}\n className=\"font-medium text-primary hover:underline\"\n draggable={false}\n onClick={handleActionClick}\n >\n {translate('customers.deals.pipeline.actions.openDeal', 'Open deal')}\n </Link>\n </div>\n {deal.people.length ? (\n <div className=\"flex flex-wrap gap-2\">\n {deal.people.map((person) => (\n <Link\n key={person.id}\n className=\"rounded-full bg-primary/5 px-3 py-1 text-xs text-primary transition-colors hover:bg-primary/10\"\n href={`/backend/customers/people-v2/${person.id}`}\n draggable={false}\n onClick={handleActionClick}\n >\n {person.label}\n </Link>\n ))}\n </div>\n ) : null}\n {deal.companies.length ? (\n <div className=\"flex flex-wrap gap-2\">\n {deal.companies.map((company) => (\n <Link\n key={company.id}\n className=\"rounded-full bg-secondary/10 px-3 py-1 text-xs text-secondary-foreground transition-colors hover:bg-secondary/20\"\n href={`/backend/customers/companies-v2/${company.id}`}\n draggable={false}\n onClick={handleActionClick}\n >\n {company.label}\n </Link>\n ))}\n </div>\n ) : null}\n </div>\n )\n })\n )}\n </div>\n </div>\n )\n })\n )}\n </div>\n </div>\n )}\n </div>\n </PageBody>\n </Page>\n )\n}\n"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { ErrorNotice } from '@open-mercato/ui/primitives/ErrorNotice'\nimport { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\n\ntype DealAssociation = { id: string; label: string }\n\ntype DealRecord = {\n id: string\n title: string\n status: string | null\n pipelineStage: string | null\n pipelineId: string | null\n pipelineStageId: string | null\n valueAmount: number | null\n valueCurrency: string | null\n probability: number | null\n expectedCloseAt: string | null\n expectedCloseAtTs: number | null\n createdAt: string | null\n createdAtTs: number | null\n updatedAt: string | null\n people: DealAssociation[]\n companies: DealAssociation[]\n}\n\ntype DealsQueryData = {\n deals: DealRecord[]\n total: number\n}\n\ntype StageDefinition = {\n id: string\n value: string | null\n label: string\n color: string | null\n icon: string | null\n}\n\ntype SortOption = 'probability' | 'createdAt' | 'expectedCloseAt'\n\ntype PipelineRecord = { id: string; name: string; isDefault: boolean }\ntype PipelineStageRecord = { id: string; label: string; order: number; pipelineId: string }\n\nconst DEALS_QUERY_LIMIT = 100\n\nconst dealsQueryKey = (scopeVersion: number, pipelineId: string | null) =>\n ['customers', 'deals', 'pipeline', `scope:${scopeVersion}`, `pipeline:${pipelineId ?? 'none'}`] as const\n\nconst sortOptions: SortOption[] = ['probability', 'createdAt', 'expectedCloseAt']\n\nfunction normalizeAmount(value: unknown): number | null {\n if (typeof value === 'number' && Number.isFinite(value)) return value\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (!trimmed.length) return null\n const parsed = Number(trimmed)\n return Number.isFinite(parsed) ? parsed : null\n }\n return null\n}\n\nfunction normalizeProbability(value: unknown): number | null {\n const parsed = normalizeAmount(value)\n if (parsed === null) return null\n if (parsed < 0) return 0\n if (parsed > 100) return 100\n return Math.round(parsed)\n}\n\nfunction normalizeTimestamp(value: unknown): { iso: string | null; ts: number | null } {\n if (typeof value !== 'string' || !value.trim().length) return { iso: null, ts: null }\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return { iso: null, ts: null }\n return { iso: date.toISOString(), ts: date.getTime() }\n}\n\nfunction buildStageDefinitionsFromPipelineStages(\n pipelineStages: PipelineStageRecord[],\n deals: DealRecord[],\n t: ReturnType<typeof useT>,\n): StageDefinition[] {\n const result: StageDefinition[] = pipelineStages\n .slice()\n .sort((a, b) => a.order - b.order)\n .map((stage) => ({\n id: stage.id,\n value: stage.id,\n label: stage.label,\n color: null,\n icon: null,\n }))\n\n const knownIds = new Set(pipelineStages.map((s) => s.id))\n const hasUnassigned = deals.some((deal) => !deal.pipelineStageId || !knownIds.has(deal.pipelineStageId))\n if (hasUnassigned) {\n result.push({\n id: 'stage:__unassigned',\n value: null,\n label: translateWithFallback(t, 'customers.deals.pipeline.unassigned', 'No stage'),\n color: null,\n icon: null,\n })\n }\n\n return result\n}\n\nfunction createDealMap(deals: DealRecord[]): Map<string, DealRecord> {\n return deals.reduce<Map<string, DealRecord>>((acc, deal) => acc.set(deal.id, deal), new Map())\n}\n\nfunction groupDealsByStageId(deals: DealRecord[]): Map<string | null, DealRecord[]> {\n const byStage = new Map<string | null, DealRecord[]>()\n deals.forEach((deal) => {\n const stageKey = deal.pipelineStageId ?? null\n const bucket = byStage.get(stageKey) ?? []\n bucket.push(deal)\n byStage.set(stageKey, bucket)\n })\n return byStage\n}\n\nfunction sortDeals(deals: DealRecord[], option: SortOption): DealRecord[] {\n const sorted = [...deals]\n sorted.sort((a, b) => {\n if (option === 'probability') {\n const ap = typeof a.probability === 'number' ? a.probability : -1\n const bp = typeof b.probability === 'number' ? b.probability : -1\n if (ap !== bp) return bp - ap\n }\n if (option === 'expectedCloseAt') {\n const at = typeof a.expectedCloseAtTs === 'number' ? a.expectedCloseAtTs : Number.POSITIVE_INFINITY\n const bt = typeof b.expectedCloseAtTs === 'number' ? b.expectedCloseAtTs : Number.POSITIVE_INFINITY\n if (at !== bt) return at - bt\n }\n const at = typeof a.createdAtTs === 'number' ? a.createdAtTs : Number.NEGATIVE_INFINITY\n const bt = typeof b.createdAtTs === 'number' ? b.createdAtTs : Number.NEGATIVE_INFINITY\n if (option === 'createdAt') {\n if (at !== bt) return bt - at\n } else if (option === 'expectedCloseAt' || option === 'probability') {\n if (at !== bt) return bt - at\n }\n return a.title.localeCompare(b.title, undefined, { sensitivity: 'base' })\n })\n return sorted\n}\n\nfunction formatCurrency(amount: number | null, currency: string | null, fallback: string): string {\n if (amount === null || Number.isNaN(amount)) return fallback\n const code = currency && currency.length === 3 ? currency.toUpperCase() : 'USD'\n try {\n return new Intl.NumberFormat(undefined, {\n style: 'currency',\n currency: code,\n maximumFractionDigits: 2,\n }).format(amount)\n } catch {\n return `${code} ${amount.toFixed(2)}`\n }\n}\n\nfunction formatProbability(probability: number | null, fallback: string): string {\n if (typeof probability !== 'number' || Number.isNaN(probability)) return fallback\n return `${probability}%`\n}\n\nexport default function SalesPipelinePage(): React.ReactElement {\n const t = useT()\n const translate = React.useCallback(\n (key: string, fallback: string, params?: Record<string, string | number>) => {\n const value = translateWithFallback(t, key, fallback, params)\n if (value === fallback && params) {\n return fallback.replace(/\\{\\{(\\w+)\\}\\}|\\{(\\w+)\\}/g, (match, doubleToken, singleToken) => {\n const token = (doubleToken ?? singleToken) as string | undefined\n if (!token) return match\n const replacement = params[token]\n if (replacement === undefined) {\n return doubleToken ? `{{${token}}}` : `{${token}}`\n }\n return String(replacement)\n })\n }\n return value\n },\n [t],\n )\n const scopeVersion = useOrganizationScopeVersion()\n const queryClient = useQueryClient()\n const [sortBy, setSortBy] = React.useState<SortOption>('probability')\n const [pendingDealId, setPendingDealId] = React.useState<string | null>(null)\n const [selectedPipelineId, setSelectedPipelineId] = React.useState<string | null>(null)\n\n const pipelinesQuery = useQuery<PipelineRecord[]>({\n queryKey: ['customers', 'pipelines', `scope:${scopeVersion}`],\n staleTime: 60_000,\n queryFn: async () => {\n const payload = await readApiResultOrThrow<{ items: PipelineRecord[] }>(\n '/api/customers/pipelines',\n undefined,\n { errorMessage: translate('customers.deals.pipeline.loadError', 'Failed to load pipelines.') },\n )\n return payload?.items ?? []\n },\n })\n\n React.useEffect(() => {\n if (selectedPipelineId) return\n const pipelines = pipelinesQuery.data\n if (!pipelines || !pipelines.length) return\n const defaultPipeline = pipelines.find((p) => p.isDefault) ?? pipelines[0]\n if (defaultPipeline) setSelectedPipelineId(defaultPipeline.id)\n }, [pipelinesQuery.data, selectedPipelineId])\n\n const stagesQuery = useQuery<PipelineStageRecord[]>({\n queryKey: ['customers', 'pipeline-stages', `scope:${scopeVersion}`, `pipeline:${selectedPipelineId}`],\n enabled: !!selectedPipelineId,\n staleTime: 30_000,\n queryFn: async () => {\n const payload = await readApiResultOrThrow<{ items: PipelineStageRecord[] }>(\n `/api/customers/pipeline-stages?pipelineId=${encodeURIComponent(selectedPipelineId!)}`,\n undefined,\n { errorMessage: translate('customers.deals.pipeline.loadError', 'Failed to load stages.') },\n )\n return payload?.items ?? []\n },\n })\n\n const dealsKey = React.useMemo(() => dealsQueryKey(scopeVersion, selectedPipelineId), [scopeVersion, selectedPipelineId])\n\n const dealsQuery = useQuery<DealsQueryData>({\n queryKey: dealsKey,\n enabled: !!selectedPipelineId,\n staleTime: 30_000,\n queryFn: async () => {\n const search = new URLSearchParams()\n search.set('page', '1')\n search.set('pageSize', String(DEALS_QUERY_LIMIT))\n search.set('sortField', 'createdAt')\n search.set('sortDir', 'desc')\n if (selectedPipelineId) search.set('pipelineId', selectedPipelineId)\n const payload = await readApiResultOrThrow<Record<string, unknown>>(\n `/api/customers/deals?${search.toString()}`,\n undefined,\n { errorMessage: translate('customers.deals.pipeline.loadError', 'Failed to load deals.') },\n )\n const items = Array.isArray(payload?.items) ? payload.items : []\n const deals: DealRecord[] = []\n items.forEach((item) => {\n if (!item || typeof item !== 'object') return\n const data = item as Record<string, unknown>\n const id = typeof data.id === 'string' ? data.id : null\n if (!id) return\n const title =\n typeof data.title === 'string' && data.title.trim().length\n ? data.title.trim()\n : translate('customers.deals.pipeline.untitled', 'Untitled deal')\n const status =\n typeof data.status === 'string' && data.status.trim().length ? data.status.trim() : null\n const stage =\n typeof data.pipeline_stage === 'string' && data.pipeline_stage.trim().length\n ? data.pipeline_stage.trim()\n : null\n const amount = normalizeAmount(data.value_amount)\n const currency =\n typeof data.value_currency === 'string' && data.value_currency.trim().length\n ? data.value_currency.trim().toUpperCase()\n : null\n const probability = normalizeProbability(data.probability)\n const expected = normalizeTimestamp(data.expected_close_at)\n const created = normalizeTimestamp(data.created_at)\n const updated = normalizeTimestamp(data.updated_at)\n const rawPeople = Array.isArray(data.people) ? data.people : []\n const people: DealAssociation[] = rawPeople\n .map((entry) => {\n if (!entry || typeof entry !== 'object') return null\n const ref = entry as Record<string, unknown>\n const personId = typeof ref.id === 'string' ? ref.id : null\n if (!personId) return null\n const label =\n typeof ref.label === 'string' && ref.label.trim().length\n ? ref.label.trim()\n : personId\n return { id: personId, label }\n })\n .filter((entry): entry is DealAssociation => !!entry)\n const rawCompanies = Array.isArray(data.companies) ? data.companies : []\n const companies: DealAssociation[] = rawCompanies\n .map((entry) => {\n if (!entry || typeof entry !== 'object') return null\n const ref = entry as Record<string, unknown>\n const companyId = typeof ref.id === 'string' ? ref.id : null\n if (!companyId) return null\n const label =\n typeof ref.label === 'string' && ref.label.trim().length\n ? ref.label.trim()\n : companyId\n return { id: companyId, label }\n })\n .filter((entry): entry is DealAssociation => !!entry)\n deals.push({\n id,\n title,\n status,\n pipelineStage: stage,\n pipelineId: typeof data.pipeline_id === 'string' ? data.pipeline_id : null,\n pipelineStageId: typeof data.pipeline_stage_id === 'string' ? data.pipeline_stage_id : null,\n valueAmount: amount,\n valueCurrency: currency,\n probability,\n expectedCloseAt: expected.iso,\n expectedCloseAtTs: expected.ts,\n createdAt: created.iso,\n createdAtTs: created.ts,\n updatedAt: updated.iso,\n people,\n companies,\n })\n })\n\n const total = typeof payload?.total === 'number' ? payload.total : deals.length\n return { deals, total }\n },\n })\n\n const deals = dealsQuery.data?.deals ?? []\n const total = dealsQuery.data?.total ?? deals.length\n const dealMap = React.useMemo(() => createDealMap(deals), [deals])\n const groupedDeals = React.useMemo(() => groupDealsByStageId(deals), [deals])\n const stages = React.useMemo(\n () => buildStageDefinitionsFromPipelineStages(stagesQuery.data ?? [], deals, t),\n [stagesQuery.data, deals, t],\n )\n\n const dateFormatter = React.useMemo(\n () =>\n new Intl.DateTimeFormat(undefined, {\n dateStyle: 'medium',\n }),\n [],\n )\n\n const updateStageMutation = useMutation({\n mutationFn: async ({ id, pipelineStageId }: { id: string; pipelineStageId: string }) => {\n await apiCallOrThrow(\n '/api/customers/deals',\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id, pipelineStageId }),\n },\n { errorMessage: translate('customers.deals.pipeline.moveError', 'Failed to update deal stage.') },\n )\n return { id, pipelineStageId }\n },\n onMutate: async ({ id, pipelineStageId }) => {\n setPendingDealId(id)\n await queryClient.cancelQueries({ queryKey: dealsKey })\n const previous = queryClient.getQueryData<DealsQueryData>(dealsKey)\n if (previous) {\n const nextDeals = previous.deals.map((deal) =>\n deal.id === id ? { ...deal, pipelineStageId } : deal,\n )\n queryClient.setQueryData<DealsQueryData>(dealsKey, { ...previous, deals: nextDeals })\n }\n return { previous }\n },\n onError: (error, _variables, context) => {\n if (context?.previous) {\n queryClient.setQueryData<DealsQueryData>(dealsKey, context.previous)\n }\n const message =\n error instanceof Error && error.message\n ? error.message\n : translate('customers.deals.pipeline.moveError', 'Failed to update deal stage.')\n flash(message, 'error')\n },\n onSuccess: () => {\n flash(translate('customers.deals.pipeline.moveSuccess', 'Deal updated.'), 'success')\n },\n onSettled: () => {\n setPendingDealId(null)\n queryClient.invalidateQueries({ queryKey: dealsKey }).catch(() => {})\n },\n })\n\n const [draggingId, setDraggingId] = React.useState<string | null>(null)\n const [activeLane, setActiveLane] = React.useState<string | null>(null)\n const handleActionClick = React.useCallback((event: React.MouseEvent) => {\n event.stopPropagation()\n }, [])\n\n const handleSortChange = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => {\n const value = event.target.value as SortOption\n if (sortOptions.includes(value)) setSortBy(value)\n }, [])\n\n const handleDragStart = React.useCallback((dealId: string) => {\n setDraggingId(dealId)\n }, [])\n\n const handleDragEnd = React.useCallback(() => {\n setDraggingId(null)\n setActiveLane(null)\n }, [])\n\n const handleDrop = React.useCallback(\n (stage: StageDefinition) => async (event: React.DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n setActiveLane(null)\n const dealId = event.dataTransfer.getData('text/plain') || draggingId\n if (!dealId) return\n const deal = dealMap.get(dealId)\n if (!deal) return\n if (stage.value === null) {\n flash(\n translate('customers.deals.pipeline.unassignedDisabled', 'Moving to \"No stage\" is not supported.'),\n 'info',\n )\n return\n }\n if (deal.pipelineStageId === stage.value) return\n updateStageMutation.mutate({ id: dealId, pipelineStageId: stage.value })\n },\n [dealMap, draggingId, translate, updateStageMutation],\n )\n\n const handleDragOver = React.useCallback(\n (stageId: string) => (event: React.DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n event.dataTransfer.dropEffect = 'move'\n if (activeLane !== stageId) setActiveLane(stageId)\n },\n [activeLane],\n )\n\n const renderLaneHeader = (stage: StageDefinition, count: number) => {\n return (\n <div className=\"flex items-center justify-between gap-3 border-b border-border px-4 py-3\">\n <div className=\"flex items-center gap-2\">\n <div className=\"flex flex-col\">\n <span className=\"text-sm font-medium\">{stage.label}</span>\n <span className=\"text-xs text-muted-foreground\">\n {translate('customers.deals.pipeline.countLabel', 'Deals: {count}', { count })}\n </span>\n </div>\n </div>\n </div>\n )\n }\n\n return (\n <Page>\n <PageBody>\n <div className=\"flex flex-col gap-4\">\n <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex flex-col\">\n <h1 className=\"text-xl font-semibold text-foreground\">\n {translate('customers.deals.pipeline.title', 'Sales Pipeline')}\n </h1>\n <p className=\"text-sm text-muted-foreground\">\n {translate(\n 'customers.deals.pipeline.subtitle',\n 'Track deals by pipeline stage and drag them between lanes to update progress.',\n )}\n </p>\n </div>\n <div className=\"flex items-center gap-4\">\n {pipelinesQuery.data && pipelinesQuery.data.length > 0 ? (\n <label className=\"flex items-center gap-2 text-sm font-medium text-muted-foreground\">\n <span>{translate('customers.deals.pipeline.switch.label', 'Pipeline')}</span>\n <select\n className=\"h-9 rounded-md border border-border bg-background px-3 text-sm text-foreground shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40\"\n value={selectedPipelineId ?? ''}\n onChange={(e) => setSelectedPipelineId(e.target.value || null)}\n >\n {pipelinesQuery.data.map((p) => (\n <option key={p.id} value={p.id}>{p.name}</option>\n ))}\n </select>\n </label>\n ) : null}\n <Link\n href=\"/backend/config/customers/pipeline-stages\"\n className=\"text-sm font-medium text-primary hover:underline\"\n >\n {translate('customers.deals.pipeline.manageStages', 'Manage stages')}\n </Link>\n <label className=\"flex items-center gap-2 text-sm font-medium text-muted-foreground\">\n <span>{translate('customers.deals.pipeline.sort.label', 'Sort by')}</span>\n <select\n className=\"h-9 rounded-md border border-border bg-background px-3 text-sm text-foreground shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40\"\n value={sortBy}\n onChange={handleSortChange}\n >\n <option value=\"probability\">\n {translate('customers.deals.pipeline.sort.probability', 'Probability (high to low)')}\n </option>\n <option value=\"createdAt\">\n {translate('customers.deals.pipeline.sort.createdAt', 'Created (newest first)')}\n </option>\n <option value=\"expectedCloseAt\">\n {translate('customers.deals.pipeline.sort.expectedCloseAt', 'Expected close (soonest first)')}\n </option>\n </select>\n </label>\n </div>\n </div>\n\n {!selectedPipelineId ? (\n <div className=\"flex h-[50vh] items-center justify-center\">\n <span className=\"text-sm text-muted-foreground\">\n {translate('customers.deals.pipeline.noPipeline', 'No pipeline selected. Create a pipeline in settings.')}\n </span>\n </div>\n ) : dealsQuery.isLoading ? (\n <div className=\"flex h-[50vh] items-center justify-center\">\n <Spinner />\n </div>\n ) : dealsQuery.isError ? (\n <div className=\"max-w-xl\">\n <ErrorNotice\n message={\n dealsQuery.error instanceof Error\n ? dealsQuery.error.message\n : translate('customers.deals.pipeline.loadError', 'Failed to load deals.')\n }\n />\n </div>\n ) : (\n <div className=\"flex flex-col gap-3\">\n {total > deals.length ? (\n <div className=\"rounded-md border border-border bg-muted/30 px-4 py-2 text-sm text-muted-foreground\">\n {translate(\n 'customers.deals.pipeline.limitNotice',\n 'Showing the first {count} deals. Refine your filters to see more.',\n { count: deals.length },\n )}\n </div>\n ) : null}\n\n <div className=\"flex flex-col gap-4 pb-6 md:flex-row md:overflow-x-auto\">\n {stages.length === 0 ? (\n <div className=\"flex h-[50vh] w-full items-center justify-center rounded-lg border border-dashed border-border bg-muted/30\">\n <span className=\"text-sm text-muted-foreground\">\n {translate('customers.deals.pipeline.noStages', 'Define pipeline stages to start tracking deals.')}\n </span>\n </div>\n ) : (\n stages.map((stage) => {\n const stageKey = stage.value ?? null\n const laneDeals = groupedDeals.get(stageKey) ?? []\n const sortedLaneDeals = sortDeals(laneDeals, sortBy)\n const isActive = activeLane === stage.id\n return (\n <div\n key={stage.id}\n className={`flex min-h-[60vh] w-full flex-1 flex-col overflow-hidden rounded-lg border border-border bg-card shadow-sm transition-all md:w-72 md:flex-none ${\n isActive ? 'ring-2 ring-ring/40' : ''\n }`}\n onDragOver={handleDragOver(stage.id)}\n onDrop={handleDrop(stage)}\n >\n {renderLaneHeader(stage, laneDeals.length)}\n <div className=\"flex flex-1 flex-col gap-3 overflow-y-auto px-4 py-3\">\n {sortedLaneDeals.length === 0 ? (\n <div className=\"rounded-md border border-dashed border-border bg-muted/30 p-4 text-center text-xs text-muted-foreground\">\n {translate('customers.deals.pipeline.emptyLane', 'No deals in this stage yet.')}\n </div>\n ) : (\n sortedLaneDeals.map((deal) => {\n const isDragging = draggingId === deal.id\n || (pendingDealId === deal.id && updateStageMutation.isPending)\n const valueLabel = formatCurrency(\n deal.valueAmount,\n deal.valueCurrency,\n translate('customers.deals.list.noValue', 'No value assigned'),\n )\n const probabilityLabel = formatProbability(\n deal.probability,\n translate('customers.deals.pipeline.noProbability', 'N/A'),\n )\n const expectedLabel = deal.expectedCloseAt\n ? dateFormatter.format(new Date(deal.expectedCloseAt))\n : translate('customers.deals.pipeline.noExpectedClose', 'No date')\n return (\n <div\n key={deal.id}\n className={`group flex cursor-grab flex-col gap-2 rounded-md border border-border bg-background p-4 shadow-xs transition ${\n isDragging ? 'opacity-50' : 'hover:shadow-sm'\n }`}\n draggable\n onDragStart={(event) => {\n event.dataTransfer.effectAllowed = 'move'\n event.dataTransfer.setData('text/plain', deal.id)\n handleDragStart(deal.id)\n }}\n onDragEnd={handleDragEnd}\n >\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"flex flex-col\">\n <span className=\"line-clamp-2 text-sm font-medium text-foreground\">\n {deal.title}\n </span>\n {deal.status ? (\n <span className=\"text-xs uppercase tracking-wide text-muted-foreground\">\n {deal.status}\n </span>\n ) : null}\n </div>\n {pendingDealId === deal.id && updateStageMutation.isPending ? (\n <Spinner className=\"size-4\" />\n ) : null}\n </div>\n <div className=\"flex flex-col gap-1 text-xs text-muted-foreground\">\n <div className=\"flex items-center justify-between gap-2\">\n <span>{translate('customers.deals.pipeline.card.value', 'Value')}</span>\n <span className=\"font-medium text-foreground\">{valueLabel}</span>\n </div>\n <div className=\"flex items-center justify-between gap-2\">\n <span>{translate('customers.deals.pipeline.card.probability', 'Probability')}</span>\n <span className=\"font-medium text-foreground\">{probabilityLabel}</span>\n </div>\n <div className=\"flex items-center justify-between gap-2\">\n <span>{translate('customers.deals.pipeline.card.expectedClose', 'Expected close')}</span>\n <span className=\"font-medium text-foreground\">{expectedLabel}</span>\n </div>\n </div>\n <div className=\"mt-1 flex flex-wrap gap-2 text-xs\">\n <Link\n href={`/backend/customers/deals/${deal.id}`}\n className=\"font-medium text-primary hover:underline\"\n draggable={false}\n onClick={handleActionClick}\n >\n {translate('customers.deals.pipeline.actions.openDeal', 'Open deal')}\n </Link>\n </div>\n {deal.people.length ? (\n <div className=\"flex flex-wrap gap-2\">\n {deal.people.map((person) => (\n <Link\n key={person.id}\n className=\"rounded-full bg-primary/5 px-3 py-1 text-xs text-primary transition-colors hover:bg-primary/10\"\n href={`/backend/customers/people-v2/${person.id}`}\n draggable={false}\n onClick={handleActionClick}\n >\n {person.label}\n </Link>\n ))}\n </div>\n ) : null}\n {deal.companies.length ? (\n <div className=\"flex flex-wrap gap-2\">\n {deal.companies.map((company) => (\n <Link\n key={company.id}\n className=\"rounded-full bg-secondary/10 px-3 py-1 text-xs text-secondary-foreground transition-colors hover:bg-secondary/20\"\n href={`/backend/customers/companies-v2/${company.id}`}\n draggable={false}\n onClick={handleActionClick}\n >\n {company.label}\n </Link>\n ))}\n </div>\n ) : null}\n </div>\n )\n })\n )}\n </div>\n </div>\n )\n })\n )}\n </div>\n </div>\n )}\n </div>\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": ["at", "bt", "deals", "total"]
|
|
7
7
|
}
|
|
@@ -307,7 +307,7 @@ function CustomerAddressTiles({
|
|
|
307
307
|
(key) => /* @__PURE__ */ jsxs(
|
|
308
308
|
"div",
|
|
309
309
|
{
|
|
310
|
-
className: "rounded-lg border-2 border-dashed border-muted-foreground/50 bg-muted/
|
|
310
|
+
className: "rounded-lg border-2 border-dashed border-muted-foreground/50 bg-muted/30 p-4 text-sm",
|
|
311
311
|
onKeyDown: (event) => {
|
|
312
312
|
if (!(event.metaKey || event.ctrlKey)) return;
|
|
313
313
|
if (event.key !== "Enter") return;
|