upd calender impotz
This commit is contained in:
parent
039582b0c8
commit
aada965ff4
|
|
@ -12,6 +12,8 @@ AZURE_TENANT_ID=<azure-ad-tenant-id>
|
|||
# App Registration → Certificates & secrets → New client secret
|
||||
AZURE_GRAPH_CLIENT_ID=<graph-app-registration-client-id>
|
||||
AZURE_GRAPH_CLIENT_SECRET=<graph-client-secret-value>
|
||||
# Fallback email when auth provides no email (e.g. API key login)
|
||||
CALENDAR_USER_EMAIL=<your-email@domain.com>
|
||||
|
||||
# ── CLIENT (Vite — copy relevant lines to client/.env) ───────────────────────
|
||||
# VITE_AZURE_CLIENT_ID=<frontend-app-registration-client-id>
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
1.1.81
|
||||
1.1.84
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
import { useUnsavedGuard } from '$lib/utils/unsavedGuard.svelte';
|
||||
import EventCard from './EventCard.svelte';
|
||||
import { eventsForDate } from '$lib/stores/agenda';
|
||||
import { createEvent, updateEventNotes } from '$lib/db/repositories';
|
||||
import { createEvent, updateEventNotes, upsertContext } from '$lib/db/repositories';
|
||||
import { fetchCalendarEvents, type CalendarEvent } from '$lib/utils/calendarApi';
|
||||
import { extractMentionName, quoteMention } from '$lib/actions/mentionCore';
|
||||
import type { PersonMeta } from '@ka-note/shared';
|
||||
|
|
@ -222,6 +222,11 @@
|
|||
let calendarError = $state<string | null>(null);
|
||||
let calendarEvents = $state<CalendarEvent[]>([]);
|
||||
|
||||
// Unknown-persons dialog (shown after calendar import)
|
||||
interface UnknownAttendee { name: string; email: string; selected: boolean; }
|
||||
let unknownDialogOpen = $state(false);
|
||||
let unknownAttendees = $state<UnknownAttendee[]>([]);
|
||||
|
||||
async function openCalendarPicker() {
|
||||
if (!showNewEventForm) openNewEventForm();
|
||||
calendarLoading = true;
|
||||
|
|
@ -241,23 +246,48 @@
|
|||
newEventTitle = ev.subject;
|
||||
newEventTime = ev.start;
|
||||
pendingBodyPreview = ev.bodyPreview;
|
||||
calendarPickerOpen = false;
|
||||
|
||||
const persons = await db.contexts
|
||||
.filter(c => !c.deletedAt && c.type === 'person')
|
||||
.toArray();
|
||||
const mentions = ev.attendees.map(att => {
|
||||
|
||||
const mentions: string[] = [];
|
||||
const unknowns: UnknownAttendee[] = [];
|
||||
|
||||
for (const att of ev.attendees) {
|
||||
const match = persons.find(
|
||||
p => (p.meta as PersonMeta | null)?.email?.toLowerCase() === att.email.toLowerCase()
|
||||
);
|
||||
if (match) {
|
||||
return quoteMention('@', extractMentionName(match));
|
||||
mentions.push(quoteMention('@', extractMentionName(match)));
|
||||
} else {
|
||||
const cleaned = att.name.replace(/\s*\(.*?\)\s*$/, '').trim();
|
||||
mentions.push(quoteMention('@', cleaned));
|
||||
if (att.email) unknowns.push({ name: cleaned, email: att.email, selected: true });
|
||||
}
|
||||
// Strip company suffix e.g. "Lars Leifer (KRAH)" → "Lars Leifer"
|
||||
const cleaned = att.name.replace(/\s*\(.*?\)\s*$/, '').trim();
|
||||
return quoteMention('@', cleaned);
|
||||
});
|
||||
}
|
||||
|
||||
newEventParticipants = mentions.join(' ');
|
||||
calendarPickerOpen = false;
|
||||
|
||||
if (unknowns.length > 0) {
|
||||
unknownAttendees = unknowns;
|
||||
unknownDialogOpen = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmUnknownPersons() {
|
||||
for (const u of unknownAttendees.filter(u => u.selected)) {
|
||||
const slug = u.name.toLowerCase().replace(/\s+/g, '-');
|
||||
await upsertContext({
|
||||
id: `u-${slug}`,
|
||||
name: `Person ${u.name}`,
|
||||
type: 'person',
|
||||
sortOrder: 99,
|
||||
meta: { fullName: u.name, email: u.email, phone: '', duSince: '' } satisfies PersonMeta,
|
||||
});
|
||||
}
|
||||
unknownDialogOpen = false;
|
||||
}
|
||||
|
||||
const dateEvents = $derived(eventsForDate(selectedDate));
|
||||
|
|
@ -468,6 +498,32 @@
|
|||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if unknownDialogOpen}
|
||||
<div class="mb-3 rounded border border-[#555] bg-[#1e1e1e] p-3 shadow-lg">
|
||||
<p class="mb-2 text-sm text-[#ccc]">Unbekannte Teilnehmer anlegen?</p>
|
||||
<div class="mb-3 flex flex-col gap-1.5">
|
||||
{#each unknownAttendees as u}
|
||||
<label class="flex cursor-pointer items-center gap-2 text-sm">
|
||||
<input type="checkbox" bind:checked={u.selected} class="accent-accent" />
|
||||
<span class="text-white">{u.name}</span>
|
||||
<span class="text-xs text-muted">{u.email}</span>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="rounded bg-accent px-3 py-1 text-sm font-bold text-white hover:brightness-110"
|
||||
onclick={confirmUnknownPersons}
|
||||
>Anlegen</button>
|
||||
<button
|
||||
class="rounded bg-[#444] px-3 py-1 text-sm text-white hover:bg-[#555]"
|
||||
onclick={() => (unknownDialogOpen = false)}
|
||||
>Überspringen</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each $dateEvents ?? [] as event (event.id)}
|
||||
<div class="mb-2">
|
||||
<EventCard {event} />
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE `api_keys` ADD `email` text NOT NULL DEFAULT '';
|
||||
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE `api_keys` ADD `email` text DEFAULT '' NOT NULL;
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -99,6 +99,13 @@
|
|||
"when": 1772100000000,
|
||||
"tag": "0013_fts_search",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 14,
|
||||
"version": "6",
|
||||
"when": 1772212044510,
|
||||
"tag": "0014_classy_power_pack",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -144,6 +144,7 @@ export const apiKeys = sqliteTable('api_keys', {
|
|||
id: text('id').primaryKey(),
|
||||
userId: text('user_id').notNull(),
|
||||
label: text('label').notNull(),
|
||||
email: text('email').notNull().default(''),
|
||||
keyHash: text('key_hash').notNull().unique(),
|
||||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
||||
lastUsedAt: integer('last_used_at', { mode: 'timestamp' }),
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export const authMiddleware = createMiddleware<AuthEnv>(async (c, next) => {
|
|||
if (!key) {
|
||||
return c.json({ error: 'Invalid or revoked API key' }, 401);
|
||||
}
|
||||
c.set('auth', { userId: key.userId, name: key.label, email: '' });
|
||||
c.set('auth', { userId: key.userId, name: key.label, email: key.email ?? '' });
|
||||
// fire-and-forget lastUsedAt update
|
||||
db.update(apiKeys).set({ lastUsedAt: new Date() }).where(eq(apiKeys.id, key.id)).catch(() => {});
|
||||
await next();
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ router.get('/', handle('api-keys/list', async (c) => {
|
|||
}));
|
||||
|
||||
router.post('/', handle('api-keys/create', async (c) => {
|
||||
const { userId } = c.get('auth');
|
||||
const { userId, email } = c.get('auth');
|
||||
const body = await c.req.json<{ label: string }>();
|
||||
if (!body.label?.trim()) {
|
||||
return c.json({ error: 'label required' }, 400);
|
||||
|
|
@ -37,6 +37,7 @@ router.post('/', handle('api-keys/create', async (c) => {
|
|||
await db.insert(apiKeys).values({
|
||||
id,
|
||||
userId,
|
||||
email,
|
||||
label: body.label.trim(),
|
||||
keyHash,
|
||||
createdAt: now,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ calendar.get('/events', async (c) => {
|
|||
const auth = c.get('auth');
|
||||
|
||||
if (!auth.email) {
|
||||
return c.json({ error: 'graph_unavailable', detail: 'No email in auth context (API key or dev-bypass)' }, 502);
|
||||
return c.json({ error: 'graph_unavailable', detail: 'No email in auth context' }, 502);
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
|
|||
Loading…
Reference in New Issue