From 601ca1da48b2e6e41b6b97dea6344a9451bbc4a8 Mon Sep 17 00:00:00 2001 From: Ashwin Date: Wed, 21 Oct 2026 09:25:18 +0530 Subject: [PATCH] clean up --- .github/workflows/tests.yml | 4 +- CLAUDE.md | 2 +- backend/cases/tests/test_csat.py | 3 +- .../test_inbound_webhook_reachability.py | 2 +- backend/common/base.py | 30 -- backend/common/middleware/get_company.py | 2 +- backend/common/middleware/rls_context.py | 76 +-- backend/common/tests/test_models.py | 74 --- backend/common/tests/test_multitenancy.py | 30 -- .../common/tests/test_rls_context_reset.py | 17 +- backend/common/utils.py | 9 - backend/invoices/tests/test_portal_rls.py | 3 +- backend/pyproject.toml | 1 - backend/pytest.ini | 1 - backend/uv.lock | 11 - docs/architecture/multi-tenancy-and-rls.md | 7 +- docs/architecture/overview.md | 5 - .../adding-an-org-scoped-model.md | 2 +- docs/contributing/testing.md | 8 +- docs/self-hosting/postgresql-and-rls.md | 7 +- frontend/package.json | 1 - frontend/pnpm-lock.yaml | 3 - frontend/src/lib/api-helpers.js | 108 ---- .../components/dashboard/ActivityFeed.svelte | 228 --------- .../lib/components/dashboard/FocusBar.svelte | 113 ---- .../components/dashboard/HotLeadsPanel.svelte | 198 ------- .../lib/components/dashboard/KPICard.svelte | 164 ------ .../lib/components/dashboard/TaskList.svelte | 251 --------- .../src/lib/components/dashboard/index.js | 5 - .../lib/components/icons/GithubIcon.svelte | 22 - .../lib/components/icons/LinkedinIcon.svelte | 21 - frontend/src/lib/components/icons/index.js | 2 - .../lib/components/layout/FilterPill.svelte | 65 --- .../components/layout/FilterPopover.svelte | 40 -- .../lib/components/layout/FilterStrip.svelte | 23 - .../lib/components/layout/PageHeader.svelte | 116 ----- .../src/lib/components/layout/ViewTabs.svelte | 34 -- frontend/src/lib/components/layout/index.js | 5 - .../alert-dialog/alert-dialog-action.svelte | 21 - .../alert-dialog/alert-dialog-cancel.svelte | 21 - .../alert-dialog/alert-dialog-content.svelte | 37 -- .../alert-dialog-description.svelte | 20 - .../alert-dialog/alert-dialog-footer.svelte | 22 - .../alert-dialog/alert-dialog-header.svelte | 22 - .../alert-dialog/alert-dialog-overlay.svelte | 23 - .../ui/alert-dialog/alert-dialog-title.svelte | 20 - .../alert-dialog/alert-dialog-trigger.svelte | 7 - .../lib/components/ui/alert-dialog/index.js | 39 -- .../ui/avatar/avatar-fallback.svelte | 40 -- .../components/ui/avatar/avatar-gradients.js | 22 - .../components/ui/avatar/avatar-image.svelte | 13 - .../lib/components/ui/avatar/avatar.svelte | 48 -- .../src/lib/components/ui/avatar/index.js | 14 - .../src/lib/components/ui/badge/badge.svelte | 84 --- frontend/src/lib/components/ui/badge/index.js | 2 - .../ui/bulk-action-bar/BulkActionBar.svelte | 31 -- .../components/ui/bulk-action-bar/index.js | 1 - .../components/ui/calendar/Calendar.svelte | 135 ----- .../src/lib/components/ui/calendar/index.js | 3 - .../components/ui/card/card-content.svelte | 14 - .../ui/card/card-description.svelte | 14 - .../lib/components/ui/card/card-header.svelte | 18 - .../lib/components/ui/card/card-title.svelte | 16 - .../src/lib/components/ui/card/card.svelte | 27 - frontend/src/lib/components/ui/card/index.js | 19 - .../components/ui/crm-drawer/CrmDrawer.svelte | 301 ----------- .../ui/crm-drawer/CrmPropertyRow.svelte | 482 ------------------ .../src/lib/components/ui/crm-drawer/index.js | 2 - .../components/ui/crm-table/CrmTable.svelte | 481 ----------------- .../src/lib/components/ui/crm-table/index.js | 1 - .../dropdown-menu-checkbox-group.svelte | 12 - .../dropdown-menu-checkbox-item.svelte | 37 -- .../dropdown-menu-content.svelte | 37 -- .../dropdown-menu/dropdown-menu-group.svelte | 7 - .../dropdown-menu/dropdown-menu-item.svelte | 34 -- .../dropdown-menu/dropdown-menu-label.svelte | 23 - .../dropdown-menu-separator.svelte | 20 - .../dropdown-menu-trigger.svelte | 7 - .../lib/components/ui/dropdown-menu/index.js | 34 -- .../editable-field/EditableMultiSelect.svelte | 247 --------- .../lib/components/ui/editable-field/index.js | 1 - .../ui/filter/DateRangeFilter.svelte | 319 ------------ .../lib/components/ui/filter/FilterBar.svelte | 196 ------- .../components/ui/filter/SearchInput.svelte | 97 ---- .../components/ui/filter/SelectFilter.svelte | 212 -------- .../lib/components/ui/filter/TagFilter.svelte | 231 --------- .../src/lib/components/ui/filter/index.js | 5 - .../components/ui/form-shell/FormShell.svelte | 82 --- .../src/lib/components/ui/form-shell/index.js | 1 - frontend/src/lib/components/ui/input/index.js | 7 - .../src/lib/components/ui/input/input.svelte | 59 --- frontend/src/lib/components/ui/label/index.js | 7 - .../src/lib/components/ui/label/label.svelte | 16 - .../ui/pagination/Pagination.svelte | 157 ------ .../src/lib/components/ui/pagination/index.js | 1 - .../src/lib/components/ui/popover/index.js | 17 - .../ui/popover/popover-content.svelte | 37 -- .../ui/popover/popover-trigger.svelte | 13 - .../src/lib/components/ui/progress/index.js | 7 - .../components/ui/progress/progress.svelte | 21 - .../ui/section-card/SectionCard.svelte | 72 --- .../lib/components/ui/section-card/index.js | 2 - .../src/lib/components/ui/select/index.js | 37 -- .../ui/select/select-content.svelte | 53 -- .../ui/select/select-group-heading.svelte | 14 - .../components/ui/select/select-group.svelte | 7 - .../components/ui/select/select-item.svelte | 48 -- .../components/ui/select/select-label.svelte | 13 - .../components/ui/select/select-portal.svelte | 7 - .../select/select-scroll-down-button.svelte | 16 - .../ui/select/select-scroll-up-button.svelte | 16 - .../ui/select/select-separator.svelte | 13 - .../ui/select/select-trigger.svelte | 41 -- .../lib/components/ui/select/select.svelte | 12 - .../src/lib/components/ui/separator/index.js | 7 - .../components/ui/separator/separator.svelte | 21 - frontend/src/lib/components/ui/sheet/index.js | 27 - .../components/ui/sheet/sheet-content.svelte | 56 -- .../ui/sheet/sheet-description.svelte | 21 - .../components/ui/sheet/sheet-header.svelte | 22 - .../components/ui/sheet/sheet-overlay.svelte | 23 - .../components/ui/sheet/sheet-title.svelte | 21 - .../components/ui/side-panel/SidePanel.svelte | 75 --- .../ui/side-panel/SidePanelSection.svelte | 22 - .../src/lib/components/ui/side-panel/index.js | 27 - .../src/lib/components/ui/skeleton/index.js | 7 - .../components/ui/skeleton/skeleton.svelte | 11 - .../ui/stage-stepper/StageStepper.svelte | 80 --- .../lib/components/ui/stage-stepper/index.js | 1 - .../src/lib/components/ui/switch/index.js | 3 - .../lib/components/ui/switch/switch.svelte | 48 -- frontend/src/lib/components/ui/table/index.js | 22 - .../lib/components/ui/table/table-body.svelte | 21 - .../lib/components/ui/table/table-cell.svelte | 24 - .../lib/components/ui/table/table-head.svelte | 24 - .../components/ui/table/table-header.svelte | 21 - .../lib/components/ui/table/table-row.svelte | 24 - .../src/lib/components/ui/table/table.svelte | 24 - frontend/src/lib/components/ui/tabs/index.js | 16 - .../components/ui/tabs/tabs-content.svelte | 14 - .../lib/components/ui/tabs/tabs-list.svelte | 18 - .../components/ui/tabs/tabs-trigger.svelte | 29 -- .../src/lib/components/ui/tabs/tabs.svelte | 14 - .../src/lib/components/ui/textarea/index.js | 7 - .../components/ui/textarea/textarea.svelte | 31 -- .../components/ui/timeline/Timeline.svelte | 27 - .../ui/timeline/TimelineItem.svelte | 54 -- .../src/lib/components/ui/timeline/index.js | 2 - .../ui/view-toggle/ViewToggle.svelte | 53 -- .../lib/components/ui/view-toggle/index.js | 1 - .../src/lib/components/users/TeamCard.svelte | 126 ----- .../components/users/TeamFormDialog.svelte | 158 ------ frontend/src/lib/components/users/index.js | 2 - frontend/src/lib/constants/colors.js | 116 ----- frontend/src/lib/constants/lead-choices.js | 131 ----- frontend/src/lib/hooks/index.js | 3 - frontend/src/lib/hooks/is-mobile.svelte.js | 13 - .../src/lib/hooks/use-drawer-state.svelte.js | 194 ------- .../src/lib/hooks/use-list-filters.svelte.js | 215 -------- frontend/src/lib/stores/org.js | 27 - frontend/src/lib/utils/drag-drop.js | 110 ---- frontend/src/lib/utils/formatting.js | 148 ------ frontend/src/lib/utils/phone.js | 165 ------ frontend/src/lib/utils/table-helpers.js | 343 ------------- frontend/src/lib/utils/ui-helpers.js | 131 ----- .../test/unit/api_service_refresh_test.dart | 2 +- 166 files changed, 27 insertions(+), 8743 deletions(-) delete mode 100644 frontend/src/lib/components/dashboard/ActivityFeed.svelte delete mode 100644 frontend/src/lib/components/dashboard/FocusBar.svelte delete mode 100644 frontend/src/lib/components/dashboard/HotLeadsPanel.svelte delete mode 100644 frontend/src/lib/components/dashboard/KPICard.svelte delete mode 100644 frontend/src/lib/components/dashboard/TaskList.svelte delete mode 100644 frontend/src/lib/components/dashboard/index.js delete mode 100644 frontend/src/lib/components/icons/GithubIcon.svelte delete mode 100644 frontend/src/lib/components/icons/LinkedinIcon.svelte delete mode 100644 frontend/src/lib/components/icons/index.js delete mode 100644 frontend/src/lib/components/layout/FilterPill.svelte delete mode 100644 frontend/src/lib/components/layout/FilterPopover.svelte delete mode 100644 frontend/src/lib/components/layout/FilterStrip.svelte delete mode 100644 frontend/src/lib/components/layout/PageHeader.svelte delete mode 100644 frontend/src/lib/components/layout/ViewTabs.svelte delete mode 100644 frontend/src/lib/components/layout/index.js delete mode 100644 frontend/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte delete mode 100644 frontend/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte delete mode 100644 frontend/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte delete mode 100644 frontend/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte delete mode 100644 frontend/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte delete mode 100644 frontend/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte delete mode 100644 frontend/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte delete mode 100644 frontend/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte delete mode 100644 frontend/src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte delete mode 100644 frontend/src/lib/components/ui/alert-dialog/index.js delete mode 100644 frontend/src/lib/components/ui/avatar/avatar-fallback.svelte delete mode 100644 frontend/src/lib/components/ui/avatar/avatar-gradients.js delete mode 100644 frontend/src/lib/components/ui/avatar/avatar-image.svelte delete mode 100644 frontend/src/lib/components/ui/avatar/avatar.svelte delete mode 100644 frontend/src/lib/components/ui/avatar/index.js delete mode 100644 frontend/src/lib/components/ui/badge/badge.svelte delete mode 100644 frontend/src/lib/components/ui/badge/index.js delete mode 100644 frontend/src/lib/components/ui/bulk-action-bar/BulkActionBar.svelte delete mode 100644 frontend/src/lib/components/ui/bulk-action-bar/index.js delete mode 100644 frontend/src/lib/components/ui/calendar/Calendar.svelte delete mode 100644 frontend/src/lib/components/ui/calendar/index.js delete mode 100644 frontend/src/lib/components/ui/card/card-content.svelte delete mode 100644 frontend/src/lib/components/ui/card/card-description.svelte delete mode 100644 frontend/src/lib/components/ui/card/card-header.svelte delete mode 100644 frontend/src/lib/components/ui/card/card-title.svelte delete mode 100644 frontend/src/lib/components/ui/card/card.svelte delete mode 100644 frontend/src/lib/components/ui/card/index.js delete mode 100644 frontend/src/lib/components/ui/crm-drawer/CrmDrawer.svelte delete mode 100644 frontend/src/lib/components/ui/crm-drawer/CrmPropertyRow.svelte delete mode 100644 frontend/src/lib/components/ui/crm-drawer/index.js delete mode 100644 frontend/src/lib/components/ui/crm-table/CrmTable.svelte delete mode 100644 frontend/src/lib/components/ui/crm-table/index.js delete mode 100644 frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte delete mode 100644 frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte delete mode 100644 frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte delete mode 100644 frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte delete mode 100644 frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte delete mode 100644 frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte delete mode 100644 frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte delete mode 100644 frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte delete mode 100644 frontend/src/lib/components/ui/dropdown-menu/index.js delete mode 100644 frontend/src/lib/components/ui/editable-field/EditableMultiSelect.svelte delete mode 100644 frontend/src/lib/components/ui/editable-field/index.js delete mode 100644 frontend/src/lib/components/ui/filter/DateRangeFilter.svelte delete mode 100644 frontend/src/lib/components/ui/filter/FilterBar.svelte delete mode 100644 frontend/src/lib/components/ui/filter/SearchInput.svelte delete mode 100644 frontend/src/lib/components/ui/filter/SelectFilter.svelte delete mode 100644 frontend/src/lib/components/ui/filter/TagFilter.svelte delete mode 100644 frontend/src/lib/components/ui/filter/index.js delete mode 100644 frontend/src/lib/components/ui/form-shell/FormShell.svelte delete mode 100644 frontend/src/lib/components/ui/form-shell/index.js delete mode 100644 frontend/src/lib/components/ui/input/index.js delete mode 100644 frontend/src/lib/components/ui/input/input.svelte delete mode 100644 frontend/src/lib/components/ui/label/index.js delete mode 100644 frontend/src/lib/components/ui/label/label.svelte delete mode 100644 frontend/src/lib/components/ui/pagination/Pagination.svelte delete mode 100644 frontend/src/lib/components/ui/pagination/index.js delete mode 100644 frontend/src/lib/components/ui/popover/index.js delete mode 100644 frontend/src/lib/components/ui/popover/popover-content.svelte delete mode 100644 frontend/src/lib/components/ui/popover/popover-trigger.svelte delete mode 100644 frontend/src/lib/components/ui/progress/index.js delete mode 100644 frontend/src/lib/components/ui/progress/progress.svelte delete mode 100644 frontend/src/lib/components/ui/section-card/SectionCard.svelte delete mode 100644 frontend/src/lib/components/ui/section-card/index.js delete mode 100644 frontend/src/lib/components/ui/select/index.js delete mode 100644 frontend/src/lib/components/ui/select/select-content.svelte delete mode 100644 frontend/src/lib/components/ui/select/select-group-heading.svelte delete mode 100644 frontend/src/lib/components/ui/select/select-group.svelte delete mode 100644 frontend/src/lib/components/ui/select/select-item.svelte delete mode 100644 frontend/src/lib/components/ui/select/select-label.svelte delete mode 100644 frontend/src/lib/components/ui/select/select-portal.svelte delete mode 100644 frontend/src/lib/components/ui/select/select-scroll-down-button.svelte delete mode 100644 frontend/src/lib/components/ui/select/select-scroll-up-button.svelte delete mode 100644 frontend/src/lib/components/ui/select/select-separator.svelte delete mode 100644 frontend/src/lib/components/ui/select/select-trigger.svelte delete mode 100644 frontend/src/lib/components/ui/select/select.svelte delete mode 100644 frontend/src/lib/components/ui/separator/index.js delete mode 100644 frontend/src/lib/components/ui/separator/separator.svelte delete mode 100644 frontend/src/lib/components/ui/sheet/index.js delete mode 100644 frontend/src/lib/components/ui/sheet/sheet-content.svelte delete mode 100644 frontend/src/lib/components/ui/sheet/sheet-description.svelte delete mode 100644 frontend/src/lib/components/ui/sheet/sheet-header.svelte delete mode 100644 frontend/src/lib/components/ui/sheet/sheet-overlay.svelte delete mode 100644 frontend/src/lib/components/ui/sheet/sheet-title.svelte delete mode 100644 frontend/src/lib/components/ui/side-panel/SidePanel.svelte delete mode 100644 frontend/src/lib/components/ui/side-panel/SidePanelSection.svelte delete mode 100644 frontend/src/lib/components/ui/side-panel/index.js delete mode 100644 frontend/src/lib/components/ui/skeleton/index.js delete mode 100644 frontend/src/lib/components/ui/skeleton/skeleton.svelte delete mode 100644 frontend/src/lib/components/ui/stage-stepper/StageStepper.svelte delete mode 100644 frontend/src/lib/components/ui/stage-stepper/index.js delete mode 100644 frontend/src/lib/components/ui/switch/index.js delete mode 100644 frontend/src/lib/components/ui/switch/switch.svelte delete mode 100644 frontend/src/lib/components/ui/table/index.js delete mode 100644 frontend/src/lib/components/ui/table/table-body.svelte delete mode 100644 frontend/src/lib/components/ui/table/table-cell.svelte delete mode 100644 frontend/src/lib/components/ui/table/table-head.svelte delete mode 100644 frontend/src/lib/components/ui/table/table-header.svelte delete mode 100644 frontend/src/lib/components/ui/table/table-row.svelte delete mode 100644 frontend/src/lib/components/ui/table/table.svelte delete mode 100644 frontend/src/lib/components/ui/tabs/index.js delete mode 100644 frontend/src/lib/components/ui/tabs/tabs-content.svelte delete mode 100644 frontend/src/lib/components/ui/tabs/tabs-list.svelte delete mode 100644 frontend/src/lib/components/ui/tabs/tabs-trigger.svelte delete mode 100644 frontend/src/lib/components/ui/tabs/tabs.svelte delete mode 100644 frontend/src/lib/components/ui/textarea/index.js delete mode 100644 frontend/src/lib/components/ui/textarea/textarea.svelte delete mode 100644 frontend/src/lib/components/ui/timeline/Timeline.svelte delete mode 100644 frontend/src/lib/components/ui/timeline/TimelineItem.svelte delete mode 100644 frontend/src/lib/components/ui/timeline/index.js delete mode 100644 frontend/src/lib/components/ui/view-toggle/ViewToggle.svelte delete mode 100644 frontend/src/lib/components/ui/view-toggle/index.js delete mode 100644 frontend/src/lib/components/users/TeamCard.svelte delete mode 100644 frontend/src/lib/components/users/TeamFormDialog.svelte delete mode 100644 frontend/src/lib/components/users/index.js delete mode 100644 frontend/src/lib/constants/colors.js delete mode 100644 frontend/src/lib/constants/lead-choices.js delete mode 100644 frontend/src/lib/hooks/index.js delete mode 100644 frontend/src/lib/hooks/is-mobile.svelte.js delete mode 100644 frontend/src/lib/hooks/use-drawer-state.svelte.js delete mode 100644 frontend/src/lib/hooks/use-list-filters.svelte.js delete mode 100644 frontend/src/lib/stores/org.js delete mode 100644 frontend/src/lib/utils/drag-drop.js delete mode 100644 frontend/src/lib/utils/formatting.js delete mode 100644 frontend/src/lib/utils/phone.js delete mode 100644 frontend/src/lib/utils/table-helpers.js delete mode 100644 frontend/src/lib/utils/ui-helpers.js diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5372ac01..ebb49c81 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -64,8 +64,8 @@ jobs: working-directory: backend run: uv sync --frozen - # The suite runs with --no-migrations for speed, so nothing else would - # catch a model change that is missing its migration. + # Running the migrations cannot notice a model change that has no + # migration yet, so this checks for one explicitly. - name: Check for missing migrations working-directory: backend env: diff --git a/CLAUDE.md b/CLAUDE.md index 65282325..a2f54861 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -42,7 +42,7 @@ Frontend: - Formatting: `#lib/v2/format.js` (`money(n, currency)`, which defaults to USD, so pass `data.org.currency`; `shortDate`, `longDate`, `relativeTime`). The org's "today": `todayIn(tz)` from `#lib/v2/dates.js` with `data.org.timezone`. - Admin: `isOrgAdmin` from `#lib/admin.js`. Labels and tones: `#lib/v2/enums.js`. Classes: `cn` from `#lib/utils.js`. DRF errors to a sentence: `readableError` from `#lib/server/v2/form-errors.js`. - Lists and filters: `#lib/v2/filters.js`, `#lib/server/v2/filter-params.js`, `#lib/server/v2/list-queries.js`, `#lib/server/v2/saved-views.js`. Custom fields: `#lib/server/v2/custom-fields.js`. @mentions: `MentionTextarea` and `MentionBody` in `#lib/v2/components/`, fed by `mentionPeople()` from `#lib/v2/mentions.js`. -- Components: `#lib/v2/components/.svelte` (`PageHeader`, `Pill`, `EmptyState`, `StatCard`, `ConfirmAction`, `FilterBar`, ...). Many shadcn components in `components/ui/` and everything in `components/layout`, `components/dashboard` and `components/users` have no importers. Find a live caller before building on any of them. +- Components: `#lib/v2/components/.svelte` (`PageHeader`, `Pill`, `EmptyState`, `StatCard`, `ConfirmAction`, `FilterBar`, ...). ## Commands diff --git a/backend/cases/tests/test_csat.py b/backend/cases/tests/test_csat.py index ef3290a7..beea6c99 100644 --- a/backend/cases/tests/test_csat.py +++ b/backend/cases/tests/test_csat.py @@ -507,8 +507,7 @@ class TestPublicCsatResolvesOrgFirst: def test_public_survey_answers_under_enforced_rls(client): """The anonymous survey GET and POST work under a role that enforces RLS. - Runs only against PostgreSQL as a non-superuser (``RLS_ENFORCE=1 - --ds=crm.test_settings_postgres``): a superuser bypasses RLS, and the first + Runs only against PostgreSQL as a non-superuser (``--ds=crm.test_settings_postgres``): a superuser bypasses RLS, and the first assertion below says so rather than passing vacuously. """ from django.db import connection diff --git a/backend/cases/tests/test_inbound_webhook_reachability.py b/backend/cases/tests/test_inbound_webhook_reachability.py index e2e1af6b..44f72795 100644 --- a/backend/cases/tests/test_inbound_webhook_reachability.py +++ b/backend/cases/tests/test_inbound_webhook_reachability.py @@ -374,7 +374,7 @@ class TestBackfill: # --------------------------------------------------------------------------- -# Under a role RLS binds (RLS_ENFORCE run, --ds=crm.test_settings_postgres) +# Under a role RLS binds (--ds=crm.test_settings_postgres) # --------------------------------------------------------------------------- diff --git a/backend/common/base.py b/backend/common/base.py index a35c5d38..3e9871a9 100644 --- a/backend/common/base.py +++ b/backend/common/base.py @@ -118,33 +118,6 @@ class BaseModel(AuditModel): return str(self.id) -class OrgScopedQuerySet(models.QuerySet): - """QuerySet with automatic organization filtering support""" - - def for_org(self, org): - """Filter queryset by organization""" - return self.filter(org=org) - - def for_request(self, request): - """Filter queryset by organization from request""" - if hasattr(request, "profile") and request.profile: - return self.filter(org=request.profile.org) - return self.none() - - -class OrgScopedManager(models.Manager): - """Manager that uses OrgScopedQuerySet""" - - def get_queryset(self): - return OrgScopedQuerySet(self.model, using=self._db) - - def for_org(self, org): - return self.get_queryset().for_org(org) - - def for_request(self, request): - return self.get_queryset().for_request(request) - - class BaseOrgModel(BaseModel): """ Base model for all organization-scoped entities. @@ -153,7 +126,6 @@ class BaseOrgModel(BaseModel): This ensures: - Required org field (no null values) - Per-org indexes for performance - - OrgScopedManager for easier filtering RLS Protection: --------------- @@ -174,8 +146,6 @@ class BaseOrgModel(BaseModel): help_text="Organization this record belongs to", ) - objects = OrgScopedManager() - class Meta: abstract = True indexes = [ diff --git a/backend/common/middleware/get_company.py b/backend/common/middleware/get_company.py index 0c170e6b..d502c989 100644 --- a/backend/common/middleware/get_company.py +++ b/backend/common/middleware/get_company.py @@ -46,7 +46,7 @@ class GetProfileAndOrg: # another's without any view knowing about it. # # The deactivate is in a `finally` for the same reason the RLS reset is - # (see `rls_context.SetOrgContext.__call__`): the active timezone is a + # (see `rls_context.RequireOrgContext.__call__`): the active timezone is a # thread-local, worker threads are reused, and a request that raised # after activating would hand the next tenant its own day boundary. activate_org_timezone(getattr(request, "org", None)) diff --git a/backend/common/middleware/rls_context.py b/backend/common/middleware/rls_context.py index 5f25a262..36b12eaf 100644 --- a/backend/common/middleware/rls_context.py +++ b/backend/common/middleware/rls_context.py @@ -14,7 +14,7 @@ Usage in settings.py: MIDDLEWARE = [ ... 'common.middleware.get_company.GetProfileAndOrg', - 'common.middleware.rls_context.SetOrgContext', # After GetProfileAndOrg + 'common.middleware.rls_context.RequireOrgContext', # After GetProfileAndOrg ... ] """ @@ -27,78 +27,10 @@ from django.http import JsonResponse logger = logging.getLogger(__name__) -class SetOrgContext: - """ - Middleware to set PostgreSQL session variable for Row-Level Security. - - This sets `app.current_org` to the user's organization ID, which is - used by RLS policies to automatically filter data at the database level. - - Security: This provides defense-in-depth. Even if application code - forgets to filter by org, the database will enforce isolation. - """ - - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - # Set org context before processing request - self._set_org_context(request) - - # The reset MUST run even when the view raises. `app.current_org` is - # set at SESSION scope, so it outlives the statement and the - # transaction; it is cleared only because we clear it. Without the - # finally, an exception anywhere downstream hands the connection back - # still carrying this tenant's org id, and the next request to reuse - # that connection inherits it. That is a cross-tenant read. - try: - return self.get_response(request) - finally: - self._reset_org_context() - - def _set_org_context(self, request): - """ - Set the PostgreSQL session variable for RLS. - - Args: - request: Django request object with profile attached - """ - if not hasattr(request, "org") or request.org is None: - return - - org_id = str(request.org.id) - - try: - with connection.cursor() as cursor: - # Set the session variable (is_local=false for session scope) - # Required because Django uses autocommit mode by default - cursor.execute( - "SELECT set_config('app.current_org', %s, false)", [org_id] - ) - logger.debug("Set RLS context: app.current_org = %s", org_id) - - except Exception as e: - # RLS might not be configured - log but don't fail - logger.debug("Could not set RLS context: %s", e) - - def _reset_org_context(self): - """ - Reset the PostgreSQL session variable after request. - Critical to prevent context leakage between requests on pooled connections. - """ - try: - with connection.cursor() as cursor: - cursor.execute("SELECT set_config('app.current_org', '', false)") - except Exception: - pass - - class RequireOrgContext: """ - Stricter middleware that fails if org context is not set. - - Use this instead of SetOrgContext when you want to ensure - all requests have proper org context (after RLS is fully enabled). + Sets `app.current_org` for the request and refuses a request with no org + context, except on the exempt paths below. Usage in settings.py: MIDDLEWARE = [ @@ -263,7 +195,7 @@ class RequireOrgContext: # Set org context self._set_org_context(request) - # See SetOrgContext.__call__: the reset is in a finally because + # The reset is in a finally because # `app.current_org` is SESSION-scoped and survives an exception. On a # pooled or otherwise reused connection, skipping it leaks this # tenant's context to whoever borrows the connection next. diff --git a/backend/common/tests/test_models.py b/backend/common/tests/test_models.py index 6c694a75..a5fbf1b3 100644 --- a/backend/common/tests/test_models.py +++ b/backend/common/tests/test_models.py @@ -8,7 +8,6 @@ Covers: - SessionToken.revoke() and cleanup_expired() - Activity.created_on_arrow property - AssignableMixin properties (get_team_users, etc.) -- OrgScopedQuerySet and OrgScopedManager methods Run with: pytest common/tests/test_models.py -v """ @@ -265,76 +264,3 @@ class TestAssignableMixin: assert account.get_team_users.count() == 0 assert account.get_team_and_assigned_users.count() == 0 assert account.get_assigned_users_not_in_teams.count() == 0 - - -# --------------------------------------------------------------------------- -# OrgScopedQuerySet and OrgScopedManager -# --------------------------------------------------------------------------- - - -@pytest.mark.django_db -class TestOrgScopedManager: - """Test OrgScopedQuerySet.for_org() and for_request() via a model that uses them.""" - - def test_org_scoped_classes_instantiate(self, org_a, org_b): - """OrgScopedQuerySet and OrgScopedManager can be instantiated.""" - from common.base import OrgScopedManager, OrgScopedQuerySet - - _qs = OrgScopedQuerySet(model=Activity) - manager = OrgScopedManager() - assert manager is not None - - def test_for_request_with_profile(self, org_a, admin_profile): - """for_request should filter by org from request.profile.""" - from common.base import OrgScopedQuerySet - - # Create activities in different orgs - Activity.objects.create( - user=admin_profile, - action="CREATE", - entity_type="Account", - entity_id=uuid.uuid4(), - org=org_a, - ) - - request = MagicMock() - request.profile = admin_profile - - # Test OrgScopedQuerySet.for_request directly - qs = OrgScopedQuerySet(model=Activity, using="default") - result = qs.for_request(request) - assert result.count() == 1 - - def test_for_request_without_profile(self): - """for_request without profile should return empty queryset.""" - from common.base import OrgScopedQuerySet - - request = MagicMock(spec=[]) # no profile attribute - - qs = OrgScopedQuerySet(model=Activity, using="default") - result = qs.for_request(request) - assert result.count() == 0 - - def test_for_org(self, org_a, org_b, admin_profile): - """for_org should filter activities by specified org.""" - from common.base import OrgScopedQuerySet - - Activity.objects.create( - user=admin_profile, - action="CREATE", - entity_type="Account", - entity_id=uuid.uuid4(), - org=org_a, - ) - Activity.objects.create( - user=admin_profile, - action="UPDATE", - entity_type="Lead", - entity_id=uuid.uuid4(), - org=org_b, - ) - - qs = OrgScopedQuerySet(model=Activity, using="default") - result = qs.for_org(org_a) - assert result.count() == 1 - assert result.first().org == org_a diff --git a/backend/common/tests/test_multitenancy.py b/backend/common/tests/test_multitenancy.py index d6b2a39c..39c02f99 100644 --- a/backend/common/tests/test_multitenancy.py +++ b/backend/common/tests/test_multitenancy.py @@ -314,36 +314,6 @@ class TestBaseOrgModel(TestCase): self.assertIsNotNone(team.id) -class TestOrgScopedQuerySet(MultiTenancyBaseTestCase): - """Test OrgScopedQuerySet helper methods""" - - def setUp(self): - super().setUp() - - # Create teams in each org - self.team_a = Teams.objects.create( - name="Team A", description="In Org A", org=self.org_a - ) - self.team_b = Teams.objects.create( - name="Team B", description="In Org B", org=self.org_b - ) - - def test_for_org_filters_correctly(self): - """for_org() should filter by organization""" - teams = Teams.objects.filter(org=self.org_a) - self.assertEqual(teams.count(), 1) - self.assertEqual(teams.first().name, "Team A") - - def test_for_request_uses_profile_org(self): - """for_request() should use org from request.profile""" - # Create mock request - request = MagicMock() - request.profile = self.profile_a - - # This would be used with OrgScopedManager - # Teams.objects.for_request(request) - - class TestNullOrgPrevention(MultiTenancyBaseTestCase): """Test that models properly handle org field""" diff --git a/backend/common/tests/test_rls_context_reset.py b/backend/common/tests/test_rls_context_reset.py index 019e5e86..eb6eb92f 100644 --- a/backend/common/tests/test_rls_context_reset.py +++ b/backend/common/tests/test_rls_context_reset.py @@ -18,7 +18,7 @@ a real PostgreSQL with a non-superuser role. import pytest -from common.middleware.rls_context import RequireOrgContext, SetOrgContext +from common.middleware.rls_context import RequireOrgContext class _Boom(Exception): @@ -44,16 +44,14 @@ def _instrument(middleware_cls, get_response): return mw, calls -@pytest.mark.parametrize("middleware_cls", [SetOrgContext, RequireOrgContext]) -def test_reset_runs_on_the_happy_path(middleware_cls): - mw, calls = _instrument(middleware_cls, lambda request: "response") +def test_reset_runs_on_the_happy_path(): + mw, calls = _instrument(RequireOrgContext, lambda request: "response") assert mw(_FakeRequest()) == "response" assert calls == ["set", "reset"] -@pytest.mark.parametrize("middleware_cls", [SetOrgContext, RequireOrgContext]) -def test_reset_runs_when_the_view_raises(middleware_cls): +def test_reset_runs_when_the_view_raises(): """The regression this file exists for. Without the try/finally the middleware returned early on the exception and @@ -63,7 +61,7 @@ def test_reset_runs_when_the_view_raises(middleware_cls): def boom(request): raise _Boom("view exploded") - mw, calls = _instrument(middleware_cls, boom) + mw, calls = _instrument(RequireOrgContext, boom) with pytest.raises(_Boom): mw(_FakeRequest()) @@ -74,14 +72,13 @@ def test_reset_runs_when_the_view_raises(middleware_cls): ) -@pytest.mark.parametrize("middleware_cls", [SetOrgContext, RequireOrgContext]) -def test_the_exception_still_propagates(middleware_cls): +def test_the_exception_still_propagates(): """The finally must not swallow the error into a silent 200.""" def boom(request): raise _Boom("view exploded") - mw, _ = _instrument(middleware_cls, boom) + mw, _ = _instrument(RequireOrgContext, boom) with pytest.raises(_Boom, match="view exploded"): mw(_FakeRequest()) diff --git a/backend/common/utils.py b/backend/common/utils.py index 96f7b22c..a16ba493 100644 --- a/backend/common/utils.py +++ b/backend/common/utils.py @@ -1,4 +1,3 @@ -import pytz from django.utils.translation import gettext_lazy as _ INDCHOICES = ( @@ -423,14 +422,6 @@ CURRENCY_SYMBOLS = { } -def convert_to_custom_timezone(custom_date, custom_timezone, to_utc=False): - user_time_zone = pytz.timezone(custom_timezone) - if to_utc: - custom_date = user_time_zone.localize(custom_date.replace(tzinfo=None)) - user_time_zone = pytz.UTC - return custom_date.astimezone(user_time_zone) - - # ============================================================================= # CRM Entity Utilities # ============================================================================= diff --git a/backend/invoices/tests/test_portal_rls.py b/backend/invoices/tests/test_portal_rls.py index bf6b210b..fd5d6993 100644 --- a/backend/invoices/tests/test_portal_rls.py +++ b/backend/invoices/tests/test_portal_rls.py @@ -1,7 +1,6 @@ """RLS-enforced proof for the portal token → org resolution (Option 1). -These run only under a non-superuser Postgres role (``RLS_ENFORCE=1 ---ds=crm.test_settings_postgres``), because the whole point is the behaviour +These run only under a non-superuser Postgres role (``--ds=crm.test_settings_postgres``), because the whole point is the behaviour that a superuser dev DB hides: with an empty ``app.current_org`` the isolation policy makes the estimate row invisible, so the anonymous portal view can only work if it resolves the org from the unscoped lookup first. diff --git a/backend/pyproject.toml b/backend/pyproject.toml index dd29eb86..d075d6bd 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -81,7 +81,6 @@ dependencies = [ "django-crum>=0.7.9", "disposable-email-domains>=0.0.280", "redis>=8.1.0", - "pytz>=2026.5", "requests>=2.34.2", "faker>=40.43.0", "python-dateutil>=2.9.0.post0", diff --git a/backend/pytest.ini b/backend/pytest.ini index d4fe498c..a493d3f7 100644 --- a/backend/pytest.ini +++ b/backend/pytest.ini @@ -11,5 +11,4 @@ addopts = --cov-report=html:htmlcov --cov-report=xml:coverage.xml markers = - slow: marks tests as slow postgres_only: marks tests that require PostgreSQL diff --git a/backend/uv.lock b/backend/uv.lock index 60a3f5b6..f28d7d65 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -728,7 +728,6 @@ dependencies = [ { name = "psycopg", extra = ["binary", "pool"] }, { name = "python-dateutil" }, { name = "python-dotenv" }, - { name = "pytz" }, { name = "redis" }, { name = "requests" }, { name = "sentry-sdk" }, @@ -772,7 +771,6 @@ requires-dist = [ { name = "psycopg", extras = ["binary", "pool"], specifier = ">=3.3.6" }, { name = "python-dateutil", specifier = ">=2.9.0.post0" }, { name = "python-dotenv", specifier = ">=1.2.4" }, - { name = "pytz", specifier = ">=2026.5" }, { name = "redis", specifier = ">=8.1.0" }, { name = "requests", specifier = ">=2.34.2" }, { name = "sentry-sdk", specifier = ">=2.72.0" }, @@ -1782,15 +1780,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/60/d1/38f3a3405989a89ac18390803e70c6ad7c7760da4f9b83cbeca0c44a0c72/python_dotenv-1.2.4-py3-none-any.whl", hash = "sha256:42269a8a5b3fd54ffa6f3d84b18abed50064717576b4ecf03dc4a55d8aa04fdc", size = 23266, upload-time = "2026-10-01T05:36:08.633Z" }, ] -[[package]] -name = "pytz" -version = "2026.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/14/21/d83d6ef28c4c912c4bb4d1dcf591f7b8c6bde87b9c66f9f454677314e16d/pytz-2026.5.tar.gz", hash = "sha256:fa23724b9c486543b9ff54a327ee7569ac83ade54bb9afd0fc18676620401c86", size = 318572, upload-time = "2026-10-04T02:37:58.719Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/ef/c66110d46fb800dda0bf33164182dfadabe26a90e4476844d502a23dca8e/pytz-2026.5-py2.py3-none-any.whl", hash = "sha256:e658af3757f9e26a9d25dd2aff38335acd92bc9104f890a894b2c1ba28311b03", size = 506342, upload-time = "2026-10-04T02:37:56.814Z" }, -] - [[package]] name = "pyyaml" version = "6.0.3" diff --git a/docs/architecture/multi-tenancy-and-rls.md b/docs/architecture/multi-tenancy-and-rls.md index f1f4cab5..fdf4c58b 100644 --- a/docs/architecture/multi-tenancy-and-rls.md +++ b/docs/architecture/multi-tenancy-and-rls.md @@ -26,8 +26,7 @@ Neither layer is optional, and neither is a superset of the other in practice: s ## BaseOrgModel `BaseOrgModel` (`backend/common/base.py`) is a `BaseModel` subclass that adds an `org` foreign key -to `common.Org`, an `OrgScopedManager` (giving you `Model.objects.for_org(org)` and -`Model.objects.for_request(request)` helpers), and a `["org", "-created_at"]` index. Its own +to `common.Org` and a `["org", "-created_at"]` index. Its own docstring states the intent plainly: "All tenant-owned models should inherit from this instead of `BaseModel`." @@ -58,9 +57,7 @@ class CaseWatcher(BaseModel): checkout; treat the reasoning captured in the code comments themselves as the source of truth.) The practical takeaway: inheriting `BaseOrgModel` is a correct and recommended way to add an `org` -field to a new model. Doing so also gets you the `OrgScopedManager` helpers -(`Model.objects.for_org(org)`, `Model.objects.for_request(request)`) for free, which is itself a -reason to prefer it, and you should default to it unless you have a specific reason to match this +field to a new model, and you should default to it unless you have a specific reason to match this codebase's more common hand-rolled-FK convention instead (for consistency with a sibling model in the same app, for instance). But inheriting it is not what makes RLS protection work, and it is not sufficient on its own; `PersonalAccessToken` inherits `BaseOrgModel` and is diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 58f2dd74..b58fef8e 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -66,11 +66,6 @@ never runs before both steps have completed; `RequireOrgContext` sits after `Get the list above and returns a response itself (the 403) rather than calling `get_response` when the context is missing, so a request that fails tenancy resolution never reaches URL dispatch. -This file also defines an older class, `SetOrgContext`, whose own docstring describes it as the -one wired into `MIDDLEWARE`. That docstring is stale. `SetOrgContext` sets the session variable -but never rejects a context-less request; `RequireOrgContext` is what `settings.py` actually -lists, and it is the one enforced. - One layer above middleware, DRF's own authentication runs at view dispatch time (`DEFAULT_AUTHENTICATION_CLASSES` in `REST_FRAMEWORK`: `PATAuthentication`, then simplejwt's `JWTAuthentication`, then `APIKeyAuthentication`) to populate `request.user` for `IsAuthenticated` diff --git a/docs/contributing/adding-an-org-scoped-model.md b/docs/contributing/adding-an-org-scoped-model.md index fc7b471f..c6c39988 100644 --- a/docs/contributing/adding-an-org-scoped-model.md +++ b/docs/contributing/adding-an-org-scoped-model.md @@ -9,7 +9,7 @@ these steps exists; this page is the checklist for *doing* it. ## The checklist 1. **Inherit `BaseOrgModel`** (`common.base`) so the model gets an `org` foreign key, the UUID - primary key, audit fields, and the `OrgScopedManager` helpers for free. + primary key and audit fields. 2. **Add the table's `db_table` name to `ORG_SCOPED_TABLES`** in `backend/common/rls/__init__.py`. 3. **Write a migration** that calls `get_enable_policy_sql('your_table')` inside a `RunPython` operation, with a `dependencies` entry that guarantees the table already exists when it runs. diff --git a/docs/contributing/testing.md b/docs/contributing/testing.md index c7bbbdbb..b94d82c7 100644 --- a/docs/contributing/testing.md +++ b/docs/contributing/testing.md @@ -37,22 +37,20 @@ uv run pytest --no-cov -x rather than being silently ignored. Every custom marker you use has to be declared in `pytest.ini`'s `markers` list. It doesn't touch markers a plugin registers itself, like `@pytest.mark.django_db` (from `pytest-django`). Those work without appearing in `pytest.ini` at all, which is why every -example on this page uses `django_db` freely alongside the two markers that actually are declared. +example on this page uses `django_db` freely alongside the one marker that is declared. ## Markers -`pytest.ini` declares two custom markers: +`pytest.ini` declares one custom marker: ```ini markers = - slow: marks tests as slow postgres_only: marks tests that require PostgreSQL ``` `postgres_only` marks the tests that need a real PostgreSQL: RLS policy state, isolation behaviour under a role that RLS binds, and session-scoped connection state. List them with -`uv run pytest -m postgres_only --collect-only -q`. `slow` is declared and currently applied to -nothing. +`uv run pytest -m postgres_only --collect-only -q`. ## The SQLite caveat diff --git a/docs/self-hosting/postgresql-and-rls.md b/docs/self-hosting/postgresql-and-rls.md index 13594bb7..91065620 100644 --- a/docs/self-hosting/postgresql-and-rls.md +++ b/docs/self-hosting/postgresql-and-rls.md @@ -155,11 +155,8 @@ by `RequireOrgContext` (`backend/common/middleware/rls_context.py`, wired into ` `backend/crm/settings.py`) from `request.org`, which is itself derived earlier in the same middleware chain by `GetProfileAndOrg` (`backend/common/middleware/get_company.py`) from the `org_id` claim inside the caller's signed JWT, validated against an active `Profile` for that -user in that org before it's trusted. (The file also defines a second, older middleware class, -`SetOrgContext`, referenced in that file's own module docstring, but `settings.py` wires -`RequireOrgContext` instead, which additionally rejects unauthenticated/context-less requests -with a 403 rather than silently proceeding without a context; `RequireOrgContext` is the class -actually enforced.) Every policy uses `NULLIF(current_setting('app.current_org', true), '')`, +user in that org before it's trusted. A request that arrives with no org context is refused with a +403 rather than run without one. Every policy uses `NULLIF(current_setting('app.current_org', true), '')`, when no context has been set, `current_setting` returns an empty string, `NULLIF` turns that into SQL `NULL`, and `org_id::text = NULL` is never true. **No context means zero rows, by design**. A fail-safe default, not an edge case to special-case around. diff --git a/frontend/package.json b/frontend/package.json index af9be1e8..b5daa625 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,7 +18,6 @@ "devDependencies": { "@eslint/compat": "^2.1.1", "@eslint/js": "^10.0.1", - "@internationalized/date": "^3.12.4", "@lucide/svelte": "^1.57.0", "@sveltejs/adapter-node": "^6.0.0", "@sveltejs/kit": "^3.0.1", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 1aa40fcf..d6d3dfc2 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -43,9 +43,6 @@ importers: '@eslint/js': specifier: ^10.0.1 version: 10.0.1(eslint@10.12.0(jiti@2.7.0)(supports-color@8.1.1)) - '@internationalized/date': - specifier: ^3.12.4 - version: 3.12.4 '@lucide/svelte': specifier: ^1.57.0 version: 1.57.0(svelte@5.57.2) diff --git a/frontend/src/lib/api-helpers.js b/frontend/src/lib/api-helpers.js index 036cb068..37aee93e 100644 --- a/frontend/src/lib/api-helpers.js +++ b/frontend/src/lib/api-helpers.js @@ -160,111 +160,3 @@ export async function apiRequest(endpoint, options = {}, locals) { throw error; } } - -/** - * Transform Django field names to camelCase for SvelteKit - * - * @param {any} obj - Django object with snake_case fields - * @returns {any} Object with camelCase fields - */ -export function transformFromDjango(obj) { - if (!obj) return obj; - if (Array.isArray(obj)) return obj.map(transformFromDjango); - if (typeof obj !== 'object') return obj; - - /** @type {Record} */ - const transformed = {}; - for (const [key, value] of Object.entries(obj)) { - const camelKey = key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); - transformed[camelKey] = typeof value === 'object' ? transformFromDjango(value) : value; - } - return transformed; -} - -/** - * Transform camelCase field names to Django snake_case - * - * @param {any} obj - SvelteKit object with camelCase fields - * @returns {any} Object with snake_case fields - */ -export function transformToDjango(obj) { - if (!obj) return obj; - if (Array.isArray(obj)) return obj.map(transformToDjango); - if (typeof obj !== 'object') return obj; - - /** @type {Record} */ - const transformed = {}; - for (const [key, value] of Object.entries(obj)) { - const snakeKey = key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); - transformed[snakeKey] = typeof value === 'object' ? transformToDjango(value) : value; - } - return transformed; -} - -/** - * Build Django query parameters from filter object - * - * @param {Object} filters - Filter object - * @param {number} [filters.page] - Page number (1-indexed) - * @param {number} [filters.limit] - Items per page - * @param {string} [filters.sort] - Sort field name - * @param {string} [filters.order] - Sort order ('asc' or 'desc') - * @returns {URLSearchParams} Query parameters for Django API - */ -export function buildQueryParams(filters = {}) { - const { page = 1, limit = 10, sort, order = 'asc', ...otherFilters } = filters; - - const params = new URLSearchParams(); - - // Pagination - params.append('limit', limit.toString()); - params.append('offset', ((page - 1) * limit).toString()); - - // Sorting (Django uses - prefix for descending) - if (sort) { - const ordering = order === 'desc' ? `-${sort}` : sort; - params.append('ordering', ordering); - } - - // Other filters - for (const [key, value] of Object.entries(otherFilters)) { - if (value !== null && value !== undefined && value !== '') { - params.append(key, value.toString()); - } - } - - return params; -} - -/** - * Extract pagination info from Django response - * - * @param {{ count?: number, next?: string | null, previous?: string | null, results?: unknown[] }} response - Django paginated response - * @param {number} [limit=10] - Items per page - * @returns {{ page: number, limit: number, total: number, totalPages: number, hasNext: boolean, hasPrevious: boolean }} Pagination info compatible with SvelteKit - */ -export function extractPagination(response, limit = 10) { - const total = response.count || 0; - const totalPages = Math.ceil(total / limit); - - // Determine current page from offset in next/previous URLs - let currentPage = 1; - if (response.next) { - const url = new URL(response.next, 'http://localhost'); - const offset = parseInt(url.searchParams.get('offset') || '0'); - currentPage = Math.floor(offset / limit); - } else if (response.previous) { - const url = new URL(response.previous, 'http://localhost'); - const offset = parseInt(url.searchParams.get('offset') || '0'); - currentPage = Math.floor(offset / limit) + 2; - } - - return { - page: currentPage, - limit, - total, - totalPages, - hasNext: response.next !== null, - hasPrevious: response.previous !== null - }; -} diff --git a/frontend/src/lib/components/dashboard/ActivityFeed.svelte b/frontend/src/lib/components/dashboard/ActivityFeed.svelte deleted file mode 100644 index 8a87ceef..00000000 --- a/frontend/src/lib/components/dashboard/ActivityFeed.svelte +++ /dev/null @@ -1,228 +0,0 @@ - - -
- -
-
-
- -
-

