mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:01:09 +02:00
🔧 chore: misc updates across contacts, mail, and shared-ui
- Contacts app improvements and fixes - Mail IMAP sync provider updates - Shared UI package updates - Updated pnpm-lock.yaml 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
043acf33bd
commit
cda300440d
11 changed files with 4773 additions and 2185 deletions
539
apps/contacts/apps/backend/src/db/seed.ts
Normal file
539
apps/contacts/apps/backend/src/db/seed.ts
Normal file
|
|
@ -0,0 +1,539 @@
|
|||
import 'dotenv/config';
|
||||
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||
import postgres from 'postgres';
|
||||
import { contacts } from './schema';
|
||||
|
||||
const DATABASE_URL = process.env.DATABASE_URL || 'postgresql://manacore:devpassword@localhost:5432/contacts';
|
||||
|
||||
// User ID - can be set via environment variable or defaults to test user
|
||||
const USER_ID = process.env.SEED_USER_ID || 'seed-user-001';
|
||||
|
||||
interface SeedContact {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
mobile?: string;
|
||||
company?: string;
|
||||
jobTitle?: string;
|
||||
street?: string;
|
||||
city?: string;
|
||||
postalCode?: string;
|
||||
country?: string;
|
||||
notes?: string;
|
||||
isFavorite?: boolean;
|
||||
}
|
||||
|
||||
const seedContacts: SeedContact[] = [
|
||||
// Tech Industry
|
||||
{
|
||||
firstName: 'Anna',
|
||||
lastName: 'Müller',
|
||||
email: 'anna.mueller@techstartup.de',
|
||||
phone: '+49 30 12345678',
|
||||
mobile: '+49 170 1234567',
|
||||
company: 'TechStartup GmbH',
|
||||
jobTitle: 'CEO',
|
||||
street: 'Alexanderplatz 1',
|
||||
city: 'Berlin',
|
||||
postalCode: '10178',
|
||||
country: 'Deutschland',
|
||||
notes: 'Kennengelernt auf der TechConf 2024. Interessiert an AI-Lösungen.',
|
||||
isFavorite: true,
|
||||
},
|
||||
{
|
||||
firstName: 'Max',
|
||||
lastName: 'Schmidt',
|
||||
email: 'max.schmidt@devhouse.io',
|
||||
phone: '+49 89 98765432',
|
||||
mobile: '+49 171 9876543',
|
||||
company: 'DevHouse Solutions',
|
||||
jobTitle: 'Lead Developer',
|
||||
street: 'Marienplatz 8',
|
||||
city: 'München',
|
||||
postalCode: '80331',
|
||||
country: 'Deutschland',
|
||||
notes: 'Full-Stack Entwickler, spezialisiert auf React und Node.js',
|
||||
isFavorite: true,
|
||||
},
|
||||
{
|
||||
firstName: 'Sophie',
|
||||
lastName: 'Weber',
|
||||
email: 'sophie.weber@cloudify.com',
|
||||
mobile: '+49 172 5551234',
|
||||
company: 'Cloudify AG',
|
||||
jobTitle: 'Cloud Architect',
|
||||
city: 'Hamburg',
|
||||
country: 'Deutschland',
|
||||
},
|
||||
{
|
||||
firstName: 'Felix',
|
||||
lastName: 'Becker',
|
||||
email: 'felix.becker@datainsights.de',
|
||||
phone: '+49 69 44556677',
|
||||
company: 'Data Insights',
|
||||
jobTitle: 'Data Scientist',
|
||||
street: 'Zeil 15',
|
||||
city: 'Frankfurt',
|
||||
postalCode: '60313',
|
||||
country: 'Deutschland',
|
||||
notes: 'Machine Learning Experte',
|
||||
},
|
||||
{
|
||||
firstName: 'Laura',
|
||||
lastName: 'Klein',
|
||||
email: 'laura.klein@uxdesign.studio',
|
||||
mobile: '+49 173 8889990',
|
||||
company: 'UX Design Studio',
|
||||
jobTitle: 'UX Designer',
|
||||
city: 'Köln',
|
||||
country: 'Deutschland',
|
||||
isFavorite: true,
|
||||
},
|
||||
|
||||
// Business & Finance
|
||||
{
|
||||
firstName: 'Thomas',
|
||||
lastName: 'Hoffmann',
|
||||
email: 'thomas.hoffmann@financeplus.de',
|
||||
phone: '+49 211 7778899',
|
||||
mobile: '+49 174 1112233',
|
||||
company: 'FinancePlus Consulting',
|
||||
jobTitle: 'Managing Director',
|
||||
street: 'Königsallee 27',
|
||||
city: 'Düsseldorf',
|
||||
postalCode: '40212',
|
||||
country: 'Deutschland',
|
||||
notes: 'Spezialist für Unternehmensfinanzierung',
|
||||
},
|
||||
{
|
||||
firstName: 'Julia',
|
||||
lastName: 'Fischer',
|
||||
email: 'julia.fischer@legalexperts.de',
|
||||
phone: '+49 30 5556789',
|
||||
company: 'Legal Experts',
|
||||
jobTitle: 'Rechtsanwältin',
|
||||
city: 'Berlin',
|
||||
country: 'Deutschland',
|
||||
notes: 'Spezialisiert auf IT-Recht und Datenschutz',
|
||||
},
|
||||
{
|
||||
firstName: 'Michael',
|
||||
lastName: 'Wagner',
|
||||
email: 'm.wagner@investcorp.de',
|
||||
phone: '+49 89 3334455',
|
||||
mobile: '+49 175 6667788',
|
||||
company: 'InvestCorp',
|
||||
jobTitle: 'Investment Manager',
|
||||
city: 'München',
|
||||
country: 'Deutschland',
|
||||
isFavorite: true,
|
||||
},
|
||||
{
|
||||
firstName: 'Christina',
|
||||
lastName: 'Braun',
|
||||
email: 'christina.braun@taxadvisors.de',
|
||||
phone: '+49 711 9998877',
|
||||
company: 'Tax Advisors Stuttgart',
|
||||
jobTitle: 'Steuerberaterin',
|
||||
street: 'Königstraße 42',
|
||||
city: 'Stuttgart',
|
||||
postalCode: '70173',
|
||||
country: 'Deutschland',
|
||||
},
|
||||
|
||||
// Creative & Media
|
||||
{
|
||||
firstName: 'David',
|
||||
lastName: 'Zimmermann',
|
||||
email: 'david@creativemind.agency',
|
||||
mobile: '+49 176 2223344',
|
||||
company: 'CreativeMind Agency',
|
||||
jobTitle: 'Creative Director',
|
||||
city: 'Berlin',
|
||||
country: 'Deutschland',
|
||||
notes: 'Award-winning Kreativagentur für Branding',
|
||||
},
|
||||
{
|
||||
firstName: 'Nina',
|
||||
lastName: 'Krause',
|
||||
email: 'nina.krause@mediahouse.de',
|
||||
phone: '+49 40 1234000',
|
||||
company: 'MediaHouse Hamburg',
|
||||
jobTitle: 'Content Manager',
|
||||
city: 'Hamburg',
|
||||
country: 'Deutschland',
|
||||
},
|
||||
{
|
||||
firstName: 'Patrick',
|
||||
lastName: 'Lehmann',
|
||||
email: 'patrick@photostudio.com',
|
||||
mobile: '+49 177 4445566',
|
||||
company: 'Lehmann Fotografie',
|
||||
jobTitle: 'Fotograf',
|
||||
city: 'Köln',
|
||||
country: 'Deutschland',
|
||||
notes: 'Spezialisiert auf Produktfotografie und Events',
|
||||
isFavorite: true,
|
||||
},
|
||||
|
||||
// Healthcare
|
||||
{
|
||||
firstName: 'Dr. Sarah',
|
||||
lastName: 'König',
|
||||
email: 'dr.koenig@praxis-koenig.de',
|
||||
phone: '+49 30 8889900',
|
||||
company: 'Praxis Dr. König',
|
||||
jobTitle: 'Allgemeinmedizinerin',
|
||||
street: 'Friedrichstraße 120',
|
||||
city: 'Berlin',
|
||||
postalCode: '10117',
|
||||
country: 'Deutschland',
|
||||
},
|
||||
{
|
||||
firstName: 'Dr. Martin',
|
||||
lastName: 'Schäfer',
|
||||
email: 'martin.schaefer@klinikum.de',
|
||||
phone: '+49 89 4445500',
|
||||
company: 'Klinikum München',
|
||||
jobTitle: 'Oberarzt Kardiologie',
|
||||
city: 'München',
|
||||
country: 'Deutschland',
|
||||
},
|
||||
|
||||
// Education
|
||||
{
|
||||
firstName: 'Prof. Elisabeth',
|
||||
lastName: 'Hartmann',
|
||||
email: 'hartmann@uni-berlin.de',
|
||||
phone: '+49 30 9876000',
|
||||
company: 'Freie Universität Berlin',
|
||||
jobTitle: 'Professorin für Informatik',
|
||||
city: 'Berlin',
|
||||
country: 'Deutschland',
|
||||
notes: 'Forschungsschwerpunkt: Künstliche Intelligenz',
|
||||
},
|
||||
{
|
||||
firstName: 'Andreas',
|
||||
lastName: 'Richter',
|
||||
email: 'a.richter@gymnasium.de',
|
||||
phone: '+49 711 5554433',
|
||||
company: 'Schiller-Gymnasium',
|
||||
jobTitle: 'Schulleiter',
|
||||
city: 'Stuttgart',
|
||||
country: 'Deutschland',
|
||||
},
|
||||
|
||||
// Retail & Gastronomy
|
||||
{
|
||||
firstName: 'Stefanie',
|
||||
lastName: 'Wolf',
|
||||
email: 'stefanie@wolfs-bistro.de',
|
||||
phone: '+49 69 1112200',
|
||||
mobile: '+49 178 3334455',
|
||||
company: "Wolf's Bistro",
|
||||
jobTitle: 'Inhaberin',
|
||||
street: 'Goethestraße 15',
|
||||
city: 'Frankfurt',
|
||||
postalCode: '60313',
|
||||
country: 'Deutschland',
|
||||
notes: 'Tolle Location für Team-Events',
|
||||
},
|
||||
{
|
||||
firstName: 'Oliver',
|
||||
lastName: 'Neumann',
|
||||
email: 'oliver@weinhandel-neumann.de',
|
||||
phone: '+49 6131 778899',
|
||||
company: 'Weinhandel Neumann',
|
||||
jobTitle: 'Sommelier',
|
||||
city: 'Mainz',
|
||||
country: 'Deutschland',
|
||||
},
|
||||
|
||||
// International Contacts
|
||||
{
|
||||
firstName: 'James',
|
||||
lastName: 'Wilson',
|
||||
email: 'james.wilson@techcorp.com',
|
||||
phone: '+1 415 555 0123',
|
||||
company: 'TechCorp Inc.',
|
||||
jobTitle: 'VP Engineering',
|
||||
city: 'San Francisco',
|
||||
country: 'USA',
|
||||
notes: 'Met at Web Summit. Interested in European expansion.',
|
||||
isFavorite: true,
|
||||
},
|
||||
{
|
||||
firstName: 'Marie',
|
||||
lastName: 'Dubois',
|
||||
email: 'marie.dubois@startup.fr',
|
||||
phone: '+33 1 42 68 53 00',
|
||||
mobile: '+33 6 12 34 56 78',
|
||||
company: 'Paris Startup Hub',
|
||||
jobTitle: 'Directrice',
|
||||
street: '25 Rue de Rivoli',
|
||||
city: 'Paris',
|
||||
postalCode: '75001',
|
||||
country: 'Frankreich',
|
||||
},
|
||||
{
|
||||
firstName: 'Marco',
|
||||
lastName: 'Rossi',
|
||||
email: 'marco.rossi@designitalia.it',
|
||||
mobile: '+39 333 123 4567',
|
||||
company: 'Design Italia',
|
||||
jobTitle: 'Art Director',
|
||||
city: 'Mailand',
|
||||
country: 'Italien',
|
||||
},
|
||||
{
|
||||
firstName: 'Yuki',
|
||||
lastName: 'Tanaka',
|
||||
email: 'y.tanaka@techcompany.jp',
|
||||
phone: '+81 3 1234 5678',
|
||||
company: 'Tech Company Tokyo',
|
||||
jobTitle: 'Product Manager',
|
||||
city: 'Tokyo',
|
||||
country: 'Japan',
|
||||
},
|
||||
|
||||
// Freelancers & Consultants
|
||||
{
|
||||
firstName: 'Sebastian',
|
||||
lastName: 'Maier',
|
||||
email: 'seb.maier@freelance.de',
|
||||
mobile: '+49 179 1234567',
|
||||
jobTitle: 'Freelance Developer',
|
||||
city: 'Berlin',
|
||||
country: 'Deutschland',
|
||||
notes: 'Vue.js und Python Spezialist. Verfügbar ab Q2.',
|
||||
},
|
||||
{
|
||||
firstName: 'Katharina',
|
||||
lastName: 'Peters',
|
||||
email: 'k.peters@marketing-beratung.de',
|
||||
mobile: '+49 170 9876543',
|
||||
company: 'Peters Marketing Beratung',
|
||||
jobTitle: 'Marketing Consultant',
|
||||
city: 'Hamburg',
|
||||
country: 'Deutschland',
|
||||
isFavorite: true,
|
||||
},
|
||||
{
|
||||
firstName: 'Daniel',
|
||||
lastName: 'Schneider',
|
||||
email: 'daniel@agile-coach.de',
|
||||
mobile: '+49 171 5556677',
|
||||
jobTitle: 'Agile Coach',
|
||||
city: 'München',
|
||||
country: 'Deutschland',
|
||||
notes: 'Certified Scrum Master, 10+ Jahre Erfahrung',
|
||||
},
|
||||
|
||||
// Personal Contacts
|
||||
{
|
||||
firstName: 'Lisa',
|
||||
lastName: 'Berger',
|
||||
email: 'lisa.berger@gmail.com',
|
||||
mobile: '+49 172 1112233',
|
||||
city: 'Köln',
|
||||
country: 'Deutschland',
|
||||
notes: 'Freundin aus Studienzeit',
|
||||
isFavorite: true,
|
||||
},
|
||||
{
|
||||
firstName: 'Markus',
|
||||
lastName: 'Schulze',
|
||||
email: 'markus.schulze@web.de',
|
||||
mobile: '+49 173 4445566',
|
||||
city: 'Düsseldorf',
|
||||
country: 'Deutschland',
|
||||
notes: 'Tennispartner',
|
||||
},
|
||||
{
|
||||
firstName: 'Eva',
|
||||
lastName: 'Friedrich',
|
||||
email: 'eva.friedrich@outlook.com',
|
||||
phone: '+49 30 1234321',
|
||||
street: 'Prenzlauer Allee 42',
|
||||
city: 'Berlin',
|
||||
postalCode: '10405',
|
||||
country: 'Deutschland',
|
||||
notes: 'Nachbarin',
|
||||
},
|
||||
|
||||
// More Tech
|
||||
{
|
||||
firstName: 'Tobias',
|
||||
lastName: 'Keller',
|
||||
email: 'tobias@backend-solutions.de',
|
||||
mobile: '+49 174 7778899',
|
||||
company: 'Backend Solutions',
|
||||
jobTitle: 'Backend Engineer',
|
||||
city: 'Frankfurt',
|
||||
country: 'Deutschland',
|
||||
},
|
||||
{
|
||||
firstName: 'Hannah',
|
||||
lastName: 'Vogel',
|
||||
email: 'hannah.vogel@securityfirst.de',
|
||||
phone: '+49 30 9998877',
|
||||
company: 'Security First GmbH',
|
||||
jobTitle: 'Security Analyst',
|
||||
city: 'Berlin',
|
||||
country: 'Deutschland',
|
||||
notes: 'Expertin für Penetration Testing',
|
||||
},
|
||||
{
|
||||
firstName: 'Jan',
|
||||
lastName: 'Schwarz',
|
||||
email: 'jan@mobile-apps.de',
|
||||
mobile: '+49 175 2223344',
|
||||
company: 'Mobile Apps Studio',
|
||||
jobTitle: 'iOS Developer',
|
||||
city: 'Hamburg',
|
||||
country: 'Deutschland',
|
||||
},
|
||||
{
|
||||
firstName: 'Melanie',
|
||||
lastName: 'Krüger',
|
||||
email: 'melanie.krueger@ailab.de',
|
||||
phone: '+49 89 6665544',
|
||||
company: 'AI Lab München',
|
||||
jobTitle: 'ML Engineer',
|
||||
city: 'München',
|
||||
country: 'Deutschland',
|
||||
isFavorite: true,
|
||||
},
|
||||
{
|
||||
firstName: 'Robert',
|
||||
lastName: 'Lang',
|
||||
email: 'robert.lang@devops-pro.de',
|
||||
mobile: '+49 176 8889900',
|
||||
company: 'DevOps Pro',
|
||||
jobTitle: 'DevOps Engineer',
|
||||
city: 'Stuttgart',
|
||||
country: 'Deutschland',
|
||||
notes: 'AWS und Kubernetes Spezialist',
|
||||
},
|
||||
|
||||
// More Business
|
||||
{
|
||||
firstName: 'Claudia',
|
||||
lastName: 'Bauer',
|
||||
email: 'claudia@hr-consulting.de',
|
||||
phone: '+49 211 3332211',
|
||||
company: 'HR Consulting Plus',
|
||||
jobTitle: 'HR Director',
|
||||
city: 'Düsseldorf',
|
||||
country: 'Deutschland',
|
||||
},
|
||||
{
|
||||
firstName: 'Frank',
|
||||
lastName: 'Huber',
|
||||
email: 'f.huber@salesforce-partner.de',
|
||||
mobile: '+49 177 1234567',
|
||||
company: 'CRM Solutions',
|
||||
jobTitle: 'Sales Director',
|
||||
city: 'Frankfurt',
|
||||
country: 'Deutschland',
|
||||
},
|
||||
{
|
||||
firstName: 'Sabine',
|
||||
lastName: 'Walter',
|
||||
email: 'sabine@walter-events.de',
|
||||
phone: '+49 30 4443322',
|
||||
mobile: '+49 178 5556677',
|
||||
company: 'Walter Events',
|
||||
jobTitle: 'Event Manager',
|
||||
city: 'Berlin',
|
||||
country: 'Deutschland',
|
||||
notes: 'Organisiert unsere Firmenevents',
|
||||
isFavorite: true,
|
||||
},
|
||||
|
||||
// Minimal contacts (just name and one contact method)
|
||||
{
|
||||
firstName: 'Peter',
|
||||
lastName: 'Engel',
|
||||
email: 'peter.engel@email.de',
|
||||
},
|
||||
{
|
||||
firstName: 'Maria',
|
||||
lastName: 'Sommer',
|
||||
mobile: '+49 170 1111222',
|
||||
},
|
||||
{
|
||||
firstName: 'Alexander',
|
||||
lastName: 'Winter',
|
||||
phone: '+49 40 9998877',
|
||||
company: 'Winter & Partner',
|
||||
},
|
||||
];
|
||||
|
||||
async function seed() {
|
||||
console.log('🌱 Starting seed...');
|
||||
console.log(`📊 Preparing to insert ${seedContacts.length} contacts`);
|
||||
|
||||
const connection = postgres(DATABASE_URL);
|
||||
const db = drizzle(connection);
|
||||
|
||||
try {
|
||||
// Clear existing contacts for the test user
|
||||
console.log('🧹 Clearing existing seed contacts...');
|
||||
const { sql } = await import('drizzle-orm');
|
||||
await db.delete(contacts).where(sql`${contacts.userId} = ${USER_ID}`);
|
||||
|
||||
// Insert seed contacts
|
||||
console.log('📝 Inserting contacts...');
|
||||
const contactsToInsert = seedContacts.map((contact) => ({
|
||||
userId: USER_ID,
|
||||
createdBy: USER_ID,
|
||||
firstName: contact.firstName,
|
||||
lastName: contact.lastName,
|
||||
displayName: `${contact.firstName} ${contact.lastName}`,
|
||||
email: contact.email || null,
|
||||
phone: contact.phone || null,
|
||||
mobile: contact.mobile || null,
|
||||
company: contact.company || null,
|
||||
jobTitle: contact.jobTitle || null,
|
||||
street: contact.street || null,
|
||||
city: contact.city || null,
|
||||
postalCode: contact.postalCode || null,
|
||||
country: contact.country || null,
|
||||
notes: contact.notes || null,
|
||||
isFavorite: contact.isFavorite || false,
|
||||
isArchived: false,
|
||||
visibility: 'private',
|
||||
}));
|
||||
|
||||
await db.insert(contacts).values(contactsToInsert);
|
||||
|
||||
console.log(`✅ Successfully inserted ${seedContacts.length} contacts`);
|
||||
console.log('');
|
||||
console.log('📋 Summary:');
|
||||
console.log(` - Favorites: ${seedContacts.filter((c) => c.isFavorite).length}`);
|
||||
console.log(` - With company: ${seedContacts.filter((c) => c.company).length}`);
|
||||
console.log(` - International: ${seedContacts.filter((c) => c.country && c.country !== 'Deutschland').length}`);
|
||||
console.log('');
|
||||
console.log(`🔑 User ID: ${USER_ID}`);
|
||||
console.log('');
|
||||
console.log('💡 To see these contacts, log in with a user that has this ID.');
|
||||
console.log(' Or run with: SEED_USER_ID=your-user-id pnpm db:seed');
|
||||
} catch (error) {
|
||||
console.error('❌ Seed failed:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
await connection.end();
|
||||
}
|
||||
}
|
||||
|
||||
seed()
|
||||
.then(() => {
|
||||
console.log('🎉 Seed completed!');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('💥 Seed error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -120,40 +120,45 @@ export const contactsApi = {
|
|||
return fetchWithAuth(`/contacts${query ? `?${query}` : ''}`);
|
||||
},
|
||||
|
||||
async get(id: string) {
|
||||
return fetchWithAuth(`/contacts/${id}`);
|
||||
async get(id: string): Promise<Contact> {
|
||||
const response = await fetchWithAuth(`/contacts/${id}`);
|
||||
return response.contact;
|
||||
},
|
||||
|
||||
async create(data: Partial<Contact>) {
|
||||
return fetchWithAuth('/contacts', {
|
||||
async create(data: Partial<Contact>): Promise<Contact> {
|
||||
const response = await fetchWithAuth('/contacts', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return response.contact;
|
||||
},
|
||||
|
||||
async update(id: string, data: Partial<Contact>) {
|
||||
return fetchWithAuth(`/contacts/${id}`, {
|
||||
async update(id: string, data: Partial<Contact>): Promise<Contact> {
|
||||
const response = await fetchWithAuth(`/contacts/${id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return response.contact;
|
||||
},
|
||||
|
||||
async delete(id: string) {
|
||||
return fetchWithAuth(`/contacts/${id}`, {
|
||||
async delete(id: string): Promise<void> {
|
||||
await fetchWithAuth(`/contacts/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
},
|
||||
|
||||
async toggleFavorite(id: string) {
|
||||
return fetchWithAuth(`/contacts/${id}/favorite`, {
|
||||
async toggleFavorite(id: string): Promise<Contact> {
|
||||
const response = await fetchWithAuth(`/contacts/${id}/favorite`, {
|
||||
method: 'POST',
|
||||
});
|
||||
return response.contact;
|
||||
},
|
||||
|
||||
async toggleArchive(id: string) {
|
||||
return fetchWithAuth(`/contacts/${id}/archive`, {
|
||||
async toggleArchive(id: string): Promise<Contact> {
|
||||
const response = await fetchWithAuth(`/contacts/${id}/archive`, {
|
||||
method: 'POST',
|
||||
});
|
||||
return response.contact;
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
1182
apps/contacts/apps/web/src/lib/components/ContactDetailModal.svelte
Normal file
1182
apps/contacts/apps/web/src/lib/components/ContactDetailModal.svelte
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -64,9 +64,9 @@ export const contactsStore = {
|
|||
error = null;
|
||||
|
||||
try {
|
||||
const result = await contactsApi.get(id);
|
||||
selectedContact = result.contact;
|
||||
return result.contact;
|
||||
const contact = await contactsApi.get(id);
|
||||
selectedContact = contact;
|
||||
return contact;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to load contact';
|
||||
console.error('Failed to load contact:', e);
|
||||
|
|
@ -84,11 +84,11 @@ export const contactsStore = {
|
|||
error = null;
|
||||
|
||||
try {
|
||||
const result = await contactsApi.create(data);
|
||||
const contact = await contactsApi.create(data);
|
||||
// Add to local state
|
||||
contacts = [result.contact, ...contacts];
|
||||
contacts = [contact, ...contacts];
|
||||
total += 1;
|
||||
return result.contact;
|
||||
return contact;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to create contact';
|
||||
console.error('Failed to create contact:', e);
|
||||
|
|
@ -106,13 +106,13 @@ export const contactsStore = {
|
|||
error = null;
|
||||
|
||||
try {
|
||||
const result = await contactsApi.update(id, data);
|
||||
const contact = await contactsApi.update(id, data);
|
||||
// Update in local state
|
||||
contacts = contacts.map((c) => (c.id === id ? result.contact : c));
|
||||
contacts = contacts.map((c) => (c.id === id ? contact : c));
|
||||
if (selectedContact?.id === id) {
|
||||
selectedContact = result.contact;
|
||||
selectedContact = contact;
|
||||
}
|
||||
return result.contact;
|
||||
return contact;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to update contact';
|
||||
console.error('Failed to update contact:', e);
|
||||
|
|
@ -151,13 +151,13 @@ export const contactsStore = {
|
|||
*/
|
||||
async toggleFavorite(id: string) {
|
||||
try {
|
||||
const result = await contactsApi.toggleFavorite(id);
|
||||
const contact = await contactsApi.toggleFavorite(id);
|
||||
// Update in local state
|
||||
contacts = contacts.map((c) => (c.id === id ? result.contact : c));
|
||||
contacts = contacts.map((c) => (c.id === id ? contact : c));
|
||||
if (selectedContact?.id === id) {
|
||||
selectedContact = result.contact;
|
||||
selectedContact = contact;
|
||||
}
|
||||
return result.contact;
|
||||
return contact;
|
||||
} catch (e) {
|
||||
console.error('Failed to toggle favorite:', e);
|
||||
throw e;
|
||||
|
|
@ -169,14 +169,14 @@ export const contactsStore = {
|
|||
*/
|
||||
async toggleArchive(id: string) {
|
||||
try {
|
||||
const result = await contactsApi.toggleArchive(id);
|
||||
const contact = await contactsApi.toggleArchive(id);
|
||||
// Remove from current view if archived/unarchived
|
||||
contacts = contacts.filter((c) => c.id !== id);
|
||||
total -= 1;
|
||||
if (selectedContact?.id === id) {
|
||||
selectedContact = null;
|
||||
}
|
||||
return result.contact;
|
||||
return contact;
|
||||
} catch (e) {
|
||||
console.error('Failed to toggle archive:', e);
|
||||
throw e;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,13 @@
|
|||
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
|
||||
import { getPillAppItems } from '@manacore/shared-branding';
|
||||
import { setLocale, supportedLocales } from '$lib/i18n';
|
||||
import ContactDetailModal from '$lib/components/ContactDetailModal.svelte';
|
||||
import { contactsStore } from '$lib/stores/contacts.svelte';
|
||||
|
||||
// Check if we're on a contact detail route
|
||||
const contactDetailMatch = $derived($page.url.pathname.match(/^\/contacts\/([0-9a-f-]{36})$/i));
|
||||
const showContactModal = $derived(!!contactDetailMatch);
|
||||
const modalContactId = $derived(contactDetailMatch?.[1] || null);
|
||||
|
||||
// App switcher items
|
||||
const appItems = getPillAppItems('contacts');
|
||||
|
|
@ -131,6 +138,12 @@
|
|||
goto('/login');
|
||||
}
|
||||
|
||||
async function handleCloseContactModal() {
|
||||
// Refresh contacts list in case something was changed
|
||||
await contactsStore.loadContacts();
|
||||
goto('/', { replaceState: false });
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
// Redirect to login if not authenticated
|
||||
if (!authStore.isAuthenticated) {
|
||||
|
|
@ -206,6 +219,11 @@
|
|||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Contact Detail Modal -->
|
||||
{#if showContactModal && modalContactId}
|
||||
<ContactDetailModal contactId={modalContactId} onClose={handleCloseContactModal} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
|
|||
|
|
@ -1,179 +1,11 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { contactsStore } from '$lib/stores/contacts.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import ContactList from '$lib/components/ContactList.svelte';
|
||||
import '$lib/i18n';
|
||||
|
||||
let searchQuery = $state('');
|
||||
let searchTimeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
function handleSearch() {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
contactsStore.setSearch(searchQuery);
|
||||
contactsStore.loadContacts();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function getInitials(contact: (typeof contactsStore.contacts)[0]) {
|
||||
const first = contact.firstName?.[0] || '';
|
||||
const last = contact.lastName?.[0] || '';
|
||||
return (first + last).toUpperCase() || contact.email?.[0]?.toUpperCase() || '?';
|
||||
}
|
||||
|
||||
function getDisplayName(contact: (typeof contactsStore.contacts)[0]) {
|
||||
if (contact.displayName) return contact.displayName;
|
||||
if (contact.firstName || contact.lastName) {
|
||||
return [contact.firstName, contact.lastName].filter(Boolean).join(' ');
|
||||
}
|
||||
return contact.email || 'Unbekannt';
|
||||
}
|
||||
|
||||
async function handleToggleFavorite(e: MouseEvent, id: string) {
|
||||
e.stopPropagation();
|
||||
await contactsStore.toggleFavorite(id);
|
||||
}
|
||||
|
||||
function handleContactClick(id: string) {
|
||||
goto(`/contacts/${id}`);
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await contactsStore.loadContacts();
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$_('contacts.title')} - Contacts</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-2xl font-bold text-foreground">{$_('contacts.title')}</h1>
|
||||
<a href="/contacts/new" class="btn btn-primary flex items-center gap-2">
|
||||
<span>+</span>
|
||||
<span>{$_('contacts.new')}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Search -->
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={$_('contacts.search')}
|
||||
bind:value={searchQuery}
|
||||
oninput={handleSearch}
|
||||
class="input w-full pl-10"
|
||||
/>
|
||||
<svg
|
||||
class="absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
{#if contactsStore.loading}
|
||||
<div class="flex justify-center py-12">
|
||||
<div
|
||||
class="h-8 w-8 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent"
|
||||
></div>
|
||||
</div>
|
||||
{:else if contactsStore.contacts.length === 0}
|
||||
<!-- Empty state -->
|
||||
<div class="text-center py-12">
|
||||
<div class="text-6xl mb-4">👤</div>
|
||||
<h2 class="text-xl font-semibold text-foreground mb-2">{$_('contacts.noContacts')}</h2>
|
||||
<p class="text-muted-foreground mb-4">{$_('contacts.addFirst')}</p>
|
||||
<a href="/contacts/new" class="btn btn-primary">
|
||||
{$_('contacts.new')}
|
||||
</a>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Contacts List -->
|
||||
<div class="space-y-2">
|
||||
{#each contactsStore.contacts as contact (contact.id)}
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onclick={() => handleContactClick(contact.id)}
|
||||
onkeydown={(e) => e.key === 'Enter' && handleContactClick(contact.id)}
|
||||
class="contact-card w-full text-left cursor-pointer"
|
||||
>
|
||||
<!-- Avatar -->
|
||||
<div class="avatar">
|
||||
{#if contact.photoUrl}
|
||||
<img
|
||||
src={contact.photoUrl}
|
||||
alt={getDisplayName(contact)}
|
||||
class="h-full w-full rounded-full object-cover"
|
||||
/>
|
||||
{:else}
|
||||
{getInitials(contact)}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Contact Info -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="font-medium text-foreground truncate">
|
||||
{getDisplayName(contact)}
|
||||
</div>
|
||||
{#if contact.company || contact.jobTitle}
|
||||
<div class="text-sm text-muted-foreground truncate">
|
||||
{[contact.jobTitle, contact.company].filter(Boolean).join(' @ ')}
|
||||
</div>
|
||||
{/if}
|
||||
{#if contact.email}
|
||||
<div class="text-sm text-muted-foreground truncate">
|
||||
{contact.email}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Favorite button -->
|
||||
<button
|
||||
onclick={(e) => handleToggleFavorite(e, contact.id)}
|
||||
class="p-2 rounded-full hover:bg-accent transition-colors"
|
||||
>
|
||||
{#if contact.isFavorite}
|
||||
<svg class="h-5 w-5 text-red-500 fill-current" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg
|
||||
class="h-5 w-5 text-muted-foreground"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Total count -->
|
||||
<p class="text-sm text-muted-foreground text-center">
|
||||
{contactsStore.total} Kontakte
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<ContactList />
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -113,6 +113,10 @@ export class ImapProvider implements EmailProvider {
|
|||
const uid = parseInt(externalId, 10);
|
||||
|
||||
for await (const message of this.client.fetch(uid, { source: true }, { uid: true })) {
|
||||
if (!message.source) {
|
||||
this.logger.warn(`Email ${externalId} has no source`);
|
||||
continue;
|
||||
}
|
||||
const parsed = await simpleParser(message.source);
|
||||
return this.parseEmail(message, parsed);
|
||||
}
|
||||
|
|
@ -161,6 +165,10 @@ export class ImapProvider implements EmailProvider {
|
|||
{ uid: true }
|
||||
)) {
|
||||
try {
|
||||
if (!message.source) {
|
||||
this.logger.warn(`Email UID ${message.uid} has no source`);
|
||||
continue;
|
||||
}
|
||||
const parsed = await simpleParser(message.source);
|
||||
const email = this.parseEmail(message, parsed);
|
||||
emails.push(email);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue