mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:01:08 +02:00
feat(calendar): add responsible person and attendees to events
- Add ResponsiblePerson interface to shared types - Create ResponsiblePersonSelector component with contact integration - Integrate responsible person selector into EventForm - Add people selectors to QuickEventOverlay with compact layout - Fix click-outside detection for elements removed from DOM - Support both contacts app integration and manual email entry 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
88c91e22b0
commit
8eb295d491
4 changed files with 576 additions and 5 deletions
|
|
@ -5,6 +5,7 @@
|
|||
import { eventTagsStore } from '$lib/stores/event-tags.svelte';
|
||||
import { TagSelector, type Tag } from '@manacore/shared-ui';
|
||||
import AttendeeSelector from './AttendeeSelector.svelte';
|
||||
import ResponsiblePersonSelector from './ResponsiblePersonSelector.svelte';
|
||||
import type {
|
||||
CalendarEvent,
|
||||
CreateEventInput,
|
||||
|
|
@ -12,6 +13,7 @@
|
|||
LocationDetails,
|
||||
EventTag,
|
||||
EventAttendee,
|
||||
ResponsiblePerson,
|
||||
} from '@calendar/shared';
|
||||
import { format, addMinutes, parseISO } from 'date-fns';
|
||||
|
||||
|
|
@ -51,6 +53,11 @@
|
|||
})) || []
|
||||
);
|
||||
|
||||
// Responsible person state
|
||||
let responsiblePerson = $state<ResponsiblePerson | null>(
|
||||
event?.metadata?.responsiblePerson || null
|
||||
);
|
||||
|
||||
// Attendees state
|
||||
let attendees = $state<EventAttendee[]>(event?.metadata?.attendees || []);
|
||||
|
||||
|
|
@ -172,6 +179,13 @@
|
|||
delete metadata.locationDetails;
|
||||
}
|
||||
|
||||
// Add responsible person
|
||||
if (responsiblePerson) {
|
||||
metadata.responsiblePerson = responsiblePerson;
|
||||
} else {
|
||||
delete metadata.responsiblePerson;
|
||||
}
|
||||
|
||||
// Add attendees
|
||||
if (attendees.length > 0) {
|
||||
metadata.attendees = attendees;
|
||||
|
|
@ -406,6 +420,15 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Verantwortliche Person -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-sm font-medium text-foreground">Verantwortliche Person</span>
|
||||
<ResponsiblePersonSelector
|
||||
{responsiblePerson}
|
||||
onResponsiblePersonChange={(person) => (responsiblePerson = person)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Teilnehmer -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-sm font-medium text-foreground">Teilnehmer</span>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,17 @@
|
|||
import { calendarsStore } from '$lib/stores/calendars.svelte';
|
||||
import { eventsStore } from '$lib/stores/events.svelte';
|
||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { contactsStore } from '$lib/stores/contacts.svelte';
|
||||
import { toast } from '$lib/stores/toast';
|
||||
import type { LocationDetails, CalendarEvent } from '@calendar/shared';
|
||||
import type {
|
||||
LocationDetails,
|
||||
CalendarEvent,
|
||||
ResponsiblePerson,
|
||||
EventAttendee,
|
||||
} from '@calendar/shared';
|
||||
import type { ContactSummary, ContactOrManual, ManualContactEntry } from '@manacore/shared-types';
|
||||
import { ContactSelector, ContactAvatar } from '@manacore/shared-ui';
|
||||
import { Users } from 'lucide-svelte';
|
||||
import { format, addMinutes, parseISO } from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
import { tick, onMount, onDestroy } from 'svelte';
|
||||
|
|
@ -109,6 +118,13 @@
|
|||
}
|
||||
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
// If target was removed from DOM by state change (e.g., button that toggles its own visibility),
|
||||
// ignore the click to prevent false "outside" detection
|
||||
if (!target.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const overlay = document.querySelector('.quick-event-overlay');
|
||||
const eventSelector = isEditMode
|
||||
? `[data-event-id="${event!.id}"]`
|
||||
|
|
@ -185,6 +201,19 @@
|
|||
let locationCountry = $state('');
|
||||
let submitting = $state(false);
|
||||
|
||||
// People state
|
||||
let responsiblePerson = $state<ResponsiblePerson | null>(null);
|
||||
let attendees = $state<EventAttendee[]>([]);
|
||||
let showPeopleSelector = $state(false);
|
||||
let contactsAvailable = $state<boolean | null>(null);
|
||||
|
||||
// Check contacts availability
|
||||
$effect(() => {
|
||||
contactsStore.checkAvailability().then((available) => {
|
||||
contactsAvailable = available;
|
||||
});
|
||||
});
|
||||
|
||||
// Editable date/time strings (for form inputs)
|
||||
let startDateStr = $state('');
|
||||
let startTimeStr = $state('');
|
||||
|
|
@ -212,6 +241,10 @@
|
|||
locationCountry = loc.country || '';
|
||||
}
|
||||
|
||||
// Initialize people
|
||||
responsiblePerson = event.metadata?.responsiblePerson || null;
|
||||
attendees = event.metadata?.attendees || [];
|
||||
|
||||
// Initialize time fields
|
||||
const eventStart =
|
||||
typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
|
|
@ -348,6 +381,112 @@
|
|||
// Overlay style
|
||||
let overlayStyle = $derived(`left: ${overlayPosition.left}px; top: ${overlayPosition.top}px;`);
|
||||
|
||||
// People helpers
|
||||
function handleContactSearch(query: string): Promise<ContactSummary[]> {
|
||||
return contactsStore.searchContacts(query);
|
||||
}
|
||||
|
||||
function handleResponsiblePersonChange(contacts: ContactOrManual[]) {
|
||||
if (contacts.length === 0) {
|
||||
responsiblePerson = null;
|
||||
return;
|
||||
}
|
||||
const contact = contacts[0];
|
||||
if ('isManual' in contact && contact.isManual) {
|
||||
const manual = contact as ManualContactEntry;
|
||||
responsiblePerson = { email: manual.email, name: manual.name };
|
||||
} else {
|
||||
const ref = contact as {
|
||||
contactId: string;
|
||||
displayName: string;
|
||||
email?: string;
|
||||
photoUrl?: string;
|
||||
company?: string;
|
||||
};
|
||||
responsiblePerson = {
|
||||
email: ref.email || '',
|
||||
name: ref.displayName,
|
||||
contactId: ref.contactId,
|
||||
photoUrl: ref.photoUrl,
|
||||
company: ref.company,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function handleAttendeesChange(contacts: ContactOrManual[]) {
|
||||
attendees = contacts.map((contact) => {
|
||||
if ('isManual' in contact && contact.isManual) {
|
||||
const manual = contact as ManualContactEntry;
|
||||
const existing = attendees.find((a) => a.email === manual.email);
|
||||
return {
|
||||
email: manual.email,
|
||||
name: manual.name,
|
||||
status: existing?.status || ('pending' as const),
|
||||
};
|
||||
}
|
||||
const ref = contact as {
|
||||
contactId: string;
|
||||
displayName: string;
|
||||
email?: string;
|
||||
photoUrl?: string;
|
||||
company?: string;
|
||||
};
|
||||
const existing = attendees.find(
|
||||
(a) => a.contactId === ref.contactId || a.email === ref.email
|
||||
);
|
||||
return {
|
||||
email: ref.email || '',
|
||||
name: ref.displayName,
|
||||
status: existing?.status || ('pending' as const),
|
||||
contactId: ref.contactId,
|
||||
photoUrl: ref.photoUrl,
|
||||
company: ref.company,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Convert to ContactOrManual for selectors
|
||||
const responsibleAsContact = $derived<ContactOrManual[]>(
|
||||
responsiblePerson
|
||||
? responsiblePerson.contactId
|
||||
? [
|
||||
{
|
||||
contactId: responsiblePerson.contactId,
|
||||
displayName: responsiblePerson.name || responsiblePerson.email,
|
||||
email: responsiblePerson.email,
|
||||
photoUrl: responsiblePerson.photoUrl,
|
||||
company: responsiblePerson.company,
|
||||
fetchedAt: new Date().toISOString(),
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
email: responsiblePerson.email,
|
||||
name: responsiblePerson.name,
|
||||
isManual: true as const,
|
||||
},
|
||||
]
|
||||
: []
|
||||
);
|
||||
|
||||
const attendeesAsContacts = $derived<ContactOrManual[]>(
|
||||
attendees.map((a) =>
|
||||
a.contactId
|
||||
? {
|
||||
contactId: a.contactId,
|
||||
displayName: a.name || a.email,
|
||||
email: a.email,
|
||||
photoUrl: a.photoUrl,
|
||||
company: a.company,
|
||||
fetchedAt: new Date().toISOString(),
|
||||
}
|
||||
: { email: a.email, name: a.name, isManual: true as const }
|
||||
)
|
||||
);
|
||||
|
||||
// Count of people assigned
|
||||
const peopleCount = $derived((responsiblePerson ? 1 : 0) + attendees.length);
|
||||
|
||||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
if (!title.trim() || !calendarId) return;
|
||||
|
|
@ -396,6 +535,20 @@
|
|||
delete metadata.locationDetails;
|
||||
}
|
||||
|
||||
// Add responsible person
|
||||
if (responsiblePerson) {
|
||||
metadata = { ...(metadata || {}), responsiblePerson };
|
||||
} else if (metadata) {
|
||||
delete metadata.responsiblePerson;
|
||||
}
|
||||
|
||||
// Add attendees
|
||||
if (attendees.length > 0) {
|
||||
metadata = { ...(metadata || {}), attendees };
|
||||
} else if (metadata) {
|
||||
delete metadata.attendees;
|
||||
}
|
||||
|
||||
// Clean up empty metadata
|
||||
if (metadata && Object.keys(metadata).length === 0) {
|
||||
metadata = undefined;
|
||||
|
|
@ -581,6 +734,95 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- People (compact) -->
|
||||
<div class="form-row">
|
||||
<div class="row-icon">
|
||||
<Users class="icon" size={18} />
|
||||
</div>
|
||||
<div class="row-content">
|
||||
<!-- Responsible person - always show directly -->
|
||||
<div class="people-subsection">
|
||||
<span class="field-label">Verantwortlich</span>
|
||||
{#if responsiblePerson}
|
||||
<div class="person-chip">
|
||||
<ContactAvatar
|
||||
photoUrl={responsiblePerson.photoUrl}
|
||||
name={responsiblePerson.name || responsiblePerson.email}
|
||||
size="xs"
|
||||
/>
|
||||
<span class="person-name">{responsiblePerson.name || responsiblePerson.email}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="remove-person"
|
||||
onclick={() => (responsiblePerson = null)}>×</button
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<ContactSelector
|
||||
selectedContacts={[]}
|
||||
onContactsChange={handleResponsiblePersonChange}
|
||||
onSearch={handleContactSearch}
|
||||
allowManualEntry={true}
|
||||
placeholder="Person auswählen..."
|
||||
addLabel="Auswählen"
|
||||
searchPlaceholder="Name oder E-Mail..."
|
||||
isAvailable={contactsAvailable ?? false}
|
||||
singleSelect={true}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Attendees - show when expanded or when there are attendees -->
|
||||
{#if showPeopleSelector || attendees.length > 0}
|
||||
<div class="people-subsection">
|
||||
<span class="field-label">Teilnehmer</span>
|
||||
{#if attendees.length > 0}
|
||||
<div class="people-chips">
|
||||
{#each attendees as attendee (attendee.email)}
|
||||
<div class="person-chip">
|
||||
<ContactAvatar
|
||||
photoUrl={attendee.photoUrl}
|
||||
name={attendee.name || attendee.email}
|
||||
size="xs"
|
||||
/>
|
||||
<span class="person-name">{attendee.name || attendee.email}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="remove-person"
|
||||
onclick={() =>
|
||||
(attendees = attendees.filter((a) => a.email !== attendee.email))}
|
||||
>×</button
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<ContactSelector
|
||||
selectedContacts={attendeesAsContacts}
|
||||
onContactsChange={handleAttendeesChange}
|
||||
onSearch={handleContactSearch}
|
||||
allowManualEntry={true}
|
||||
placeholder={attendees.length > 0
|
||||
? 'Weitere hinzufügen...'
|
||||
: 'Teilnehmer hinzufügen...'}
|
||||
addLabel="Hinzufügen"
|
||||
searchPlaceholder="Name oder E-Mail..."
|
||||
isAvailable={contactsAvailable ?? false}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Show expand button for attendees -->
|
||||
<button
|
||||
type="button"
|
||||
class="add-attendees-btn"
|
||||
onclick={() => (showPeopleSelector = true)}
|
||||
>
|
||||
+ Teilnehmer hinzufügen
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- All day toggle -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions a11y_no_noninteractive_element_to_interactive_role -->
|
||||
<div class="form-row clickable" onclick={handleAllDayToggle} role="button" tabindex="0">
|
||||
|
|
@ -828,12 +1070,12 @@
|
|||
position: fixed;
|
||||
width: 380px;
|
||||
max-height: 450px;
|
||||
background: hsl(var(--color-surface));
|
||||
background: hsl(var(--color-surface-elevated-2));
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow:
|
||||
0 20px 60px rgba(0, 0, 0, 0.2),
|
||||
0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
0 20px 60px hsl(var(--color-foreground) / 0.2),
|
||||
0 4px 16px hsl(var(--color-foreground) / 0.1);
|
||||
z-index: 99999 !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -1203,4 +1445,78 @@
|
|||
.address-field.city {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* People section */
|
||||
.add-attendees-btn {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.25rem 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
font-size: 0.8125rem;
|
||||
cursor: pointer;
|
||||
transition: color 150ms;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.add-attendees-btn:hover {
|
||||
color: hsl(var(--color-primary));
|
||||
}
|
||||
|
||||
.people-subsection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.people-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.375rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.person-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.25rem 0.5rem 0.25rem 0.25rem;
|
||||
background: hsl(var(--color-muted) / 0.5);
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 0.8125rem;
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
.person-name {
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.remove-person {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-full);
|
||||
transition: all 150ms;
|
||||
}
|
||||
|
||||
.remove-person:hover {
|
||||
background: hsl(var(--color-error) / 0.1);
|
||||
color: hsl(var(--color-error));
|
||||
}
|
||||
|
||||
.people-subsection + .people-subsection {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,216 @@
|
|||
<script lang="ts">
|
||||
import type { ResponsiblePerson } from '@calendar/shared';
|
||||
import type { ContactSummary, ContactOrManual, ManualContactEntry } from '@manacore/shared-types';
|
||||
import { ContactSelector, ContactAvatar } from '@manacore/shared-ui';
|
||||
import { X, ExternalLink } from 'lucide-svelte';
|
||||
import { contactsStore } from '$lib/stores/contacts.svelte';
|
||||
|
||||
interface Props {
|
||||
responsiblePerson: ResponsiblePerson | null;
|
||||
onResponsiblePersonChange: (person: ResponsiblePerson | null) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
let { responsiblePerson, onResponsiblePersonChange, disabled = false }: Props = $props();
|
||||
|
||||
let contactsAvailable = $state<boolean | null>(null);
|
||||
let showSelector = $state(false);
|
||||
|
||||
// Check contacts availability on mount
|
||||
$effect(() => {
|
||||
contactsStore.checkAvailability().then((available) => {
|
||||
contactsAvailable = available;
|
||||
});
|
||||
});
|
||||
|
||||
// Convert responsible person to ContactOrManual format for the selector
|
||||
const selectedContacts = $derived<ContactOrManual[]>(
|
||||
responsiblePerson
|
||||
? responsiblePerson.contactId
|
||||
? [
|
||||
{
|
||||
contactId: responsiblePerson.contactId,
|
||||
displayName: responsiblePerson.name || responsiblePerson.email,
|
||||
email: responsiblePerson.email,
|
||||
photoUrl: responsiblePerson.photoUrl,
|
||||
company: responsiblePerson.company,
|
||||
fetchedAt: new Date().toISOString(),
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
email: responsiblePerson.email,
|
||||
name: responsiblePerson.name,
|
||||
isManual: true as const,
|
||||
},
|
||||
]
|
||||
: []
|
||||
);
|
||||
|
||||
function handleContactsChange(contacts: ContactOrManual[]) {
|
||||
if (contacts.length === 0) {
|
||||
onResponsiblePersonChange(null);
|
||||
showSelector = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const contact = contacts[0];
|
||||
|
||||
if ('isManual' in contact && contact.isManual) {
|
||||
// Manual entry
|
||||
const manual = contact as ManualContactEntry;
|
||||
onResponsiblePersonChange({
|
||||
email: manual.email,
|
||||
name: manual.name,
|
||||
});
|
||||
} else {
|
||||
// Contact reference
|
||||
const contactRef = contact as {
|
||||
contactId: string;
|
||||
displayName: string;
|
||||
email?: string;
|
||||
photoUrl?: string;
|
||||
company?: string;
|
||||
};
|
||||
onResponsiblePersonChange({
|
||||
email: contactRef.email || '',
|
||||
name: contactRef.displayName,
|
||||
contactId: contactRef.contactId,
|
||||
photoUrl: contactRef.photoUrl,
|
||||
company: contactRef.company,
|
||||
});
|
||||
}
|
||||
|
||||
showSelector = false;
|
||||
}
|
||||
|
||||
function handleSearch(query: string): Promise<ContactSummary[]> {
|
||||
return contactsStore.searchContacts(query);
|
||||
}
|
||||
|
||||
function handleRemove() {
|
||||
onResponsiblePersonChange(null);
|
||||
}
|
||||
|
||||
function handleOpenContact() {
|
||||
if (responsiblePerson?.contactId) {
|
||||
// Open contacts app with this contact
|
||||
window.open(`/contacts/${responsiblePerson.contactId}`, '_blank');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="responsible-person-selector">
|
||||
{#if responsiblePerson}
|
||||
<!-- Selected Person Display -->
|
||||
<div class="flex items-center gap-3 p-2 rounded-lg bg-gray-50 dark:bg-gray-800/50">
|
||||
<ContactAvatar
|
||||
photoUrl={responsiblePerson.photoUrl}
|
||||
name={responsiblePerson.name || responsiblePerson.email}
|
||||
size="sm"
|
||||
/>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium text-foreground truncate">
|
||||
{responsiblePerson.name || responsiblePerson.email}
|
||||
</div>
|
||||
{#if responsiblePerson.name && responsiblePerson.email}
|
||||
<div class="text-xs text-muted-foreground truncate">
|
||||
{responsiblePerson.email}
|
||||
</div>
|
||||
{/if}
|
||||
{#if responsiblePerson.company}
|
||||
<div class="text-xs text-muted-foreground truncate">
|
||||
{responsiblePerson.company}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Open Contact Button (only if linked to contact) -->
|
||||
{#if responsiblePerson.contactId}
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleOpenContact}
|
||||
class="
|
||||
p-1.5 rounded-md
|
||||
text-gray-400 hover:text-blue-500
|
||||
hover:bg-blue-50 dark:hover:bg-blue-900/20
|
||||
transition-colors
|
||||
"
|
||||
title="Kontakt öffnen"
|
||||
>
|
||||
<ExternalLink size={16} />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<!-- Remove Button -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleRemove}
|
||||
class="
|
||||
p-1.5 rounded-md
|
||||
text-gray-400 hover:text-red-500
|
||||
hover:bg-red-50 dark:hover:bg-red-900/20
|
||||
transition-colors
|
||||
"
|
||||
title="Entfernen"
|
||||
{disabled}
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
{:else if showSelector}
|
||||
<!-- Contact Selector -->
|
||||
<ContactSelector
|
||||
selectedContacts={[]}
|
||||
onContactsChange={handleContactsChange}
|
||||
onSearch={handleSearch}
|
||||
allowManualEntry={true}
|
||||
placeholder="Person suchen oder E-Mail eingeben..."
|
||||
addLabel="Verantwortlich"
|
||||
searchPlaceholder="Name oder E-Mail..."
|
||||
isAvailable={contactsAvailable ?? false}
|
||||
{disabled}
|
||||
singleSelect={true}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (showSelector = false)}
|
||||
class="mt-2 text-sm text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
{:else}
|
||||
<!-- Add Button -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (showSelector = true)}
|
||||
class="
|
||||
w-full flex items-center justify-center gap-2
|
||||
px-4 py-2.5 rounded-lg
|
||||
border-2 border-dashed border-gray-200 dark:border-gray-700
|
||||
text-sm text-muted-foreground
|
||||
hover:border-gray-300 dark:hover:border-gray-600
|
||||
hover:text-foreground
|
||||
transition-colors
|
||||
"
|
||||
{disabled}
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
Verantwortliche Person hinzufügen
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.responsible-person-selector {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -18,6 +18,20 @@ export interface EventAttendee {
|
|||
company?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsible person for an event (single person accountable for the event)
|
||||
*/
|
||||
export interface ResponsiblePerson {
|
||||
email: string;
|
||||
name?: string;
|
||||
/** Contact reference for linked contacts */
|
||||
contactId?: string;
|
||||
/** Cached photo URL from contact */
|
||||
photoUrl?: string;
|
||||
/** Cached company from contact */
|
||||
company?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event tag with color
|
||||
*/
|
||||
|
|
@ -57,7 +71,9 @@ export interface EventMetadata {
|
|||
url?: string;
|
||||
/** Video conference URL (Zoom, Meet, etc.) */
|
||||
conferenceUrl?: string;
|
||||
/** Event attendees */
|
||||
/** Responsible person for this event */
|
||||
responsiblePerson?: ResponsiblePerson;
|
||||
/** Event attendees/participants */
|
||||
attendees?: EventAttendee[];
|
||||
/** Event organizer email */
|
||||
organizer?: string;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue