Add sentiment feature to employee reports
- 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:
@@ -0,0 +1,37 @@
|
||||
import Root from "./select.svelte";
|
||||
import Group from "./select-group.svelte";
|
||||
import Label from "./select-label.svelte";
|
||||
import Item from "./select-item.svelte";
|
||||
import Content from "./select-content.svelte";
|
||||
import Trigger from "./select-trigger.svelte";
|
||||
import Separator from "./select-separator.svelte";
|
||||
import ScrollDownButton from "./select-scroll-down-button.svelte";
|
||||
import ScrollUpButton from "./select-scroll-up-button.svelte";
|
||||
import GroupHeading from "./select-group-heading.svelte";
|
||||
import Portal from "./select-portal.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Group,
|
||||
Label,
|
||||
Item,
|
||||
Content,
|
||||
Trigger,
|
||||
Separator,
|
||||
ScrollDownButton,
|
||||
ScrollUpButton,
|
||||
GroupHeading,
|
||||
Portal,
|
||||
//
|
||||
Root as Select,
|
||||
Group as SelectGroup,
|
||||
Label as SelectLabel,
|
||||
Item as SelectItem,
|
||||
Content as SelectContent,
|
||||
Trigger as SelectTrigger,
|
||||
Separator as SelectSeparator,
|
||||
ScrollDownButton as SelectScrollDownButton,
|
||||
ScrollUpButton as SelectScrollUpButton,
|
||||
GroupHeading as SelectGroupHeading,
|
||||
Portal as SelectPortal,
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import SelectPortal from "./select-portal.svelte";
|
||||
import SelectScrollUpButton from "./select-scroll-up-button.svelte";
|
||||
import SelectScrollDownButton from "./select-scroll-down-button.svelte";
|
||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||
import type { ComponentProps } from "svelte";
|
||||
import type { WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
sideOffset = 4,
|
||||
portalProps,
|
||||
children,
|
||||
preventScroll = true,
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.ContentProps> & {
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof SelectPortal>>;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<SelectPortal {...portalProps}>
|
||||
<SelectPrimitive.Content
|
||||
bind:ref
|
||||
{sideOffset}
|
||||
{preventScroll}
|
||||
data-slot="select-content"
|
||||
class={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-end-2 data-[side=right]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--bits-select-content-available-height) min-w-[8rem] origin-(--bits-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
class={cn(
|
||||
"h-(--bits-select-anchor-height) w-full min-w-(--bits-select-anchor-width) scroll-my-1 p-1"
|
||||
)}
|
||||
>
|
||||
{@render children?.()}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPortal>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: ComponentProps<typeof SelectPrimitive.GroupHeading> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.GroupHeading
|
||||
bind:ref
|
||||
data-slot="select-group-heading"
|
||||
class={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</SelectPrimitive.GroupHeading>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: SelectPrimitive.GroupProps = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Group bind:ref data-slot="select-group" {...restProps} />
|
||||
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import CheckIcon from "@lucide/svelte/icons/check";
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
value,
|
||||
label,
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.ItemProps> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Item
|
||||
bind:ref
|
||||
{value}
|
||||
data-slot="select-item"
|
||||
class={cn(
|
||||
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 ps-2 pe-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ selected, highlighted })}
|
||||
<span class="absolute end-2 flex size-3.5 items-center justify-center">
|
||||
{#if selected}
|
||||
<CheckIcon class="size-4" />
|
||||
{/if}
|
||||
</span>
|
||||
{#if childrenProp}
|
||||
{@render childrenProp({ selected, highlighted })}
|
||||
{:else}
|
||||
{label || value}
|
||||
{/if}
|
||||
{/snippet}
|
||||
</SelectPrimitive.Item>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {} = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="select-label"
|
||||
class={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
|
||||
let { ...restProps }: SelectPrimitive.PortalProps = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Portal {...restProps} />
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SelectPrimitive.ScrollDownButtonProps> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
bind:ref
|
||||
data-slot="select-scroll-down-button"
|
||||
class={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||
{...restProps}
|
||||
>
|
||||
<ChevronDownIcon class="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import ChevronUpIcon from "@lucide/svelte/icons/chevron-up";
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SelectPrimitive.ScrollUpButtonProps> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
bind:ref
|
||||
data-slot="select-scroll-up-button"
|
||||
class={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||
{...restProps}
|
||||
>
|
||||
<ChevronUpIcon class="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import type { Separator as SeparatorPrimitive } from "bits-ui";
|
||||
import { Separator } from "$lib/components/ui/separator/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SeparatorPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<Separator
|
||||
bind:ref
|
||||
data-slot="select-separator"
|
||||
class={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
size = "default",
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.TriggerProps> & {
|
||||
size?: "sm" | "default";
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Trigger
|
||||
bind:ref
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
class={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none select-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<ChevronDownIcon class="size-4 opacity-50" />
|
||||
</SelectPrimitive.Trigger>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
value = $bindable(),
|
||||
...restProps
|
||||
}: SelectPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Root bind:open bind:value={value as never} {...restProps} />
|
||||
@@ -0,0 +1,7 @@
|
||||
import Root from "./separator.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Separator,
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { Separator as SeparatorPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
"data-slot": dataSlot = "separator",
|
||||
...restProps
|
||||
}: SeparatorPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<SeparatorPrimitive.Root
|
||||
bind:ref
|
||||
data-slot={dataSlot}
|
||||
class={cn(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:min-h-full data-[orientation=vertical]:w-px",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Utilities for converting between local date/time and UTC ISO strings for PocketBase.
|
||||
* PocketBase stores dates in UTC (ISO 8601). We display and edit in the user's local timezone.
|
||||
*/
|
||||
|
||||
/** Format expected by <input type="datetime-local">: "YYYY-MM-DDTHH:mm" in local time */
|
||||
export const DATETIME_LOCAL_FORMAT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/;
|
||||
|
||||
/**
|
||||
* Convert a UTC ISO string (from PocketBase) to local datetime for datetime-local input.
|
||||
* Handles both "2024-01-15T12:00:00.000Z" and "2024-01-15 12:00:00.000Z" styles.
|
||||
*/
|
||||
export function utcIsoToLocalDatetimeLocal(isoUtc: string | undefined): string {
|
||||
if (!isoUtc || typeof isoUtc !== 'string') return '';
|
||||
const d = new Date(isoUtc);
|
||||
if (Number.isNaN(d.getTime())) return '';
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
const h = String(d.getHours()).padStart(2, '0');
|
||||
const min = String(d.getMinutes()).padStart(2, '0');
|
||||
return `${y}-${m}-${day}T${h}:${min}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert current local date/time to datetime-local string (for default "now" when creating).
|
||||
*/
|
||||
export function nowToLocalDatetimeLocal(): string {
|
||||
const d = new Date();
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
const h = String(d.getHours()).padStart(2, '0');
|
||||
const min = String(d.getMinutes()).padStart(2, '0');
|
||||
return `${y}-${m}-${day}T${h}:${min}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a datetime-local value (local time) and return an ISO UTC string for PocketBase.
|
||||
*/
|
||||
export function localDatetimeLocalToUtcIso(localStr: string | undefined): string {
|
||||
if (!localStr || typeof localStr !== 'string' || !DATETIME_LOCAL_FORMAT.test(localStr.trim())) {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
const d = new Date(localStr);
|
||||
if (Number.isNaN(d.getTime())) return new Date().toISOString();
|
||||
return d.toISOString();
|
||||
}
|
||||
@@ -21,12 +21,15 @@ export interface VoxerLink {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export type ReportSentiment = 'Positive' | 'Negative' | 'Neutral';
|
||||
|
||||
export interface EmployeeReportRecord {
|
||||
id: string;
|
||||
report?: string;
|
||||
voxers?: VoxerLink[] | string; // JSON array or string from PB
|
||||
created: string;
|
||||
updated: string;
|
||||
Sentiment?: ReportSentiment;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -33,12 +33,19 @@ export const POST: RequestHandler = async ({ params, request, locals }) => {
|
||||
voxers = [];
|
||||
}
|
||||
}
|
||||
const report = await locals.pb.collection(REPORTS_COLLECTION).create<EmployeeReportRecord>({
|
||||
const createPayload: Record<string, unknown> = {
|
||||
Employee: employeeId,
|
||||
report: body.report ?? '',
|
||||
voxers,
|
||||
delete: false,
|
||||
Sentiment: body.Sentiment === 'Positive' || body.Sentiment === 'Negative' ? body.Sentiment : 'Neutral',
|
||||
...(locals.user?.id && { entered_by: locals.user.id })
|
||||
});
|
||||
};
|
||||
if (typeof body.created === 'string' && body.created) {
|
||||
createPayload.created = body.created;
|
||||
}
|
||||
const report = await locals.pb
|
||||
.collection(REPORTS_COLLECTION)
|
||||
.create<EmployeeReportRecord>(createPayload);
|
||||
return json({ report });
|
||||
};
|
||||
|
||||
@@ -21,6 +21,10 @@ export const PATCH: RequestHandler = async ({ params, request, locals }) => {
|
||||
if (body.voxers !== undefined) {
|
||||
update.voxers = Array.isArray(body.voxers) ? body.voxers : JSON.parse(String(body.voxers));
|
||||
}
|
||||
if (typeof body.created === 'string' && body.created) update.created = body.created;
|
||||
if (body.Sentiment !== undefined && (body.Sentiment === 'Positive' || body.Sentiment === 'Negative' || body.Sentiment === 'Neutral')) {
|
||||
update.Sentiment = body.Sentiment;
|
||||
}
|
||||
const report = await locals.pb
|
||||
.collection(REPORTS_COLLECTION)
|
||||
.update<EmployeeReportRecord>(reportId, update);
|
||||
|
||||
Reference in New Issue
Block a user