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:
327
src/lib/queries/contacts.js
Normal file
327
src/lib/queries/contacts.js
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user