2026-01-26 11:25:38 -05:00

302 lines
8.6 KiB
Svelte

<script lang="ts">
import { goto } from '$app/navigation';
import { calendarService, type CreateEventPayload } from '$lib/services/calendar';
import { Alert, Button, Spinner, Input, Label, Textarea } from 'flowbite-svelte';
import { GetTeamProfilesStore } from '$houdini';
// Form state
let summary = $state('');
let description = $state('');
let location = $state('');
let startDate = $state<string>(''); // yyyy-mm-dd
let startTime = $state<string>(''); // HH:mm
let endDate = $state<string>('');
let endTime = $state<string>('');
let timeZone = $state<string>(Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC');
let loading = $state(false);
let error = $state<string | null>(null);
// Attendees (team + manual)
type Attendee = {
id: string;
email: string;
displayName?: string;
isFromTeam: boolean;
profileId?: string;
};
const newId = () =>
crypto?.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2);
let attendees: Attendee[] = $state([]);
// Team members lookup
let profiles = $state(new GetTeamProfilesStore());
let selectedTeamMembers = $state<string[]>([]);
$effect(() => {
profiles.fetch({ policy: 'NetworkOnly' }).catch(() => {});
});
function handleTeamMemberChange(profileId: string, isChecked: boolean) {
if (isChecked) {
if (!selectedTeamMembers.includes(profileId)) {
selectedTeamMembers = [...selectedTeamMembers, profileId];
}
const profile = ($profiles.data?.teamProfiles ?? []).find((p) => String(p.id) === profileId);
if (profile) {
const displayName =
profile.fullName ||
`${profile.firstName ?? ''} ${profile.lastName ?? ''}`.trim() ||
'Unknown';
const newAttendee: Attendee = {
id: newId(),
email: profile.email || '',
displayName,
isFromTeam: true,
profileId
};
attendees = [...attendees, newAttendee];
}
} else {
selectedTeamMembers = selectedTeamMembers.filter((id) => id !== profileId);
attendees = attendees.filter((a) => a.profileId !== profileId);
}
}
function addManualAttendee() {
attendees = [
...attendees,
{
id: newId(),
email: '',
displayName: '',
isFromTeam: false
}
];
}
function removeAttendee(index: number) {
const attendee = attendees[index];
if (attendee.isFromTeam && attendee.profileId) {
selectedTeamMembers = selectedTeamMembers.filter((id) => id !== attendee.profileId);
}
attendees = attendees.filter((_, i) => i !== index);
}
function toISO(date: string, time: string): string | null {
if (!date || !time) return null;
// Compose local datetime and convert to ISO string
const local = new Date(`${date}T${time}`);
if (isNaN(local.getTime())) return null;
return local.toISOString();
}
async function onSubmit(e: SubmitEvent) {
e.preventDefault();
error = null;
loading = true;
try {
const startISO = toISO(startDate, startTime);
const endISO = toISO(endDate, endTime);
if (!startISO || !endISO) {
error = 'Please provide valid start and end date & time.';
loading = false;
return;
}
if (new Date(endISO).getTime() <= new Date(startISO).getTime()) {
error = 'End time must be after start time.';
loading = false;
return;
}
const payload: CreateEventPayload = {
summary: summary.trim() || 'Untitled event',
description: description.trim() || undefined,
location: location.trim() || undefined,
start: { dateTime: startISO, timeZone },
end: { dateTime: endISO, timeZone },
attendees: attendees
.filter((a) => a.email && a.email.trim())
.map((a) => ({
email: a.email.trim(),
displayName: a.displayName?.trim() || undefined
}))
};
const created = await calendarService.createEvent(payload);
await goto(`/calendar/${created.id}`);
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to create event';
} finally {
loading = false;
}
}
</script>
{#if error}
<Alert color="red" class="mb-4">{error}</Alert>
{/if}
<form onsubmit={onSubmit} class="space-y-6">
<div class="grid gap-6 md:grid-cols-2">
<div class="md:col-span-2">
<Label for="title" class="mb-2">Title</Label>
<Input
id="title"
type="text"
placeholder="Event title"
bind:value={summary}
required
disabled={loading}
/>
</div>
<div class="md:col-span-2">
<Label for="description" class="mb-2">Description</Label>
<Textarea
id="description"
bind:value={description}
placeholder="Details (optional)"
rows={4}
disabled={loading}
/>
</div>
<div class="md:col-span-2">
<Label for="location" class="mb-2">Location</Label>
<Input
id="location"
type="text"
placeholder="Location (optional)"
bind:value={location}
disabled={loading}
/>
</div>
<div>
<Label for="start-date" class="mb-2">Start Date</Label>
<Input id="start-date" type="date" bind:value={startDate} required disabled={loading} />
</div>
<div>
<Label for="start-time" class="mb-2">Start Time</Label>
<Input id="start-time" type="time" bind:value={startTime} required disabled={loading} />
</div>
<div>
<Label for="end-date" class="mb-2">End Date</Label>
<Input id="end-date" type="date" bind:value={endDate} required disabled={loading} />
</div>
<div>
<Label for="end-time" class="mb-2">End Time</Label>
<Input id="end-time" type="time" bind:value={endTime} required disabled={loading} />
</div>
<div class="md:col-span-2">
<Label for="time-zone" class="mb-2">Time Zone</Label>
<Input
id="time-zone"
type="text"
placeholder="e.g. UTC or America/New_York"
bind:value={timeZone}
disabled={loading}
/>
</div>
<!-- Team Members -->
<div class="md:col-span-2">
<Label class="mb-2">Select Team Members</Label>
{#if $profiles.fetching}
<div class="text-sm text-gray-500">Loading team profiles...</div>
{:else if ($profiles.data?.teamProfiles?.length ?? 0) === 0}
<div class="text-sm text-gray-500">No team profiles found.</div>
{:else}
<div class="grid gap-2 md:grid-cols-2">
{#each ($profiles.data?.teamProfiles ?? []).filter((p) => String(p.status) === 'ACTIVE') as profile (profile.id)}
<label class="flex items-center gap-2">
<input
type="checkbox"
checked={selectedTeamMembers.includes(String(profile.id))}
onchange={(e) =>
handleTeamMemberChange(String(profile.id), e.currentTarget.checked)}
disabled={loading}
/>
<span
>{profile.fullName ||
`${profile.firstName ?? ''} ${profile.lastName ?? ''}`.trim()}</span
>
</label>
{/each}
</div>
{/if}
</div>
<!-- Attendees -->
<div class="space-y-4 md:col-span-2">
<div class="font-medium">Attendees</div>
{#if attendees.length === 0}
<div class="text-sm text-gray-500 italic dark:text-gray-400">No attendees added</div>
{:else}
{#each attendees as attendee, i (attendee.id)}
<div
class="grid items-end gap-2 rounded border border-gray-200 bg-gray-50 p-3 md:grid-cols-3 dark:border-gray-700 dark:bg-gray-800 {attendee.isFromTeam
? 'border-blue-200 bg-blue-50 dark:border-blue-700 dark:bg-blue-900/30'
: ''}"
>
<div>
<Label>Email</Label>
{#if attendee.isFromTeam}
<div
class="rounded bg-gray-100 p-2 text-sm text-gray-900 dark:bg-gray-700 dark:text-gray-100"
>
{attendee.email || 'Loading...'}
</div>
{:else}
<Input bind:value={attendees[i].email} placeholder="name@example.com" />
{/if}
</div>
<div>
<Label>Display Name {attendee.isFromTeam ? '' : '(optional)'}</Label>
{#if attendee.isFromTeam}
<div
class="rounded bg-gray-100 p-2 text-sm text-gray-900 dark:bg-gray-700 dark:text-gray-100"
>
{attendee.displayName}
</div>
{:else}
<Input bind:value={attendees[i].displayName} placeholder="Jane Doe" />
{/if}
</div>
<div class="flex items-center gap-2">
{#if attendee.isFromTeam}
<span class="text-xs font-medium text-blue-700 dark:text-blue-400">Team Member</span
>
{:else}
<Button type="button" color="light" onclick={() => removeAttendee(i)}>
Remove
</Button>
{/if}
</div>
</div>
{/each}
{/if}
<Button type="button" color="light" onclick={addManualAttendee}>Add Manual Attendee</Button>
</div>
</div>
<div class="flex items-center gap-3">
<Button color="primary" type="submit" disabled={loading}>
{#if loading}
<Spinner size="4" class="mr-2" />
Creating...
{:else}
Create Event
{/if}
</Button>
<Button color="light" href="/calendar">Cancel</Button>
</div>
</form>