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:
40
src/app/api/contacts/[id]/projects/route.js
Normal file
40
src/app/api/contacts/[id]/projects/route.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import db from "@/lib/db";
|
||||
import { withAuth } from "@/lib/middleware/auth";
|
||||
|
||||
// GET /api/contacts/[id]/projects - Get all projects linked to a contact
|
||||
export const GET = withAuth(async (request, { params }) => {
|
||||
try {
|
||||
const contactId = params.id;
|
||||
|
||||
// Get all projects linked to this contact with relationship details
|
||||
const projects = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT
|
||||
p.project_id,
|
||||
p.project_name,
|
||||
p.project_status,
|
||||
pc.relationship_type,
|
||||
pc.is_primary,
|
||||
pc.added_at
|
||||
FROM projects p
|
||||
INNER JOIN project_contacts pc ON p.project_id = pc.project_id
|
||||
WHERE pc.contact_id = ?
|
||||
ORDER BY pc.is_primary DESC, p.project_name ASC
|
||||
`
|
||||
)
|
||||
.all(contactId);
|
||||
|
||||
return NextResponse.json({
|
||||
projects,
|
||||
count: projects.length,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching contact projects:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch projects" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
});
|
||||
103
src/app/api/contacts/[id]/route.js
Normal file
103
src/app/api/contacts/[id]/route.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import {
|
||||
getContactById,
|
||||
updateContact,
|
||||
deleteContact,
|
||||
hardDeleteContact,
|
||||
} from "@/lib/queries/contacts";
|
||||
import { withAuth } from "@/lib/middleware/auth";
|
||||
|
||||
// GET: Get contact by ID
|
||||
async function getContactHandler(req, { params }) {
|
||||
try {
|
||||
const contactId = parseInt(params.id);
|
||||
const contact = getContactById(contactId);
|
||||
|
||||
if (!contact) {
|
||||
return NextResponse.json(
|
||||
{ error: "Contact not found" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(contact);
|
||||
} catch (error) {
|
||||
console.error("Error fetching contact:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch contact" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// PUT: Update contact
|
||||
async function updateContactHandler(req, { params }) {
|
||||
try {
|
||||
const contactId = parseInt(params.id);
|
||||
const data = await req.json();
|
||||
|
||||
// Validate contact type if provided
|
||||
if (data.contact_type) {
|
||||
const validTypes = [
|
||||
"project",
|
||||
"contractor",
|
||||
"office",
|
||||
"supplier",
|
||||
"other",
|
||||
];
|
||||
if (!validTypes.includes(data.contact_type)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid contact type" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const contact = updateContact(contactId, data);
|
||||
|
||||
if (!contact) {
|
||||
return NextResponse.json(
|
||||
{ error: "Contact not found" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(contact);
|
||||
} catch (error) {
|
||||
console.error("Error updating contact:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to update contact" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE: Delete contact (soft delete or hard delete)
|
||||
async function deleteContactHandler(req, { params }) {
|
||||
try {
|
||||
const contactId = parseInt(params.id);
|
||||
const { searchParams } = new URL(req.url);
|
||||
const hard = searchParams.get("hard") === "true";
|
||||
|
||||
if (hard) {
|
||||
// Hard delete - permanently remove
|
||||
hardDeleteContact(contactId);
|
||||
} else {
|
||||
// Soft delete - set is_active to 0
|
||||
deleteContact(contactId);
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: "Contact deleted successfully" });
|
||||
} catch (error) {
|
||||
console.error("Error deleting contact:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to delete contact" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Protected routes - require authentication
|
||||
export const GET = withAuth(getContactHandler);
|
||||
export const PUT = withAuth(updateContactHandler);
|
||||
export const DELETE = withAuth(deleteContactHandler);
|
||||
73
src/app/api/contacts/route.js
Normal file
73
src/app/api/contacts/route.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import {
|
||||
getAllContacts,
|
||||
createContact,
|
||||
getContactStats,
|
||||
} from "@/lib/queries/contacts";
|
||||
import { withAuth } from "@/lib/middleware/auth";
|
||||
|
||||
// GET: Get all contacts with optional filters
|
||||
async function getContactsHandler(req) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const filters = {
|
||||
is_active: searchParams.get("is_active")
|
||||
? searchParams.get("is_active") === "true"
|
||||
: undefined,
|
||||
contact_type: searchParams.get("contact_type") || undefined,
|
||||
search: searchParams.get("search") || undefined,
|
||||
};
|
||||
|
||||
// Check if stats are requested
|
||||
if (searchParams.get("stats") === "true") {
|
||||
const stats = getContactStats();
|
||||
return NextResponse.json(stats);
|
||||
}
|
||||
|
||||
const contacts = getAllContacts(filters);
|
||||
return NextResponse.json(contacts);
|
||||
} catch (error) {
|
||||
console.error("Error fetching contacts:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch contacts" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// POST: Create new contact
|
||||
async function createContactHandler(req) {
|
||||
try {
|
||||
const data = await req.json();
|
||||
|
||||
// Validate required fields
|
||||
if (!data.name) {
|
||||
return NextResponse.json(
|
||||
{ error: "Contact name is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate contact type
|
||||
const validTypes = ["project", "contractor", "office", "supplier", "other"];
|
||||
if (data.contact_type && !validTypes.includes(data.contact_type)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid contact type" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const contact = createContact(data);
|
||||
return NextResponse.json(contact, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error("Error creating contact:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to create contact" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Protected routes - require authentication
|
||||
export const GET = withAuth(getContactsHandler);
|
||||
export const POST = withAuth(createContactHandler);
|
||||
111
src/app/api/projects/[id]/contacts/route.js
Normal file
111
src/app/api/projects/[id]/contacts/route.js
Normal file
@@ -0,0 +1,111 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import {
|
||||
getProjectContacts,
|
||||
linkContactToProject,
|
||||
unlinkContactFromProject,
|
||||
setPrimaryContact,
|
||||
} from "@/lib/queries/contacts";
|
||||
import { withAuth } from "@/lib/middleware/auth";
|
||||
|
||||
// GET: Get all contacts for a project
|
||||
async function getProjectContactsHandler(req, { params }) {
|
||||
try {
|
||||
const projectId = parseInt(params.id);
|
||||
const contacts = getProjectContacts(projectId);
|
||||
return NextResponse.json(contacts);
|
||||
} catch (error) {
|
||||
console.error("Error fetching project contacts:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch project contacts" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// POST: Link contact to project
|
||||
async function linkContactHandler(req, { params }) {
|
||||
try {
|
||||
const projectId = parseInt(params.id);
|
||||
const { contactId, relationshipType, isPrimary } = await req.json();
|
||||
const userId = req.user?.id;
|
||||
|
||||
if (!contactId) {
|
||||
return NextResponse.json(
|
||||
{ error: "Contact ID is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
linkContactToProject(
|
||||
projectId,
|
||||
contactId,
|
||||
relationshipType || "general",
|
||||
isPrimary || false,
|
||||
userId
|
||||
);
|
||||
|
||||
const contacts = getProjectContacts(projectId);
|
||||
return NextResponse.json(contacts);
|
||||
} catch (error) {
|
||||
console.error("Error linking contact to project:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to link contact" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE: Unlink contact from project
|
||||
async function unlinkContactHandler(req, { params }) {
|
||||
try {
|
||||
const projectId = parseInt(params.id);
|
||||
const { searchParams } = new URL(req.url);
|
||||
const contactId = parseInt(searchParams.get("contactId"));
|
||||
|
||||
if (!contactId) {
|
||||
return NextResponse.json(
|
||||
{ error: "Contact ID is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
unlinkContactFromProject(projectId, contactId);
|
||||
return NextResponse.json({ message: "Contact unlinked successfully" });
|
||||
} catch (error) {
|
||||
console.error("Error unlinking contact from project:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to unlink contact" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// PATCH: Set primary contact
|
||||
async function setPrimaryContactHandler(req, { params }) {
|
||||
try {
|
||||
const projectId = parseInt(params.id);
|
||||
const { contactId } = await req.json();
|
||||
|
||||
if (!contactId) {
|
||||
return NextResponse.json(
|
||||
{ error: "Contact ID is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
setPrimaryContact(projectId, contactId);
|
||||
const contacts = getProjectContacts(projectId);
|
||||
return NextResponse.json(contacts);
|
||||
} catch (error) {
|
||||
console.error("Error setting primary contact:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to set primary contact" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const GET = withAuth(getProjectContactsHandler);
|
||||
export const POST = withAuth(linkContactHandler);
|
||||
export const DELETE = withAuth(unlinkContactHandler);
|
||||
export const PATCH = withAuth(setPrimaryContactHandler);
|
||||
Reference in New Issue
Block a user