mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-08-17 16:07:18 +02:00
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user