Compare commits

...

9 Commits

21 changed files with 2080 additions and 187 deletions

View File

@@ -17,7 +17,9 @@ function exportProjectsToExcel() {
'Adres': project.address || '', 'Adres': project.address || '',
'Działka': project.plot || '', 'Działka': project.plot || '',
'WP': project.wp || '', 'WP': project.wp || '',
'Data zakończenia': project.finish_date || '' 'Data wpływu': project.start_date || '',
'Termin zakończenia': project.finish_date || '',
'Data odbioru': project.completion_date || ''
}); });
return acc; return acc;
}, {}); }, {});

View 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();

View File

@@ -15,7 +15,9 @@ const sampleProjects = [
unit: 'Unit A', unit: 'Unit A',
city: 'Warszawa', city: 'Warszawa',
investment_number: 'INV-2025-001', investment_number: 'INV-2025-001',
start_date: '2025-01-15',
finish_date: '2025-06-30', finish_date: '2025-06-30',
completion_date: null,
wp: 'WP-001', wp: 'WP-001',
contact: 'Jan Kowalski, tel. 123-456-789', contact: 'Jan Kowalski, tel. 123-456-789',
notes: 'Modern residential building with 50 apartments', notes: 'Modern residential building with 50 apartments',
@@ -32,7 +34,9 @@ const sampleProjects = [
unit: 'Unit B', unit: 'Unit B',
city: 'Warszawa', city: 'Warszawa',
investment_number: 'INV-2025-002', investment_number: 'INV-2025-002',
start_date: '2025-02-01',
finish_date: '2025-09-15', finish_date: '2025-09-15',
completion_date: null,
wp: 'WP-002', wp: 'WP-002',
contact: 'Anna Nowak, tel. 987-654-321', contact: 'Anna Nowak, tel. 987-654-321',
notes: 'Commercial office space, 10 floors', notes: 'Commercial office space, 10 floors',
@@ -49,7 +53,9 @@ const sampleProjects = [
unit: 'Unit C', unit: 'Unit C',
city: 'Kraków', city: 'Kraków',
investment_number: 'INV-2025-003', investment_number: 'INV-2025-003',
start_date: '2025-01-10',
finish_date: '2025-12-20', finish_date: '2025-12-20',
completion_date: null,
wp: 'WP-003', wp: 'WP-003',
contact: 'Piotr Wiśniewski, tel. 555-123-456', contact: 'Piotr Wiśniewski, tel. 555-123-456',
notes: 'Large shopping center with parking', notes: 'Large shopping center with parking',
@@ -66,7 +72,9 @@ const sampleProjects = [
unit: 'Unit D', unit: 'Unit D',
city: 'Łódź', city: 'Łódź',
investment_number: 'INV-2025-004', investment_number: 'INV-2025-004',
start_date: '2024-11-01',
finish_date: '2025-08-10', finish_date: '2025-08-10',
completion_date: '2025-08-05',
wp: 'WP-004', wp: 'WP-004',
contact: 'Maria Lewandowska, tel. 444-789-012', contact: 'Maria Lewandowska, tel. 444-789-012',
notes: 'Logistics warehouse facility', notes: 'Logistics warehouse facility',
@@ -83,7 +91,9 @@ const sampleProjects = [
unit: 'Unit E', unit: 'Unit E',
city: 'Gdańsk', city: 'Gdańsk',
investment_number: 'INV-2025-005', investment_number: 'INV-2025-005',
start_date: '2025-01-20',
finish_date: '2025-11-05', finish_date: '2025-11-05',
completion_date: null,
wp: 'WP-005', wp: 'WP-005',
contact: 'Tomasz Malinowski, tel. 333-456-789', contact: 'Tomasz Malinowski, tel. 333-456-789',
notes: 'Luxury hotel with conference facilities', notes: 'Luxury hotel with conference facilities',
@@ -100,7 +110,9 @@ const sampleProjects = [
unit: 'Unit F', unit: 'Unit F',
city: 'Poznań', city: 'Poznań',
investment_number: 'INV-2025-006', investment_number: 'INV-2025-006',
start_date: '2025-02-10',
finish_date: '2025-07-20', finish_date: '2025-07-20',
completion_date: null,
wp: 'WP-006', wp: 'WP-006',
contact: 'Ewa Dombrowska, tel. 222-333-444', contact: 'Ewa Dombrowska, tel. 222-333-444',
notes: 'Modern educational facility with sports complex', notes: 'Modern educational facility with sports complex',
@@ -117,7 +129,9 @@ const sampleProjects = [
unit: 'Unit G', unit: 'Unit G',
city: 'Wrocław', city: 'Wrocław',
investment_number: 'INV-2025-007', investment_number: 'INV-2025-007',
start_date: '2024-12-15',
finish_date: '2025-10-30', finish_date: '2025-10-30',
completion_date: null,
wp: 'WP-007', wp: 'WP-007',
contact: 'Dr. Marek Szymankowski, tel. 111-222-333', contact: 'Dr. Marek Szymankowski, tel. 111-222-333',
notes: 'Specialized medical center with emergency department', notes: 'Specialized medical center with emergency department',
@@ -134,7 +148,9 @@ const sampleProjects = [
unit: 'Unit H', unit: 'Unit H',
city: 'Szczecin', city: 'Szczecin',
investment_number: 'INV-2025-008', investment_number: 'INV-2025-008',
start_date: '2024-09-01',
finish_date: '2025-05-15', finish_date: '2025-05-15',
completion_date: '2025-05-12',
wp: 'WP-008', wp: 'WP-008',
contact: 'Katarzyna Wojcik, tel. 999-888-777', contact: 'Katarzyna Wojcik, tel. 999-888-777',
notes: 'Multi-purpose sports stadium with seating for 20,000', notes: 'Multi-purpose sports stadium with seating for 20,000',
@@ -151,7 +167,9 @@ const sampleProjects = [
unit: 'Unit I', unit: 'Unit I',
city: 'Lublin', city: 'Lublin',
investment_number: 'INV-2025-009', investment_number: 'INV-2025-009',
start_date: '2025-01-05',
finish_date: '2025-08-25', finish_date: '2025-08-25',
completion_date: null,
wp: 'WP-009', wp: 'WP-009',
contact: 'Prof. Andrzej Kowalewski, tel. 777-666-555', contact: 'Prof. Andrzej Kowalewski, tel. 777-666-555',
notes: 'Modern library with digital archives and community spaces', notes: 'Modern library with digital archives and community spaces',
@@ -174,9 +192,9 @@ sampleProjects.forEach((projectData, index) => {
const result = db.prepare(` const result = db.prepare(`
INSERT INTO projects ( INSERT INTO projects (
contract_id, project_name, project_number, address, plot, district, unit, city, contract_id, project_name, project_number, address, plot, district, unit, city,
investment_number, finish_date, wp, contact, notes, coordinates, investment_number, start_date, finish_date, completion_date, wp, contact, notes, coordinates,
project_type, project_status, created_at, updated_at project_type, project_status, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`).run( `).run(
projectData.contract_id, projectData.contract_id,
projectData.project_name, projectData.project_name,
@@ -187,7 +205,9 @@ sampleProjects.forEach((projectData, index) => {
projectData.unit, projectData.unit,
projectData.city, projectData.city,
projectData.investment_number, projectData.investment_number,
projectData.start_date,
projectData.finish_date, projectData.finish_date,
projectData.completion_date,
projectData.wp, projectData.wp,
projectData.contact, projectData.contact,
projectData.notes, projectData.notes,

View File

@@ -1,6 +1,6 @@
import db from "@/lib/db"; import db from "@/lib/db";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { withReadAuth, withUserAuth } from "@/lib/middleware/auth"; import { withReadAuth, withTeamLeadAuth, withUserAuth } from "@/lib/middleware/auth";
async function getContractHandler(req, { params }) { async function getContractHandler(req, { params }) {
const { id } = await params; const { id } = await params;
@@ -21,6 +21,79 @@ async function getContractHandler(req, { params }) {
return NextResponse.json(contract); return NextResponse.json(contract);
} }
async function updateContractHandler(req, { params }) {
const { id } = await params;
try {
const body = await req.json();
const {
contract_number,
contract_name,
customer_contract_number,
customer,
investor,
date_signed,
finish_date,
} = body;
// Check if contract exists
const existingContract = db
.prepare("SELECT * FROM contracts WHERE contract_id = ?")
.get(id);
if (!existingContract) {
return NextResponse.json(
{ error: "Contract not found" },
{ status: 404 }
);
}
// Update the contract
const result = db
.prepare(
`UPDATE contracts
SET contract_number = ?,
contract_name = ?,
customer_contract_number = ?,
customer = ?,
investor = ?,
date_signed = ?,
finish_date = ?
WHERE contract_id = ?`
)
.run(
contract_number,
contract_name || null,
customer_contract_number || null,
customer || null,
investor || null,
date_signed || null,
finish_date || null,
id
);
if (result.changes === 0) {
return NextResponse.json(
{ error: "Failed to update contract" },
{ status: 500 }
);
}
// Fetch and return the updated contract
const updatedContract = db
.prepare("SELECT * FROM contracts WHERE contract_id = ?")
.get(id);
return NextResponse.json(updatedContract);
} catch (error) {
console.error("Error updating contract:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
async function deleteContractHandler(req, { params }) { async function deleteContractHandler(req, { params }) {
const { id } = params; const { id } = params;
@@ -61,4 +134,5 @@ async function deleteContractHandler(req, { params }) {
// Protected routes - require authentication // Protected routes - require authentication
export const GET = withReadAuth(getContractHandler); export const GET = withReadAuth(getContractHandler);
export const DELETE = withUserAuth(deleteContractHandler); export const PUT = withUserAuth(updateContractHandler);
export const DELETE = withTeamLeadAuth(deleteContractHandler);

View File

@@ -35,6 +35,9 @@ export async function GET(request) {
}; };
}); });
// Calculate values by contract
const contractSummary = {};
projects.forEach(project => { projects.forEach(project => {
const value = parseFloat(project.wartosc_zlecenia) || 0; const value = parseFloat(project.wartosc_zlecenia) || 0;
const type = project.project_type; const type = project.project_type;
@@ -46,6 +49,26 @@ export async function GET(request) {
} else if (project.wartosc_zlecenia && project.project_status !== 'cancelled') { } else if (project.wartosc_zlecenia && project.project_status !== 'cancelled') {
typeSummary[type].unrealisedValue += value; typeSummary[type].unrealisedValue += value;
} }
// Group by contract
if (project.contract_number && project.wartosc_zlecenia && project.project_status !== 'cancelled') {
const contractKey = project.contract_number;
if (!contractSummary[contractKey]) {
contractSummary[contractKey] = {
contract_name: project.contract_name || project.contract_number,
realisedValue: 0,
unrealisedValue: 0,
totalValue: 0
};
}
if (project.project_status === 'fulfilled' && project.completion_date) {
contractSummary[contractKey].realisedValue += value;
} else {
contractSummary[contractKey].unrealisedValue += value;
}
contractSummary[contractKey].totalValue += value;
}
}); });
// Calculate overall totals // Calculate overall totals
@@ -132,6 +155,26 @@ export async function GET(request) {
realisedValue: 158000, realisedValue: 158000,
unrealisedValue: 242000 unrealisedValue: 242000
} }
},
byContract: {
'UMK/001/2024': {
contract_name: 'Modernizacja budynku głównego',
realisedValue: 320000,
unrealisedValue: 180000,
totalValue: 500000
},
'UMK/002/2024': {
contract_name: 'Budowa parkingu wielopoziomowego',
realisedValue: 480000,
unrealisedValue: 320000,
totalValue: 800000
},
'UMK/003/2024': {
contract_name: 'Remont elewacji',
realisedValue: 158000,
unrealisedValue: 242000,
totalValue: 400000
}
} }
}; };
} else { } else {
@@ -251,6 +294,17 @@ export async function GET(request) {
unrealisedValue: Math.round(data.unrealisedValue) unrealisedValue: Math.round(data.unrealisedValue)
} }
]) ])
),
byContract: Object.fromEntries(
Object.entries(contractSummary).map(([contractNumber, data]) => [
contractNumber,
{
contract_name: data.contract_name,
realisedValue: Math.round(data.realisedValue),
unrealisedValue: Math.round(data.unrealisedValue),
totalValue: Math.round(data.totalValue)
}
])
) )
} }
}); });

