diff --git a/apps/contacts/apps/backend/src/contact/contact.controller.ts b/apps/contacts/apps/backend/src/contact/contact.controller.ts index 04c38177b..61efe2cb4 100644 --- a/apps/contacts/apps/backend/src/contact/contact.controller.ts +++ b/apps/contacts/apps/backend/src/contact/contact.controller.ts @@ -20,8 +20,22 @@ import { IsDateString, IsUUID, MaxLength, + IsArray, + ValidateNested, } from 'class-validator'; -import { Transform } from 'class-transformer'; +import { Transform, Type } from 'class-transformer'; + +class CustomDateDto { + @IsUUID() + id: string; + + @IsString() + @MaxLength(100) + label: string; + + @IsDateString() + date: string; +} class CreateContactDto { @IsString() @@ -107,6 +121,12 @@ class CreateContactDto { @IsOptional() notes?: string; + @IsArray() + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => CustomDateDto) + customDates?: CustomDateDto[]; + // Social Media @IsString() @IsOptional() diff --git a/apps/contacts/apps/backend/src/db/schema/contacts.schema.ts b/apps/contacts/apps/backend/src/db/schema/contacts.schema.ts index 1bd84672f..60870ae26 100644 --- a/apps/contacts/apps/backend/src/db/schema/contacts.schema.ts +++ b/apps/contacts/apps/backend/src/db/schema/contacts.schema.ts @@ -31,6 +31,7 @@ export const contacts = pgTable('contacts', { birthday: date('birthday'), notes: text('notes'), photoUrl: varchar('photo_url', { length: 500 }), + customDates: jsonb('custom_dates').$type().default([]), // Social Media linkedin: varchar('linkedin', { length: 255 }), @@ -65,3 +66,9 @@ export const contacts = pgTable('contacts', { export type Contact = typeof contacts.$inferSelect; export type NewContact = typeof contacts.$inferInsert; + +export interface CustomDate { + id: string; + label: string; + date: string; +} diff --git a/apps/contacts/apps/web/src/lib/api/contacts.ts b/apps/contacts/apps/web/src/lib/api/contacts.ts index 9ff6ef786..f99090ec1 100644 --- a/apps/contacts/apps/web/src/lib/api/contacts.ts +++ b/apps/contacts/apps/web/src/lib/api/contacts.ts @@ -25,6 +25,11 @@ export interface Contact { birthday?: string | null; notes?: string | null; photoUrl?: string | null; + customDates?: Array<{ + id: string; + label: string; + date: string; + }> | null; // Social Media linkedin?: string | null; twitter?: string | null; diff --git a/apps/contacts/apps/web/src/lib/components/ContactDetailModal.svelte b/apps/contacts/apps/web/src/lib/components/ContactDetailModal.svelte index 386e2f0b2..13b4bf4f7 100644 --- a/apps/contacts/apps/web/src/lib/components/ContactDetailModal.svelte +++ b/apps/contacts/apps/web/src/lib/components/ContactDetailModal.svelte @@ -6,6 +6,7 @@ import ContactTasks from './ContactTasks.svelte'; import { ContactDetailSkeleton } from '$lib/components/skeletons'; import SocialMediaFields from './forms/SocialMediaFields.svelte'; + import DateFields from './forms/DateFields.svelte'; import SocialMediaLinks from './SocialMediaLinks.svelte'; interface Props { @@ -38,6 +39,10 @@ let country = $state(''); let notes = $state(''); + // Dates + let birthday = $state(''); + let customDates = $state>([]); + // Social Media let linkedin = $state(''); let twitter = $state(''); @@ -87,6 +92,9 @@ postalCode = contact.postalCode || ''; country = contact.country || ''; notes = contact.notes || ''; + // Dates + birthday = contact.birthday || ''; + customDates = contact.customDates ? [...contact.customDates] : []; // Social Media linkedin = contact.linkedin || ''; twitter = contact.twitter || ''; @@ -142,6 +150,9 @@ postalCode: postalCode || null, country: country || null, notes: notes || null, + // Dates + birthday: birthday || null, + customDates: customDates.filter((d) => d.label && d.date), // Social Media linkedin: linkedin || null, twitter: twitter || null, @@ -523,6 +534,9 @@ + + + {/if} + {#if contact.birthday || (contact.customDates && contact.customDates.length > 0)} +
+
+
+ + + +
+

Daten

+
+
+ {#if contact.birthday} +
+
+ Geburtstag + {new Date(contact.birthday).toLocaleDateString('de-DE', { + day: 'numeric', + month: 'long', + year: 'numeric', + })} +
+
+ {/if} + {#if contact.customDates} + {#each contact.customDates as customDate} +
+
+ {customDate.label} + {new Date(customDate.date).toLocaleDateString('de-DE', { + day: 'numeric', + month: 'long', + year: 'numeric', + })} +
+
+ {/each} + {/if} +
+
+ {/if} + {#if contact.notes}
diff --git a/apps/contacts/apps/web/src/lib/components/NewContactModal.svelte b/apps/contacts/apps/web/src/lib/components/NewContactModal.svelte index 4bad53fab..9dd640127 100644 --- a/apps/contacts/apps/web/src/lib/components/NewContactModal.svelte +++ b/apps/contacts/apps/web/src/lib/components/NewContactModal.svelte @@ -4,6 +4,7 @@ import { contactsStore } from '$lib/stores/contacts.svelte'; import { newContactModalStore } from '$lib/stores/new-contact-modal.svelte'; import SocialMediaFields from './forms/SocialMediaFields.svelte'; + import DateFields from './forms/DateFields.svelte'; interface Props { onClose: () => void; @@ -35,6 +36,10 @@ let country = $state(''); let notes = $state(''); + // Dates + let birthday = $state(''); + let customDates = $state>([]); + // Social Media let linkedin = $state(''); let twitter = $state(''); @@ -136,6 +141,9 @@ postalCode: postalCode || null, country: country || null, notes: notes || null, + // Dates + birthday: birthday || null, + customDates: customDates.filter((d) => d.label && d.date), // Social Media linkedin: linkedin || null, twitter: twitter || null, @@ -533,6 +541,9 @@ >
+ + + + interface CustomDate { + id: string; + label: string; + date: string; + } + + interface Props { + birthday: string; + customDates: CustomDate[]; + initiallyOpen?: boolean; + } + + let { + birthday = $bindable(''), + customDates = $bindable([]), + initiallyOpen = false, + }: Props = $props(); + + let isOpen = $state(initiallyOpen); + + // Auto-open if any field has data + $effect(() => { + if (birthday || customDates.length > 0) { + isOpen = true; + } + }); + + function addCustomDate() { + customDates = [ + ...customDates, + { + id: crypto.randomUUID(), + label: '', + date: '', + }, + ]; + } + + function removeCustomDate(id: string) { + customDates = customDates.filter((d) => d.id !== id); + } + + function updateCustomDate(id: string, field: 'label' | 'date', value: string) { + customDates = customDates.map((d) => (d.id === id ? { ...d, [field]: value } : d)); + } + + +
+ + {#if isOpen} +
+ +
+ + +
+ + + {#each customDates as customDate (customDate.id)} +
+
+ + updateCustomDate(customDate.id, 'label', e.currentTarget.value)} + class="input" + placeholder="z.B. Hochzeitstag, Kennenlerndatum" + /> +
+
+ + updateCustomDate(customDate.id, 'date', e.currentTarget.value)} + class="input" + /> +
+ +
+ {/each} + + + +
+ {/if} +
+ +