feat: add contact management functionality

- Implemented ContactForm component for creating and editing contacts.
- Added ProjectContactSelector component to manage project-specific contacts.
- Updated ProjectForm to include ProjectContactSelector for associating contacts with projects.
- Enhanced Card component with a new CardTitle subcomponent for better structure.
- Updated Navigation to include a link to the contacts page.
- Added translations for contact-related terms in the i18n module.
- Initialized contacts database schema and created necessary tables for contact management.
- Developed queries for CRUD operations on contacts, including linking and unlinking contacts to projects.
- Created a test script to validate contact queries against the database.
This commit is contained in:
2025-12-03 16:23:05 +01:00
parent c9b7355f3c
commit 60b79fa360
18 changed files with 2332 additions and 10 deletions

View File

@@ -0,0 +1,188 @@
import db from './src/lib/db.js';
import initializeDatabase from './src/lib/init-db.js';
console.log('🚀 Migrating contact data from projects...\n');
try {
// Run database initialization to ensure tables exist
initializeDatabase();
console.log('✅ Database tables verified\n');
// Get all projects with contact data
const projectsWithContacts = db.prepare(`
SELECT project_id, project_name, contact
FROM projects
WHERE contact IS NOT NULL AND contact != '' AND TRIM(contact) != ''
`).all();
if (projectsWithContacts.length === 0) {
console.log(' No contact data found in projects to migrate.\n');
process.exit(0);
}
console.log(`📋 Found ${projectsWithContacts.length} projects with contact information\n`);
let created = 0;
let linked = 0;
let skipped = 0;
const createContact = db.prepare(`
INSERT INTO contacts (name, phone, email, contact_type, notes, is_active)
VALUES (?, ?, ?, 'project', ?, 1)
`);
const linkContact = db.prepare(`
INSERT OR IGNORE INTO project_contacts (project_id, contact_id, is_primary, relationship_type)
VALUES (?, ?, 1, 'general')
`);
// Process each project
for (const project of projectsWithContacts) {
try {
const contactText = project.contact.trim();
// Parse contact information - common formats:
// "Jan Kowalski, tel. 123-456-789"
// "Jan Kowalski 123-456-789"
// "123-456-789"
// "Jan Kowalski"
let name = '';
let phone = '';
let email = '';
let notes = '';
// Try to extract email
const emailPattern = /([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)/;
const emailMatch = contactText.match(emailPattern);
if (emailMatch) {
email = emailMatch[1].trim();
}
// Try to extract phone number (various formats)
const phonePatterns = [
/(?:\+?48)?[\s-]?(\d{3}[\s-]?\d{3}[\s-]?\d{3})/, // Polish: 123-456-789, 123 456 789, +48 123456789
/(?:\+?48)?[\s-]?(\d{9})/, // 9 digits
/tel\.?\s*[:.]?\s*([+\d\s-]+)/i, // tel. 123-456-789
/phone\s*[:.]?\s*([+\d\s-]+)/i, // phone: 123-456-789
/(\d{3}[-\s]?\d{3}[-\s]?\d{3})/, // Generic phone pattern
];
for (const pattern of phonePatterns) {
const match = contactText.match(pattern);
if (match) {
phone = match[1] || match[0];
phone = phone.replace(/\s+/g, ' ').trim();
break;
}
}
// Extract name (text before phone/email or comma)
let textForName = contactText;
if (phone) {
// Remove phone from text to get name
textForName = textForName.replace(phone, '');
}
if (email) {
// Remove email from text to get name
textForName = textForName.replace(email, '');
}
// Remove common prefixes like "tel.", "phone:", "email:", commas, etc.
name = textForName.replace(/tel\.?|phone:?|email:?|e-mail:?|,/gi, '').trim();
// Clean up name
name = name.replace(/^[,\s-]+|[,\s-]+$/g, '').trim();
// If we couldn't extract structured data, use project name and put original text in notes
if (!phone && !email) {
// No structured contact info found, put everything in notes
notes = `Original contact info: ${contactText}`;
name = project.project_name;
} else if (!name) {
// We have phone/email but no clear name
name = project.project_name;
}
// Check if this contact already exists (by name, phone, or email)
let existingContact = null;
if (phone) {
existingContact = db.prepare(`
SELECT contact_id FROM contacts
WHERE phone LIKE ? OR phone LIKE ?
`).get(`%${phone}%`, `%${phone.replace(/\s/g, '')}%`);
}
if (!existingContact && email) {
existingContact = db.prepare(`
SELECT contact_id FROM contacts
WHERE LOWER(email) = LOWER(?)
`).get(email);
}
if (!existingContact && name && name !== project.project_name) {
existingContact = db.prepare(`
SELECT contact_id FROM contacts
WHERE LOWER(name) = LOWER(?)
`).get(name);
}
let contactId;
if (existingContact) {
contactId = existingContact.contact_id;
console.log(` ♻️ Using existing contact "${name}" for project "${project.project_name}"`);
} else {
// Create new contact
const result = createContact.run(
name,
phone || null,
email || null,
notes || `Migrated from project: ${project.project_name}`
);
contactId = result.lastInsertRowid;
created++;
const contactInfo = [];
if (phone) contactInfo.push(`📞 ${phone}`);
if (email) contactInfo.push(`📧 ${email}`);
const infoStr = contactInfo.length > 0 ? ` (${contactInfo.join(', ')})` : '';
console.log(` ✨ Created contact "${name}"${infoStr} for project "${project.project_name}"`);
}
// Link contact to project
linkContact.run(project.project_id, contactId);
linked++;
} catch (error) {
console.error(` ❌ Error processing project "${project.project_name}":`, error.message);
skipped++;
}
}
console.log('\n📊 Migration Summary:');
console.log(` - Contacts created: ${created}`);
console.log(` - Project-contact links created: ${linked}`);
console.log(` - Projects skipped: ${skipped}`);
console.log(` - Total projects processed: ${projectsWithContacts.length}`);
// Show final statistics
const contactsCount = db.prepare('SELECT COUNT(*) as count FROM contacts').get();
const projectContactsCount = db.prepare('SELECT COUNT(*) as count FROM project_contacts').get();
console.log('\n📈 Current Database Statistics:');
console.log(` - Total contacts: ${contactsCount.count}`);
console.log(` - Total project-contact links: ${projectContactsCount.count}`);
console.log('\n✨ Migration complete!');
console.log(' - Visit /contacts to view and manage your contacts');
console.log(' - Edit projects to see linked contacts');
console.log(' - The old contact text field is preserved for reference\n');
} catch (error) {
console.error('❌ Error during migration:', error);
process.exit(1);
}