feat: Add comprehensive test data generator and enhance user management with initials
This commit is contained in:
844
scripts/create-comprehensive-test-data.js
Normal file
844
scripts/create-comprehensive-test-data.js
Normal file
@@ -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();
|
||||||
@@ -397,6 +397,34 @@ export default function initializeDatabase() {
|
|||||||
console.warn("Migration warning:", e.message);
|
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
|
// Migration: Rename project_type to task_category in task_sets
|
||||||
try {
|
try {
|
||||||
// Check if the old column exists and rename it
|
// Check if the old column exists and rename it
|
||||||
|
|||||||
@@ -11,11 +11,15 @@ export async function createUser({ name, username, password, role = 'user', is_a
|
|||||||
|
|
||||||
const passwordHash = await bcrypt.hash(password, 12)
|
const passwordHash = await bcrypt.hash(password, 12)
|
||||||
const userId = randomBytes(16).toString('hex')
|
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(`
|
const result = db.prepare(`
|
||||||
INSERT INTO users (id, name, username, password_hash, role, is_active, can_be_assigned)
|
INSERT INTO users (id, name, username, password_hash, role, initial, is_active, can_be_assigned)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`).run(userId, name, username, passwordHash, role, is_active ? 1 : 0, can_be_assigned ? 1 : 0)
|
`).run(userId, name, username, passwordHash, role, initial, is_active ? 1 : 0, can_be_assigned ? 1 : 0)
|
||||||
|
|
||||||
return db.prepare(`
|
return db.prepare(`
|
||||||
SELECT id, name, username, role, created_at, updated_at, last_login,
|
SELECT id, name, username, role, created_at, updated_at, last_login,
|
||||||
|
|||||||
Reference in New Issue
Block a user