diff --git a/CONTACTS_SYSTEM_README.md b/CONTACTS_SYSTEM_README.md new file mode 100644 index 0000000..8ad6ee2 --- /dev/null +++ b/CONTACTS_SYSTEM_README.md @@ -0,0 +1,174 @@ +# Contacts Management System + +## Overview + +A comprehensive contacts management system has been implemented to replace the simple text field for project contacts. This system allows you to: + +- **Create and manage a centralized contact database** +- **Link multiple contacts to each project** +- **Categorize contacts** (Project contacts, Contractors, Offices, Suppliers, etc.) +- **Track contact details** (name, phone, email, company, position) +- **Set primary contacts** for projects +- **Search and filter** contacts easily + +## What Was Implemented + +### 1. Database Schema + +**New Tables:** + +- **`contacts`** - Stores all contact information + - `contact_id` (Primary Key) + - `name`, `phone`, `email`, `company`, `position` + - `contact_type` (project/contractor/office/supplier/other) + - `notes`, `is_active` + - `created_at`, `updated_at` + +- **`project_contacts`** - Junction table linking projects to contacts (many-to-many) + - `project_id`, `contact_id` (Composite Primary Key) + - `relationship_type`, `is_primary` + - `added_at`, `added_by` + +### 2. API Endpoints + +- **`GET /api/contacts`** - List all contacts (with filters) +- **`POST /api/contacts`** - Create new contact +- **`GET /api/contacts/[id]`** - Get contact details +- **`PUT /api/contacts/[id]`** - Update contact +- **`DELETE /api/contacts/[id]`** - Delete contact (soft/hard) +- **`GET /api/projects/[id]/contacts`** - Get project's contacts +- **`POST /api/projects/[id]/contacts`** - Link contact to project +- **`DELETE /api/projects/[id]/contacts`** - Unlink contact from project +- **`PATCH /api/projects/[id]/contacts`** - Set primary contact + +### 3. UI Components + +- **`ContactForm`** - Create/edit contact form +- **`/contacts` page** - Full contacts management interface with: + - Statistics dashboard + - Search and filtering + - Contact cards with quick actions + - CRUD operations +- **`ProjectContactSelector`** - Multi-contact selector for projects + - View linked contacts + - Add/remove contacts + - Set primary contact + - Real-time search + +### 4. Integration + +- **Navigation** - "Kontakty" link added to main navigation +- **ProjectForm** - Contact text field replaced with `ProjectContactSelector` +- **Translations** - Polish translations added to i18n +- **Query Functions** - Comprehensive database query functions in `src/lib/queries/contacts.js` + +## How to Use + +### Initial Setup + +1. **Run the migration script** to create the new tables: + ```bash + node migrate-contacts.mjs + ``` + +2. **Start your development server**: + ```bash + npm run dev + ``` + +3. **Visit** `http://localhost:3000/contacts` to start adding contacts + +### Managing Contacts + +1. **Create Contacts**: + - Go to `/contacts` + - Click "Dodaj kontakt" + - Fill in contact details + - Select contact type (Project/Contractor/Office/Supplier/Other) + +2. **Link Contacts to Projects**: + - Edit any project + - In the "Kontakty do projektu" section + - Click "+ Dodaj kontakt" + - Search and add contacts + - Set one as primary if needed + +3. **View Contact Details**: + - Contacts page shows all contacts with: + - Contact information (phone, email, company) + - Number of linked projects + - Contact type badges + - Edit or delete contacts as needed + +### Contact Types + +- **Kontakt projektowy (Project)** - Project-specific contacts +- **Wykonawca (Contractor)** - Construction contractors +- **Urząd (Office)** - Government offices, municipalities +- **Dostawca (Supplier)** - Material suppliers, vendors +- **Inny (Other)** - Any other type of contact + +### Features + +- **Search** - Search by name, phone, email, or company +- **Filter** - Filter by contact type +- **Statistics** - See breakdown of contacts by type +- **Multiple Contacts per Project** - Link as many contacts as needed +- **Primary Contact** - Mark one contact as primary for each project +- **Bidirectional Links** - See which projects a contact is linked to +- **Soft Delete** - Deleted contacts are marked inactive, not removed + +## Database Migration Notes + +- The **old `contact` text field** in the `projects` table is still present +- It hasn't been removed for backward compatibility +- You can manually migrate old contact data by: + 1. Creating contacts from the old text data + 2. Linking them to the appropriate projects + 3. The old field will remain for reference + +## File Structure + +``` +src/ +├── app/ +│ ├── api/ +│ │ ├── contacts/ +│ │ │ ├── route.js # List/Create contacts +│ │ │ └── [id]/ +│ │ │ └── route.js # Get/Update/Delete contact +│ │ └── projects/ +│ │ └── [id]/ +│ │ └── contacts/ +│ │ └── route.js # Link/unlink contacts to project +│ └── contacts/ +│ └── page.js # Contacts management page +├── components/ +│ ├── ContactForm.js # Contact form component +│ └── ProjectContactSelector.js # Project contact selector +└── lib/ + ├── queries/ + │ └── contacts.js # Database query functions + └── init-db.js # Database schema with new tables +``` + +## Future Enhancements + +Potential improvements you could add: + +- Contact import/export (CSV, Excel) +- Contact groups or tags +- Contact activity history +- Email integration +- Contact notes and history +- Duplicate contact detection +- Contact merge functionality +- Advanced relationship types +- Contact sharing between projects +- Contact reminders/follow-ups + +## Support + +The old contact text field remains in the database, so no existing data is lost. You can gradually migrate to the new system at your own pace. + +Enjoy your new contacts management system! 🎉 diff --git a/check-contacts.mjs b/check-contacts.mjs new file mode 100644 index 0000000..c48c11c --- /dev/null +++ b/check-contacts.mjs @@ -0,0 +1,22 @@ +import db from './src/lib/db.js'; + +console.log('Checking contacts in database...\n'); + +const contacts = db.prepare('SELECT contact_id, name, phone, email, is_active, contact_type FROM contacts LIMIT 10').all(); + +console.log(`Total contacts found: ${contacts.length}\n`); + +if (contacts.length > 0) { + console.log('Sample contacts:'); + contacts.forEach(c => { + console.log(` ID: ${c.contact_id}, Name: ${c.name}, Phone: ${c.phone || 'N/A'}, Email: ${c.email || 'N/A'}, Active: ${c.is_active}, Type: ${c.contact_type}`); + }); +} else { + console.log('No contacts found in database!'); +} + +const activeCount = db.prepare('SELECT COUNT(*) as count FROM contacts WHERE is_active = 1').get(); +console.log(`\nActive contacts: ${activeCount.count}`); + +const totalCount = db.prepare('SELECT COUNT(*) as count FROM contacts').get(); +console.log(`Total contacts: ${totalCount.count}`); diff --git a/migrate-contacts.mjs b/migrate-contacts.mjs new file mode 100644 index 0000000..ba3447f --- /dev/null +++ b/migrate-contacts.mjs @@ -0,0 +1,46 @@ +import db from './src/lib/db.js'; +import initializeDatabase from './src/lib/init-db.js'; + +console.log('🚀 Initializing contacts tables...\n'); + +try { + // Run database initialization which will create the new contacts tables + initializeDatabase(); + + console.log('✅ Contacts tables created successfully!\n'); + + // Check if there are projects with contact data in the old text field + const projectsWithContacts = db.prepare(` + SELECT project_id, project_name, contact + FROM projects + WHERE contact IS NOT NULL AND contact != '' + `).all(); + + if (projectsWithContacts.length > 0) { + console.log(`📋 Found ${projectsWithContacts.length} projects with contact information in the old text field.\n`); + console.log('Sample contacts that could be migrated:'); + projectsWithContacts.slice(0, 5).forEach(p => { + console.log(` - ${p.project_name}: "${p.contact}"`); + }); + console.log('\nℹ️ You can manually create contacts from the /contacts page and link them to projects.'); + console.log(' The old contact field will remain in the database for reference.\n'); + } else { + console.log('ℹ️ No existing contact data found in projects.\n'); + } + + // Show table 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('📊 Database Statistics:'); + console.log(` - Contacts: ${contactsCount.count}`); + console.log(` - Project-Contact Links: ${projectContactsCount.count}`); + console.log('\n✨ Migration complete! You can now:'); + console.log(' 1. Visit /contacts to manage your contacts'); + console.log(' 2. Add/edit projects to link contacts'); + console.log(' 3. View linked contacts in project details\n'); + +} catch (error) { + console.error('❌ Error during migration:', error); + process.exit(1); +} diff --git a/migrate-project-contacts.mjs b/migrate-project-contacts.mjs new file mode 100644 index 0000000..1ddf75b --- /dev/null +++ b/migrate-project-contacts.mjs @@ -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); +} diff --git a/src/app/api/contacts/[id]/projects/route.js b/src/app/api/contacts/[id]/projects/route.js new file mode 100644 index 0000000..f425aca --- /dev/null +++ b/src/app/api/contacts/[id]/projects/route.js @@ -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 } + ); + } +}); diff --git a/src/app/api/contacts/[id]/route.js b/src/app/api/contacts/[id]/route.js new file mode 100644 index 0000000..62d237f --- /dev/null +++ b/src/app/api/contacts/[id]/route.js @@ -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); diff --git a/src/app/api/contacts/route.js b/src/app/api/contacts/route.js new file mode 100644 index 0000000..f1a9581 --- /dev/null +++ b/src/app/api/contacts/route.js @@ -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); diff --git a/src/app/api/projects/[id]/contacts/route.js b/src/app/api/projects/[id]/contacts/route.js new file mode 100644 index 0000000..062aaaa --- /dev/null +++ b/src/app/api/projects/[id]/contacts/route.js @@ -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); diff --git a/src/app/contacts/page.js b/src/app/contacts/page.js new file mode 100644 index 0000000..b34db1f --- /dev/null +++ b/src/app/contacts/page.js @@ -0,0 +1,598 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { useSession } from "next-auth/react"; +import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/Card"; +import Button from "@/components/ui/Button"; +import Badge from "@/components/ui/Badge"; +import ContactForm from "@/components/ContactForm"; + +export default function ContactsPage() { + const router = useRouter(); + const { data: session, status } = useSession(); + const [contacts, setContacts] = useState([]); + const [filteredContacts, setFilteredContacts] = useState([]); + const [loading, setLoading] = useState(true); + const [showForm, setShowForm] = useState(false); + const [editingContact, setEditingContact] = useState(null); + const [searchTerm, setSearchTerm] = useState(""); + const [typeFilter, setTypeFilter] = useState("all"); + const [stats, setStats] = useState(null); + const [selectedContact, setSelectedContact] = useState(null); + const [contactProjects, setContactProjects] = useState([]); + const [loadingProjects, setLoadingProjects] = useState(false); + + // Redirect if not authenticated + useEffect(() => { + if (status === "unauthenticated") { + router.push("/auth/signin"); + } + }, [status, router]); + + // Fetch contacts + useEffect(() => { + fetchContacts(); + fetchStats(); + }, []); + + // Filter contacts + useEffect(() => { + let filtered = contacts; + + // Filter by search term + if (searchTerm) { + const search = searchTerm.toLowerCase(); + filtered = filtered.filter( + (contact) => + contact.name?.toLowerCase().includes(search) || + contact.phone?.toLowerCase().includes(search) || + contact.email?.toLowerCase().includes(search) || + contact.company?.toLowerCase().includes(search) + ); + } + + // Filter by type + if (typeFilter !== "all") { + filtered = filtered.filter( + (contact) => contact.contact_type === typeFilter + ); + } + + setFilteredContacts(filtered); + }, [contacts, searchTerm, typeFilter]); + + async function fetchContacts() { + try { + const response = await fetch("/api/contacts?is_active=true"); + if (response.ok) { + const data = await response.json(); + console.log('Fetched contacts:', data); + setContacts(data); + } else { + console.error('Failed to fetch contacts, status:', response.status); + } + } catch (error) { + console.error("Error fetching contacts:", error); + } finally { + setLoading(false); + } + } + + async function fetchStats() { + try { + const response = await fetch("/api/contacts?stats=true"); + if (response.ok) { + const data = await response.json(); + setStats(data); + } + } catch (error) { + console.error("Error fetching stats:", error); + } + } + + async function handleDelete(contactId) { + if (!confirm("Czy na pewno chcesz usunąć ten kontakt?")) return; + + try { + const response = await fetch(`/api/contacts/${contactId}`, { + method: "DELETE", + }); + + if (response.ok) { + fetchContacts(); + fetchStats(); + } + } catch (error) { + console.error("Error deleting contact:", error); + alert("Nie udało się usunąć kontaktu"); + } + } + + function handleEdit(contact) { + setEditingContact(contact); + setShowForm(true); + } + + async function handleViewDetails(contact) { + setSelectedContact(contact); + setLoadingProjects(true); + + try { + // Fetch projects linked to this contact + const response = await fetch(`/api/contacts/${contact.contact_id}/projects`); + if (response.ok) { + const data = await response.json(); + setContactProjects(data.projects || []); + } + } catch (error) { + console.error("Error fetching contact projects:", error); + setContactProjects([]); + } finally { + setLoadingProjects(false); + } + } + + function closeDetails() { + setSelectedContact(null); + setContactProjects([]); + } + + function handleFormSave(contact) { + setShowForm(false); + setEditingContact(null); + fetchContacts(); + fetchStats(); + } + + function handleFormCancel() { + setShowForm(false); + setEditingContact(null); + } + + const getContactTypeBadge = (type) => { + const types = { + project: { label: "Projekt", variant: "primary" }, + contractor: { label: "Wykonawca", variant: "warning" }, + office: { label: "Urząd", variant: "info" }, + supplier: { label: "Dostawca", variant: "success" }, + other: { label: "Inny", variant: "secondary" }, + }; + return types[type] || types.other; + }; + + if (status === "loading" || loading) { + return ( +
+ Zarządzaj kontaktami do projektów i współpracy +
++ {searchTerm || typeFilter !== "all" + ? "Nie znaleziono kontaktów" + : "Brak kontaktów. Dodaj pierwszy kontakt."} +
++ {selectedContact.notes} +
+Brak powiązanych projektów
+ ) : ( ++ {searchTerm + ? "Nie znaleziono kontaktów" + : "Wszystkie kontakty są już dodane"} +
+ ) : ( + filteredAvailable.map((contact) => { + const typeBadge = getContactTypeBadge(contact.contact_type); + return ( +