- Recent Activity -

-
- -
- - -
- {#if activities.length === 0} -
-
- -
-

No recent activity

-

Actions will appear here

-
- {:else} -
- {#each groupedActivities as group (group.category)} -
-

- {group.category} -

-
- {#each group.items as activity (activity.id)} - {@const config = actionConfig[activity.action || ''] || defaultConfig} - {@const Icon = config.icon} -
- -
- -
- - -
-

- {activity.description || - activity.message || - `${activity.action} ${activity.entityType}: ${activity.entityName}`} -

-
- {activity.user?.name || 'System'} - · - {formatTime(activity.timestamp || activity.createdAt)} -
-
-
- {/each} -
-
- {/each} -
- - {#if activities.length > 5 && !showAll} - - {/if} - {/if} -
-
diff --git a/frontend/src/lib/components/dashboard/FocusBar.svelte b/frontend/src/lib/components/dashboard/FocusBar.svelte deleted file mode 100644 index 9981a296..00000000 --- a/frontend/src/lib/components/dashboard/FocusBar.svelte +++ /dev/null @@ -1,113 +0,0 @@ - - -{#if visibleItems.length > 0} -
- -
- -
- - - - - - - -
- {#each visibleItems as item (item.href)} - -
- -
-
- {item.count} - -
-
- {/each} -
-
-
-{/if} diff --git a/frontend/src/lib/components/dashboard/HotLeadsPanel.svelte b/frontend/src/lib/components/dashboard/HotLeadsPanel.svelte deleted file mode 100644 index 5e9f120d..00000000 --- a/frontend/src/lib/components/dashboard/HotLeadsPanel.svelte +++ /dev/null @@ -1,198 +0,0 @@ - - -
- -
-
-
- -
-

Hot Leads

-
- -
- - -
- {#if leads.length === 0} -
-
- -
-

No hot leads

-

Mark leads as "Hot" to see them here

-
- {:else} - - {/if} -
-
diff --git a/frontend/src/lib/components/dashboard/KPICard.svelte b/frontend/src/lib/components/dashboard/KPICard.svelte deleted file mode 100644 index 82e14051..00000000 --- a/frontend/src/lib/components/dashboard/KPICard.svelte +++ /dev/null @@ -1,164 +0,0 @@ - - -
- -
-
-
- -
-
- -

- {label} -

- - -

- {value} -

- - - {#if subtitle} -

{subtitle}

- {/if} - - - {#if hasTrend} -
- {#if isPositive} -
- - - +{trend}% - -
- {:else} -
- - - {trend}% - -
- {/if} - {#if trendLabel} - {trendLabel} - {/if} -
- {/if} -
- - - {#if icon} -
-
- {@render icon()} -
-
- {/if} -
-
diff --git a/frontend/src/lib/components/dashboard/TaskList.svelte b/frontend/src/lib/components/dashboard/TaskList.svelte deleted file mode 100644 index b8357ecf..00000000 --- a/frontend/src/lib/components/dashboard/TaskList.svelte +++ /dev/null @@ -1,251 +0,0 @@ - - -
- -
-
-
- -
-

My Tasks

-
- -
- - -
- {#each filterButtons as btn (btn.id)} - - {/each} -
- - -
- {#if filteredTasks().length === 0} -
-
- -
-

No tasks found

-

All caught up!

-
- {:else} - - {/if} -
-
diff --git a/frontend/src/lib/components/dashboard/index.js b/frontend/src/lib/components/dashboard/index.js deleted file mode 100644 index e9af3baf..00000000 --- a/frontend/src/lib/components/dashboard/index.js +++ /dev/null @@ -1,5 +0,0 @@ -export { default as KPICard } from './KPICard.svelte'; -export { default as ActivityFeed } from './ActivityFeed.svelte'; -export { default as FocusBar } from './FocusBar.svelte'; -export { default as TaskList } from './TaskList.svelte'; -export { default as HotLeadsPanel } from './HotLeadsPanel.svelte'; diff --git a/frontend/src/lib/components/icons/GithubIcon.svelte b/frontend/src/lib/components/icons/GithubIcon.svelte deleted file mode 100644 index a46e570b..00000000 --- a/frontend/src/lib/components/icons/GithubIcon.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - diff --git a/frontend/src/lib/components/icons/LinkedinIcon.svelte b/frontend/src/lib/components/icons/LinkedinIcon.svelte deleted file mode 100644 index 69c650bb..00000000 --- a/frontend/src/lib/components/icons/LinkedinIcon.svelte +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - diff --git a/frontend/src/lib/components/icons/index.js b/frontend/src/lib/components/icons/index.js deleted file mode 100644 index 275c64e2..00000000 --- a/frontend/src/lib/components/icons/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default as LinkedinIcon } from './LinkedinIcon.svelte'; -export { default as GithubIcon } from './GithubIcon.svelte'; diff --git a/frontend/src/lib/components/layout/FilterPill.svelte b/frontend/src/lib/components/layout/FilterPill.svelte deleted file mode 100644 index 387c7b59..00000000 --- a/frontend/src/lib/components/layout/FilterPill.svelte +++ /dev/null @@ -1,65 +0,0 @@ - - - diff --git a/frontend/src/lib/components/layout/FilterPopover.svelte b/frontend/src/lib/components/layout/FilterPopover.svelte deleted file mode 100644 index f68b5118..00000000 --- a/frontend/src/lib/components/layout/FilterPopover.svelte +++ /dev/null @@ -1,40 +0,0 @@ - - - - - {#snippet child({ props })} - - {/snippet} - - -
-

Filters

- {@render children?.()} - {#if onClear} - - {/if} -
-
-
diff --git a/frontend/src/lib/components/layout/FilterStrip.svelte b/frontend/src/lib/components/layout/FilterStrip.svelte deleted file mode 100644 index cd019184..00000000 --- a/frontend/src/lib/components/layout/FilterStrip.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - -
-
- {@render children?.()} -
- {#if meta} -
- {@render meta()} -
- {/if} -
diff --git a/frontend/src/lib/components/layout/PageHeader.svelte b/frontend/src/lib/components/layout/PageHeader.svelte deleted file mode 100644 index 5d9029f9..00000000 --- a/frontend/src/lib/components/layout/PageHeader.svelte +++ /dev/null @@ -1,116 +0,0 @@ - - -
- {#if breadcrumb.length > 0} - - {/if} - -
-
- {#if titleIcon} -
- {@render titleIcon()} -
- {/if} -
- {#if eyebrow} -

{eyebrow}

- {/if} -

{title}

- {#if subtitle} -

{subtitle}

- {/if} -
-
- {#if amount || actions} -
- {#if amount} -
- {@render amount()} -
- {/if} - {#if actions} -
- {@render actions()} -
- {/if} -
- {/if} -
- - {#if meta} -
- {@render meta()} -
- {/if} - - {#if tabs} -
- {@render tabs()} -
- {/if} -
diff --git a/frontend/src/lib/components/layout/ViewTabs.svelte b/frontend/src/lib/components/layout/ViewTabs.svelte deleted file mode 100644 index c669c8e3..00000000 --- a/frontend/src/lib/components/layout/ViewTabs.svelte +++ /dev/null @@ -1,34 +0,0 @@ - - -
- {#each views as v (v.id)} - {@const isActive = v.id === active} - - {/each} -
diff --git a/frontend/src/lib/components/layout/index.js b/frontend/src/lib/components/layout/index.js deleted file mode 100644 index d336987e..00000000 --- a/frontend/src/lib/components/layout/index.js +++ /dev/null @@ -1,5 +0,0 @@ -export { default as FilterPopover } from './FilterPopover.svelte'; -export { default as PageHeader } from './PageHeader.svelte'; -export { default as FilterPill } from './FilterPill.svelte'; -export { default as FilterStrip } from './FilterStrip.svelte'; -export { default as ViewTabs } from './ViewTabs.svelte'; diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte deleted file mode 100644 index 4df874c2..00000000 --- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte +++ /dev/null @@ -1,21 +0,0 @@ - - - diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte deleted file mode 100644 index c9a17a59..00000000 --- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte +++ /dev/null @@ -1,21 +0,0 @@ - - - diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte deleted file mode 100644 index a51a0827..00000000 --- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - {@render children?.()} - - diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte deleted file mode 100644 index ca8a4f27..00000000 --- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - - diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte deleted file mode 100644 index a7d5466e..00000000 --- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - -
- {@render children?.()} -
diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte deleted file mode 100644 index 4994099b..00000000 --- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - -
- {@render children?.()} -
diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte deleted file mode 100644 index b682823f..00000000 --- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - - diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte deleted file mode 100644 index 93212cc7..00000000 --- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - - diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte deleted file mode 100644 index a45e563a..00000000 --- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/frontend/src/lib/components/ui/alert-dialog/index.js b/frontend/src/lib/components/ui/alert-dialog/index.js deleted file mode 100644 index 54fb20bf..00000000 --- a/frontend/src/lib/components/ui/alert-dialog/index.js +++ /dev/null @@ -1,39 +0,0 @@ -import { AlertDialog as AlertDialogPrimitive } from 'bits-ui'; -import Trigger from './alert-dialog-trigger.svelte'; -import Overlay from './alert-dialog-overlay.svelte'; -import Content from './alert-dialog-content.svelte'; -import Header from './alert-dialog-header.svelte'; -import Footer from './alert-dialog-footer.svelte'; -import Title from './alert-dialog-title.svelte'; -import Description from './alert-dialog-description.svelte'; -import Cancel from './alert-dialog-cancel.svelte'; -import Action from './alert-dialog-action.svelte'; - -const Root = AlertDialogPrimitive.Root; -const Portal = AlertDialogPrimitive.Portal; - -export { - Root, - Trigger, - Portal, - Overlay, - Content, - Header, - Footer, - Title, - Description, - Cancel, - Action, - // - Root as AlertDialog, - Trigger as AlertDialogTrigger, - Portal as AlertDialogPortal, - Overlay as AlertDialogOverlay, - Content as AlertDialogContent, - Header as AlertDialogHeader, - Footer as AlertDialogFooter, - Title as AlertDialogTitle, - Description as AlertDialogDescription, - Cancel as AlertDialogCancel, - Action as AlertDialogAction -}; diff --git a/frontend/src/lib/components/ui/avatar/avatar-fallback.svelte b/frontend/src/lib/components/ui/avatar/avatar-fallback.svelte deleted file mode 100644 index c4b5c491..00000000 --- a/frontend/src/lib/components/ui/avatar/avatar-fallback.svelte +++ /dev/null @@ -1,40 +0,0 @@ - - - diff --git a/frontend/src/lib/components/ui/avatar/avatar-gradients.js b/frontend/src/lib/components/ui/avatar/avatar-gradients.js deleted file mode 100644 index 36e2c7aa..00000000 --- a/frontend/src/lib/components/ui/avatar/avatar-gradients.js +++ /dev/null @@ -1,22 +0,0 @@ -export const AVATAR_GRADIENTS = [ - 'linear-gradient(135deg, #a78bfa, #7c3aed)', // 0 purple - 'linear-gradient(135deg, #f472b6, #db2777)', // 1 pink - 'linear-gradient(135deg, #34d399, #059669)', // 2 green - 'linear-gradient(135deg, #60a5fa, #2563eb)', // 3 blue - 'linear-gradient(135deg, #fbbf24, #ef4444)', // 4 orange-red - 'linear-gradient(135deg, #67e8f9, #0891b2)', // 5 cyan - 'linear-gradient(135deg, #fcd34d, #d97706)' // 6 amber -]; - -export const WORKSPACE_GRADIENT = 'linear-gradient(135deg, #ea580c, #c2410c)'; - -// Stable string hash → bucket [0,6]. djb2-ish; only needs to be deterministic & well-spread. -export function gradientFor(seed) { - if (seed == null) return AVATAR_GRADIENTS[0]; - const s = String(seed); - let h = 5381; - for (let i = 0; i < s.length; i++) { - h = ((h << 5) + h + s.charCodeAt(i)) | 0; - } - return AVATAR_GRADIENTS[Math.abs(h) % AVATAR_GRADIENTS.length]; -} diff --git a/frontend/src/lib/components/ui/avatar/avatar-image.svelte b/frontend/src/lib/components/ui/avatar/avatar-image.svelte deleted file mode 100644 index 1744e136..00000000 --- a/frontend/src/lib/components/ui/avatar/avatar-image.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - - diff --git a/frontend/src/lib/components/ui/avatar/avatar.svelte b/frontend/src/lib/components/ui/avatar/avatar.svelte deleted file mode 100644 index 1dd2db28..00000000 --- a/frontend/src/lib/components/ui/avatar/avatar.svelte +++ /dev/null @@ -1,48 +0,0 @@ - - - diff --git a/frontend/src/lib/components/ui/avatar/index.js b/frontend/src/lib/components/ui/avatar/index.js deleted file mode 100644 index a32de14d..00000000 --- a/frontend/src/lib/components/ui/avatar/index.js +++ /dev/null @@ -1,14 +0,0 @@ -import Root from './avatar.svelte'; -import Image from './avatar-image.svelte'; -import Fallback from './avatar-fallback.svelte'; - -export { - Root, - Image, - Fallback, - // - Root as Avatar, - Image as AvatarImage, - Fallback as AvatarFallback -}; -export { gradientFor, AVATAR_GRADIENTS, WORKSPACE_GRADIENT } from './avatar-gradients.js'; diff --git a/frontend/src/lib/components/ui/badge/badge.svelte b/frontend/src/lib/components/ui/badge/badge.svelte deleted file mode 100644 index bb5af8ab..00000000 --- a/frontend/src/lib/components/ui/badge/badge.svelte +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - {#if showDot} - - {/if} - {@render children?.()} - diff --git a/frontend/src/lib/components/ui/badge/index.js b/frontend/src/lib/components/ui/badge/index.js deleted file mode 100644 index cde98224..00000000 --- a/frontend/src/lib/components/ui/badge/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default as Badge } from './badge.svelte'; -export { badgeVariants } from './badge.svelte'; diff --git a/frontend/src/lib/components/ui/bulk-action-bar/BulkActionBar.svelte b/frontend/src/lib/components/ui/bulk-action-bar/BulkActionBar.svelte deleted file mode 100644 index af8858af..00000000 --- a/frontend/src/lib/components/ui/bulk-action-bar/BulkActionBar.svelte +++ /dev/null @@ -1,31 +0,0 @@ - - -{#if count > 0} -
- {count} selected - - {#each actions as a (a.label)} - - {/each} - -
-{/if} diff --git a/frontend/src/lib/components/ui/bulk-action-bar/index.js b/frontend/src/lib/components/ui/bulk-action-bar/index.js deleted file mode 100644 index 8976d1d4..00000000 --- a/frontend/src/lib/components/ui/bulk-action-bar/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as BulkActionBar } from './BulkActionBar.svelte'; diff --git a/frontend/src/lib/components/ui/calendar/Calendar.svelte b/frontend/src/lib/components/ui/calendar/Calendar.svelte deleted file mode 100644 index cf419bfa..00000000 --- a/frontend/src/lib/components/ui/calendar/Calendar.svelte +++ /dev/null @@ -1,135 +0,0 @@ - - - - {#snippet children({ months: calendarMonths, weekdays })} - - - - - -
- - - - - -
- - - - -
- - {#each calendarMonths as month (month.value.toString())} - - - - {#each weekdays as day (day)} - - {day.slice(0, 2)} - - {/each} - - - - {#each month.weeks as week, weekIndex (weekIndex)} - - {#each week as date (date.toString())} - - - {date.day} - - - {/each} - - {/each} - - - {/each} - {/snippet} -
diff --git a/frontend/src/lib/components/ui/calendar/index.js b/frontend/src/lib/components/ui/calendar/index.js deleted file mode 100644 index fa9f6624..00000000 --- a/frontend/src/lib/components/ui/calendar/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import Calendar from './Calendar.svelte'; - -export { Calendar }; diff --git a/frontend/src/lib/components/ui/card/card-content.svelte b/frontend/src/lib/components/ui/card/card-content.svelte deleted file mode 100644 index 844c8681..00000000 --- a/frontend/src/lib/components/ui/card/card-content.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - -
- {@render children?.()} -
diff --git a/frontend/src/lib/components/ui/card/card-description.svelte b/frontend/src/lib/components/ui/card/card-description.svelte deleted file mode 100644 index 86378638..00000000 --- a/frontend/src/lib/components/ui/card/card-description.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - -

- {@render children?.()} -

diff --git a/frontend/src/lib/components/ui/card/card-header.svelte b/frontend/src/lib/components/ui/card/card-header.svelte deleted file mode 100644 index df901bcb..00000000 --- a/frontend/src/lib/components/ui/card/card-header.svelte +++ /dev/null @@ -1,18 +0,0 @@ - - -
- {@render children?.()} -
diff --git a/frontend/src/lib/components/ui/card/card-title.svelte b/frontend/src/lib/components/ui/card/card-title.svelte deleted file mode 100644 index c81ec6ef..00000000 --- a/frontend/src/lib/components/ui/card/card-title.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - -
- {@render children?.()} -
diff --git a/frontend/src/lib/components/ui/card/card.svelte b/frontend/src/lib/components/ui/card/card.svelte deleted file mode 100644 index 0c0fc423..00000000 --- a/frontend/src/lib/components/ui/card/card.svelte +++ /dev/null @@ -1,27 +0,0 @@ - - -
- {@render children?.()} -
diff --git a/frontend/src/lib/components/ui/card/index.js b/frontend/src/lib/components/ui/card/index.js deleted file mode 100644 index 0fcc220f..00000000 --- a/frontend/src/lib/components/ui/card/index.js +++ /dev/null @@ -1,19 +0,0 @@ -import Root from './card.svelte'; -import Content from './card-content.svelte'; -import Description from './card-description.svelte'; -import Header from './card-header.svelte'; -import Title from './card-title.svelte'; - -export { - Root, - Content, - Description, - Header, - Title, - // - Root as Card, - Content as CardContent, - Description as CardDescription, - Header as CardHeader, - Title as CardTitle -}; diff --git a/frontend/src/lib/components/ui/crm-drawer/CrmDrawer.svelte b/frontend/src/lib/components/ui/crm-drawer/CrmDrawer.svelte deleted file mode 100644 index d73abbc7..00000000 --- a/frontend/src/lib/components/ui/crm-drawer/CrmDrawer.svelte +++ /dev/null @@ -1,301 +0,0 @@ - - - onOpenChange?.(value)}> - - {#if loading} - -
- -
-
- - -
-
- - -
-
- -
-
- {#each { length: 6 } as _, index (index)} -
- - - -
- {/each} -
-
-
- {:else} -
- -
- - - -
-
- - {headerLabel} - - {#if mode === 'create'} - New - {/if} -
-
- {#if fullPageHref && mode !== 'create'} - - - {/if} - -
-
-
- - -
- {#if banner} - {@render banner()} - {/if} - - -
- {#if titleEditable} - - {:else} -

- {titleValue || titlePlaceholder} -

- {/if} - {#if metaSection && mode !== 'create'} -
- {@render metaSection()} -
- {/if} -
- - -
-
- {#each visibleColumns as col (col.key)} - handleFieldChange(col.key, value)} - /> - {/each} -
-
- - - {#if activitySection} -
-
- {@render activitySection()} -
- {/if} -
- - -
-
- - {#if onDelete && mode !== 'create'} - - {:else} -
- {/if} - - - {#if footerActions} -
- {@render footerActions()} -
- {/if} -
-
-
- {/if} -
-
diff --git a/frontend/src/lib/components/ui/crm-drawer/CrmPropertyRow.svelte b/frontend/src/lib/components/ui/crm-drawer/CrmPropertyRow.svelte deleted file mode 100644 index 8acba289..00000000 --- a/frontend/src/lib/components/ui/crm-drawer/CrmPropertyRow.svelte +++ /dev/null @@ -1,482 +0,0 @@ - - -
-
- - - - -
- {#if isPlainReadonly} - - {#if value !== null && value !== undefined && value !== ''} - - {type === 'number' ? formatNumber(value) : value} - - {:else} - {emptyText || '—'} - {/if} - - {:else if type === 'text' || type === 'email'} - - {:else if type === 'number'} -
- {#if prefix} - - {/if} - -
- {:else if isReadonlyDate} - - {#if value} - {formatDateDisplay(value)} - {:else} - {emptyText || '—'} - {/if} - - {:else if type === 'date'} - - - {#snippet child({ props })} - - {/snippet} - - - - - - {:else if type === 'textarea'} - - {:else if isReadonlySelect} - - - {getOptionLabel(value)} - - {:else if type === 'select'} - - - {#snippet child({ props })} - - {/snippet} - - - {#each options as option (option.value)} - handleSelectChange(option.value)} - class="flex items-center gap-2.5 rounded-lg px-2.5 py-2" - > - - {option.label} - {#if value === option.value} - - {/each} - - - {:else if type === 'checkbox' || type === 'boolean'} - - {:else if type === 'color'} -
- - {value || '#3B82F6'} -
- {:else if type === 'multiselect'} - - {:else if type === 'readonly'} - - {#if value} - {value} - {:else} - {emptyText || '—'} - {/if} - - {/if} -
-
- - {#if error} - - {/if} -
diff --git a/frontend/src/lib/components/ui/crm-drawer/index.js b/frontend/src/lib/components/ui/crm-drawer/index.js deleted file mode 100644 index 81d814b8..00000000 --- a/frontend/src/lib/components/ui/crm-drawer/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default as CrmDrawer } from './CrmDrawer.svelte'; -export { default as CrmPropertyRow } from './CrmPropertyRow.svelte'; diff --git a/frontend/src/lib/components/ui/crm-table/CrmTable.svelte b/frontend/src/lib/components/ui/crm-table/CrmTable.svelte deleted file mode 100644 index 4fae2995..00000000 --- a/frontend/src/lib/components/ui/crm-table/CrmTable.svelte +++ /dev/null @@ -1,481 +0,0 @@ - - -{#snippet tableBody()} - {#if data.length === 0} - {#if emptyState} - {@render emptyState()} - {:else} -
-

No items found

-
- {/if} - {:else} - - - - {#if selectable} - - {/if} - {#each visibleColumnDefs as column, colIndex (column.key)} - - {/each} - - - - {#each data as row (row.id)} - { - if (!onRowClick) return; - const target = /** @type {HTMLElement} */ (e.target); - if ( - target.closest('button') || - target.closest('input') || - target.closest('[data-dropdown-trigger]') || - target.closest('[role="menu"]') - ) { - return; - } - activeRowId = row[rowIdKey]; - onRowClick(row); - }} - > - {#if selectable} - - {/if} - {#each visibleColumnDefs as column, colIndex (column.key)} - {@const value = getCellValue(row, column)} - {@const formattedValue = formatValue(value, column, row)} - - {/each} - - {/each} - -
- 0 && selectedIds.length === data.length} - indeterminate={selectedIds.length > 0 && selectedIds.length < data.length} - onchange={(e) => { - const checked = /** @type {HTMLInputElement} */ (e.currentTarget).checked; - selectedIds = checked ? data.map((r) => r[rowIdKey]) : []; - }} - /> - - {column.label} -
e.stopPropagation()}> - { - const id = row[rowIdKey]; - const checked = /** @type {HTMLInputElement} */ (e.currentTarget).checked; - selectedIds = checked - ? [...selectedIds, id] - : selectedIds.filter((x) => x !== id); - }} - /> - - {#if cellContent} - {@render cellContent(row, column)} - {:else if column.type === 'text' || column.type === 'email' || column.type === 'number' || !column.type} - {#if editingCell?.rowId === row.id && editingCell?.columnKey === column.key && onRowChange} - stopEditing(true)} - data-edit-input="{row.id}-{column.key}" - class="w-full rounded bg-white px-2 py-1.5 text-sm shadow-sm ring-1 ring-gray-200 transition-shadow duration-100 outline-none focus:ring-blue-300 dark:bg-gray-900 dark:ring-gray-700" - /> - {:else if onRowChange && column.editable !== false} - - {:else} - - {#if formattedValue} - {formattedValue} - {:else} - {column.emptyText || 'Empty'} - {/if} - - {/if} - {:else if column.type === 'date'} - {#if editingCell?.rowId === row.id && editingCell?.columnKey === column.key && onRowChange} - stopEditing(true)} - data-edit-input="{row.id}-{column.key}" - class="w-full rounded bg-white px-2 py-1.5 text-sm shadow-sm ring-1 ring-gray-200 transition-shadow duration-100 outline-none focus:ring-blue-300 dark:bg-gray-900 dark:ring-gray-700" - /> - {:else if onRowChange && column.editable !== false} - - {:else} - - {#if formattedValue} - {formattedValue} - {:else} - {column.emptyText || 'Empty'} - {/if} - - {/if} - {:else if column.type === 'select' && column.options} - {#if onRowChange} - - - {#snippet child({ props })} - - {/snippet} - - - {#each column.options as option (option.value)} - updateSelectValue(row.id, column.key, option.value)} - class="flex items-center gap-2" - > - - {option.label} - {#if value === option.value} - - {/if} - - {/each} - - - {:else} - - {getOptionLabel(value, column.options)} - - {/if} - {:else if column.type === 'checkbox'} - {#if onRowChange} - - {:else} - - {#if value} - - {/if} - - {/if} - {:else if column.type === 'relation'} - - {/if} - {#if cellSuffix} - {@render cellSuffix(row, column)} - {/if} -
- {/if} -{/snippet} - -{@render tableBody()} diff --git a/frontend/src/lib/components/ui/crm-table/index.js b/frontend/src/lib/components/ui/crm-table/index.js deleted file mode 100644 index f97d9026..00000000 --- a/frontend/src/lib/components/ui/crm-table/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as CrmTable } from './CrmTable.svelte'; diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte deleted file mode 100644 index 0cea5591..00000000 --- a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte deleted file mode 100644 index 02f7070e..00000000 --- a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +++ /dev/null @@ -1,37 +0,0 @@ - - - - {#snippet children({ checked, indeterminate })} - - {#if indeterminate} - - {:else} - - {/if} - - {@render childrenProp?.()} - {/snippet} - diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte deleted file mode 100644 index 9f62b8c8..00000000 --- a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte +++ /dev/null @@ -1,37 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte deleted file mode 100644 index bcba08b3..00000000 --- a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte deleted file mode 100644 index 3de481a3..00000000 --- a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte +++ /dev/null @@ -1,34 +0,0 @@ - - - diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte deleted file mode 100644 index 430142e6..00000000 --- a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - -
- {@render children?.()} -
diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte deleted file mode 100644 index ede0be72..00000000 --- a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - - diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte deleted file mode 100644 index 825e0d41..00000000 --- a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/frontend/src/lib/components/ui/dropdown-menu/index.js b/frontend/src/lib/components/ui/dropdown-menu/index.js deleted file mode 100644 index 6c2d04a7..00000000 --- a/frontend/src/lib/components/ui/dropdown-menu/index.js +++ /dev/null @@ -1,34 +0,0 @@ -import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'; -import CheckboxGroup from './dropdown-menu-checkbox-group.svelte'; -import CheckboxItem from './dropdown-menu-checkbox-item.svelte'; -import Content from './dropdown-menu-content.svelte'; -import Group from './dropdown-menu-group.svelte'; -import Item from './dropdown-menu-item.svelte'; -import Label from './dropdown-menu-label.svelte'; -import Separator from './dropdown-menu-separator.svelte'; -import Trigger from './dropdown-menu-trigger.svelte'; -const Sub = DropdownMenuPrimitive.Sub; -const Root = DropdownMenuPrimitive.Root; - -export { - CheckboxGroup, - CheckboxItem, - Content, - Root as DropdownMenu, - CheckboxGroup as DropdownMenuCheckboxGroup, - CheckboxItem as DropdownMenuCheckboxItem, - Content as DropdownMenuContent, - Group as DropdownMenuGroup, - Item as DropdownMenuItem, - Label as DropdownMenuLabel, - Separator as DropdownMenuSeparator, - Sub as DropdownMenuSub, - Trigger as DropdownMenuTrigger, - Group, - Item, - Label, - Root, - Separator, - Sub, - Trigger -}; diff --git a/frontend/src/lib/components/ui/editable-field/EditableMultiSelect.svelte b/frontend/src/lib/components/ui/editable-field/EditableMultiSelect.svelte deleted file mode 100644 index 74a44209..00000000 --- a/frontend/src/lib/components/ui/editable-field/EditableMultiSelect.svelte +++ /dev/null @@ -1,247 +0,0 @@ - - -
- - - - {/if} - - {/each} - {#if remainingCount > 0} - +{remainingCount} - {/if} - {/if} -
-