Signed-off-by: RonniSkansing <rskansing@gmail.com>
This commit is contained in:
RonniSkansing
2026-08-15 00:52:52 +02:00
parent a7fbd573f6
commit ce1cf8efd5
3 changed files with 64 additions and 14 deletions
@@ -114,12 +114,13 @@
visibleMetrics = { ...defaultVisibleMetrics };
}
// whenever metrics change, merge into visibleMetrics but preserve user choices.
// reassign the object to ensure svelte reactivity fires.
$: if (metrics && Array.isArray(metrics)) {
// merge the known metrics into visibleMetrics but preserve user choices. both
// chart modes are merged at once so switching mode keeps the toggles of the
// mode that is not shown. reassign the object to ensure svelte reactivity fires.
$: if (allMetrics.length > 0) {
const merged = { ...visibleMetrics };
// add any newly introduced metrics (default visible)
for (const metric of metrics) {
for (const metric of allMetrics) {
if (!(metric.key in merged)) {
merged[metric.key] = true;
}
@@ -129,15 +130,15 @@
merged[mavgKey] = merged[mavgKey] ?? false;
}
}
// remove keys that no longer correspond to current metrics
// remove keys that no longer correspond to a known metric
for (const key of Object.keys(merged)) {
if (key.startsWith('mavg-')) {
const base = key.slice(5);
if (!metrics.find((m) => m.key === base)) {
if (!allMetrics.find((m) => m.key === base)) {
delete merged[key];
}
} else {
if (!metrics.find((m) => m.key === key)) {
if (!allMetrics.find((m) => m.key === key)) {
delete merged[key];
}
}
@@ -326,6 +327,7 @@
mavg: { color: '#69e1ab', label: 'Completed MA' }
}
];
const allMetrics = [...phishingMetrics, ...trainingMetrics];
$: metrics = chartMode === 'training' ? trainingMetrics : phishingMetrics;
// toggle metric visibility (reassign to trigger svelte reactivity and persist)
@@ -19,6 +19,9 @@
let error = '';
let isSubmitting = false;
let loadedForCompanyID = null;
// the phishing and training reports are separate templates; edit one at a time
let rows = [];
let reportKind = 'phishing'; // 'phishing' | 'training'
// reactive: load the company template when the modal opens
$: {
@@ -35,14 +38,12 @@
content = '';
templateID = null;
error = '';
rows = [];
try {
showIsLoading();
const response = await api.reportTemplate.getAll(company.id);
if (response.success && response.data?.rows?.length > 0) {
const tmpl = response.data.rows[0];
content = tmpl.content || '';
templateID = tmpl.id || null;
}
rows = response.success ? response.data?.rows || [] : [];
selectReportKind('phishing');
} catch (e) {
console.error('Failed to load company report template:', e);
error = 'Failed to load template';
@@ -51,6 +52,25 @@
}
};
// loads the cached template row for the selected kind into the editor
const selectReportKind = (kind) => {
reportKind = kind;
error = '';
const row = rows.find((r) => !!r.isTraining === (kind === 'training'));
content = row?.content || '';
templateID = row?.id || null;
};
// re-fetch the templates after a save so the active kind keeps its id
const refreshRows = async () => {
const response = await api.reportTemplate.getAll(company.id);
rows = response.success ? response.data?.rows || [] : [];
const row = rows.find((r) => !!r.isTraining === (reportKind === 'training'));
if (row?.id) {
templateID = row.id;
}
};
const close = () => {
visible = false;
error = '';
@@ -67,13 +87,15 @@
} else {
response = await api.reportTemplate.create({
content,
companyID: company.id
companyID: company.id,
isTraining: reportKind === 'training'
});
if (response.success && response.data?.id) {
templateID = response.data.id;
}
}
if (response.success) {
await refreshRows();
addToast('Report template saved', 'Success');
if (!saveOnly) {
visible = false;
@@ -96,6 +118,7 @@
const response = await api.reportTemplate.delete(templateID);
if (response.success) {
addToast('Report template deleted', 'Success');
rows = rows.filter((r) => r.id !== templateID);
templateID = null;
content = '';
visible = false;
@@ -117,6 +140,31 @@
<div
class="w-80vw col-start-1 col-end-4 row-start-1 py-8 px-6 flex flex-col bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 transition-colors duration-200"
>
<div class="flex items-center gap-2 mb-4">
<span class="text-sm font-semibold text-gray-600 dark:text-gray-300">Report for</span>
<div
class="flex items-center rounded overflow-hidden border border-gray-300 dark:border-gray-600 text-sm"
>
<button
type="button"
class="px-3 py-1 transition-colors duration-200 {reportKind === 'phishing'
? 'bg-cta-blue text-white'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}"
on:click={() => selectReportKind('phishing')}
>
Phishing
</button>
<button
type="button"
class="px-3 py-1 transition-colors duration-200 {reportKind === 'training'
? 'bg-training-completed text-white'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}"
on:click={() => selectReportKind('training')}
>
Training
</button>
</div>
</div>
<Editor contentType="report" bind:value={content} />
<FormError message={error} />
{#if templateID}
@@ -160,7 +160,7 @@
{/if}
<BigButton on:click={onClickExport}>Export events</BigButton>
<div>
<div class="grid grid-cols-1 lg:grid-cols-7 gap-6 mb-8 mt-4">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-7 gap-6 mb-8 mt-4">
<!-- Campaigns card -->
<StatsCard
title="Campaigns"