From 184a299ba05848d4eb6548df6ffb715df8204370 Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Wed, 18 Feb 2026 22:05:28 +0100 Subject: [PATCH] Add dashboard overview, campaigns and events Signed-off-by: Ronni Skansing --- .../src/lib/components/DashboardNav.svelte | 38 ++ frontend/src/routes/dashboard/+page.svelte | 409 ++-------------- .../routes/dashboard/campaigns/+page.svelte | 438 ++++++++++++++++++ .../src/routes/dashboard/events/+page.svelte | 252 ++++++++++ 4 files changed, 759 insertions(+), 378 deletions(-) create mode 100644 frontend/src/lib/components/DashboardNav.svelte create mode 100644 frontend/src/routes/dashboard/campaigns/+page.svelte create mode 100644 frontend/src/routes/dashboard/events/+page.svelte diff --git a/frontend/src/lib/components/DashboardNav.svelte b/frontend/src/lib/components/DashboardNav.svelte new file mode 100644 index 0000000..73ef92a --- /dev/null +++ b/frontend/src/lib/components/DashboardNav.svelte @@ -0,0 +1,38 @@ + + + diff --git a/frontend/src/routes/dashboard/+page.svelte b/frontend/src/routes/dashboard/+page.svelte index 0b17d0f..576f7fb 100644 --- a/frontend/src/routes/dashboard/+page.svelte +++ b/frontend/src/routes/dashboard/+page.svelte @@ -7,27 +7,17 @@ import { onMount } from 'svelte'; import { showIsLoading, hideIsLoading } from '$lib/store/loading.js'; import { addToast } from '$lib/store/toast'; - import { newTableURLParams } from '$lib/service/tableURLParams'; - import Table from '$lib/components/table/Table.svelte'; - import TableRow from '$lib/components/table/TableRow.svelte'; - import TableCell from '$lib/components/table/TableCell.svelte'; - import TableCellEmpty from '$lib/components/table/TableCellEmpty.svelte'; - import TableCellAction from '$lib/components/table/TableCellAction.svelte'; - import TableViewButton from '$lib/components/table/TableViewButton.svelte'; - import { goto } from '$app/navigation'; - import TableDropDownEllipsis from '$lib/components/table/TableDropDownEllipsis.svelte'; - import TestLabel from '$lib/components/TestLabel.svelte'; import AutoRefresh from '$lib/components/AutoRefresh.svelte'; import StatsCard from '$lib/components/StatsCard.svelte'; import CampaignCalender from '$lib/components/CampaignCalendar.svelte'; import CampaignTrendChart from '$lib/components/CampaignTrendChart.svelte'; - import CheckboxField from '$lib/components/CheckboxField.svelte'; import { fetchAllRows } from '$lib/utils/api-utils'; import { tick } from 'svelte'; - import EventName from '$lib/components/table/EventName.svelte'; import TextFieldSelect from '$lib/components/TextFieldSelect.svelte'; import { autoRefreshStore, setPageAutoRefresh } from '$lib/store/autoRefresh'; import { BiMap } from '$lib/utils/maps'; + import { goto } from '$app/navigation'; + import DashboardNav from '$lib/components/DashboardNav.svelte'; // services const appStateService = AppStateService.instance; @@ -44,61 +34,24 @@ // local state let contextCompanyID = null; let contextCompanyName = ''; - let scheduledTableURLParams = newTableURLParams({ - prefix: 'scheduled', - sortBy: 'send_start_at', - sortOrder: 'desc', - noScroll: true - }); - let activeTableURLParams = newTableURLParams({ - prefix: 'active', - sortBy: 'send_start_at', - sortOrder: 'desc', - noScroll: true - }); - let eventsTableURLParams = newTableURLParams({ - prefix: 'events', - sortBy: 'created_at', - sortOrder: 'desc', - noScroll: true - }); - - let isActiveCampaignsLoading = false; - let isUpcomingCampaignsLoading = false; - let isEventsLoading = false; let active = 0; let scheduled = 0; let finished = 0; let repeatOffenders = 0; - let finishedCustomStats = 0; let calendarCampaigns = []; - let activeCampaigns = []; - let activeCampaignsHasNextPage = true; - let scheduledCampaigns = []; - let scheduledCampaignsHasNextPage = true; - let events = []; - let eventsHasNextPage = true; - let eventTypesIDToNameMap = {}; - let availableEventTypes = []; - let selectedEventType = ''; let campaignStats = []; let isCampaignStatsLoading = false; let calendarStartDate = null; let calendarEndDate = null; - // Toggle for including test campaigns let includeTestCampaigns = false; - // Use consistent colors with campaign detail page - these are already well-suited for both light and dark modes - - // Handler for when toggle changes + // handler for when toggle changes const handleToggleChange = async () => { - // Wait for binding to update await tick(); - // Refresh all data with new toggle state await refresh(false); }; @@ -117,15 +70,6 @@ contextCompanyName = context.companyName; } refresh(); - activeTableURLParams.onChange(() => refreshActiveCampaigns(true)); - scheduledTableURLParams.onChange(() => refreshScheduledCampaigns(true)); - eventsTableURLParams.onChange(() => refreshEvents(true)); - - return () => { - activeTableURLParams.unsubscribe(); - scheduledTableURLParams.unsubscribe(); - eventsTableURLParams.unsubscribe(); - }; }); const refresh = async (showLoading = true) => { @@ -144,11 +88,7 @@ active = res.data.active; scheduled = res.data.upcoming; finished = res.data.finished; - await setEventTypes(); await refreshCalendarCampaings(); - await refreshActiveCampaigns(showLoading); - await refreshScheduledCampaigns(showLoading); - await refreshEvents(showLoading); await refreshCampaignStats(showLoading); } catch (e) { addToast('Failed to load data', 'Error'); @@ -178,110 +118,6 @@ } catch (e) { addToast('Failed to load calendar campaigns', 'Error'); console.error('Failed to load calendar campaigns', e); - } finally { - } - }; - - const refreshActiveCampaigns = async (showLoading = true) => { - if (showLoading) { - isActiveCampaignsLoading = true; - } - try { - const options = { - page: activeTableURLParams.currentPage, - perPage: activeTableURLParams.perPage, - sortBy: activeTableURLParams.sortBy, - sortOrder: activeTableURLParams.sortOrder, - search: activeTableURLParams.search, - includeTest: includeTestCampaigns - }; - const res = await api.campaign.getAllActive(options, contextCompanyID); - if (!res.success) { - throw res.error; - } - activeCampaigns = res.data.rows; - activeCampaignsHasNextPage = res.data.hasNextPage; - } catch (e) { - addToast('Failed to load active campaigns', 'Error'); - console.error('Failed to load active campaigns', e); - } finally { - if (showLoading) { - isActiveCampaignsLoading = false; - } - } - }; - - const refreshScheduledCampaigns = async (showLoading = true) => { - if (showLoading) { - isUpcomingCampaignsLoading = true; - } - try { - const options = { - page: scheduledTableURLParams.currentPage, - perPage: scheduledTableURLParams.perPage, - sortBy: scheduledTableURLParams.sortBy, - sortOrder: scheduledTableURLParams.sortOrder, - search: scheduledTableURLParams.search, - includeTest: includeTestCampaigns - }; - const res = await api.campaign.getAllUpcoming(options, contextCompanyID); - if (!res.success) { - throw res.error; - } - scheduledCampaigns = res.data.rows; - scheduledCampaignsHasNextPage = res.data.hasNextPage; - } catch (e) { - addToast('Failed to load scheduled campaigns', 'Error'); - console.error('Failed to load scheduled campaigns', e); - } finally { - if (showLoading) { - isUpcomingCampaignsLoading = false; - } - } - }; - - const setEventTypes = async () => { - try { - const res = await api.campaign.getAllEventTypes(); - if (!res.success) { - addToast('Failed to load event types', 'Error'); - console.error('failed to load event types', res.error); - return; - } - res.data.map((t) => (eventTypesIDToNameMap[t.id] = t.name)); - availableEventTypes = res.data.map((t) => ({ value: t.id, label: t.name })); - availableEventTypes.unshift({ value: '', label: 'All Events' }); - } catch (e) { - addToast('Failed to load event types', 'Error'); - console.error('failed to load event types', e); - } - }; - - const refreshEvents = async (showIsLoading = true) => { - try { - if (showIsLoading) { - isEventsLoading = true; - } - const options = { - page: eventsTableURLParams.page, - perPage: eventsTableURLParams.perPage, - sortBy: eventsTableURLParams.sortBy, - sortOrder: eventsTableURLParams.sortOrder, - search: eventsTableURLParams.search, - includeTest: includeTestCampaigns - }; - const res = await api.campaign.getAllEvents(options, contextCompanyID); - if (res.success) { - events = res.data?.rows ?? []; - eventsHasNextPage = res.data?.hasNextPage ?? false; - } - } catch (e) { - addToast('Failed to load events', 'Error'); - console.error('failed to load events', e); - } finally { - if (showIsLoading) { - isEventsLoading = false; - } } }; @@ -308,8 +144,6 @@ throw res.error; } campaignStats = res.data.rows || []; - // stats without a campaign ID is custom stats - finishedCustomStats = res.data.rows.filter((c) => !c.campaignId).length; } catch (e) { addToast('Failed to load campaign statistics', 'Error'); console.error('Failed to load campaign statistics', e); @@ -319,17 +153,16 @@ } } }; - - /** @param {string} id */ - const onClickViewCampaign = (id) => { - goto(`/campaign/${id}`); - };
+ Dashboard + + +
- Dashboard + Overview
+ { - // refresh all data let res = await api.campaign.getStats(contextCompanyID, { includeTest: includeTestCampaigns }); @@ -398,51 +231,6 @@ scheduled = res.data.upcoming; finished = res.data.finished; - // refresh table data directly like campaign page does - const activeOptions = { - page: activeTableURLParams.currentPage, - perPage: activeTableURLParams.perPage, - sortBy: activeTableURLParams.sortBy, - sortOrder: activeTableURLParams.sortOrder, - search: activeTableURLParams.search, - includeTest: includeTestCampaigns - }; - const activeRes = await api.campaign.getAllActive(activeOptions, contextCompanyID); - if (activeRes.success) { - activeCampaigns = activeRes.data.rows; - activeCampaignsHasNextPage = activeRes.data.hasNextPage; - } - - // refresh scheduled campaigns - const scheduledOptions = { - page: scheduledTableURLParams.currentPage, - perPage: scheduledTableURLParams.perPage, - sortBy: scheduledTableURLParams.sortBy, - sortOrder: scheduledTableURLParams.sortOrder, - search: scheduledTableURLParams.search, - includeTest: includeTestCampaigns - }; - const scheduledRes = await api.campaign.getAllUpcoming(scheduledOptions, contextCompanyID); - if (scheduledRes.success) { - scheduledCampaigns = scheduledRes.data.rows; - scheduledCampaignsHasNextPage = scheduledRes.data.hasNextPage; - } - - // refresh events - const eventsOptions = { - page: eventsTableURLParams.currentPage, - perPage: eventsTableURLParams.perPage, - sortBy: eventsTableURLParams.sortBy, - sortOrder: eventsTableURLParams.sortOrder, - search: eventsTableURLParams.search, - includeTest: includeTestCampaigns - }; - const eventsRes = await api.campaign.getAllEvents(eventsOptions, contextCompanyID); - if (eventsRes.success) { - events = eventsRes.data?.rows ?? []; - eventsHasNextPage = eventsRes.data?.hasNextPage ?? false; - } - const statsRes = await api.campaign.getAllCampaignStats(contextCompanyID); if (statsRes.success) { campaignStats = []; @@ -453,22 +241,22 @@ await refreshCalendarCampaings(); }} /> + {#if contextCompanyName} {contextCompanyName} {/if} - Recent events -
- - {#each events as event (event.id)} - - - - - - - {#if event.campaign?.name} - - {event.campaign.name} - - {/if} - - - {#if event.recipient?.email} - - {event.recipient.email} - - {/if} - - {#if !contextCompanyID} - - {#if event.campaign?.company?.name} - {event.campaign.company.name} - {/if} - - {/if} - - {/each} -
-
- {contextCompanyName ? 'Calendar' : 'Shared Calendar'}
- - Active campaigns -
- - {#each activeCampaigns as campaign} - - - - {#if campaign.isTest} - - {/if} - - {campaign.name} - - - - - - - - - - onClickViewCampaign(campaign.id)} /> - - - - {/each} -
-
- - Scheduled campaigns -
- - {#each scheduledCampaigns as campaign} - - - - {#if campaign.isTest} - - {/if} - - {campaign.name} - - - - - - - - - - onClickViewCampaign(campaign.id)} /> - - - - {/each} -
-
diff --git a/frontend/src/routes/dashboard/campaigns/+page.svelte b/frontend/src/routes/dashboard/campaigns/+page.svelte new file mode 100644 index 0000000..c5b1ae2 --- /dev/null +++ b/frontend/src/routes/dashboard/campaigns/+page.svelte @@ -0,0 +1,438 @@ + + + +
+ Dashboard + + + +
+ Campaigns +
+ +
+ + Auto-Refresh + + +
+
+
+ + { + const activeOptions = { + page: activeTableURLParams.currentPage, + perPage: activeTableURLParams.perPage, + sortBy: activeTableURLParams.sortBy, + sortOrder: activeTableURLParams.sortOrder, + search: activeTableURLParams.search, + includeTest: includeTestCampaigns + }; + const activeRes = await api.campaign.getAllActive(activeOptions, contextCompanyID); + if (activeRes.success) { + activeCampaigns = activeRes.data.rows; + activeCampaignsHasNextPage = activeRes.data.hasNextPage; + } + + const scheduledOptions = { + page: scheduledTableURLParams.currentPage, + perPage: scheduledTableURLParams.perPage, + sortBy: scheduledTableURLParams.sortBy, + sortOrder: scheduledTableURLParams.sortOrder, + search: scheduledTableURLParams.search, + includeTest: includeTestCampaigns + }; + const scheduledRes = await api.campaign.getAllUpcoming(scheduledOptions, contextCompanyID); + if (scheduledRes.success) { + scheduledCampaigns = scheduledRes.data.rows; + scheduledCampaignsHasNextPage = scheduledRes.data.hasNextPage; + } + + const completedOptions = { + page: completedTableURLParams.currentPage, + perPage: completedTableURLParams.perPage, + sortBy: completedTableURLParams.sortBy, + sortOrder: completedTableURLParams.sortOrder, + search: completedTableURLParams.search, + includeTest: includeTestCampaigns + }; + const completedRes = await api.campaign.getAllFinished(completedOptions, contextCompanyID); + if (completedRes.success) { + completedCampaigns = completedRes.data.rows; + completedCampaignsHasNextPage = completedRes.data.hasNextPage; + } + }} + /> + + Active campaigns +
+ + {#each activeCampaigns as campaign} + + + + {#if campaign.isTest} + + {/if} + + {campaign.name} + + + + {#if !contextCompanyID} + + {/if} + + + + + + onClickViewCampaign(campaign.id)} /> + + + + {/each} +
+
+ + Scheduled campaigns +
+ + {#each scheduledCampaigns as campaign} + + + + {#if campaign.isTest} + + {/if} + + {campaign.name} + + + + {#if !contextCompanyID} + + {/if} + + + + + + onClickViewCampaign(campaign.id)} /> + + + + {/each} +
+
+ + Completed campaigns +
+ + {#each completedCampaigns as campaign} + + + + {#if campaign.isTest} + + {/if} + + {campaign.name} + + + + {#if !contextCompanyID} + + {/if} + + + + + + onClickViewCampaign(campaign.id)} /> + + + + {/each} +
+
+
diff --git a/frontend/src/routes/dashboard/events/+page.svelte b/frontend/src/routes/dashboard/events/+page.svelte new file mode 100644 index 0000000..b316ca3 --- /dev/null +++ b/frontend/src/routes/dashboard/events/+page.svelte @@ -0,0 +1,252 @@ + + + +
+ Dashboard + + + +
+ Recent Events +
+ +
+ + Auto-Refresh + + +
+
+
+ + { + const eventsOptions = { + page: eventsTableURLParams.currentPage, + perPage: eventsTableURLParams.perPage, + sortBy: eventsTableURLParams.sortBy, + sortOrder: eventsTableURLParams.sortOrder, + search: eventsTableURLParams.search, + includeTest: includeTestCampaigns + }; + const eventsRes = await api.campaign.getAllEvents(eventsOptions, contextCompanyID); + if (eventsRes.success) { + events = eventsRes.data?.rows ?? []; + eventsHasNextPage = eventsRes.data?.hasNextPage ?? false; + } + }} + /> + +
+ + {#each events as event (event.id)} + + + + + + + {#if event.campaign?.name} + + {event.campaign.name} + + {/if} + + + {#if event.recipient?.email} + + {event.recipient.email} + + {/if} + + {#if !contextCompanyID} + + {#if event.campaign?.company?.name} + {event.campaign.company.name} + {/if} + + {/if} + + {/each} +
+
+