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

327
src/lib/queries/contacts.js Normal file
View File

@@ -0,0 +1,327 @@
import db from "../db.js";
// Get all contacts with optional filters
export function getAllContacts(filters = {}) {
let query = `
SELECT
c.*,
COUNT(DISTINCT pc.project_id) as project_count
FROM contacts c
LEFT JOIN project_contacts pc ON c.contact_id = pc.contact_id
WHERE 1=1
`;
const params = [];
// Filter by active status
if (filters.is_active !== undefined) {
query += ` AND c.is_active = ?`;
params.push(filters.is_active ? 1 : 0);
}
// Filter by contact type
if (filters.contact_type) {
query += ` AND c.contact_type = ?`;
params.push(filters.contact_type);
}
// Search by name, phone, email, or company
if (filters.search) {
query += ` AND (
c.name LIKE ? OR
c.phone LIKE ? OR
c.email LIKE ? OR
c.company LIKE ?
)`;
const searchTerm = `%${filters.search}%`;
params.push(searchTerm, searchTerm, searchTerm, searchTerm);
}
query += ` GROUP BY c.contact_id ORDER BY c.name ASC`;
return db.prepare(query).all(...params);
}
// Get contact by ID
export function getContactById(contactId) {
const contact = db
.prepare(
`
SELECT c.*
FROM contacts c
WHERE c.contact_id = ?
`
)
.get(contactId);
if (!contact) return null;
// Get associated projects
const projects = db
.prepare(
`
SELECT
p.project_id,
p.project_name,
p.project_number,
pc.relationship_type,
pc.is_primary,
pc.added_at
FROM project_contacts pc
JOIN projects p ON pc.project_id = p.project_id
WHERE pc.contact_id = ?
ORDER BY pc.is_primary DESC, pc.added_at DESC
`
)
.all(contactId);
return { ...contact, projects };
}
// Create new contact
export function createContact(data) {
const stmt = db.prepare(`
INSERT INTO contacts (
name, phone, email, company, position, contact_type, notes, is_active
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`);
const result = stmt.run(
data.name,
data.phone || null,
data.email || null,
data.company || null,
data.position || null,
data.contact_type || "other",
data.notes || null,
data.is_active !== undefined ? (data.is_active ? 1 : 0) : 1
);
return getContactById(result.lastInsertRowid);
}
// Update contact
export function updateContact(contactId, data) {
const updates = [];
const params = [];
if (data.name !== undefined) {
updates.push("name = ?");
params.push(data.name);
}
if (data.phone !== undefined) {
updates.push("phone = ?");
params.push(data.phone || null);
}
if (data.email !== undefined) {
updates.push("email = ?");
params.push(data.email || null);
}
if (data.company !== undefined) {
updates.push("company = ?");
params.push(data.company || null);
}
if (data.position !== undefined) {
updates.push("position = ?");
params.push(data.position || null);
}
if (data.contact_type !== undefined) {
updates.push("contact_type = ?");
params.push(data.contact_type);
}
if (data.notes !== undefined) {
updates.push("notes = ?");
params.push(data.notes || null);
}
if (data.is_active !== undefined) {
updates.push("is_active = ?");
params.push(data.is_active ? 1 : 0);
}
if (updates.length === 0) {
return getContactById(contactId);
}
updates.push("updated_at = CURRENT_TIMESTAMP");
params.push(contactId);
const query = `UPDATE contacts SET ${updates.join(", ")} WHERE contact_id = ?`;
db.prepare(query).run(...params);
return getContactById(contactId);
}
// Delete contact (soft delete by setting is_active = 0)
export function deleteContact(contactId) {
const stmt = db.prepare(`
UPDATE contacts
SET is_active = 0, updated_at = CURRENT_TIMESTAMP
WHERE contact_id = ?
`);
return stmt.run(contactId);
}
// Hard delete contact (permanent deletion)
export function hardDeleteContact(contactId) {
// First remove all project associations
db.prepare(`DELETE FROM project_contacts WHERE contact_id = ?`).run(
contactId
);
// Then delete the contact
const stmt = db.prepare(`DELETE FROM contacts WHERE contact_id = ?`);
return stmt.run(contactId);
}
// Get contacts for a specific project
export function getProjectContacts(projectId) {
return db
.prepare(
`
SELECT
c.*,
pc.relationship_type,
pc.is_primary,
pc.added_at,
u.name as added_by_name
FROM project_contacts pc
JOIN contacts c ON pc.contact_id = c.contact_id
LEFT JOIN users u ON pc.added_by = u.id
WHERE pc.project_id = ? AND c.is_active = 1
ORDER BY pc.is_primary DESC, c.name ASC
`
)
.all(projectId);
}
// Link contact to project
export function linkContactToProject(
projectId,
contactId,
relationshipType = "general",
isPrimary = false,
userId = null
) {
const stmt = db.prepare(`
INSERT OR REPLACE INTO project_contacts (
project_id, contact_id, relationship_type, is_primary, added_by
) VALUES (?, ?, ?, ?, ?)
`);
return stmt.run(
projectId,
contactId,
relationshipType,
isPrimary ? 1 : 0,
userId
);
}
// Unlink contact from project
export function unlinkContactFromProject(projectId, contactId) {
const stmt = db.prepare(`
DELETE FROM project_contacts
WHERE project_id = ? AND contact_id = ?
`);
return stmt.run(projectId, contactId);
}
// Set primary contact for a project
export function setPrimaryContact(projectId, contactId) {
// First, remove primary flag from all contacts for this project
db.prepare(`
UPDATE project_contacts
SET is_primary = 0
WHERE project_id = ?
`).run(projectId);
// Then set the specified contact as primary
const stmt = db.prepare(`
UPDATE project_contacts
SET is_primary = 1
WHERE project_id = ? AND contact_id = ?
`);
return stmt.run(projectId, contactId);
}
// Get contact statistics
export function getContactStats() {
return db
.prepare(
`
SELECT
COUNT(*) as total_contacts,
COUNT(CASE WHEN is_active = 1 THEN 1 END) as active_contacts,
COUNT(CASE WHEN is_active = 0 THEN 1 END) as inactive_contacts,
COUNT(CASE WHEN contact_type = 'project' THEN 1 END) as project_contacts,
COUNT(CASE WHEN contact_type = 'contractor' THEN 1 END) as contractor_contacts,
COUNT(CASE WHEN contact_type = 'office' THEN 1 END) as office_contacts,
COUNT(CASE WHEN contact_type = 'supplier' THEN 1 END) as supplier_contacts,
COUNT(CASE WHEN contact_type = 'other' THEN 1 END) as other_contacts
FROM contacts
`
)
.get();
}
// Search contacts by phone number
export function searchContactsByPhone(phoneNumber) {
const searchTerm = `%${phoneNumber}%`;
return db
.prepare(
`
SELECT * FROM contacts
WHERE phone LIKE ? AND is_active = 1
ORDER BY name ASC
`
)
.all(searchTerm);
}
// Search contacts by email
export function searchContactsByEmail(email) {
const searchTerm = `%${email}%`;
return db
.prepare(
`
SELECT * FROM contacts
WHERE email LIKE ? AND is_active = 1
ORDER BY name ASC
`
)
.all(searchTerm);
}
// Bulk link contacts to project
export function bulkLinkContactsToProject(projectId, contactIds, userId = null) {
const stmt = db.prepare(`
INSERT OR IGNORE INTO project_contacts (
project_id, contact_id, added_by
) VALUES (?, ?, ?)
`);
const results = contactIds.map((contactId) =>
stmt.run(projectId, contactId, userId)
);
return results;
}
// Get contacts not linked to a specific project (for selection)
export function getAvailableContactsForProject(projectId) {
return db
.prepare(
`
SELECT c.*
FROM contacts c
WHERE c.is_active = 1
AND c.contact_id NOT IN (
SELECT contact_id
FROM project_contacts
WHERE project_id = ?
)
ORDER BY c.name ASC
`
)
.all(projectId);
}