realtimex-crm 0.1.2
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/LICENSE.md +21 -0
- package/README.md +104 -0
- package/dist/assets/DealList-DqDrFeDV.js +59 -0
- package/dist/assets/DealList-DqDrFeDV.js.map +1 -0
- package/dist/assets/index-BiQoGq1P.css +1 -0
- package/dist/assets/index-CDIy4x-0.js +152 -0
- package/dist/assets/index-CDIy4x-0.js.map +1 -0
- package/dist/auth-callback.html +140 -0
- package/dist/favicon.ico +0 -0
- package/dist/img/adding-users.png +0 -0
- package/dist/img/empty.svg +42 -0
- package/dist/index.html +1 -0
- package/dist/logo192.png +0 -0
- package/dist/logo512.png +0 -0
- package/dist/logos/0.png +0 -0
- package/dist/logos/1.png +0 -0
- package/dist/logos/10.png +0 -0
- package/dist/logos/11.png +0 -0
- package/dist/logos/12.png +0 -0
- package/dist/logos/13.png +0 -0
- package/dist/logos/14.png +0 -0
- package/dist/logos/15.png +0 -0
- package/dist/logos/16.png +0 -0
- package/dist/logos/17.png +0 -0
- package/dist/logos/18.png +0 -0
- package/dist/logos/19.png +0 -0
- package/dist/logos/2.png +0 -0
- package/dist/logos/20.png +0 -0
- package/dist/logos/21.png +0 -0
- package/dist/logos/22.png +0 -0
- package/dist/logos/23.png +0 -0
- package/dist/logos/24.png +0 -0
- package/dist/logos/25.png +0 -0
- package/dist/logos/26.png +0 -0
- package/dist/logos/27.png +0 -0
- package/dist/logos/28.png +0 -0
- package/dist/logos/29.png +0 -0
- package/dist/logos/3.png +0 -0
- package/dist/logos/30.png +0 -0
- package/dist/logos/31.png +0 -0
- package/dist/logos/32.png +0 -0
- package/dist/logos/33.png +0 -0
- package/dist/logos/34.png +0 -0
- package/dist/logos/35.png +0 -0
- package/dist/logos/36.png +0 -0
- package/dist/logos/37.png +0 -0
- package/dist/logos/38.png +0 -0
- package/dist/logos/39.png +0 -0
- package/dist/logos/4.png +0 -0
- package/dist/logos/40.png +0 -0
- package/dist/logos/41.png +0 -0
- package/dist/logos/42.png +0 -0
- package/dist/logos/43.png +0 -0
- package/dist/logos/44.png +0 -0
- package/dist/logos/45.png +0 -0
- package/dist/logos/46.png +0 -0
- package/dist/logos/47.png +0 -0
- package/dist/logos/48.png +0 -0
- package/dist/logos/49.png +0 -0
- package/dist/logos/5.png +0 -0
- package/dist/logos/50.png +0 -0
- package/dist/logos/51.png +0 -0
- package/dist/logos/52.png +0 -0
- package/dist/logos/53.png +0 -0
- package/dist/logos/54.png +0 -0
- package/dist/logos/55.png +0 -0
- package/dist/logos/6.png +0 -0
- package/dist/logos/7.png +0 -0
- package/dist/logos/8.png +0 -0
- package/dist/logos/9.png +0 -0
- package/dist/logos/Readme.md +1 -0
- package/dist/logos/logo_atomic_crm.svg +14 -0
- package/dist/logos/logo_atomic_crm_dark.svg +14 -0
- package/dist/logos/logo_atomic_crm_light.svg +14 -0
- package/dist/manifest.json +25 -0
- package/dist/robots.txt +3 -0
- package/dist/stats.html +4949 -0
- package/package.json +152 -0
- package/public/auth-callback.html +140 -0
- package/public/favicon.ico +0 -0
- package/public/img/adding-users.png +0 -0
- package/public/img/empty.svg +42 -0
- package/public/logo192.png +0 -0
- package/public/logo512.png +0 -0
- package/public/logos/0.png +0 -0
- package/public/logos/1.png +0 -0
- package/public/logos/10.png +0 -0
- package/public/logos/11.png +0 -0
- package/public/logos/12.png +0 -0
- package/public/logos/13.png +0 -0
- package/public/logos/14.png +0 -0
- package/public/logos/15.png +0 -0
- package/public/logos/16.png +0 -0
- package/public/logos/17.png +0 -0
- package/public/logos/18.png +0 -0
- package/public/logos/19.png +0 -0
- package/public/logos/2.png +0 -0
- package/public/logos/20.png +0 -0
- package/public/logos/21.png +0 -0
- package/public/logos/22.png +0 -0
- package/public/logos/23.png +0 -0
- package/public/logos/24.png +0 -0
- package/public/logos/25.png +0 -0
- package/public/logos/26.png +0 -0
- package/public/logos/27.png +0 -0
- package/public/logos/28.png +0 -0
- package/public/logos/29.png +0 -0
- package/public/logos/3.png +0 -0
- package/public/logos/30.png +0 -0
- package/public/logos/31.png +0 -0
- package/public/logos/32.png +0 -0
- package/public/logos/33.png +0 -0
- package/public/logos/34.png +0 -0
- package/public/logos/35.png +0 -0
- package/public/logos/36.png +0 -0
- package/public/logos/37.png +0 -0
- package/public/logos/38.png +0 -0
- package/public/logos/39.png +0 -0
- package/public/logos/4.png +0 -0
- package/public/logos/40.png +0 -0
- package/public/logos/41.png +0 -0
- package/public/logos/42.png +0 -0
- package/public/logos/43.png +0 -0
- package/public/logos/44.png +0 -0
- package/public/logos/45.png +0 -0
- package/public/logos/46.png +0 -0
- package/public/logos/47.png +0 -0
- package/public/logos/48.png +0 -0
- package/public/logos/49.png +0 -0
- package/public/logos/5.png +0 -0
- package/public/logos/50.png +0 -0
- package/public/logos/51.png +0 -0
- package/public/logos/52.png +0 -0
- package/public/logos/53.png +0 -0
- package/public/logos/54.png +0 -0
- package/public/logos/55.png +0 -0
- package/public/logos/6.png +0 -0
- package/public/logos/7.png +0 -0
- package/public/logos/8.png +0 -0
- package/public/logos/9.png +0 -0
- package/public/logos/Readme.md +1 -0
- package/public/logos/logo_atomic_crm.svg +14 -0
- package/public/logos/logo_atomic_crm_dark.svg +14 -0
- package/public/logos/logo_atomic_crm_light.svg +14 -0
- package/public/manifest.json +25 -0
- package/public/robots.txt +3 -0
- package/src/App.css +42 -0
- package/src/App.tsx +58 -0
- package/src/assets/react.svg +1 -0
- package/src/components/admin/Readme.md +40 -0
- package/src/components/admin/admin.tsx +132 -0
- package/src/components/admin/app-sidebar.tsx +166 -0
- package/src/components/admin/array-field.tsx +59 -0
- package/src/components/admin/array-input.tsx +201 -0
- package/src/components/admin/authentication.tsx +86 -0
- package/src/components/admin/autocomplete-array-input.tsx +254 -0
- package/src/components/admin/autocomplete-input.tsx +296 -0
- package/src/components/admin/badge-field.tsx +65 -0
- package/src/components/admin/boolean-input.tsx +116 -0
- package/src/components/admin/breadcrumb.tsx +135 -0
- package/src/components/admin/bulk-actions-toolbar.tsx +83 -0
- package/src/components/admin/bulk-delete-button.tsx +70 -0
- package/src/components/admin/bulk-export-button.tsx +76 -0
- package/src/components/admin/cancel-button.tsx +46 -0
- package/src/components/admin/columns-button.tsx +345 -0
- package/src/components/admin/confirm.tsx +166 -0
- package/src/components/admin/count.tsx +94 -0
- package/src/components/admin/create-button.tsx +58 -0
- package/src/components/admin/create.tsx +132 -0
- package/src/components/admin/data-table.tsx +520 -0
- package/src/components/admin/date-field.tsx +136 -0
- package/src/components/admin/date-input.tsx +317 -0
- package/src/components/admin/date-time-input.tsx +331 -0
- package/src/components/admin/delete-button.tsx +113 -0
- package/src/components/admin/edit-button.tsx +64 -0
- package/src/components/admin/edit-guesser.tsx +157 -0
- package/src/components/admin/edit.tsx +152 -0
- package/src/components/admin/email-field.tsx +74 -0
- package/src/components/admin/error.tsx +111 -0
- package/src/components/admin/export-button.tsx +126 -0
- package/src/components/admin/field-toggle.tsx +164 -0
- package/src/components/admin/file-field.tsx +123 -0
- package/src/components/admin/file-input.tsx +361 -0
- package/src/components/admin/filter-form.tsx +510 -0
- package/src/components/admin/form.tsx +312 -0
- package/src/components/admin/icon-button-with-tooltip.tsx +85 -0
- package/src/components/admin/index.ts +73 -0
- package/src/components/admin/input-helper-text.tsx +29 -0
- package/src/components/admin/layout.tsx +69 -0
- package/src/components/admin/list-guesser.tsx +239 -0
- package/src/components/admin/list-pagination.tsx +247 -0
- package/src/components/admin/list.tsx +178 -0
- package/src/components/admin/loading.tsx +40 -0
- package/src/components/admin/locales-menu-button.tsx +60 -0
- package/src/components/admin/login-page.tsx +104 -0
- package/src/components/admin/notification.tsx +114 -0
- package/src/components/admin/number-field.tsx +84 -0
- package/src/components/admin/number-input.tsx +124 -0
- package/src/components/admin/radio-button-group-input.tsx +184 -0
- package/src/components/admin/ready.tsx +55 -0
- package/src/components/admin/record-field.tsx +132 -0
- package/src/components/admin/reference-array-field.tsx +152 -0
- package/src/components/admin/reference-array-input.tsx +68 -0
- package/src/components/admin/reference-field.tsx +153 -0
- package/src/components/admin/reference-input.tsx +46 -0
- package/src/components/admin/reference-many-count.tsx +92 -0
- package/src/components/admin/reference-many-field.tsx +132 -0
- package/src/components/admin/refresh-button.tsx +31 -0
- package/src/components/admin/saved-queries.tsx +174 -0
- package/src/components/admin/search-input.tsx +57 -0
- package/src/components/admin/select-field.tsx +111 -0
- package/src/components/admin/select-input.tsx +323 -0
- package/src/components/admin/show-button.tsx +57 -0
- package/src/components/admin/show-guesser.tsx +215 -0
- package/src/components/admin/show.tsx +184 -0
- package/src/components/admin/simple-form-iterator.tsx +582 -0
- package/src/components/admin/simple-form.tsx +95 -0
- package/src/components/admin/simple-show-layout.tsx +8 -0
- package/src/components/admin/single-field-list.tsx +67 -0
- package/src/components/admin/sort-button.tsx +152 -0
- package/src/components/admin/spinner.tsx +46 -0
- package/src/components/admin/text-field.tsx +60 -0
- package/src/components/admin/text-input.tsx +77 -0
- package/src/components/admin/theme-mode-toggle.tsx +48 -0
- package/src/components/admin/theme-provider.tsx +74 -0
- package/src/components/admin/toggle-filter-button.tsx +77 -0
- package/src/components/admin/url-field.tsx +83 -0
- package/src/components/admin/user-menu.tsx +84 -0
- package/src/components/atomic-crm/activity/ActivityLog.tsx +54 -0
- package/src/components/atomic-crm/activity/ActivityLogCompanyCreated.tsx +50 -0
- package/src/components/atomic-crm/activity/ActivityLogContactCreated.tsx +42 -0
- package/src/components/atomic-crm/activity/ActivityLogContactNoteCreated.tsx +71 -0
- package/src/components/atomic-crm/activity/ActivityLogContext.tsx +11 -0
- package/src/components/atomic-crm/activity/ActivityLogDealCreated.tsx +41 -0
- package/src/components/atomic-crm/activity/ActivityLogDealNoteCreated.tsx +84 -0
- package/src/components/atomic-crm/activity/ActivityLogIterator.tsx +80 -0
- package/src/components/atomic-crm/activity/ActivityLogNote.tsx +36 -0
- package/src/components/atomic-crm/companies/AutocompleteCompanyInput.tsx +43 -0
- package/src/components/atomic-crm/companies/CompanyAside.tsx +207 -0
- package/src/components/atomic-crm/companies/CompanyAvatar.tsx +29 -0
- package/src/components/atomic-crm/companies/CompanyCard.tsx +88 -0
- package/src/components/atomic-crm/companies/CompanyCreate.tsx +41 -0
- package/src/components/atomic-crm/companies/CompanyEdit.tsx +33 -0
- package/src/components/atomic-crm/companies/CompanyEmpty.tsx +26 -0
- package/src/components/atomic-crm/companies/CompanyInputs.tsx +160 -0
- package/src/components/atomic-crm/companies/CompanyList.tsx +54 -0
- package/src/components/atomic-crm/companies/CompanyListFilter.tsx +55 -0
- package/src/components/atomic-crm/companies/CompanyShow.tsx +241 -0
- package/src/components/atomic-crm/companies/GridList.tsx +46 -0
- package/src/components/atomic-crm/companies/index.ts +11 -0
- package/src/components/atomic-crm/companies/sizes.ts +7 -0
- package/src/components/atomic-crm/consts.ts +5 -0
- package/src/components/atomic-crm/contacts/Avatar.tsx +40 -0
- package/src/components/atomic-crm/contacts/ContactAside.tsx +187 -0
- package/src/components/atomic-crm/contacts/ContactCreate.tsx +34 -0
- package/src/components/atomic-crm/contacts/ContactEdit.tsx +32 -0
- package/src/components/atomic-crm/contacts/ContactEmpty.tsx +28 -0
- package/src/components/atomic-crm/contacts/ContactImportButton.tsx +213 -0
- package/src/components/atomic-crm/contacts/ContactInputs.tsx +209 -0
- package/src/components/atomic-crm/contacts/ContactList.tsx +116 -0
- package/src/components/atomic-crm/contacts/ContactListContent.tsx +107 -0
- package/src/components/atomic-crm/contacts/ContactListFilter.tsx +126 -0
- package/src/components/atomic-crm/contacts/ContactMergeButton.tsx +263 -0
- package/src/components/atomic-crm/contacts/ContactShow.tsx +76 -0
- package/src/components/atomic-crm/contacts/ExportVCardButton.tsx +79 -0
- package/src/components/atomic-crm/contacts/TagsList.tsx +33 -0
- package/src/components/atomic-crm/contacts/TagsListEdit.tsx +155 -0
- package/src/components/atomic-crm/contacts/contacts_export.csv +3 -0
- package/src/components/atomic-crm/contacts/exportToVCard.ts +104 -0
- package/src/components/atomic-crm/contacts/index.tsx +14 -0
- package/src/components/atomic-crm/contacts/useContactImport.tsx +206 -0
- package/src/components/atomic-crm/dashboard/Dashboard.tsx +66 -0
- package/src/components/atomic-crm/dashboard/DashboardActivityLog.tsx +22 -0
- package/src/components/atomic-crm/dashboard/DashboardStepper.tsx +72 -0
- package/src/components/atomic-crm/dashboard/DealsChart.tsx +202 -0
- package/src/components/atomic-crm/dashboard/DealsPipeline.tsx +90 -0
- package/src/components/atomic-crm/dashboard/HotContacts.tsx +92 -0
- package/src/components/atomic-crm/dashboard/LatestNotes.tsx +116 -0
- package/src/components/atomic-crm/dashboard/TasksList.tsx +69 -0
- package/src/components/atomic-crm/dashboard/TasksListEmpty.tsx +22 -0
- package/src/components/atomic-crm/dashboard/TasksListFilter.tsx +72 -0
- package/src/components/atomic-crm/dashboard/Welcome.tsx +41 -0
- package/src/components/atomic-crm/deals/ContactList.tsx +31 -0
- package/src/components/atomic-crm/deals/DealArchivedList.tsx +105 -0
- package/src/components/atomic-crm/deals/DealCard.tsx +78 -0
- package/src/components/atomic-crm/deals/DealColumn.tsx +52 -0
- package/src/components/atomic-crm/deals/DealCreate.tsx +95 -0
- package/src/components/atomic-crm/deals/DealEdit.tsx +81 -0
- package/src/components/atomic-crm/deals/DealEmpty.tsx +63 -0
- package/src/components/atomic-crm/deals/DealInputs.tsx +103 -0
- package/src/components/atomic-crm/deals/DealList.tsx +95 -0
- package/src/components/atomic-crm/deals/DealListContent.tsx +245 -0
- package/src/components/atomic-crm/deals/DealShow.tsx +260 -0
- package/src/components/atomic-crm/deals/OnlyMineInput.tsx +30 -0
- package/src/components/atomic-crm/deals/deal.ts +5 -0
- package/src/components/atomic-crm/deals/dealUtils.ts +26 -0
- package/src/components/atomic-crm/deals/index.ts +6 -0
- package/src/components/atomic-crm/deals/stages.ts +28 -0
- package/src/components/atomic-crm/filters/FilterCategory.tsx +20 -0
- package/src/components/atomic-crm/layout/FormToolbar.tsx +12 -0
- package/src/components/atomic-crm/layout/Header.tsx +134 -0
- package/src/components/atomic-crm/layout/Layout.tsx +21 -0
- package/src/components/atomic-crm/layout/TopToolbar.tsx +24 -0
- package/src/components/atomic-crm/login/LoginSkeleton.tsx +18 -0
- package/src/components/atomic-crm/login/SignupPage.tsx +150 -0
- package/src/components/atomic-crm/login/StartPage.tsx +27 -0
- package/src/components/atomic-crm/misc/AsideSection.tsx +21 -0
- package/src/components/atomic-crm/misc/ContactOption.tsx +26 -0
- package/src/components/atomic-crm/misc/ImageEditorField.tsx +206 -0
- package/src/components/atomic-crm/misc/RelativeDate.tsx +5 -0
- package/src/components/atomic-crm/misc/Status.tsx +28 -0
- package/src/components/atomic-crm/misc/fetchWithTimeout.ts +19 -0
- package/src/components/atomic-crm/misc/isLinkedInUrl.ts +15 -0
- package/src/components/atomic-crm/misc/unsupportedDomains.const.ts +105 -0
- package/src/components/atomic-crm/misc/useAppBarHeight.ts +9 -0
- package/src/components/atomic-crm/misc/usePapaParse.tsx +144 -0
- package/src/components/atomic-crm/notes/Note.tsx +187 -0
- package/src/components/atomic-crm/notes/NoteAttachments.tsx +56 -0
- package/src/components/atomic-crm/notes/NoteCreate.tsx +112 -0
- package/src/components/atomic-crm/notes/NoteInputs.tsx +92 -0
- package/src/components/atomic-crm/notes/NotesIterator.tsx +37 -0
- package/src/components/atomic-crm/notes/StatusSelector.tsx +39 -0
- package/src/components/atomic-crm/notes/index.ts +3 -0
- package/src/components/atomic-crm/notes/utils.ts +13 -0
- package/src/components/atomic-crm/providers/commons/activity.ts +174 -0
- package/src/components/atomic-crm/providers/commons/canAccess.ts +26 -0
- package/src/components/atomic-crm/providers/commons/getCompanyAvatar.spec.ts +20 -0
- package/src/components/atomic-crm/providers/commons/getCompanyAvatar.ts +21 -0
- package/src/components/atomic-crm/providers/commons/getContactAvatar.spec.ts +80 -0
- package/src/components/atomic-crm/providers/commons/getContactAvatar.ts +70 -0
- package/src/components/atomic-crm/providers/commons/mergeContacts.ts +185 -0
- package/src/components/atomic-crm/providers/fakerest/authProvider.ts +74 -0
- package/src/components/atomic-crm/providers/fakerest/dataGenerator/companies.ts +53 -0
- package/src/components/atomic-crm/providers/fakerest/dataGenerator/contactNotes.ts +25 -0
- package/src/components/atomic-crm/providers/fakerest/dataGenerator/contacts.ts +103 -0
- package/src/components/atomic-crm/providers/fakerest/dataGenerator/dealNotes.ts +19 -0
- package/src/components/atomic-crm/providers/fakerest/dataGenerator/deals.ts +53 -0
- package/src/components/atomic-crm/providers/fakerest/dataGenerator/finalize.ts +10 -0
- package/src/components/atomic-crm/providers/fakerest/dataGenerator/index.ts +25 -0
- package/src/components/atomic-crm/providers/fakerest/dataGenerator/sales.ts +37 -0
- package/src/components/atomic-crm/providers/fakerest/dataGenerator/tags.ts +14 -0
- package/src/components/atomic-crm/providers/fakerest/dataGenerator/tasks.ts +55 -0
- package/src/components/atomic-crm/providers/fakerest/dataGenerator/types.ts +21 -0
- package/src/components/atomic-crm/providers/fakerest/dataGenerator/utils.ts +28 -0
- package/src/components/atomic-crm/providers/fakerest/dataProvider.ts +518 -0
- package/src/components/atomic-crm/providers/fakerest/index.ts +2 -0
- package/src/components/atomic-crm/providers/fakerest/internal/listParser.ts +48 -0
- package/src/components/atomic-crm/providers/fakerest/internal/supabaseAdapter.spec.ts +721 -0
- package/src/components/atomic-crm/providers/fakerest/internal/supabaseAdapter.ts +49 -0
- package/src/components/atomic-crm/providers/fakerest/internal/transformContainsFilter.spec.ts +35 -0
- package/src/components/atomic-crm/providers/fakerest/internal/transformContainsFilter.ts +17 -0
- package/src/components/atomic-crm/providers/fakerest/internal/transformFilter.ts +57 -0
- package/src/components/atomic-crm/providers/fakerest/internal/transformInFilter.spec.ts +32 -0
- package/src/components/atomic-crm/providers/fakerest/internal/transformInFilter.ts +17 -0
- package/src/components/atomic-crm/providers/fakerest/internal/transformOrFilter.spec.ts +23 -0
- package/src/components/atomic-crm/providers/fakerest/internal/transformOrFilter.ts +17 -0
- package/src/components/atomic-crm/providers/supabase/authProvider.ts +121 -0
- package/src/components/atomic-crm/providers/supabase/dataProvider.ts +407 -0
- package/src/components/atomic-crm/providers/supabase/index.ts +2 -0
- package/src/components/atomic-crm/providers/supabase/supabase.ts +34 -0
- package/src/components/atomic-crm/providers/types.ts +1 -0
- package/src/components/atomic-crm/root/CRM.tsx +167 -0
- package/src/components/atomic-crm/root/ConfigurationContext.tsx +80 -0
- package/src/components/atomic-crm/root/defaultConfiguration.ts +64 -0
- package/src/components/atomic-crm/root/i18nProvider.tsx +25 -0
- package/src/components/atomic-crm/sales/SaleName.tsx +13 -0
- package/src/components/atomic-crm/sales/SalesCreate.tsx +51 -0
- package/src/components/atomic-crm/sales/SalesEdit.tsx +82 -0
- package/src/components/atomic-crm/sales/SalesInputs.tsx +31 -0
- package/src/components/atomic-crm/sales/SalesList.tsx +62 -0
- package/src/components/atomic-crm/sales/index.ts +12 -0
- package/src/components/atomic-crm/settings/DatabaseSettings.tsx +169 -0
- package/src/components/atomic-crm/settings/SettingsPage.tsx +259 -0
- package/src/components/atomic-crm/setup/SupabaseSetupWizard.tsx +215 -0
- package/src/components/atomic-crm/simple-list/ListNoResults.tsx +53 -0
- package/src/components/atomic-crm/simple-list/ListPlaceholder.tsx +9 -0
- package/src/components/atomic-crm/simple-list/SimpleList.tsx +245 -0
- package/src/components/atomic-crm/simple-list/SimpleListItem.tsx +138 -0
- package/src/components/atomic-crm/simple-list/SimpleListLoading.tsx +60 -0
- package/src/components/atomic-crm/tags/RoundButton.tsx +10 -0
- package/src/components/atomic-crm/tags/TagChip.tsx +45 -0
- package/src/components/atomic-crm/tags/TagCreateModal.tsx +39 -0
- package/src/components/atomic-crm/tags/TagDialog.tsx +118 -0
- package/src/components/atomic-crm/tags/TagEditModal.tsx +42 -0
- package/src/components/atomic-crm/tags/colors.ts +12 -0
- package/src/components/atomic-crm/tasks/AddTask.tsx +191 -0
- package/src/components/atomic-crm/tasks/Task.tsx +184 -0
- package/src/components/atomic-crm/tasks/TaskEdit.tsx +96 -0
- package/src/components/atomic-crm/tasks/TasksIterator.tsx +30 -0
- package/src/components/atomic-crm/types.ts +226 -0
- package/src/components/supabase/forgot-password-page.tsx +86 -0
- package/src/components/supabase/layout.tsx +27 -0
- package/src/components/supabase/set-password-page.tsx +119 -0
- package/src/components/ui/README.md +34 -0
- package/src/components/ui/accordion.tsx +64 -0
- package/src/components/ui/alert.tsx +66 -0
- package/src/components/ui/avatar.tsx +99 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/breadcrumb.tsx +109 -0
- package/src/components/ui/button.tsx +59 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/command.tsx +175 -0
- package/src/components/ui/dialog.tsx +133 -0
- package/src/components/ui/drawer.tsx +133 -0
- package/src/components/ui/dropdown-menu.tsx +255 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/navigation-menu.tsx +168 -0
- package/src/components/ui/pagination.tsx +127 -0
- package/src/components/ui/popover.tsx +46 -0
- package/src/components/ui/progress.tsx +29 -0
- package/src/components/ui/radio-group.tsx +43 -0
- package/src/components/ui/select.tsx +183 -0
- package/src/components/ui/separator.tsx +26 -0
- package/src/components/ui/sheet.tsx +137 -0
- package/src/components/ui/sidebar.tsx +724 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/sonner.tsx +38 -0
- package/src/components/ui/spinner.tsx +51 -0
- package/src/components/ui/switch.tsx +29 -0
- package/src/components/ui/table.tsx +114 -0
- package/src/components/ui/tabs.tsx +64 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +61 -0
- package/src/hooks/saved-queries.tsx +67 -0
- package/src/hooks/simple-form-iterator-context.tsx +70 -0
- package/src/hooks/use-mobile.ts +21 -0
- package/src/hooks/useBulkExport.tsx +61 -0
- package/src/hooks/useSupportCreateSuggestion.tsx +188 -0
- package/src/hooks/user-menu-context.tsx +24 -0
- package/src/index.css +170 -0
- package/src/lib/field.type.ts +22 -0
- package/src/lib/genericMemo.ts +18 -0
- package/src/lib/i18nProvider.ts +9 -0
- package/src/lib/sanitizeInputRestProps.ts +46 -0
- package/src/lib/supabase-config.ts +123 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +10 -0
- package/src/setupTests.js +5 -0
- package/src/vite-env.d.ts +1 -0
- package/supabase/config.toml +157 -0
- package/supabase/functions/.env.development +7 -0
- package/supabase/functions/_shared/db.ts +187 -0
- package/supabase/functions/_shared/supabaseAdmin.ts +13 -0
- package/supabase/functions/_shared/utils.ts +13 -0
- package/supabase/functions/mergeContacts/index.ts +215 -0
- package/supabase/functions/postmark/addNoteToContact.ts +129 -0
- package/supabase/functions/postmark/extractMailContactData.ts +41 -0
- package/supabase/functions/postmark/getExpectedAuthorization.ts +4 -0
- package/supabase/functions/postmark/getNoteContent.ts +6 -0
- package/supabase/functions/postmark/index.ts +210 -0
- package/supabase/functions/updatePassword/index.ts +50 -0
- package/supabase/functions/users/index.ts +206 -0
- package/supabase/migrations/20240730075029_init_db.sql +600 -0
- package/supabase/migrations/20240730075425_init_triggers.sql +57 -0
- package/supabase/migrations/20240806124555_task_sales_id.sql +1 -0
- package/supabase/migrations/20240807082449_remove-aquisition.sql +20 -0
- package/supabase/migrations/20240808141826_init_state_configure.sql +9 -0
- package/supabase/migrations/20240813084010_tags_policy.sql +18 -0
- package/supabase/migrations/20241104153231_sales_policies.sql +7 -0
- package/supabase/migrations/20250109152531_email_jsonb.sql +43 -0
- package/supabase/migrations/20250113132531_phone_jsonb.sql +67 -0
- package/supabase/migrations/20251204172855_merge_contacts_function.sql +153 -0
- package/supabase/migrations/20251204201317_drop_merge_contacts_function.sql +2 -0
- package/supabase/seed.sql +0 -0
- package/supabase/templates/invite.html +70 -0
- package/supabase/templates/recovery.html +75 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# A string used to distinguish different Supabase projects on the same host. Defaults to the
|
|
2
|
+
# working directory name when running `supabase init`.
|
|
3
|
+
project_id = "atomic-crm-demo"
|
|
4
|
+
|
|
5
|
+
[api]
|
|
6
|
+
enabled = true
|
|
7
|
+
# Port to use for the API URL.
|
|
8
|
+
port = 54321
|
|
9
|
+
# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
|
|
10
|
+
# endpoints. public and storage are always included.
|
|
11
|
+
schemas = ["public", "storage", "graphql_public"]
|
|
12
|
+
# Extra schemas to add to the search_path of every request. public is always included.
|
|
13
|
+
extra_search_path = ["public", "extensions"]
|
|
14
|
+
# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
|
|
15
|
+
# for accidental or malicious requests.
|
|
16
|
+
max_rows = 1000
|
|
17
|
+
|
|
18
|
+
[db]
|
|
19
|
+
# Port to use for the local database URL.
|
|
20
|
+
port = 54322
|
|
21
|
+
# Port used by db diff command to initialize the shadow database.
|
|
22
|
+
shadow_port = 54320
|
|
23
|
+
# The database major version to use. This has to be the same as your remote database's. Run `SHOW
|
|
24
|
+
# server_version;` on the remote database to check.
|
|
25
|
+
major_version = 15
|
|
26
|
+
|
|
27
|
+
[db.pooler]
|
|
28
|
+
enabled = false
|
|
29
|
+
# Port to use for the local connection pooler.
|
|
30
|
+
port = 54329
|
|
31
|
+
# Specifies when a server connection can be reused by other clients.
|
|
32
|
+
# Configure one of the supported pooler modes: `transaction`, `session`.
|
|
33
|
+
pool_mode = "transaction"
|
|
34
|
+
# How many server connections to allow per user/database pair.
|
|
35
|
+
default_pool_size = 20
|
|
36
|
+
# Maximum number of client connections allowed.
|
|
37
|
+
max_client_conn = 100
|
|
38
|
+
|
|
39
|
+
[realtime]
|
|
40
|
+
enabled = true
|
|
41
|
+
# Bind realtime via either IPv4 or IPv6. (default: IPv6)
|
|
42
|
+
# ip_version = "IPv6"
|
|
43
|
+
# The maximum length in bytes of HTTP request headers. (default: 4096)
|
|
44
|
+
# max_header_length = 4096
|
|
45
|
+
|
|
46
|
+
[studio]
|
|
47
|
+
enabled = true
|
|
48
|
+
# Port to use for Supabase Studio.
|
|
49
|
+
port = 54323
|
|
50
|
+
# External URL of the API server that frontend connects to.
|
|
51
|
+
api_url = "http://127.0.0.1"
|
|
52
|
+
|
|
53
|
+
# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
|
|
54
|
+
# are monitored, and you can view the emails that would have been sent from the web interface.
|
|
55
|
+
[inbucket]
|
|
56
|
+
enabled = true
|
|
57
|
+
# Port to use for the email testing server web interface.
|
|
58
|
+
port = 54324
|
|
59
|
+
# Uncomment to expose additional ports for testing user applications that send emails.
|
|
60
|
+
# smtp_port = 54325
|
|
61
|
+
# pop3_port = 54326
|
|
62
|
+
|
|
63
|
+
[storage]
|
|
64
|
+
enabled = true
|
|
65
|
+
# The maximum file size allowed (e.g. "5MB", "500KB").
|
|
66
|
+
file_size_limit = "50MiB"
|
|
67
|
+
|
|
68
|
+
[auth]
|
|
69
|
+
enabled = true
|
|
70
|
+
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
|
|
71
|
+
# in emails.
|
|
72
|
+
site_url = "http://localhost:5173/"
|
|
73
|
+
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
|
|
74
|
+
additional_redirect_urls = ["https://localhost:5173/auth-callback.html"]
|
|
75
|
+
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
|
|
76
|
+
jwt_expiry = 3600
|
|
77
|
+
# If disabled, the refresh token will never expire.
|
|
78
|
+
enable_refresh_token_rotation = true
|
|
79
|
+
# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
|
|
80
|
+
# Requires enable_refresh_token_rotation = true.
|
|
81
|
+
refresh_token_reuse_interval = 10
|
|
82
|
+
# Allow/disallow new user signups to your project.
|
|
83
|
+
enable_signup = true
|
|
84
|
+
|
|
85
|
+
[auth.email]
|
|
86
|
+
# Allow/disallow new user signups via email to your project.
|
|
87
|
+
enable_signup = true
|
|
88
|
+
# If enabled, a user will be required to confirm any email change on both the old, and new email
|
|
89
|
+
# addresses. If disabled, only the new email is required to confirm.
|
|
90
|
+
double_confirm_changes = true
|
|
91
|
+
# If enabled, users need to confirm their email address before signing in.
|
|
92
|
+
enable_confirmations = false
|
|
93
|
+
|
|
94
|
+
[auth.email.template.invite]
|
|
95
|
+
subject = "You've been invited to Atomic CRM"
|
|
96
|
+
content_path = "./supabase/templates/invite.html"
|
|
97
|
+
|
|
98
|
+
[auth.email.template.recovery]
|
|
99
|
+
subject = "Reset your Atomic CRM Password"
|
|
100
|
+
content_path = "./supabase/templates/recovery.html"
|
|
101
|
+
|
|
102
|
+
[auth.sms]
|
|
103
|
+
# Allow/disallow new user signups via SMS to your project.
|
|
104
|
+
enable_signup = true
|
|
105
|
+
# If enabled, users need to confirm their phone number before signing in.
|
|
106
|
+
enable_confirmations = false
|
|
107
|
+
# Template for sending OTP to users
|
|
108
|
+
template = "Your code is {{ .Code }} ."
|
|
109
|
+
|
|
110
|
+
# Use pre-defined map of phone number to OTP for testing.
|
|
111
|
+
[auth.sms.test_otp]
|
|
112
|
+
# 4152127777 = "123456"
|
|
113
|
+
|
|
114
|
+
# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`.
|
|
115
|
+
[auth.sms.twilio]
|
|
116
|
+
enabled = false
|
|
117
|
+
account_sid = ""
|
|
118
|
+
message_service_sid = ""
|
|
119
|
+
# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead:
|
|
120
|
+
auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"
|
|
121
|
+
|
|
122
|
+
# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
|
|
123
|
+
# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin`, `notion`, `twitch`,
|
|
124
|
+
# `twitter`, `slack`, `spotify`, `workos`, `zoom`.
|
|
125
|
+
[auth.external.apple]
|
|
126
|
+
enabled = false
|
|
127
|
+
client_id = ""
|
|
128
|
+
# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
|
|
129
|
+
secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
|
|
130
|
+
# Overrides the default auth redirectUrl.
|
|
131
|
+
redirect_uri = ""
|
|
132
|
+
# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
|
|
133
|
+
# or any other third-party OIDC providers.
|
|
134
|
+
url = ""
|
|
135
|
+
|
|
136
|
+
[analytics]
|
|
137
|
+
enabled = false
|
|
138
|
+
port = 54327
|
|
139
|
+
vector_port = 54328
|
|
140
|
+
# Configure one of the supported backends: `postgres`, `bigquery`.
|
|
141
|
+
backend = "postgres"
|
|
142
|
+
|
|
143
|
+
# Experimental features may be deprecated any time
|
|
144
|
+
[experimental]
|
|
145
|
+
# Configures Postgres storage engine to use OrioleDB (S3)
|
|
146
|
+
orioledb_version = ""
|
|
147
|
+
# Configures S3 bucket URL, eg. <bucket_name>.s3-<region>.amazonaws.com
|
|
148
|
+
s3_host = "env(S3_HOST)"
|
|
149
|
+
# Configures S3 bucket region, eg. us-east-1
|
|
150
|
+
s3_region = "env(S3_REGION)"
|
|
151
|
+
# Configures AWS_ACCESS_KEY_ID for S3 bucket
|
|
152
|
+
s3_access_key = "env(S3_ACCESS_KEY)"
|
|
153
|
+
# Configures AWS_SECRET_ACCESS_KEY for S3 bucket
|
|
154
|
+
s3_secret_key = "env(S3_SECRET_KEY)"
|
|
155
|
+
|
|
156
|
+
[functions.postmark]
|
|
157
|
+
verify_jwt = false
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
POSTMARK_WEBHOOK_USER=testuser
|
|
2
|
+
POSTMARK_WEBHOOK_PASSWORD=testpwd
|
|
3
|
+
|
|
4
|
+
# IPs can be found at https://postmarkapp.com/support/article/800-ips-for-firewalls#webhooks
|
|
5
|
+
# 172.18.0.1 and 172.19.0.1 are for local dev only
|
|
6
|
+
# 192.168.65.1 is for Docker Desktop on Mac
|
|
7
|
+
POSTMARK_WEBHOOK_AUTHORIZED_IPS=3.134.147.250,50.31.156.6,50.31.156.77,18.217.206.57,172.18.0.1,172.19.0.1,192.168.65.1
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DatabaseConnection,
|
|
3
|
+
Driver,
|
|
4
|
+
QueryResult,
|
|
5
|
+
} from "https://esm.sh/kysely@0.27.2";
|
|
6
|
+
import {
|
|
7
|
+
CompiledQuery,
|
|
8
|
+
Kysely,
|
|
9
|
+
PostgresAdapter,
|
|
10
|
+
PostgresIntrospector,
|
|
11
|
+
PostgresQueryCompiler,
|
|
12
|
+
type Generated,
|
|
13
|
+
} from "https://esm.sh/kysely@0.27.2";
|
|
14
|
+
|
|
15
|
+
export { CompiledQuery };
|
|
16
|
+
import type { PoolClient } from "https://deno.land/x/postgres@v0.17.0/mod.ts";
|
|
17
|
+
import { Pool } from "https://deno.land/x/postgres@v0.17.0/mod.ts";
|
|
18
|
+
|
|
19
|
+
// Database schema types
|
|
20
|
+
export interface ContactsTable {
|
|
21
|
+
id: Generated<number>;
|
|
22
|
+
first_name: string | null;
|
|
23
|
+
last_name: string | null;
|
|
24
|
+
gender: string | null;
|
|
25
|
+
title: string | null;
|
|
26
|
+
email_jsonb: unknown | null; // JSONB array
|
|
27
|
+
phone_jsonb: unknown | null; // JSONB array
|
|
28
|
+
background: string | null;
|
|
29
|
+
avatar: unknown | null; // JSONB
|
|
30
|
+
first_seen: Date | null;
|
|
31
|
+
last_seen: Date | null;
|
|
32
|
+
has_newsletter: boolean | null;
|
|
33
|
+
status: string | null;
|
|
34
|
+
tags: number[] | null;
|
|
35
|
+
company_id: number | null;
|
|
36
|
+
sales_id: number | null;
|
|
37
|
+
linkedin_url: string | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface TasksTable {
|
|
41
|
+
id: Generated<number>;
|
|
42
|
+
contact_id: number;
|
|
43
|
+
type: string | null;
|
|
44
|
+
text: string | null;
|
|
45
|
+
due_date: Date;
|
|
46
|
+
done_date: Date | null;
|
|
47
|
+
sales_id: number | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface ContactNotesTable {
|
|
51
|
+
id: Generated<number>;
|
|
52
|
+
contact_id: number;
|
|
53
|
+
text: string | null;
|
|
54
|
+
date: Date | null;
|
|
55
|
+
sales_id: number | null;
|
|
56
|
+
status: string | null;
|
|
57
|
+
attachments: unknown[] | null; // JSONB array
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface DealsTable {
|
|
61
|
+
id: Generated<number>;
|
|
62
|
+
name: string;
|
|
63
|
+
company_id: number | null;
|
|
64
|
+
contact_ids: number[];
|
|
65
|
+
category: string | null;
|
|
66
|
+
stage: string;
|
|
67
|
+
description: string | null;
|
|
68
|
+
amount: number | null;
|
|
69
|
+
created_at: Date;
|
|
70
|
+
updated_at: Date;
|
|
71
|
+
archived_at: Date | null;
|
|
72
|
+
expected_closing_date: Date | null;
|
|
73
|
+
sales_id: number | null;
|
|
74
|
+
index: number | null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface Database {
|
|
78
|
+
contacts: ContactsTable;
|
|
79
|
+
tasks: TasksTable;
|
|
80
|
+
contactNotes: ContactNotesTable;
|
|
81
|
+
deals: DealsTable;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Deno Postgres Driver for Kysely
|
|
85
|
+
class DenoPostgresDriver implements Driver {
|
|
86
|
+
private pool: Pool;
|
|
87
|
+
private connections = new WeakMap<PoolClient, DatabaseConnection>();
|
|
88
|
+
|
|
89
|
+
constructor(pool: Pool) {
|
|
90
|
+
this.pool = pool;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async init(): Promise<void> {
|
|
94
|
+
// Connection pool is already initialized
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async acquireConnection(): Promise<DatabaseConnection> {
|
|
98
|
+
const client = await this.pool.connect();
|
|
99
|
+
let connection = this.connections.get(client);
|
|
100
|
+
|
|
101
|
+
if (!connection) {
|
|
102
|
+
connection = new DenoPostgresConnection(client);
|
|
103
|
+
this.connections.set(client, connection);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return connection;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async beginTransaction(connection: DatabaseConnection): Promise<void> {
|
|
110
|
+
await connection.executeQuery(CompiledQuery.raw("begin"));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async commitTransaction(connection: DatabaseConnection): Promise<void> {
|
|
114
|
+
await connection.executeQuery(CompiledQuery.raw("commit"));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async rollbackTransaction(connection: DatabaseConnection): Promise<void> {
|
|
118
|
+
await connection.executeQuery(CompiledQuery.raw("rollback"));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async releaseConnection(connection: DatabaseConnection): Promise<void> {
|
|
122
|
+
(connection as DenoPostgresConnection).release();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async destroy(): Promise<void> {
|
|
126
|
+
await this.pool.end();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
class DenoPostgresConnection implements DatabaseConnection {
|
|
131
|
+
private client: PoolClient;
|
|
132
|
+
|
|
133
|
+
constructor(client: PoolClient) {
|
|
134
|
+
this.client = client;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async executeQuery<O>(compiledQuery: CompiledQuery): Promise<QueryResult<O>> {
|
|
138
|
+
const result = await this.client.queryObject<O>({
|
|
139
|
+
text: compiledQuery.sql,
|
|
140
|
+
args: compiledQuery.parameters as unknown[],
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (
|
|
144
|
+
result.command === "INSERT" ||
|
|
145
|
+
result.command === "UPDATE" ||
|
|
146
|
+
result.command === "DELETE"
|
|
147
|
+
) {
|
|
148
|
+
return {
|
|
149
|
+
numAffectedRows: BigInt(result.rowCount ?? 0),
|
|
150
|
+
rows: result.rows ?? [],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
rows: result.rows ?? [],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
streamQuery<O>(
|
|
160
|
+
_compiledQuery: CompiledQuery,
|
|
161
|
+
_chunkSize?: number,
|
|
162
|
+
): AsyncIterableIterator<QueryResult<O>> {
|
|
163
|
+
throw new Error("Deno Postgres driver does not support streaming");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
release() {
|
|
167
|
+
this.client.release();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Create connection pool
|
|
172
|
+
// Use SUPABASE_DB_URL if available (production), otherwise fall back to local dev connection string
|
|
173
|
+
const connectionString =
|
|
174
|
+
Deno.env.get("SUPABASE_DB_URL") ||
|
|
175
|
+
"postgresql://postgres:postgres@db:5432/postgres";
|
|
176
|
+
|
|
177
|
+
const pool = new Pool(connectionString, 1); // Single connection for edge functions
|
|
178
|
+
|
|
179
|
+
// Create and export Kysely instance
|
|
180
|
+
export const db = new Kysely<Database>({
|
|
181
|
+
dialect: {
|
|
182
|
+
createAdapter: () => new PostgresAdapter(),
|
|
183
|
+
createDriver: () => new DenoPostgresDriver(pool),
|
|
184
|
+
createIntrospector: (db: Kysely<any>) => new PostgresIntrospector(db),
|
|
185
|
+
createQueryCompiler: () => new PostgresQueryCompiler(),
|
|
186
|
+
},
|
|
187
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SupabaseClient } from "jsr:@supabase/supabase-js@2";
|
|
2
|
+
import { createClient } from "jsr:@supabase/supabase-js@2";
|
|
3
|
+
|
|
4
|
+
export const supabaseAdmin: SupabaseClient = createClient(
|
|
5
|
+
Deno.env.get("SUPABASE_URL") ?? "",
|
|
6
|
+
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "",
|
|
7
|
+
{
|
|
8
|
+
auth: {
|
|
9
|
+
autoRefreshToken: false,
|
|
10
|
+
persistSession: false,
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const corsHeaders = {
|
|
2
|
+
"Access-Control-Allow-Origin": "*",
|
|
3
|
+
"Access-Control-Allow-Headers":
|
|
4
|
+
"authorization, x-client-info, apikey, content-type",
|
|
5
|
+
"Access-Control-Allow-Methods": "POST, PATCH, DELETE",
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function createErrorResponse(status: number, message: string) {
|
|
9
|
+
return new Response(JSON.stringify({ status, message }), {
|
|
10
|
+
headers: { "Content-Type": "application/json", ...corsHeaders },
|
|
11
|
+
status,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
|
2
|
+
import { createClient } from "jsr:@supabase/supabase-js@2";
|
|
3
|
+
import { sql, type Selectable } from "https://esm.sh/kysely@0.27.2";
|
|
4
|
+
import { db, type ContactsTable, CompiledQuery } from "../_shared/db.ts";
|
|
5
|
+
import { corsHeaders, createErrorResponse } from "../_shared/utils.ts";
|
|
6
|
+
|
|
7
|
+
type Contact = Selectable<ContactsTable>;
|
|
8
|
+
|
|
9
|
+
// Helper functions to merge arrays
|
|
10
|
+
function mergeArraysUnique<T>(arr1: T[], arr2: T[]): T[] {
|
|
11
|
+
return [...new Set([...arr1, ...arr2])];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function mergeObjectArraysUnique<T>(
|
|
15
|
+
arr1: T[],
|
|
16
|
+
arr2: T[],
|
|
17
|
+
getKey: (item: T) => string,
|
|
18
|
+
): T[] {
|
|
19
|
+
const map = new Map<string, T>();
|
|
20
|
+
|
|
21
|
+
arr1.forEach((item) => {
|
|
22
|
+
const key = getKey(item);
|
|
23
|
+
if (key) map.set(key, item);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
arr2.forEach((item) => {
|
|
27
|
+
const key = getKey(item);
|
|
28
|
+
if (key && !map.has(key)) {
|
|
29
|
+
map.set(key, item);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return Array.from(map.values());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function mergeContactData(winner: Contact, loser: Contact) {
|
|
37
|
+
// Merge emails
|
|
38
|
+
const mergedEmails = mergeObjectArraysUnique(
|
|
39
|
+
winner.email_jsonb || [],
|
|
40
|
+
loser.email_jsonb || [],
|
|
41
|
+
(email: any) => email.email,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// Merge phones
|
|
45
|
+
const mergedPhones = mergeObjectArraysUnique(
|
|
46
|
+
winner.phone_jsonb || [],
|
|
47
|
+
loser.phone_jsonb || [],
|
|
48
|
+
(phone: any) => phone.number,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const selectedAvatar =
|
|
52
|
+
winner.avatar && winner.avatar.src ? winner.avatar : loser.avatar;
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
avatar: selectedAvatar ? (JSON.stringify(selectedAvatar) as any) : null,
|
|
56
|
+
gender: winner.gender ?? loser.gender,
|
|
57
|
+
first_name: winner.first_name ?? loser.first_name,
|
|
58
|
+
last_name: winner.last_name ?? loser.last_name,
|
|
59
|
+
title: winner.title ?? loser.title,
|
|
60
|
+
company_id: winner.company_id ?? loser.company_id,
|
|
61
|
+
email_jsonb: JSON.stringify(mergedEmails) as any,
|
|
62
|
+
phone_jsonb: JSON.stringify(mergedPhones) as any,
|
|
63
|
+
linkedin_url: winner.linkedin_url || loser.linkedin_url,
|
|
64
|
+
background: winner.background ?? loser.background,
|
|
65
|
+
has_newsletter: winner.has_newsletter ?? loser.has_newsletter,
|
|
66
|
+
first_seen: winner.first_seen ?? loser.first_seen,
|
|
67
|
+
last_seen:
|
|
68
|
+
winner.last_seen && loser.last_seen
|
|
69
|
+
? winner.last_seen > loser.last_seen
|
|
70
|
+
? winner.last_seen
|
|
71
|
+
: loser.last_seen
|
|
72
|
+
: (winner.last_seen ?? loser.last_seen),
|
|
73
|
+
sales_id: winner.sales_id ?? loser.sales_id,
|
|
74
|
+
tags: mergeArraysUnique(winner.tags || [], loser.tags || []),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function mergeContacts(
|
|
79
|
+
loserId: number,
|
|
80
|
+
winnerId: number,
|
|
81
|
+
userId: string,
|
|
82
|
+
) {
|
|
83
|
+
try {
|
|
84
|
+
return await db.transaction().execute(async (trx) => {
|
|
85
|
+
// Enable RLS by switching to authenticated role and setting user context
|
|
86
|
+
await trx.executeQuery(CompiledQuery.raw("SET LOCAL ROLE authenticated"));
|
|
87
|
+
await trx.executeQuery(
|
|
88
|
+
CompiledQuery.raw(
|
|
89
|
+
`SELECT set_config('request.jwt.claim.sub', '${userId}', true)`,
|
|
90
|
+
),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// 1. Fetch both contacts
|
|
94
|
+
const [winner, loser] = await Promise.all([
|
|
95
|
+
trx
|
|
96
|
+
.selectFrom("contacts")
|
|
97
|
+
.selectAll()
|
|
98
|
+
.where("id", "=", winnerId)
|
|
99
|
+
.executeTakeFirstOrThrow(),
|
|
100
|
+
trx
|
|
101
|
+
.selectFrom("contacts")
|
|
102
|
+
.selectAll()
|
|
103
|
+
.where("id", "=", loserId)
|
|
104
|
+
.executeTakeFirstOrThrow(),
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
// 2. Reassign tasks from loser to winner
|
|
108
|
+
await trx
|
|
109
|
+
.updateTable("tasks")
|
|
110
|
+
.set({ contact_id: winnerId })
|
|
111
|
+
.where("contact_id", "=", loserId)
|
|
112
|
+
.execute();
|
|
113
|
+
|
|
114
|
+
// 3. Reassign notes from loser to winner
|
|
115
|
+
await trx
|
|
116
|
+
.updateTable("contactNotes")
|
|
117
|
+
.set({ contact_id: winnerId })
|
|
118
|
+
.where("contact_id", "=", loserId)
|
|
119
|
+
.execute();
|
|
120
|
+
|
|
121
|
+
// 4. Update deals - replace loserId with winnerId in contact_ids array
|
|
122
|
+
const deals = await trx
|
|
123
|
+
.selectFrom("deals")
|
|
124
|
+
.selectAll()
|
|
125
|
+
.where(sql`contact_ids @> ARRAY[${loserId}]::bigint[]`)
|
|
126
|
+
.execute();
|
|
127
|
+
|
|
128
|
+
for (const deal of deals) {
|
|
129
|
+
const newContactIds = [
|
|
130
|
+
...new Set(
|
|
131
|
+
deal.contact_ids.filter((id) => id !== loserId).concat(winnerId),
|
|
132
|
+
),
|
|
133
|
+
];
|
|
134
|
+
await trx
|
|
135
|
+
.updateTable("deals")
|
|
136
|
+
.set({ contact_ids: newContactIds })
|
|
137
|
+
.where("id", "=", deal.id)
|
|
138
|
+
.execute();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 5. Merge and update winner contact
|
|
142
|
+
const mergedData = mergeContactData(winner as Contact, loser as Contact);
|
|
143
|
+
await trx
|
|
144
|
+
.updateTable("contacts")
|
|
145
|
+
.set(mergedData)
|
|
146
|
+
.where("id", "=", winnerId)
|
|
147
|
+
.execute();
|
|
148
|
+
|
|
149
|
+
// 6. Delete loser contact
|
|
150
|
+
await trx.deleteFrom("contacts").where("id", "=", loserId).execute();
|
|
151
|
+
|
|
152
|
+
return { success: true, winnerId };
|
|
153
|
+
});
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error("Transaction failed:", error);
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
Deno.serve(async (req: Request) => {
|
|
161
|
+
// Handle CORS preflight
|
|
162
|
+
if (req.method === "OPTIONS") {
|
|
163
|
+
return new Response(null, {
|
|
164
|
+
status: 204,
|
|
165
|
+
headers: corsHeaders,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Authenticate user via Supabase client (bypasses RLS, so we need explicit auth check)
|
|
170
|
+
const authHeader = req.headers.get("Authorization");
|
|
171
|
+
if (!authHeader) {
|
|
172
|
+
return createErrorResponse(401, "Missing Authorization header");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const supabaseClient = createClient(
|
|
176
|
+
Deno.env.get("SUPABASE_URL") ?? "",
|
|
177
|
+
Deno.env.get("SUPABASE_ANON_KEY") ?? "",
|
|
178
|
+
{ global: { headers: { Authorization: authHeader } } },
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const {
|
|
182
|
+
data: { user },
|
|
183
|
+
error: authError,
|
|
184
|
+
} = await supabaseClient.auth.getUser();
|
|
185
|
+
if (!user || authError) {
|
|
186
|
+
return createErrorResponse(401, "Unauthorized");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Handle POST request
|
|
190
|
+
if (req.method === "POST") {
|
|
191
|
+
try {
|
|
192
|
+
const { loserId, winnerId } = await req.json();
|
|
193
|
+
|
|
194
|
+
if (!loserId || !winnerId) {
|
|
195
|
+
return createErrorResponse(400, "Missing loserId or winnerId");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const result = await mergeContacts(loserId, winnerId, user.id);
|
|
199
|
+
|
|
200
|
+
return new Response(JSON.stringify(result), {
|
|
201
|
+
headers: { "Content-Type": "application/json", ...corsHeaders },
|
|
202
|
+
});
|
|
203
|
+
} catch (error) {
|
|
204
|
+
console.error("Merge failed:", error);
|
|
205
|
+
return createErrorResponse(
|
|
206
|
+
500,
|
|
207
|
+
`Failed to merge contacts: ${
|
|
208
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
209
|
+
}`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return createErrorResponse(405, "Method Not Allowed");
|
|
215
|
+
});
|