View File

@@ -11,7 +11,7 @@ import { logFieldChange } from "@/lib/queries/fieldHistory";
import { addNoteToProject } from "@/lib/queries/notes"; import { addNoteToProject } from "@/lib/queries/notes";
import initializeDatabase from "@/lib/init-db"; import initializeDatabase from "@/lib/init-db";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { withReadAuth, withUserAuth } from "@/lib/middleware/auth"; import { withReadAuth, withUserAuth, withTeamLeadAuth } from "@/lib/middleware/auth";
import { import {
logApiActionSafe, logApiActionSafe,
AUDIT_ACTIONS, AUDIT_ACTIONS,
@@ -155,4 +155,4 @@ async function deleteProjectHandler(req, { params }) {
// Protected routes - require authentication // Protected routes - require authentication
export const GET = withReadAuth(getProjectHandler); export const GET = withReadAuth(getProjectHandler);
export const PUT = withUserAuth(updateProjectHandler); export const PUT = withUserAuth(updateProjectHandler);
export const DELETE = withUserAuth(deleteProjectHandler); export const DELETE = withTeamLeadAuth(deleteProjectHandler);

View File

@@ -207,7 +207,10 @@ export default function ContactsPage() {
{/* Stats */} {/* Stats */}
{stats && ( {stats && (
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-4 mb-6"> <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-4 mb-6">
<Card> <Card
className={`cursor-pointer transition-all hover:shadow-lg ${typeFilter === 'all' ? 'ring-2 ring-gray-900' : ''}`}
onClick={() => setTypeFilter('all')}
>
<CardContent className="p-4"> <CardContent className="p-4">
<div className="text-2xl font-bold text-gray-900"> <div className="text-2xl font-bold text-gray-900">
{stats.total_contacts} {stats.total_contacts}
@@ -215,7 +218,10 @@ export default function ContactsPage() {
<div className="text-sm text-gray-600">Wszystkie</div> <div className="text-sm text-gray-600">Wszystkie</div>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card
className={`cursor-pointer transition-all hover:shadow-lg ${typeFilter === 'project' ? 'ring-2 ring-blue-600' : ''}`}
onClick={() => setTypeFilter('project')}
>
<CardContent className="p-4"> <CardContent className="p-4">
<div className="text-2xl font-bold text-blue-600"> <div className="text-2xl font-bold text-blue-600">
{stats.project_contacts} {stats.project_contacts}
@@ -223,7 +229,10 @@ export default function ContactsPage() {
<div className="text-sm text-gray-600">Projekty</div> <div className="text-sm text-gray-600">Projekty</div>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card
className={`cursor-pointer transition-all hover:shadow-lg ${typeFilter === 'contractor' ? 'ring-2 ring-orange-600' : ''}`}
onClick={() => setTypeFilter('contractor')}
>
<CardContent className="p-4"> <CardContent className="p-4">
<div className="text-2xl font-bold text-orange-600"> <div className="text-2xl font-bold text-orange-600">
{stats.contractor_contacts} {stats.contractor_contacts}
@@ -231,7 +240,10 @@ export default function ContactsPage() {
<div className="text-sm text-gray-600">Wykonawcy</div> <div className="text-sm text-gray-600">Wykonawcy</div>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card
className={`cursor-pointer transition-all hover:shadow-lg ${typeFilter === 'office' ? 'ring-2 ring-purple-600' : ''}`}
onClick={() => setTypeFilter('office')}
>
<CardContent className="p-4"> <CardContent className="p-4">
<div className="text-2xl font-bold text-purple-600"> <div className="text-2xl font-bold text-purple-600">
{stats.office_contacts} {stats.office_contacts}
@@ -239,7 +251,10 @@ export default function ContactsPage() {
<div className="text-sm text-gray-600">Urzędy</div> <div className="text-sm text-gray-600">Urzędy</div>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card
className={`cursor-pointer transition-all hover:shadow-lg ${typeFilter === 'supplier' ? 'ring-2 ring-green-600' : ''}`}
onClick={() => setTypeFilter('supplier')}
>
<CardContent className="p-4"> <CardContent className="p-4">
<div className="text-2xl font-bold text-green-600"> <div className="text-2xl font-bold text-green-600">
{stats.supplier_contacts} {stats.supplier_contacts}
@@ -247,7 +262,10 @@ export default function ContactsPage() {
<div className="text-sm text-gray-600">Dostawcy</div> <div className="text-sm text-gray-600">Dostawcy</div>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card
className={`cursor-pointer transition-all hover:shadow-lg ${typeFilter === 'other' ? 'ring-2 ring-gray-600' : ''}`}
onClick={() => setTypeFilter('other')}
>
<CardContent className="p-4"> <CardContent className="p-4">
<div className="text-2xl font-bold text-gray-600"> <div className="text-2xl font-bold text-gray-600">
{stats.other_contacts} {stats.other_contacts}

View File

@@ -0,0 +1,139 @@
"use client";
import { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import ContractForm from "@/components/ContractForm";
import PageContainer from "@/components/ui/PageContainer";
import PageHeader from "@/components/ui/PageHeader";
import Button from "@/components/ui/Button";
import Link from "next/link";
import { LoadingState } from "@/components/ui/States";
import { useTranslation } from "@/lib/i18n";
export default function EditContractPage() {
const params = useParams();
const id = params.id;
const [contract, setContract] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const { t } = useTranslation();
useEffect(() => {
async function fetchContract() {
setLoading(true);
setError(null);
try {
const res = await fetch(`/api/contracts/${id}`);
if (!res.ok) {
throw new Error("Failed to fetch contract");
}
const data = await res.json();
setContract(data);
} catch (error) {
console.error("Error fetching contract:", error);
setError("Nie udało się pobrać danych umowy.");
} finally {
setLoading(false);
}
}
if (id) {
fetchContract();
}
}, [id]);
if (loading) {
return (
<PageContainer>
<PageHeader
title={t('contracts.editContract')}
description={t('contracts.editContractDescription')}
/>
<LoadingState message={t('navigation.loading')} />
</PageContainer>
);
}
if (error || !contract) {
return (
<PageContainer>
<PageHeader
title={t('contracts.editContract')}
description={t('contracts.editContractDescription')}
/>
<div className="bg-red-50 border border-red-200 rounded-lg p-4 flex items-start mb-6">
<svg
className="w-5 h-5 text-red-600 mr-3 mt-0.5 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<div className="flex-1">
<p className="text-sm font-medium text-red-800">
{error || "Nie znaleziono umowy."}
</p>
</div>
</div>
<Link href="/contracts">
<Button variant="outline">
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
{t('contracts.backToContracts')}
</Button>
</Link>
</PageContainer>
);
}
return (
<PageContainer>
<PageHeader
title={t('contracts.editContract')}
description={`${t('contracts.editing')} ${contract.contract_number}`}
action={
<Link href={`/contracts/${id}`}>
<Button variant="outline" size="sm">
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
{t('contracts.backToContract')}
</Button>
</Link>
}
/>
<div className="max-w-2xl">
<ContractForm initialData={contract} />
</div>
</PageContainer>
);
}

View File

@@ -113,6 +113,24 @@ export default function ContractDetailsPage() {
{t('contracts.backToContracts')} {t('contracts.backToContracts')}
</Button> </Button>
</Link> </Link>
<Link href={`/contracts/${contractId}/edit`}>
<Button variant="outline" size="sm">
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/>
</svg>
{t('contracts.editContract')}
</Button>
</Link>
<Link href={`/projects/new?contract_id=${contractId}`}> <Link href={`/projects/new?contract_id=${contractId}`}>
<Button variant="primary" size="sm"> <Button variant="primary" size="sm">
<svg <svg

View File

@@ -2,6 +2,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { useSession } from "next-auth/react";
import { Card, CardHeader, CardContent } from "@/components/ui/Card"; import { Card, CardHeader, CardContent } from "@/components/ui/Card";
import Button from "@/components/ui/Button"; import Button from "@/components/ui/Button";
import Badge from "@/components/ui/Badge"; import Badge from "@/components/ui/Badge";
@@ -15,6 +16,7 @@ import { useTranslation } from "@/lib/i18n";
export default function ContractsMainPage() { export default function ContractsMainPage() {
const { t } = useTranslation(); const { t } = useTranslation();
const { data: session } = useSession();
const [contracts, setContracts] = useState([]); const [contracts, setContracts] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
@@ -22,17 +24,27 @@ export default function ContractsMainPage() {
const [sortBy, setSortBy] = useState("date_signed"); const [sortBy, setSortBy] = useState("date_signed");
const [sortOrder, setSortOrder] = useState("desc"); const [sortOrder, setSortOrder] = useState("desc");
const [statusFilter, setStatusFilter] = useState("all"); const [statusFilter, setStatusFilter] = useState("all");
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [contractToDelete, setContractToDelete] = useState(null);
const [deleting, setDeleting] = useState(false);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(null);
useEffect(() => { useEffect(() => {
async function fetchContracts() { async function fetchContracts() {
setLoading(true); setLoading(true);
setError(null);
try { try {
const res = await fetch("/api/contracts"); const res = await fetch("/api/contracts");
if (!res.ok) {
throw new Error("Failed to fetch contracts");
}
const data = await res.json(); const data = await res.json();
setContracts(data); setContracts(data);
setFilteredContracts(data); setFilteredContracts(data);
} catch (error) { } catch (error) {
console.error("Error fetching contracts:", error); console.error("Error fetching contracts:", error);
setError("Nie udało się pobrać listy umów. Spróbuj ponownie później.");
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -93,27 +105,6 @@ export default function ContractsMainPage() {
setFilteredContracts(filtered); setFilteredContracts(filtered);
}, [searchTerm, contracts, sortBy, sortOrder, statusFilter]); }, [searchTerm, contracts, sortBy, sortOrder, statusFilter]);
async function handleDelete(id) {
const confirmed = confirm("Czy na pewno chcesz usunąć tę umowę?");
if (!confirmed) return;
try {
const res = await fetch(`/api/contracts/${id}`, {
method: "DELETE",
});
if (res.ok) {
setContracts(contracts.filter((c) => c.contract_id !== id));
} else {
alert("Błąd podczas usuwania umowy.");
}
} catch (error) {
console.error("Error deleting contract:", error);
alert("Błąd podczas usuwania umowy.");
}
}
// Get contract statistics
const getContractStats = () => { const getContractStats = () => {
const currentDate = new Date(); const currentDate = new Date();
const total = contracts.length; const total = contracts.length;
@@ -148,25 +139,50 @@ export default function ContractsMainPage() {
} }
}; };
async function handleDelete(id) { const initiateDelete = (contract) => {
const confirmed = confirm("Czy na pewno chcesz usunąć tę umowę?"); setContractToDelete(contract);
if (!confirmed) return; setShowDeleteModal(true);
};
const handleDelete = async () => {
if (!contractToDelete) return;
setDeleting(true);
setError(null);
try { try {
const res = await fetch(`/api/contracts/${id}`, { const res = await fetch(`/api/contracts/${contractToDelete.contract_id}`, {
method: "DELETE", method: "DELETE",
}); });
const data = await res.json();
if (res.ok) { if (res.ok) {
setContracts(contracts.filter((c) => c.contract_id !== id)); setContracts(contracts.filter((c) => c.contract_id !== contractToDelete.contract_id));
setSuccess(`Umowa "${contractToDelete.contract_number}" została usunięta.`);
setShowDeleteModal(false);
setContractToDelete(null);
// Auto-hide success message after 5 seconds
setTimeout(() => setSuccess(null), 5000);
} else { } else {
alert("Błąd podczas usuwania umowy."); setError(data.error || "Nie udało się usunąć umowy.");
} }
} catch (error) { } catch (error) {
console.error("Error deleting contract:", error); console.error("Error deleting contract:", error);
alert("Błąd podczas usuwania umowy."); setError("Wystąpił błąd podczas usuwania umowy. Spróbuj ponownie.");
} finally {
setDeleting(false);
} }
};
const cancelDelete = () => {
if (!deleting) {
setShowDeleteModal(false);
setContractToDelete(null);
setError(null);
} }
};
const handleSearchChange = (e) => { const handleSearchChange = (e) => {
setSearchTerm(e.target.value); setSearchTerm(e.target.value);
@@ -264,6 +280,67 @@ export default function ContractsMainPage() {
</Button> </Button>
</Link>{" "} </Link>{" "}
</PageHeader> </PageHeader>
{/* Success Message */}
{success && (
<div className="mb-6 bg-green-50 border border-green-200 rounded-lg p-4 flex items-start">
<svg
className="w-5 h-5 text-green-600 mr-3 mt-0.5 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<div className="flex-1">
<p className="text-sm font-medium text-green-800">{success}</p>
</div>
<button
onClick={() => setSuccess(null)}
className="text-green-600 hover:text-green-800 ml-3"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
)}
{/* Error Message */}
{error && (
<div className="mb-6 bg-red-50 border border-red-200 rounded-lg p-4 flex items-start">
<svg
className="w-5 h-5 text-red-600 mr-3 mt-0.5 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<div className="flex-1">
<p className="text-sm font-medium text-red-800">{error}</p>
</div>
<button
onClick={() => setError(null)}
className="text-red-600 hover:text-red-800 ml-3"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
)}
{/* Statistics Cards */} {/* Statistics Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6"> <div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6">
<Card> <Card>
@@ -573,10 +650,11 @@ export default function ContractsMainPage() {
</svg> </svg>
Szczegóły Szczegóły
</Link> </Link>
{session?.user?.role === 'team_lead' && (
<Button <Button
variant="danger" variant="danger"
size="sm" size="sm"
onClick={() => handleDelete(contract.contract_id)} onClick={() => initiateDelete(contract)}
> >
<svg <svg
className="w-4 h-4 mr-1" className="w-4 h-4 mr-1"
@@ -593,6 +671,7 @@ export default function ContractsMainPage() {
</svg> </svg>
Usuń Usuń
</Button> </Button>
)}
</div> </div>
</div> </div>
</CardContent> </CardContent>
@@ -620,6 +699,124 @@ export default function ContractsMainPage() {
</p>{" "} </p>{" "}
</div> </div>
)} )}
{/* Delete Confirmation Modal */}
{showDeleteModal && contractToDelete && (
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999]"
onClick={(e) => e.target === e.currentTarget && !deleting && cancelDelete()}
>
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 w-full max-w-md mx-4">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center">
<div className="flex-shrink-0 w-10 h-10 bg-red-100 rounded-full flex items-center justify-center">
<svg
className="w-6 h-6 text-red-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
</div>
<h3 className="ml-3 text-lg font-semibold text-gray-900 dark:text-white">
Potwierdź usunięcie
</h3>
</div>
{!deleting && (
<button
onClick={cancelDelete}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
</div>
<div className="mb-6">
<p className="text-gray-700 dark:text-gray-300 mb-3">
Czy na pewno chcesz usunąć umowę <strong className="font-semibold">"{contractToDelete.contract_number}"</strong>
{contractToDelete.contract_name && (
<> <strong className="font-semibold">{contractToDelete.contract_name}</strong></>
)}?
</p>
<p className="text-sm text-red-600 dark:text-red-400">
Ta operacja jest nieodwracalna.
</p>
{contractToDelete.customer && (
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
Zleceniodawca: <strong>{contractToDelete.customer}</strong>
</p>
)}
</div>
<div className="flex gap-3 justify-end">
<Button
variant="outline"
onClick={cancelDelete}
disabled={deleting}
>
Anuluj
</Button>
<Button
variant="danger"
onClick={handleDelete}
disabled={deleting}
>
{deleting ? (
<>
<svg
className="animate-spin -ml-1 mr-2 h-4 w-4 text-white inline-block"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
Usuwanie...
</>
) : (
<>
<svg
className="w-4 h-4 mr-2 inline-block"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
Usuń umowę
</>
)}
</Button>
</div>
</div>
</div>
)}
</PageContainer> </PageContainer>
); );
} }

View File

@@ -284,6 +284,87 @@ export default function TeamLeadsDashboard() {
</div> </div>
</div> </div>
</div> </div>
{/* By Contract Section */}
{summaryData?.byContract && Object.keys(summaryData.byContract).length > 0 && (
<div className="mt-8">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-6">
{t('teamDashboard.byContract')}
</h2>
<div className="h-96">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={Object.entries(summaryData.byContract).map(([contractNumber, data]) => ({
name: contractNumber,
fullName: data.contract_name,
realised: data.realisedValue,
unrealised: data.unrealisedValue,
total: data.totalValue
}))}
margin={{
top: 20,
right: 30,
left: 20,
bottom: 100,
}}
>
<CartesianGrid strokeDasharray="3 3" className="opacity-30" />
<XAxis
dataKey="name"
angle={-45}
textAnchor="end"
height={100}
className="text-gray-600 dark:text-gray-400"
fontSize={11}
/>
<YAxis
className="text-gray-600 dark:text-gray-400"
fontSize={12}
tickFormatter={(value) => `${(value / 1000).toFixed(0)}k`}
/>
<Tooltip
content={({ active, payload }) => {
if (active && payload && payload.length) {
return (
<div className="bg-white dark:bg-gray-800 p-3 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg">
<p className="font-medium text-gray-900 dark:text-white mb-2">{payload[0].payload.fullName}</p>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">{payload[0].payload.name}</p>
<p className="text-green-600 dark:text-green-400 text-sm">
{`${t('teamDashboard.realised')}: ${formatCurrency(payload[0].payload.realised)}`}
</p>
<p className="text-purple-600 dark:text-purple-400 text-sm">
{`${t('teamDashboard.unrealised')}: ${formatCurrency(payload[0].payload.unrealised)}`}
</p>
<p className="text-blue-600 dark:text-blue-400 text-sm font-semibold mt-1">
{`${t('teamDashboard.total')}: ${formatCurrency(payload[0].payload.total)}`}
</p>
</div>
);
}
return null;
}}
/>
<Legend />
<Bar
dataKey="realised"
stackId="a"
fill="#10b981"
name={t('teamDashboard.realised')}
radius={[0, 0, 0, 0]}
/>
<Bar
dataKey="unrealised"
stackId="a"
fill="#8b5cf6"
name={t('teamDashboard.unrealised')}
radius={[4, 4, 0, 0]}
/>
</BarChart>
</ResponsiveContainer>
</div>
</div>
)}
</div> </div>
</PageContainer> </PageContainer>
); );

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { useEffect, useState, useRef } from "react"; import { useEffect, useState, useRef } from "react";
import { useParams } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import ProjectForm from "@/components/ProjectForm"; import ProjectForm from "@/components/ProjectForm";
import PageContainer from "@/components/ui/PageContainer"; import PageContainer from "@/components/ui/PageContainer";
import PageHeader from "@/components/ui/PageHeader"; import PageHeader from "@/components/ui/PageHeader";
@@ -9,16 +9,44 @@ import Button from "@/components/ui/Button";
import Link from "next/link"; import Link from "next/link";
import { LoadingState } from "@/components/ui/States"; import { LoadingState } from "@/components/ui/States";
import { useTranslation } from "@/lib/i18n"; import { useTranslation } from "@/lib/i18n";
import { useSession } from "next-auth/react";
export default function EditProjectPage() { export default function EditProjectPage() {
const params = useParams(); const params = useParams();
const router = useRouter();
const id = params.id; const id = params.id;
const [project, setProject] = useState(null); const [project, setProject] = useState(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [deleting, setDeleting] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const { data: session } = useSession();
const formRef = useRef(); const formRef = useRef();
const handleDelete = async () => {
setDeleting(true);
try {
const res = await fetch(`/api/projects/${id}`, {
method: 'DELETE',
});
if (res.ok) {
router.push('/projects');
} else {
const data = await res.json();
alert(data.error || 'Błąd podczas usuwania projektu');
setDeleting(false);
setShowDeleteModal(false);
}
} catch (error) {
console.error('Error deleting project:', error);
alert('Błąd podczas usuwania projektu');
setDeleting(false);
setShowDeleteModal(false);
}
};
useEffect(() => { useEffect(() => {
const fetchProject = async () => { const fetchProject = async () => {
try { try {
@@ -130,7 +158,159 @@ export default function EditProjectPage() {
/> />
<div className="max-w-2xl"> <div className="max-w-2xl">
<ProjectForm ref={formRef} initialData={project} /> <ProjectForm ref={formRef} initialData={project} />
{/* Delete Button - Only for team_lead */}
{session?.user?.role === 'team_lead' && (
<div className="mt-8 pt-6 border-t border-gray-200">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-gray-900">
Usuwanie projektu
</h3>
<p className="mt-1 text-sm text-gray-500">
Operacja nieodwracalna. Wszystkie powiązane dane zostaną trwale usunięte.
</p>
</div> </div>
<Button
variant="danger"
size="sm"
onClick={() => setShowDeleteModal(true)}
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
Usuń projekt
</Button>
</div>
</div>
)}
</div>
{/* Delete Confirmation Modal */}
{showDeleteModal && (
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999]"
onClick={(e) => e.target === e.currentTarget && !deleting && setShowDeleteModal(false)}
>
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 w-full max-w-md mx-4">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center">
<div className="flex-shrink-0 w-10 h-10 bg-red-100 rounded-full flex items-center justify-center">
<svg
className="w-6 h-6 text-red-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
</div>
<h3 className="ml-3 text-lg font-semibold text-gray-900 dark:text-white">
Potwierdź usunięcie
</h3>
</div>
{!deleting && (
<button
onClick={() => setShowDeleteModal(false)}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
</div>
<div className="mb-6">
<p className="text-gray-700 dark:text-gray-300 mb-3">
Czy na pewno chcesz usunąć projekt <strong className="font-semibold">"{project?.project_name}"</strong>?
</p>
<p className="text-sm text-red-600 dark:text-red-400">
Ta operacja jest nieodwracalna. Zostaną usunięte wszystkie powiązane dane, w tym:
</p>
<ul className="mt-2 text-sm text-gray-600 dark:text-gray-400 list-disc list-inside space-y-1">
<li>Notatki projektu</li>
<li>Załączone pliki</li>
<li>Zadania projektu</li>
<li>Historia zmian</li>
</ul>
</div>
<div className="flex gap-3 justify-end">
<Button
variant="outline"
onClick={() => setShowDeleteModal(false)}
disabled={deleting}
>
Anuluj
</Button>
<Button
variant="danger"
onClick={handleDelete}
disabled={deleting}
>
{deleting ? (
<>
<svg
className="animate-spin -ml-1 mr-2 h-4 w-4 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Usuwanie...
</>
) : (
<>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
Tak, usuń projekt
</>
)}
</Button>
</div>
</div>
</div>
)}
</PageContainer> </PageContainer>
); );
} }

View File

@@ -33,6 +33,23 @@ export default function ProjectViewPage() {
const [editText, setEditText] = useState(''); const [editText, setEditText] = useState('');
const [projectContacts, setProjectContacts] = useState([]); const [projectContacts, setProjectContacts] = useState([]);
const [showDocumentModal, setShowDocumentModal] = useState(false); const [showDocumentModal, setShowDocumentModal] = useState(false);
const [copied, setCopied] = useState(false);
// Helper function to copy WP/Investment number to clipboard
const handleCopyReference = async () => {
const wp = project.wp || '';
const investmentNumber = project.investment_number ? project.investment_number.split('-').pop() : "";
const reference = `${wp}/${investmentNumber}`;
try {
await navigator.clipboard.writeText(reference);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (error) {
console.error('Failed to copy:', error);
alert('Nie udało się skopiować');
}
};
// Helper function to parse note text with links // Helper function to parse note text with links
const parseNoteText = (text) => { const parseNoteText = (text) => {
@@ -446,7 +463,17 @@ export default function ProjectViewPage() {
<p className="text-gray-900 font-medium"> <p className="text-gray-900 font-medium">
{project.unit || "N/A"} {project.unit || "N/A"}
</p> </p>
</div>{" "} </div>
{project.start_date && (
<div>
<span className="text-sm font-medium text-gray-500 block mb-1">
Data wpływu
</span>
<p className="text-gray-900 font-medium">
{formatDate(project.start_date)}
</p>
</div>
)}
<FieldWithHistory <FieldWithHistory
tableName="projects" tableName="projects"
recordId={project.project_id} recordId={project.project_id}
@@ -457,7 +484,7 @@ export default function ProjectViewPage() {
{project.completion_date && ( {project.completion_date && (
<div> <div>
<span className="text-sm font-medium text-gray-500 block mb-1"> <span className="text-sm font-medium text-gray-500 block mb-1">
Data zakończenia projektu Data odbioru
</span> </span>
<p className="text-gray-900 font-medium"> <p className="text-gray-900 font-medium">
{formatDate(project.completion_date)} {formatDate(project.completion_date)}
@@ -736,72 +763,6 @@ export default function ProjectViewPage() {
</h2> </h2>
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3">
<Link href={`/projects/${params.id}/edit`} className="block">
<Button
variant="outline"
size="sm"
className="w-full justify-start"
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/>
</svg>
Edytuj projekt
</Button>
</Link>{" "}
<Link href="/projects" className="block">
<Button
variant="outline"
size="sm"
className="w-full justify-start"
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Powrót do projektów
</Button>
</Link>
<Link href="/projects/map" className="block">
<Button
variant="outline"
size="sm"
className="w-full justify-start"
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-1.447-.894L15 4m0 13V4m0 0L9 7"
/>
</svg>
Zobacz wszystkie na mapie
</Button>
</Link>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
@@ -823,6 +784,48 @@ export default function ProjectViewPage() {
</svg> </svg>
Generuj dokument Generuj dokument
</Button> </Button>
<Button
variant="outline"
size="sm"
className="w-full justify-start"
onClick={handleCopyReference}
>
{copied ? (
<>
<svg
className="w-4 h-4 mr-2 text-green-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
<span className="text-green-600">Skopiowano!</span>
</>
) : (
<>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
Kopiuj {project.wp || 'N/A'}/{project.investment_number ? project.investment_number.split('-').pop() : ""}
</>
)}
</Button>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -29,6 +29,10 @@ export default function ProjectListPage() {
}); });
const [customers, setCustomers] = useState([]); const [customers, setCustomers] = useState([]);
const [sortConfig, setSortConfig] = useState({
key: 'finish_date',
direction: 'desc'
});
// Load phoneOnly filter from localStorage after mount to avoid hydration issues // Load phoneOnly filter from localStorage after mount to avoid hydration issues
useEffect(() => { useEffect(() => {
@@ -125,8 +129,44 @@ export default function ProjectListPage() {
setSearchMatchType(null); setSearchMatchType(null);
} }
// Apply sorting
if (sortConfig.key) {
filtered = [...filtered].sort((a, b) => {
let aVal = a[sortConfig.key];
let bVal = b[sortConfig.key];
// Handle null/undefined
if (!aVal && !bVal) return 0;
if (!aVal) return sortConfig.direction === 'asc' ? 1 : -1;
if (!bVal) return sortConfig.direction === 'asc' ? -1 : 1;
// Handle dates
if (sortConfig.key === 'finish_date') {
aVal = new Date(aVal);
bVal = new Date(bVal);
}
// Handle numbers (project_number)
else if (sortConfig.key === 'project_number') {
// Extract numeric part if it's a string like "P-123" or "123"
const aNum = parseInt(String(aVal).replace(/\D/g, '')) || 0;
const bNum = parseInt(String(bVal).replace(/\D/g, '')) || 0;
aVal = aNum;
bVal = bNum;
}
// Handle strings
else if (typeof aVal === 'string') {
aVal = aVal.toLowerCase();
bVal = String(bVal).toLowerCase();
}
if (aVal < bVal) return sortConfig.direction === 'asc' ? -1 : 1;
if (aVal > bVal) return sortConfig.direction === 'asc' ? 1 : -1;
return 0;
});
}
setFilteredProjects(filtered); setFilteredProjects(filtered);
}, [searchTerm, projects, filters, session]); }, [searchTerm, projects, filters, session, sortConfig]);
async function handleDelete(id) { async function handleDelete(id) {
const confirmed = confirm(t('projects.deleteConfirm')); const confirmed = confirm(t('projects.deleteConfirm'));
@@ -171,6 +211,13 @@ export default function ProjectListPage() {
setSearchTerm(''); setSearchTerm('');
}; };
const handleSort = (key) => {
setSortConfig(prev => ({
key,
direction: prev.key === key && prev.direction === 'asc' ? 'desc' : 'asc'
}));
};
const handleExportExcel = async () => { const handleExportExcel = async () => {
try { try {
const response = await fetch('/api/projects/export'); const response = await fetch('/api/projects/export');
@@ -228,6 +275,42 @@ export default function ProjectListPage() {
default: return "-"; default: return "-";
} }
}; };
// Sortable header component
const SortableHeader = ({ columnKey, label, className = "" }) => {
const isSorted = sortConfig.key === columnKey;
const direction = isSorted ? sortConfig.direction : null;
return (
<th
className={`text-left px-2 py-3 font-semibold text-xs text-gray-700 dark:text-gray-300 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors select-none ${className}`}
onClick={() => handleSort(columnKey)}
title={`Sort by ${label}`}
>
<div className="flex items-center gap-1">
<span>{label}</span>
<span className="text-gray-400 flex-shrink-0">
{!isSorted && (
<svg className="w-3 h-3 opacity-30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
</svg>
)}
{isSorted && direction === 'asc' && (
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 24 24">
<path d="M7 14l5-5 5 5H7z" />
</svg>
)}
{isSorted && direction === 'desc' && (
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 24 24">
<path d="M7 10l5 5 5-5H7z" />
</svg>
)}
</span>
</div>
</th>
);
};
return ( return (
<PageContainer> <PageContainer>
<PageHeader title={t('projects.title')} description={t('projects.subtitle')}> <PageHeader title={t('projects.title')} description={t('projects.subtitle')}>
@@ -606,9 +689,30 @@ export default function ProjectListPage() {
{/* Results and clear button row */} {/* Results and clear button row */}
<div className="flex items-center justify-between pt-2 border-t border-gray-100"> <div className="flex items-center justify-between pt-2 border-t border-gray-100">
<div className="flex items-center gap-4">
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">
{t('projects.showingResults', { shown: filteredProjects.length, total: projects.length }) || `Wyświetlono ${filteredProjects.length} z ${projects.length} projektów`} {t('projects.showingResults', { shown: filteredProjects.length, total: projects.length }) || `Wyświetlono ${filteredProjects.length} z ${projects.length} projektów`}
</div> </div>
{sortConfig.key && (
<div className="text-xs text-gray-500 flex items-center gap-1">
<span></span>
<span>Sortowanie:</span>
<span className="font-medium text-gray-700 dark:text-gray-300">
{sortConfig.key === 'project_number' && 'Nr.'}
{sortConfig.key === 'project_name' && t('projects.projectName')}
{sortConfig.key === 'address' && t('projects.address')}
{sortConfig.key === 'wp' && 'WP'}
{sortConfig.key === 'city' && t('projects.city')}
{sortConfig.key === 'plot' && t('projects.plot')}
{sortConfig.key === 'finish_date' && t('projects.finishDate')}
{sortConfig.key === 'project_type' && t('common.type')}
{sortConfig.key === 'project_status' && t('common.status')}
{sortConfig.key === 'assigned_to' && t('projects.assigned')}
</span>
<span>{sortConfig.direction === 'asc' ? '↑' : '↓'}</span>
</div>
)}
</div>
{(filters.status !== 'all' || filters.type !== 'all' || filters.customer !== 'all' || filters.mine || searchTerm) && ( {(filters.status !== 'all' || filters.type !== 'all' || filters.customer !== 'all' || filters.mine || searchTerm) && (
<Button <Button
@@ -699,36 +803,56 @@ export default function ProjectListPage() {
<table className="w-full min-w-[600px] table-fixed"> <table className="w-full min-w-[600px] table-fixed">
<thead> <thead>
<tr className="bg-gray-100 dark:bg-gray-700 border-b dark:border-gray-600"> <tr className="bg-gray-100 dark:bg-gray-700 border-b dark:border-gray-600">
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 dark:text-gray-300 w-20 md:w-24"> <SortableHeader
Nr. columnKey="project_number"
</th> label="Nr."
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 dark:text-gray-300 w-[200px] md:w-[250px]"> className="w-20 md:w-24"
{t('projects.projectName')} />
</th> <SortableHeader
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 dark:text-gray-300 w-20 md:w-24 hidden lg:table-cell"> columnKey="project_name"
{t('projects.address')} label={t('projects.projectName')}
</th> className="w-[200px] md:w-[250px]"
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 dark:text-gray-300 w-16 md:w-20 hidden sm:table-cell"> />
WP <SortableHeader
</th> columnKey="address"
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 dark:text-gray-300 w-14 md:w-16 hidden md:table-cell"> label={t('projects.address')}
{t('projects.city')} className="w-20 md:w-24 hidden lg:table-cell"
</th> />
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 dark:text-gray-300 w-14 md:w-16 hidden sm:table-cell"> <SortableHeader
{t('projects.plot')} columnKey="wp"
</th> label="WP"
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 dark:text-gray-300 w-18 md:w-20 hidden md:table-cell"> className="w-16 md:w-20 hidden sm:table-cell"
{t('projects.finishDate')} />
</th> <SortableHeader
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 dark:text-gray-300 w-10"> columnKey="city"
{t('common.type') || 'Typ'} label={t('projects.city')}
</th> className="w-14 md:w-16 hidden md:table-cell"
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 dark:text-gray-300 w-10"> />
{t('common.status') || 'Status'} <SortableHeader
</th> columnKey="plot"
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 dark:text-gray-300 w-14 md:w-16"> label={t('projects.plot')}
{t('projects.assigned') || 'Przypisany'} className="w-14 md:w-16 hidden sm:table-cell"
</th> />
<SortableHeader
columnKey="finish_date"
label={t('projects.finishDate')}
className="w-18 md:w-20 hidden md:table-cell"
/>
<SortableHeader
columnKey="project_type"
label={t('common.type') || 'Typ'}
className="w-10"
/>
<SortableHeader
columnKey="project_status"
label={t('common.status') || 'Status'}
className="w-10"
/>
<SortableHeader
columnKey="assigned_to"
label={t('projects.assigned') || 'Przypisany'}
className="w-14 md:w-16"
/>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { useState } from "react"; import { useState, useEffect } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Card, CardHeader, CardContent } from "@/components/ui/Card"; import { Card, CardHeader, CardContent } from "@/components/ui/Card";
import Button from "@/components/ui/Button"; import Button from "@/components/ui/Button";
@@ -8,8 +8,9 @@ import { Input } from "@/components/ui/Input";
import { formatDateForInput } from "@/lib/utils"; import { formatDateForInput } from "@/lib/utils";
import { useTranslation } from "@/lib/i18n"; import { useTranslation } from "@/lib/i18n";
export default function ContractForm() { export default function ContractForm({ initialData = null }) {
const { t } = useTranslation(); const { t } = useTranslation();
const isEdit = !!initialData;
const [form, setForm] = useState({ const [form, setForm] = useState({
contract_number: "", contract_number: "",
contract_name: "", contract_name: "",
@@ -23,6 +24,21 @@ export default function ContractForm() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const router = useRouter(); const router = useRouter();
// Update form when initialData changes (for edit mode)
useEffect(() => {
if (initialData) {
setForm({
contract_number: initialData.contract_number || "",
contract_name: initialData.contract_name || "",
customer_contract_number: initialData.customer_contract_number || "",
customer: initialData.customer || "",
investor: initialData.investor || "",
date_signed: initialData.date_signed || "",
finish_date: initialData.finish_date || "",
});
}
}, [initialData]);
function handleChange(e) { function handleChange(e) {
setForm({ ...form, [e.target.name]: e.target.value }); setForm({ ...form, [e.target.name]: e.target.value });
} }
@@ -34,21 +50,32 @@ export default function ContractForm() {
try { try {
console.log("Submitting form:", form); console.log("Submitting form:", form);
const res = await fetch("/api/contracts", { const url = isEdit
method: "POST", ? `/api/contracts/${initialData.contract_id}`
: "/api/contracts";
const method = isEdit ? "PUT" : "POST";
const res = await fetch(url, {
method,
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(form), body: JSON.stringify(form),
}); });
if (res.ok) { if (res.ok) {
const contract = await res.json(); const contract = await res.json();
router.push(`/contracts/${contract.contract_id}`); router.push(`/contracts/${contract.contract_id || initialData.contract_id}`);
} else { } else {
alert(t('contracts.failedToCreateContract')); const errorMessage = isEdit
? t('contracts.failedToUpdateContract')
: t('contracts.failedToCreateContract');
alert(errorMessage);
} }
} catch (error) { } catch (error) {
console.error("Error creating contract:", error); console.error(`Error ${isEdit ? 'updating' : 'creating'} contract:`, error);
alert(t('contracts.failedToCreateContract')); const errorMessage = isEdit
? t('contracts.failedToUpdateContract')
: t('contracts.failedToCreateContract');
alert(errorMessage);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -189,7 +216,7 @@ export default function ContractForm() {
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path> ></path>
</svg> </svg>
{t('common.creating')} {isEdit ? t('common.updating') : t('common.creating')}
</> </>
) : ( ) : (
<> <>
@@ -203,10 +230,10 @@ export default function ContractForm() {
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
strokeWidth={2} strokeWidth={2}
d="M12 4v16m8-8H4" d={isEdit ? "M5 13l4 4L19 7" : "M12 4v16m8-8H4"}
/> />
</svg> </svg>
{t('contracts.createContract')} {isEdit ? t('contracts.updateContract') : t('contracts.createContract')}
</> </>
)} )}
</Button> </Button>

View File

@@ -22,6 +22,7 @@ const ProjectForm = forwardRef(function ProjectForm({ initialData = null }, ref)
unit: "", unit: "",
city: "", city: "",
investment_number: "", investment_number: "",
start_date: "",
finish_date: "", finish_date: "",
completion_date: "", completion_date: "",
wp: "", wp: "",
@@ -63,6 +64,7 @@ const ProjectForm = forwardRef(function ProjectForm({ initialData = null }, ref)
unit: "", unit: "",
city: "", city: "",
investment_number: "", investment_number: "",
start_date: "",
finish_date: "", finish_date: "",
completion_date: "", completion_date: "",
wp: "", wp: "",
@@ -78,6 +80,9 @@ const ProjectForm = forwardRef(function ProjectForm({ initialData = null }, ref)
assigned_to: initialData.assigned_to || "", assigned_to: initialData.assigned_to || "",
wartosc_zlecenia: initialData.wartosc_zlecenia || "", wartosc_zlecenia: initialData.wartosc_zlecenia || "",
// Format dates for input if they exist // Format dates for input if they exist
start_date: initialData.start_date
? formatDateForInput(initialData.start_date)
: "",
finish_date: initialData.finish_date finish_date: initialData.finish_date
? formatDateForInput(initialData.finish_date) ? formatDateForInput(initialData.finish_date)
: "", : "",
@@ -292,7 +297,19 @@ const ProjectForm = forwardRef(function ProjectForm({ initialData = null }, ref)
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
{t('projects.finishDate')} Data wpływu
</label>
<Input
type="date"
name="start_date"
value={formatDateForInput(form.start_date)}
onChange={handleChange}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Termin zakończenia
</label> </label>
<Input <Input
type="date" type="date"
@@ -304,7 +321,7 @@ const ProjectForm = forwardRef(function ProjectForm({ initialData = null }, ref)
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Data zakończenia projektu Data odbioru
</label> </label>
<Input <Input
type="date" type="date"

View File

@@ -136,6 +136,8 @@ const translations = {
realisedValue: "Wartość zrealizowana", realisedValue: "Wartość zrealizowana",
unrealisedValue: "Wartość niezrealizowana", unrealisedValue: "Wartość niezrealizowana",
byProjectType: "Według typu projektu", byProjectType: "Według typu projektu",
byContract: "Według umowy",
total: "Razem",
monthLabel: "Miesiąc:", monthLabel: "Miesiąc:",
monthlyValue: "Wartość miesięczna:", monthlyValue: "Wartość miesięczna:",
cumulative: "Skumulowana:", cumulative: "Skumulowana:",
@@ -184,7 +186,7 @@ const translations = {
plot: "Działka", plot: "Działka",
district: "Jednostka ewidencyjna", district: "Jednostka ewidencyjna",
unit: "Obręb", unit: "Obręb",
finishDate: "Data zakończenia", finishDate: "Termin zakończenia",
type: "Typ", type: "Typ",
contact: "Kontakt", contact: "Kontakt",
coordinates: "Współrzędne", coordinates: "Współrzędne",
@@ -285,6 +287,11 @@ const translations = {
contract: "Umowa", contract: "Umowa",
newContract: "Nowa umowa", newContract: "Nowa umowa",
editContract: "Edytuj umowę", editContract: "Edytuj umowę",
editContractDescription: "Edycja szczegółów umowy",
editing: "Edycja umowy",
backToContract: "Powrót do umowy",
updateContract: "Zaktualizuj umowę",
failedToUpdateContract: "Nie udało się zaktualizować umowy. Sprawdź dane i spróbuj ponownie.",
deleteContract: "Usuń umowę", deleteContract: "Usuń umowę",
contractNumber: "Numer umowy", contractNumber: "Numer umowy",
contractName: "Nazwa umowy", contractName: "Nazwa umowy",
@@ -292,7 +299,7 @@ const translations = {
customer: "Klient", customer: "Klient",
investor: "Inwestor", investor: "Inwestor",
dateSigned: "Data zawarcia", dateSigned: "Data zawarcia",
finishDate: "Data zakończenia", finishDate: "Termin zakończenia",
searchPlaceholder: "Szukaj umów po numerze, nazwie, kliencie lub inwestorze...", searchPlaceholder: "Szukaj umów po numerze, nazwie, kliencie lub inwestorze...",
noContracts: "Brak umów", noContracts: "Brak umów",
noContractsMessage: "Rozpocznij od utworzenia swojej pierwszej umowy.", noContractsMessage: "Rozpocznij od utworzenia swojej pierwszej umowy.",
@@ -323,7 +330,7 @@ const translations = {
customer: "Klient", customer: "Klient",
investor: "Inwestor", investor: "Inwestor",
dateSigned: "Data zawarcia", dateSigned: "Data zawarcia",
finishDate: "Data zakończenia", finishDate: "Termin zakończenia",
summary: "Podsumowanie", summary: "Podsumowanie",
projectsCount: "Liczba projektów", projectsCount: "Liczba projektów",
projects: "projektów", projects: "projektów",
@@ -532,7 +539,7 @@ const translations = {
dateCreated: "Data utworzenia", dateCreated: "Data utworzenia",
dateModified: "Data modyfikacji", dateModified: "Data modyfikacji",
startDate: "Data rozpoczęcia", startDate: "Data rozpoczęcia",
finishDate: "Data zakończenia" finishDate: "Termin zakończenia"
}, },
// Date formats // Date formats
@@ -769,6 +776,8 @@ const translations = {
realisedValue: "Realised Value", realisedValue: "Realised Value",
unrealisedValue: "Unrealised Value", unrealisedValue: "Unrealised Value",
byProjectType: "By Project Type", byProjectType: "By Project Type",
byContract: "By Contract",
total: "Total",
monthLabel: "Month:", monthLabel: "Month:",
monthlyValue: "Monthly Value:", monthlyValue: "Monthly Value:",
cumulative: "Cumulative:", cumulative: "Cumulative:",
@@ -936,8 +945,14 @@ const translations = {
contracts: { contracts: {
title: "Contracts", title: "Contracts",
subtitle: "Manage your contracts and agreements", subtitle: "Manage your contracts and agreements",
contract: "Contract",
newContract: "New Contract", newContract: "New Contract",
editContract: "Edit Contract", editContract: "Edit Contract",
editContractDescription: "Edit contract details",
editing: "Editing contract",
backToContract: "Back to Contract",
updateContract: "Update Contract",
failedToUpdateContract: "Failed to update contract. Please check your data and try again.",
deleteContract: "Delete Contract", deleteContract: "Delete Contract",
contractNumber: "Contract Number", contractNumber: "Contract Number",
contractName: "Contract Name", contractName: "Contract Name",
@@ -964,7 +979,40 @@ const translations = {
signedOn: "Signed:", signedOn: "Signed:",
finishOn: "Finish:", finishOn: "Finish:",
customerLabel: "Customer:", customerLabel: "Customer:",
investorLabel: "Investor:" investorLabel: "Investor:",
loadingContractDetails: "Loading contract details...",
contractNotFound: "Contract not found.",
backToContracts: "Back to Contracts",
addProject: "Add Project",
contractInformation: "Contract Information",
summary: "Summary",
projectsCount: "Projects Count",
projects: "projects",
contractStatus: "Contract Status",
contractDocuments: "Contract Documents",
uploadDocument: "Upload Document",
associatedProjects: "Associated Projects",
noProjectsYet: "No projects yet",
getStartedMessage: "Get started by creating the first project for this contract",
createFirstProject: "Create First Project",
viewDetails: "View Details",
createNewContract: "Create New Contract",
addNewContractDescription: "Add a new contract to your portfolio",
contractDetails: "Contract Details",
failedToCreateContract: "Failed to create contract. Please check your data and try again.",
uploadDocumentTitle: "Upload Document",
descriptionOptional: "Description (optional)",
descriptionPlaceholder: "Short description of the document...",
uploading: "Uploading...",
dropFilesHere: "Drop files here or click to browse",
supportedFiles: "PDF, DOC, XLS, Images up to 10MB",
chooseFile: "Choose File",
failedToUploadFile: "Failed to upload file",
loadingFiles: "Loading files...",
noDocumentsUploaded: "No documents uploaded",
download: "Download",
confirmDeleteFile: "Are you sure you want to delete this file?",
failedToDeleteFile: "Failed to delete file"
}, },
tasks: { tasks: {

View File

@@ -289,6 +289,14 @@ export default function initializeDatabase() {
// Column already exists, ignore error // Column already exists, ignore error
} }
try {
db.exec(`
ALTER TABLE projects ADD COLUMN start_date TEXT;
`);
} catch (e) {
// Column already exists, ignore error
}
// Migration: Update task status system - add 'not_started' status // Migration: Update task status system - add 'not_started' status
// DISABLED: This migration was running on every init and converting legitimate // DISABLED: This migration was running on every init and converting legitimate
// 'pending' tasks back to 'not_started'. The initial migration has been completed. // 'pending' tasks back to 'not_started'. The initial migration has been completed.
@@ -389,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

View File

@@ -75,3 +75,8 @@ export function withAdminAuth(handler) {
export function withManagerAuth(handler) { export function withManagerAuth(handler) {
return withAuth(handler, { requiredRole: 'project_manager' }) return withAuth(handler, { requiredRole: 'project_manager' })
} }
// Helper for team lead operations
export function withTeamLeadAuth(handler) {
return withAuth(handler, { requiredRole: 'team_lead' })
}

View File

@@ -75,9 +75,9 @@ export function createProject(data, userId = null) {
const stmt = db.prepare(` const stmt = db.prepare(`
INSERT INTO projects ( INSERT INTO projects (
contract_id, project_name, project_number, address, plot, district, unit, city, investment_number, finish_date, completion_date, contract_id, project_name, project_number, address, plot, district, unit, city, investment_number, start_date, finish_date, completion_date,
wp, contact, notes, wartosc_zlecenia, project_type, project_status, coordinates, created_by, assigned_to, created_at, updated_at wp, contact, notes, wartosc_zlecenia, project_type, project_status, coordinates, created_by, assigned_to, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now', 'localtime'), datetime('now', 'localtime')) ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now', 'localtime'), datetime('now', 'localtime'))
`); `);
const result = stmt.run( const result = stmt.run(
@@ -90,6 +90,7 @@ export function createProject(data, userId = null) {
data.unit, data.unit,
data.city, data.city,
data.investment_number, data.investment_number,
data.start_date || null,
data.finish_date, data.finish_date,
data.completion_date, data.completion_date,
data.wp, data.wp,
@@ -110,7 +111,7 @@ export function updateProject(id, data, userId = null) {
const stmt = db.prepare(` const stmt = db.prepare(`
UPDATE projects SET UPDATE projects SET
contract_id = ?, project_name = ?, project_number = ?, address = ?, plot = ?, district = ?, unit = ?, city = ?, contract_id = ?, project_name = ?, project_number = ?, address = ?, plot = ?, district = ?, unit = ?, city = ?,
investment_number = ?, finish_date = ?, completion_date = ?, wp = ?, contact = ?, notes = ?, wartosc_zlecenia = ?, project_type = ?, project_status = ?, investment_number = ?, start_date = ?, finish_date = ?, completion_date = ?, wp = ?, contact = ?, notes = ?, wartosc_zlecenia = ?, project_type = ?, project_status = ?,
coordinates = ?, assigned_to = ?, updated_at = CURRENT_TIMESTAMP coordinates = ?, assigned_to = ?, updated_at = CURRENT_TIMESTAMP
WHERE project_id = ? WHERE project_id = ?
`); `);
@@ -124,6 +125,7 @@ export function updateProject(id, data, userId = null) {
data.unit, data.unit,
data.city, data.city,
data.investment_number, data.investment_number,
data.start_date || null,
data.finish_date, data.finish_date,
data.completion_date, data.completion_date,
data.wp, data.wp,

View File

@@ -12,10 +12,14 @@ 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,