Add sentiment feature to employee reports
CI / build (push) Has been skipped
CI / deploy (push) Successful in 49s

- Introduced a new `Sentiment` field in the `EmployeeReportRecord` interface to capture report sentiment as 'Positive', 'Negative', or 'Neutral'.
- Updated the report creation and editing forms in Svelte to include a sentiment selection dropdown.
- Enhanced API endpoints to handle sentiment data during report creation and updates.
- Adjusted the display logic to show sentiment information in the report details.
This commit is contained in:
eewing
2026-02-17 10:59:31 -06:00
parent 069f38a313
commit debc6559c9
19 changed files with 442 additions and 4 deletions
+77 -2
View File
@@ -10,8 +10,15 @@
import type {
EmployeeRecord,
EmployeeReportRecord,
ReportSentiment,
VoxerLink
} from './+page.server';
import * as Select from '$lib/components/ui/select/index.js';
import {
utcIsoToLocalDatetimeLocal,
nowToLocalDatetimeLocal,
localDatetimeLocalToUtcIso
} from '$lib/date-utils';
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
import LinkIcon from '@lucide/svelte/icons/link';
import PencilIcon from '@lucide/svelte/icons/pencil';
@@ -48,6 +55,9 @@
let openDeleteReport = $state(false);
let reportFormText = $state('');
let reportVoxers = $state<VoxerLink[]>([]);
const SENTIMENT_OPTIONS: ReportSentiment[] = ['Positive', 'Negative', 'Neutral'];
let reportCreatedLocal = $state('');
let reportSentiment = $state<ReportSentiment>('Neutral');
let editingReport = $state<EmployeeReportRecord | null>(null);
let deletingReport = $state<EmployeeReportRecord | null>(null);
let reportSaving = $state(false);
@@ -143,6 +153,8 @@
function openCreateReportDialog() {
reportFormText = '';
reportVoxers = [];
reportCreatedLocal = nowToLocalDatetimeLocal();
reportSentiment = 'Neutral';
reportError = null;
editingReport = null;
openCreateReport = true;
@@ -152,6 +164,8 @@
editingReport = report;
reportFormText = report.report ?? '';
reportVoxers = parseVoxers(report).map((v) => ({ ...v }));
reportCreatedLocal = utcIsoToLocalDatetimeLocal(report.created);
reportSentiment = (report.Sentiment === 'Positive' || report.Sentiment === 'Negative' ? report.Sentiment : 'Neutral') as ReportSentiment;
reportError = null;
openEditReport = true;
}
@@ -177,7 +191,12 @@
const res = await fetch(`/api/employees/${employeeId}/reports`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ report: reportFormText, voxers: reportVoxers })
body: JSON.stringify({
report: reportFormText,
voxers: reportVoxers,
created: localDatetimeLocalToUtcIso(reportCreatedLocal),
Sentiment: reportSentiment
})
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
@@ -200,7 +219,12 @@
const res = await fetch(`/api/employees/${employeeId}/reports/${editingReport.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ report: reportFormText, voxers: reportVoxers })
body: JSON.stringify({
report: reportFormText,
voxers: reportVoxers,
created: localDatetimeLocalToUtcIso(reportCreatedLocal),
Sentiment: reportSentiment
})
});
if (!res.ok) throw new Error('Update failed');
openEditReport = false;
@@ -416,6 +440,9 @@
<Card.Title class="text-base">Report</Card.Title>
<p class="text-muted-foreground text-xs">
Created {formatDate(report.created)} · Updated {formatDate(report.updated)}
{#if report.Sentiment}
· <span class="capitalize">{report.Sentiment}</span>
{/if}
</p>
</div>
<div class="flex shrink-0 gap-1">
@@ -512,6 +539,30 @@
}}
class="grid gap-4 py-4"
>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 sm:items-end">
<div class="space-y-2">
<Label for="new-created">Created (local time)</Label>
<Input
id="new-created"
type="datetime-local"
bind:value={reportCreatedLocal}
disabled={reportSaving}
/>
</div>
<div class="space-y-2">
<Label for="new-sentiment">Sentiment</Label>
<Select.Root bind:value={reportSentiment} disabled={reportSaving}>
<Select.Trigger class="w-full" id="new-sentiment">
{reportSentiment}
</Select.Trigger>
<Select.Content>
{#each SENTIMENT_OPTIONS as opt}
<Select.Item value={opt} label={opt}>{opt}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
</div>
<div class="space-y-2">
<Label for="new-report">Report</Label>
<textarea
@@ -588,6 +639,30 @@
}}
class="grid gap-4 py-4"
>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 sm:items-end">
<div class="space-y-2">
<Label for="edit-created">Created (local time)</Label>
<Input
id="edit-created"
type="datetime-local"
bind:value={reportCreatedLocal}
disabled={reportSaving}
/>
</div>
<div class="space-y-2">
<Label for="edit-sentiment">Sentiment</Label>
<Select.Root bind:value={reportSentiment} disabled={reportSaving}>
<Select.Trigger class="w-full" id="edit-sentiment">
{reportSentiment}
</Select.Trigger>
<Select.Content>
{#each SENTIMENT_OPTIONS as opt}
<Select.Item value={opt} label={opt}>{opt}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
</div>
<div class="space-y-2">
<Label for="edit-report">Report</Label>
<textarea