From c13a99177810ff356c42f8f958441c8c30d7854b Mon Sep 17 00:00:00 2001 From: chop Date: Mon, 26 Jan 2026 23:18:49 +0100 Subject: [PATCH] feat: Add comprehensive test data generator and enhance user management with initials --- scripts/create-comprehensive-test-data.js | 844 ++++++++++++++++++++++ src/lib/init-db.js | 28 + src/lib/userManagement.js | 10 +- 3 files changed, 879 insertions(+), 3 deletions(-) create mode 100644 scripts/create-comprehensive-test-data.js diff --git a/scripts/create-comprehensive-test-data.js b/scripts/create-comprehensive-test-data.js new file mode 100644 index 0000000..e587759 --- /dev/null +++ b/scripts/create-comprehensive-test-data.js @@ -0,0 +1,844 @@ +#!/usr/bin/env node +/** + * Comprehensive Test Data Generator + * + * Creates realistic test data for the panel application including: + * - Users with different roles + * - Contracts with realistic data + * - Projects scattered across Poland with person/company names + * - Task templates and sets + * - Project tasks with various statuses + * - Contacts + * - Notes and file attachments + * - Notifications and audit logs + */ + +import db from '../src/lib/db.js'; +import initializeDatabase from '../src/lib/init-db.js'; +import bcrypt from 'bcryptjs'; +import crypto from 'crypto'; + +// Configuration +const CONFIG = { + clearExistingData: true, + preserveAdmin: true, // Keep existing admin user + seed: 42, // For reproducible random data +}; + +// Seeded random number generator +class SeededRandom { + constructor(seed) { + this.seed = seed; + } + + next() { + this.seed = (this.seed * 9301 + 49297) % 233280; + return this.seed / 233280; + } + + choice(array) { + return array[Math.floor(this.next() * array.length)]; + } + + integer(min, max) { + return Math.floor(this.next() * (max - min + 1)) + min; + } + + boolean(probability = 0.5) { + return this.next() < probability; + } +} + +const random = new SeededRandom(CONFIG.seed); + +// Polish cities with coordinates +const POLISH_CITIES = [ + { name: 'Warszawa', coordinates: '52.2297,21.0122' }, + { name: 'Kraków', coordinates: '50.0647,19.9450' }, + { name: 'Wrocław', coordinates: '51.1079,17.0385' }, + { name: 'Poznań', coordinates: '52.4064,16.9252' }, + { name: 'Gdańsk', coordinates: '54.3520,18.6466' }, + { name: 'Szczecin', coordinates: '53.4289,14.5530' }, + { name: 'Lublin', coordinates: '51.2465,22.5684' }, + { name: 'Katowice', coordinates: '50.2649,19.0238' }, + { name: 'Łódź', coordinates: '51.7592,19.4600' }, + { name: 'Bydgoszcz', coordinates: '53.1235,18.0084' }, + { name: 'Białystok', coordinates: '53.1325,23.1688' }, + { name: 'Rzeszów', coordinates: '50.0412,21.9991' }, +]; + +// Street names +const STREET_TYPES = ['ul.', 'al.', 'pl.']; +const STREET_NAMES = [ + 'Główna', 'Kwiatowa', 'Słoneczna', 'Przemysłowa', 'Leśna', + 'Parkowa', 'Centralna', 'Sportowa', 'Polna', 'Krótka', + 'Długa', 'Nowa', 'Stara', 'Morska', 'Górska', 'Wolności', + 'Mickiewicza', 'Kościuszki', 'Piłsudskiego', 'Kolejowa' +]; + +// Project names - people +const PERSON_NAMES = [ + 'Jan Kowalski', 'Anna Nowak', 'Piotr Wiśniewski', 'Maria Lewandowska', + 'Tomasz Kamiński', 'Małgorzata Zielińska', 'Krzysztof Szymański', + 'Agnieszka Woźniak', 'Andrzej Dąbrowski', 'Barbara Kozłowska', + 'Józef Jankowski', 'Ewa Wojciechowska', 'Stanisław Kwiatkowski', + 'Krystyna Kaczmarek', 'Tadeusz Piotrowski' +]; + +// Project names - companies +const COMPANY_NAMES = [ + 'PolBud Sp. z o.o.', 'Constructo Group', 'BuildMaster SA', + 'EuroDevelopment', 'Invest Property', 'Metropolitan Construction', + 'Green Building Solutions', 'Nova Inwestycje', 'Prime Estate', + 'TechBuild Industries', 'Horizon Development', 'Skyline Properties', + 'Urban Solutions', 'Future Living', 'Capital Investments' +]; + +// Task templates +const DESIGN_TASKS = [ + { name: 'Wstępne uzgodnienia z klientem', max_wait_days: 7 }, + { name: 'Wizja lokalna i pomiary', max_wait_days: 5 }, + { name: 'Projekt koncepcyjny', max_wait_days: 14 }, + { name: 'Uzgodnienia projektu koncepcyjnego', max_wait_days: 7 }, + { name: 'Projekt budowlany', max_wait_days: 21 }, + { name: 'Projekt wykonawczy', max_wait_days: 21 }, + { name: 'Specyfikacja techniczna', max_wait_days: 10 }, + { name: 'Kosztorys inwestorski', max_wait_days: 7 }, + { name: 'Wniosek o pozwolenie na budowę', max_wait_days: 14 }, + { name: 'Uzyskanie pozwolenia na budowę', max_wait_days: 60 }, + { name: 'Projekt wykonawczy - instalacje', max_wait_days: 21 }, + { name: 'Projekt zagospodarowania terenu', max_wait_days: 14 }, + { name: 'Dokumentacja powykonawcza', max_wait_days: 14 }, +]; + +const CONSTRUCTION_TASKS = [ + { name: 'Przygotowanie placu budowy', max_wait_days: 7 }, + { name: 'Wykopy i fundamenty', max_wait_days: 14 }, + { name: 'Stan zero', max_wait_days: 21 }, + { name: 'Stan surowy otwarty', max_wait_days: 30 }, + { name: 'Stan surowy zamknięty', max_wait_days: 30 }, + { name: 'Instalacje wewnętrzne', max_wait_days: 21 }, + { name: 'Tynki i wylewki', max_wait_days: 14 }, + { name: 'Stolarka okienna i drzwiowa', max_wait_days: 10 }, + { name: 'Wykończenie - malowanie', max_wait_days: 14 }, + { name: 'Wykończenie - podłogi', max_wait_days: 10 }, + { name: 'Instalacje sanitarne', max_wait_days: 14 }, + { name: 'Instalacje elektryczne', max_wait_days: 14 }, + { name: 'Odbiór techniczny', max_wait_days: 7 }, + { name: 'Odbiór końcowy', max_wait_days: 7 }, + { name: 'Przekazanie dokumentacji', max_wait_days: 5 }, +]; + +// Contact types and data +const CONTACT_FIRST_NAMES = ['Jan', 'Piotr', 'Anna', 'Maria', 'Tomasz', 'Krzysztof', 'Agnieszka', 'Magdalena', 'Andrzej', 'Ewa']; +const CONTACT_LAST_NAMES = ['Kowalski', 'Nowak', 'Wiśniewski', 'Lewandowski', 'Kamiński', 'Zieliński', 'Szymański', 'Woźniak', 'Dąbrowski', 'Kozłowski']; +const POSITIONS = ['Kierownik projektu', 'Inżynier', 'Architekt', 'Inspektor nadzoru', 'Przedstawiciel inwestora', 'Dyrektor', 'Koordynator']; + +// Helper functions +function generateId() { + return crypto.randomBytes(16).toString('hex'); +} + +function generateWP() { + const part1 = String(random.integer(100000, 999999)); + const part2 = String(random.integer(1000, 9999)); + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + const part3 = Array(6).fill(0).map(() => chars[random.integer(0, chars.length - 1)]).join(''); + return `${part1}/${part2}/${part3}`; +} + +function generateInvestmentNumber() { + const letter = String.fromCharCode(65 + random.integer(0, 25)); // A-Z + const letters = String.fromCharCode(65 + random.integer(0, 25)) + String.fromCharCode(65 + random.integer(0, 25)); + const number = String(random.integer(1000000, 9999999)); + return `${letter}-${letters}-${number}`; +} + +function generateDate(startDate, endDate) { + const start = new Date(startDate).getTime(); + const end = new Date(endDate).getTime(); + const timestamp = start + random.next() * (end - start); + return new Date(timestamp).toISOString().split('T')[0]; +} + +function addDays(dateStr, days) { + const date = new Date(dateStr); + date.setDate(date.getDate() + days); + return date.toISOString().split('T')[0]; +} + +function generatePhoneNumber() { + return `${random.integer(500, 799)}-${random.integer(100, 999)}-${random.integer(100, 999)}`; +} + +// Clear existing data +function clearData() { + console.log('\n🗑️ Clearing existing data...\n'); + + const tables = [ + 'field_change_history', + 'notifications', + 'audit_logs', + 'file_attachments', + 'notes', + 'project_tasks', + 'task_set_templates', + 'task_sets', + 'tasks', + 'project_contacts', + 'contacts', + 'projects', + 'contracts', + 'password_reset_tokens', + 'sessions', + ]; + + if (!CONFIG.preserveAdmin) { + tables.push('users'); + } + + tables.forEach(table => { + try { + db.prepare(`DELETE FROM ${table}`).run(); + console.log(` ✓ Cleared ${table}`); + } catch (error) { + console.log(` ⚠ Warning clearing ${table}:`, error.message); + } + }); + + // Reset sequences + db.prepare('DELETE FROM sqlite_sequence').run(); + + console.log('\n✅ Data cleared successfully\n'); +} + +// Phase 1: Create Users +function createUsers() { + console.log('\n👥 Creating users...\n'); + + const users = []; + const defaultPassword = bcrypt.hashSync('password123', 10); + + // Keep existing admin if preserveAdmin is true + if (CONFIG.preserveAdmin) { + const existingAdmin = db.prepare('SELECT * FROM users WHERE role = ?').get('admin'); + if (existingAdmin) { + users.push(existingAdmin); + console.log(` ✓ Preserved existing admin: ${existingAdmin.username}`); + } + } + + const newUsers = [ + { name: 'Maria Kowalska', username: 'maria.kowalska', role: 'team_lead' }, + { name: 'Piotr Nowak', username: 'piotr.nowak', role: 'team_lead' }, + { name: 'Anna Wiśniewska', username: 'anna.wisniewska', role: 'project_manager' }, + { name: 'Tomasz Kamiński', username: 'tomasz.kaminski', role: 'project_manager' }, + { name: 'Krzysztof Lewandowski', username: 'krzysztof.lewandowski', role: 'project_manager' }, + { name: 'Agnieszka Zielińska', username: 'agnieszka.zielinska', role: 'user' }, + { name: 'Marek Szymański', username: 'marek.szymanski', role: 'user' }, + { name: 'Ewa Dąbrowska', username: 'ewa.dabrowska', role: 'user' }, + { name: 'Janusz Kozłowski', username: 'janusz.kozlowski', role: 'user' }, + { name: 'Barbara Wojciechowska', username: 'barbara.wojciechowska', role: 'user' }, + { name: 'Viewing Account', username: 'viewer', role: 'read_only' }, + ]; + + newUsers.forEach(userData => { + const userId = generateId(); + + // Generate initials from name + const nameParts = userData.name.trim().split(/\s+/); + const initial = nameParts.map(part => part.charAt(0).toUpperCase()).join(''); + + db.prepare(` + INSERT INTO users (id, name, username, password_hash, role, initial, is_active, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + `).run(userId, userData.name, userData.username, defaultPassword, userData.role, initial); + + users.push({ id: userId, ...userData }); + console.log(` ✓ Created ${userData.role}: ${userData.name} (${userData.username})`); + }); + + console.log(`\n✅ Created ${newUsers.length} new users (Total: ${users.length})\n`); + return users; +} + +// Phase 2: Create Contracts +function createContracts() { + console.log('\n📄 Creating contracts...\n'); + + const contracts = [ + { + number: '2025/FW-001', + name: 'Umowa ramowa - projekty mieszkaniowe 2025', + customer: 'Deweloper Mieszkaniowy Sp. z o.o.', + investor: 'Invest Property Fund', + customerContractNumber: 'DMH/2025/001', + dateSigned: '2025-01-10', + finishDate: '2026-12-31', + }, + { + number: '2025/INF-002', + name: 'Projekty infrastrukturalne miasta', + customer: 'Zarząd Dróg Miejskich', + investor: 'Gmina Miasto', + customerContractNumber: 'ZDM-2025-02-INF', + dateSigned: '2025-02-01', + finishDate: '2026-06-30', + }, + { + number: '2025/COM-003', + name: 'Obiekty komercyjne - centra handlowe', + customer: 'Retail Development Group', + investor: 'Metropolitan Investments', + customerContractNumber: 'RDG/25/COM/03', + dateSigned: '2025-01-15', + finishDate: '2026-09-30', + }, + ]; + + const contractIds = []; + + contracts.forEach((contract, index) => { + const result = db.prepare(` + INSERT INTO contracts ( + contract_number, contract_name, customer_contract_number, + customer, investor, date_signed, finish_date + ) VALUES (?, ?, ?, ?, ?, ?, ?) + `).run( + contract.number, + contract.name, + contract.customerContractNumber, + contract.customer, + contract.investor, + contract.dateSigned, + contract.finishDate + ); + + contractIds.push(result.lastInsertRowid); + console.log(` ✓ Created contract: ${contract.number} - ${contract.name}`); + }); + + console.log(`\n✅ Created ${contracts.length} contracts\n`); + return contractIds; +} + +// Phase 3: Create Projects +function createProjects(contractIds, users) { + console.log('\n🏗️ Creating projects...\n'); + + const projectCount = random.integer(12, 15); + const projects = []; + const projectStatuses = ['registered', 'in_progress_design', 'in_progress_construction', 'fulfilled', 'cancelled']; + const projectTypes = ['design', 'construction', 'design+construction']; + + const usedCities = []; + + for (let i = 0; i < projectCount; i++) { + // Select contract + const contractId = random.choice(contractIds); + const contractInfo = db.prepare('SELECT contract_number FROM contracts WHERE contract_id = ?').get(contractId); + + // Get sequential number for this contract + const existingCount = db.prepare('SELECT COUNT(*) as count FROM projects WHERE contract_id = ?').get(contractId); + const sequenceNumber = existingCount.count + 1; + const projectNumber = `${sequenceNumber}/${contractInfo.contract_number}`; + + // Select city (try to use different cities) + let city; + if (usedCities.length < POLISH_CITIES.length) { + const availableCities = POLISH_CITIES.filter(c => !usedCities.includes(c.name)); + city = random.choice(availableCities); + usedCities.push(city.name); + } else { + city = random.choice(POLISH_CITIES); + } + + // Generate address + const streetType = random.choice(STREET_TYPES); + const streetName = random.choice(STREET_NAMES); + const buildingNumber = random.integer(1, 200); + const address = `${streetType} ${streetName} ${buildingNumber}`; + + // Project name (person or company) + const projectName = random.boolean(0.6) ? random.choice(PERSON_NAMES) : random.choice(COMPANY_NAMES); + + // Project type and status + const projectType = random.choice(projectTypes); + const projectStatus = random.choice(projectStatuses); + + // Dates + const startDate = generateDate('2025-01-01', '2025-12-31'); + const finishDate = addDays(startDate, random.integer(90, 365)); + const completionDate = (projectStatus === 'fulfilled') ? addDays(finishDate, random.integer(-30, 10)) : null; + + // Other fields + const wp = generateWP(); + const investmentNumber = generateInvestmentNumber(); + const plot = `${random.integer(1, 500)}/${random.integer(1, 50)}`; + const district = random.choice(['Centrum', 'Północ', 'Południe', 'Wschód', 'Zachód', 'Śródmieście']); + const unit = random.choice(['A', 'B', 'C', 'D', 'E', '1', '2', '3']); + + // Assign to project manager + const projectManagers = users.filter(u => u.role === 'project_manager'); + const assignedTo = random.choice(projectManagers).id; + const createdBy = random.choice(users.filter(u => u.role === 'admin' || u.role === 'team_lead')).id; + + const wartoscZlecenia = random.integer(100000, 5000000); + + const result = db.prepare(` + INSERT INTO projects ( + contract_id, project_name, project_number, address, plot, district, unit, city, + investment_number, start_date, finish_date, completion_date, wp, + coordinates, project_type, project_status, wartosc_zlecenia, + created_by, assigned_to, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + `).run( + contractId, projectName, projectNumber, address, plot, district, unit, city.name, + investmentNumber, startDate, finishDate, completionDate, wp, + city.coordinates, projectType, projectStatus, wartoscZlecenia, + createdBy, assignedTo + ); + + projects.push({ + id: result.lastInsertRowid, + name: projectName, + number: projectNumber, + type: projectType, + status: projectStatus, + city: city.name, + assignedTo: assignedTo, + createdBy: createdBy, + startDate: startDate, + }); + + console.log(` ✓ ${projectNumber}: ${projectName} (${city.name}) - ${projectStatus}`); + } + + console.log(`\n✅ Created ${projects.length} projects\n`); + return projects; +} + +// Phase 4: Create Task Templates +function createTaskTemplates() { + console.log('\n✅ Creating task templates...\n'); + + const taskIds = { design: [], construction: [] }; + + console.log(' Design tasks:'); + DESIGN_TASKS.forEach(task => { + const result = db.prepare(` + INSERT INTO tasks (name, max_wait_days, is_standard, task_category) + VALUES (?, ?, 1, 'design') + `).run(task.name, task.max_wait_days); + taskIds.design.push(result.lastInsertRowid); + console.log(` ✓ ${task.name}`); + }); + + console.log('\n Construction tasks:'); + CONSTRUCTION_TASKS.forEach(task => { + const result = db.prepare(` + INSERT INTO tasks (name, max_wait_days, is_standard, task_category) + VALUES (?, ?, 1, 'construction') + `).run(task.name, task.max_wait_days); + taskIds.construction.push(result.lastInsertRowid); + console.log(` ✓ ${task.name}`); + }); + + console.log(`\n✅ Created ${DESIGN_TASKS.length + CONSTRUCTION_TASKS.length} task templates\n`); + return taskIds; +} + +// Phase 5: Create Task Sets +function createTaskSets(taskIds) { + console.log('\n📋 Creating task sets...\n'); + + const sets = [ + { + name: 'Standard - Projektowanie', + category: 'design', + tasks: taskIds.design.slice(0, 8), + }, + { + name: 'Pełny zakres - Projektowanie', + category: 'design', + tasks: taskIds.design, + }, + { + name: 'Standard - Budowa', + category: 'construction', + tasks: taskIds.construction.slice(0, 10), + }, + { + name: 'Pełny zakres - Budowa', + category: 'construction', + tasks: taskIds.construction, + }, + ]; + + const setIds = []; + + sets.forEach(set => { + const result = db.prepare(` + INSERT INTO task_sets (name, task_category, created_at, updated_at) + VALUES (?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + `).run(set.name, set.category); + + const setId = result.lastInsertRowid; + setIds.push(setId); + + // Add tasks to set + set.tasks.forEach((taskId, index) => { + db.prepare(` + INSERT INTO task_set_templates (set_id, task_template_id, sort_order) + VALUES (?, ?, ?) + `).run(setId, taskId, index); + }); + + console.log(` ✓ ${set.name} (${set.tasks.length} tasks)`); + }); + + console.log(`\n✅ Created ${sets.length} task sets\n`); + return setIds; +} + +// Phase 6: Create Project Tasks +function createProjectTasks(projects, taskIds, users) { + console.log('\n📝 Creating project tasks...\n'); + + const taskStatuses = ['not_started', 'in_progress', 'completed', 'cancelled']; + const priorities = ['normal', 'low', 'high']; + let totalTasks = 0; + + projects.forEach(project => { + // Select appropriate tasks based on project type + let availableTasks = []; + if (project.type === 'design') { + availableTasks = taskIds.design; + } else if (project.type === 'construction') { + availableTasks = taskIds.construction; + } else { + availableTasks = [...taskIds.design, ...taskIds.construction]; + } + + // Create 3-7 tasks per project + const taskCount = random.integer(3, 7); + const selectedTasks = []; + + // Select random tasks + for (let i = 0; i < taskCount && selectedTasks.length < availableTasks.length; i++) { + let taskId; + do { + taskId = random.choice(availableTasks); + } while (selectedTasks.includes(taskId)); + selectedTasks.push(taskId); + } + + selectedTasks.forEach(taskTemplateId => { + // Determine status based on project status + let status; + if (project.status === 'registered') { + status = 'not_started'; + } else if (project.status === 'fulfilled') { + status = 'completed'; + } else if (project.status === 'cancelled') { + status = random.choice(['not_started', 'cancelled']); + } else { + status = random.choice(taskStatuses.slice(0, 3)); // not_started, in_progress, completed + } + + const priority = random.choice(priorities); + + // Dates + let dateAdded = project.startDate; + let dateStarted = null; + let dateCompleted = null; + + if (status === 'in_progress' || status === 'completed') { + dateStarted = addDays(dateAdded, random.integer(1, 30)); + } + if (status === 'completed') { + dateCompleted = addDays(dateStarted, random.integer(5, 60)); + } + + // Assignment + const regularUsers = users.filter(u => u.role === 'user' || u.role === 'project_manager'); + const assignedTo = random.boolean(0.7) ? random.choice(regularUsers).id : null; + + db.prepare(` + INSERT INTO project_tasks ( + project_id, task_template_id, status, priority, + date_added, date_started, date_completed, + created_by, assigned_to, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + `).run( + project.id, taskTemplateId, status, priority, + dateAdded, dateStarted, dateCompleted, + project.createdBy, assignedTo + ); + + totalTasks++; + }); + + console.log(` ✓ ${project.number}: Created ${taskCount} tasks`); + }); + + console.log(`\n✅ Created ${totalTasks} project tasks\n`); +} + +// Phase 7: Create Contacts +function createContacts(users) { + console.log('\n👤 Creating contacts...\n'); + + const contactTypes = ['project', 'contractor', 'office', 'supplier', 'other']; + const contacts = []; + const contactCount = random.integer(25, 35); + + for (let i = 0; i < contactCount; i++) { + const firstName = random.choice(CONTACT_FIRST_NAMES); + const lastName = random.choice(CONTACT_LAST_NAMES); + const name = `${firstName} ${lastName}`; + const phone = generatePhoneNumber(); + const email = random.boolean(0.6) ? `${firstName.toLowerCase()}.${lastName.toLowerCase()}@example.com` : null; + const company = random.boolean(0.5) ? random.choice(COMPANY_NAMES) : null; + const position = random.boolean(0.7) ? random.choice(POSITIONS) : null; + const contactType = random.choice(contactTypes); + + const result = db.prepare(` + INSERT INTO contacts ( + name, phone, email, company, position, contact_type, is_active, + created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + `).run(name, phone, email, company, position, contactType); + + contacts.push({ + id: result.lastInsertRowid, + name: name, + type: contactType, + }); + } + + console.log(` ✓ Created ${contacts.length} contacts\n`); + return contacts; +} + +// Phase 8: Link Projects to Contacts +function linkProjectContacts(projects, contacts, users) { + console.log('\n🔗 Linking projects to contacts...\n'); + + let linkCount = 0; + + projects.forEach(project => { + // Link 1-4 contacts per project + const contactsToLink = random.integer(1, 4); + const linkedContacts = []; + + for (let i = 0; i < contactsToLink; i++) { + let contact; + do { + contact = random.choice(contacts); + } while (linkedContacts.includes(contact.id)); + + linkedContacts.push(contact.id); + + const isPrimary = i === 0 ? 1 : 0; + const relationshipType = random.choice(['general', 'technical', 'commercial', 'administrative']); + const addedBy = random.choice(users).id; + + try { + db.prepare(` + INSERT INTO project_contacts ( + project_id, contact_id, relationship_type, is_primary, added_by, added_at + ) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP) + `).run(project.id, contact.id, relationshipType, isPrimary, addedBy); + + linkCount++; + } catch (error) { + // Ignore duplicate key errors + } + } + }); + + console.log(` ✓ Created ${linkCount} project-contact links\n`); +} + +// Phase 9: Create Notes +function createNotes(projects, users) { + console.log('\n📝 Creating notes...\n'); + + const noteTemplates = [ + 'Spotkanie z klientem - uzgodniono zakres prac', + 'Wykonano wizję lokalną', + 'Przesłano dokumentację do uzgodnień', + 'Otrzymano uwagi do projektu', + 'Zaktualizowano dokumentację zgodnie z uwagami', + 'Projekt zatwierdzony przez inwestora', + 'Rozpoczęto prace na budowie', + 'Wykonano odbiór częściowy', + 'Zgłoszono problemy techniczne', + 'Problem rozwiązany', + 'Zamówiono materiały', + 'Dostawa materiałów opóźniona', + 'Materiały dostarczone na plac budowy', + ]; + + let noteCount = 0; + + projects.forEach(project => { + // Create 2-6 notes per project + const notesPerProject = random.integer(2, 6); + + for (let i = 0; i < notesPerProject; i++) { + const note = random.choice(noteTemplates); + const createdBy = random.choice(users).id; + const isSystem = random.boolean(0.1) ? 1 : 0; + + // Generate date between project start and now + const noteDate = generateDate(project.startDate, '2026-01-26'); + + db.prepare(` + INSERT INTO notes ( + project_id, note, note_date, is_system, created_by + ) VALUES (?, ?, ?, ?, ?) + `).run(project.id, note, noteDate, isSystem, createdBy); + + noteCount++; + } + }); + + console.log(` ✓ Created ${noteCount} notes\n`); +} + +// Phase 10: Create Audit Logs +function createAuditLogs(users, projects) { + console.log('\n📊 Creating audit logs...\n'); + + const actions = [ + 'user.login', + 'project.create', + 'project.update', + 'project.view', + 'task.create', + 'task.update', + 'task.complete', + 'file.upload', + 'contract.create', + 'contact.create', + ]; + + const ipAddresses = [ + '192.168.1.100', + '192.168.1.101', + '10.0.0.50', + '172.16.0.10', + '83.24.156.78', + ]; + + let logCount = 0; + + // Create 100-200 audit logs + const totalLogs = random.integer(100, 200); + + for (let i = 0; i < totalLogs; i++) { + const user = random.choice(users); + const action = random.choice(actions); + const timestamp = generateDate('2025-01-01', '2026-01-26'); + const ip = random.choice(ipAddresses); + + let resourceType = null; + let resourceId = null; + + if (action.includes('project')) { + resourceType = 'project'; + resourceId = String(random.choice(projects).id); + } + + db.prepare(` + INSERT INTO audit_logs ( + user_id, action, resource_type, resource_id, ip_address, + user_agent, timestamp + ) VALUES (?, ?, ?, ?, ?, ?, ?) + `).run( + user.id, + action, + resourceType, + resourceId, + ip, + 'Mozilla/5.0 (compatible)', + timestamp + ); + + logCount++; + } + + console.log(` ✓ Created ${logCount} audit logs\n`); +} + +// Main execution +async function main() { + console.log('\n╔════════════════════════════════════════════════════════╗'); + console.log('║ Comprehensive Test Data Generator ║'); + console.log('╚════════════════════════════════════════════════════════╝'); + + try { + // Initialize database + console.log('\n🔧 Initializing database schema...'); + initializeDatabase(); + console.log('✅ Database schema ready\n'); + + // Clear existing data + if (CONFIG.clearExistingData) { + clearData(); + } + + // Generate data in phases + const users = createUsers(); + const contractIds = createContracts(); + const projects = createProjects(contractIds, users); + const taskIds = createTaskTemplates(); + const taskSetIds = createTaskSets(taskIds); + createProjectTasks(projects, taskIds, users); + const contacts = createContacts(users); + linkProjectContacts(projects, contacts, users); + createNotes(projects, users); + createAuditLogs(users, projects); + + // Summary + console.log('\n╔════════════════════════════════════════════════════════╗'); + console.log('║ SUMMARY ║'); + console.log('╚════════════════════════════════════════════════════════╝\n'); + + const stats = { + users: db.prepare('SELECT COUNT(*) as count FROM users').get().count, + contracts: db.prepare('SELECT COUNT(*) as count FROM contracts').get().count, + projects: db.prepare('SELECT COUNT(*) as count FROM projects').get().count, + tasks: db.prepare('SELECT COUNT(*) as count FROM tasks').get().count, + taskSets: db.prepare('SELECT COUNT(*) as count FROM task_sets').get().count, + projectTasks: db.prepare('SELECT COUNT(*) as count FROM project_tasks').get().count, + contacts: db.prepare('SELECT COUNT(*) as count FROM contacts').get().count, + projectContacts: db.prepare('SELECT COUNT(*) as count FROM project_contacts').get().count, + notes: db.prepare('SELECT COUNT(*) as count FROM notes').get().count, + auditLogs: db.prepare('SELECT COUNT(*) as count FROM audit_logs').get().count, + }; + + console.log(` 👥 Users: ${stats.users}`); + console.log(` 📄 Contracts: ${stats.contracts}`); + console.log(` 🏗️ Projects: ${stats.projects}`); + console.log(` ✅ Task Templates: ${stats.tasks}`); + console.log(` 📋 Task Sets: ${stats.taskSets}`); + console.log(` 📝 Project Tasks: ${stats.projectTasks}`); + console.log(` 👤 Contacts: ${stats.contacts}`); + console.log(` 🔗 Project-Contacts: ${stats.projectContacts}`); + console.log(` 📝 Notes: ${stats.notes}`); + console.log(` 📊 Audit Logs: ${stats.auditLogs}`); + + console.log('\n✨ Test data generation completed successfully!\n'); + console.log('💡 Default password for all users: password123\n'); + + } catch (error) { + console.error('\n❌ Error:', error.message); + console.error(error.stack); + process.exit(1); + } +} + +main(); diff --git a/src/lib/init-db.js b/src/lib/init-db.js index 5063a38..64a0bdc 100644 --- a/src/lib/init-db.js +++ b/src/lib/init-db.js @@ -397,6 +397,34 @@ export default function initializeDatabase() { console.warn("Migration warning:", e.message); } + // Migration: Add initial column to users table + try { + const columns = db.prepare("PRAGMA table_info(users)").all(); + const hasInitial = columns.some(col => col.name === 'initial'); + + if (!hasInitial) { + // Add initial column + db.exec(`ALTER TABLE users ADD COLUMN initial TEXT;`); + + // Generate initials from existing names + const users = db.prepare('SELECT id, name FROM users WHERE initial IS NULL').all(); + const updateStmt = db.prepare('UPDATE users SET initial = ? WHERE id = ?'); + + users.forEach(user => { + if (user.name) { + // Generate initials from name (e.g., "John Doe" -> "JD") + const nameParts = user.name.trim().split(/\s+/); + const initial = nameParts.map(part => part.charAt(0).toUpperCase()).join(''); + updateStmt.run(initial, user.id); + } + }); + + console.log("✅ Added initial column to users table and generated initials"); + } + } catch (e) { + console.warn("Migration warning:", e.message); + } + // Migration: Rename project_type to task_category in task_sets try { // Check if the old column exists and rename it diff --git a/src/lib/userManagement.js b/src/lib/userManagement.js index 8cf9d82..4d21627 100644 --- a/src/lib/userManagement.js +++ b/src/lib/userManagement.js @@ -11,11 +11,15 @@ export async function createUser({ name, username, password, role = 'user', is_a const passwordHash = await bcrypt.hash(password, 12) const userId = randomBytes(16).toString('hex') + + // Generate initials from name (e.g., "John Doe" -> "JD") + const nameParts = name.trim().split(/\s+/) + const initial = nameParts.map(part => part.charAt(0).toUpperCase()).join('') const result = db.prepare(` - INSERT INTO users (id, name, username, password_hash, role, is_active, can_be_assigned) - VALUES (?, ?, ?, ?, ?, ?, ?) - `).run(userId, name, username, passwordHash, role, is_active ? 1 : 0, can_be_assigned ? 1 : 0) + INSERT INTO users (id, name, username, password_hash, role, initial, is_active, can_be_assigned) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `).run(userId, name, username, passwordHash, role, initial, is_active ? 1 : 0, can_be_assigned ? 1 : 0) return db.prepare(` SELECT id, name, username, role, created_at, updated_at, last_login,