import db from "./db.js"; export default function initializeDatabase() { db.exec(` -- Table: contracts CREATE TABLE IF NOT EXISTS contracts ( contract_id INTEGER PRIMARY KEY AUTOINCREMENT, contract_number TEXT NOT NULL, contract_name TEXT, customer_contract_number TEXT, customer TEXT, investor TEXT, date_signed TEXT, finish_date TEXT ); -- Table: projects CREATE TABLE IF NOT EXISTS projects ( project_id INTEGER PRIMARY KEY AUTOINCREMENT, contract_id INTEGER, project_name TEXT NOT NULL, project_number TEXT NOT NULL, address TEXT, plot TEXT, district TEXT, unit TEXT, city TEXT, investment_number TEXT, finish_date TEXT, completion_date TEXT, wp TEXT, contact TEXT, notes TEXT, wartosc_zlecenia REAL, project_type TEXT CHECK(project_type IN ('design', 'construction', 'design+construction')) DEFAULT 'design', project_status TEXT CHECK(project_status IN ('registered', 'in_progress_design', 'in_progress_construction', 'fulfilled', 'cancelled')) DEFAULT 'registered', FOREIGN KEY (contract_id) REFERENCES contracts(contract_id) ); -- Table: tasks CREATE TABLE IF NOT EXISTS tasks ( task_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, max_wait_days INTEGER DEFAULT 0, is_standard INTEGER NOT NULL DEFAULT 0, task_category TEXT CHECK(task_category IN ('design', 'construction')) NOT NULL DEFAULT 'design' ); -- Table: task_sets CREATE TABLE IF NOT EXISTS task_sets ( set_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, description TEXT, task_category TEXT CHECK(task_category IN ('design', 'construction')) NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP ); -- Table: task_set_templates CREATE TABLE IF NOT EXISTS task_set_templates ( set_id INTEGER NOT NULL, task_template_id INTEGER NOT NULL, sort_order INTEGER DEFAULT 0, PRIMARY KEY (set_id, task_template_id), FOREIGN KEY (set_id) REFERENCES task_sets(set_id) ON DELETE CASCADE, FOREIGN KEY (task_template_id) REFERENCES tasks(task_id) ); -- Table: project_tasks CREATE TABLE IF NOT EXISTS project_tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id INTEGER NOT NULL, task_template_id INTEGER, custom_task_name TEXT, custom_max_wait_days INTEGER DEFAULT 0, status TEXT NOT NULL DEFAULT 'not_started' CHECK(status IN ('not_started', 'pending', 'in_progress', 'completed', 'cancelled')), date_added TEXT DEFAULT CURRENT_TIMESTAMP, priority TEXT DEFAULT 'normal', FOREIGN KEY (project_id) REFERENCES projects(project_id), FOREIGN KEY (task_template_id) REFERENCES tasks(task_id) ); -- Table: notes CREATE TABLE IF NOT EXISTS notes ( note_id INTEGER PRIMARY KEY AUTOINCREMENT, project_id INTEGER, task_id INTEGER, note TEXT NOT NULL, note_date TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (project_id) REFERENCES projects(project_id), FOREIGN KEY (task_id) REFERENCES project_tasks(id) ); `); // Migration: Add custom task columns if they don't exist try { db.exec(` ALTER TABLE project_tasks ADD COLUMN custom_task_name TEXT; `); } catch (e) { // Column already exists, ignore error } try { db.exec(` ALTER TABLE project_tasks ADD COLUMN custom_max_wait_days INTEGER DEFAULT 0; `); } catch (e) { // Column already exists, ignore error } // Migration: Add description column to tasks table try { db.exec(` ALTER TABLE tasks ADD COLUMN description TEXT; `); } catch (e) { // Column already exists, ignore error } // Migration: Add description column to project_tasks table try { db.exec(` ALTER TABLE project_tasks ADD COLUMN custom_description TEXT; `); } catch (e) { // Column already exists, ignore error } // Migration: Add project_type column to projects table try { db.exec(` ALTER TABLE projects ADD COLUMN project_type TEXT CHECK(project_type IN ('design', 'construction', 'design+construction')) DEFAULT 'design'; `); } catch (e) { // Column already exists, ignore error } // Migration: Add project_status column to projects table try { db.exec(` ALTER TABLE projects ADD COLUMN project_status TEXT CHECK(project_status IN ('registered', 'in_progress_design', 'in_progress_construction', 'fulfilled', 'cancelled')) DEFAULT 'registered'; `); } catch (e) { // Column already exists, ignore error } // Migration: Add coordinates column to projects table try { db.exec(` ALTER TABLE projects ADD COLUMN coordinates TEXT; `); } catch (e) { // Column already exists, ignore error } // Migration: Copy data from geo_info to coordinates and drop geo_info try { db.exec(` UPDATE projects SET coordinates = geo_info WHERE geo_info IS NOT NULL; `); db.exec(` ALTER TABLE projects DROP COLUMN geo_info; `); } catch (e) { // Column migration already done or geo_info doesn't exist, ignore error } // Migration: Add date_started column to project_tasks table try { db.exec(` ALTER TABLE project_tasks ADD COLUMN date_started TEXT; `); } catch (e) { // Column already exists, ignore error } // Migration: Add is_system column to notes table try { db.exec(` ALTER TABLE notes ADD COLUMN is_system INTEGER DEFAULT 0; `); } catch (e) { // Column already exists, ignore error } // Migration: Add date_completed column to project_tasks table try { db.exec(` ALTER TABLE project_tasks ADD COLUMN date_completed TEXT; `); } catch (e) { // Column already exists, ignore error } // Migration: Add user tracking columns to projects table try { db.exec(` ALTER TABLE projects ADD COLUMN created_by TEXT; `); } catch (e) { // Column already exists, ignore error } try { db.exec(` ALTER TABLE projects ADD COLUMN assigned_to TEXT; `); } catch (e) { // Column already exists, ignore error } try { db.exec(` ALTER TABLE projects ADD COLUMN created_at TEXT; `); } catch (e) { // Column already exists, ignore error } try { db.exec(` ALTER TABLE projects ADD COLUMN updated_at TEXT; `); } catch (e) { // Column already exists, ignore error } // Migration: Add user tracking columns to project_tasks table try { db.exec(` ALTER TABLE project_tasks ADD COLUMN created_by TEXT; `); } catch (e) { // Column already exists, ignore error } try { db.exec(` ALTER TABLE project_tasks ADD COLUMN assigned_to TEXT; `); } catch (e) { // Column already exists, ignore error } try { db.exec(` ALTER TABLE project_tasks ADD COLUMN created_at TEXT; `); } catch (e) { // Column already exists, ignore error } try { db.exec(` ALTER TABLE project_tasks ADD COLUMN updated_at TEXT; `); } catch (e) { // Column already exists, ignore error } // Create indexes for project_tasks user tracking try { db.exec(` CREATE INDEX IF NOT EXISTS idx_project_tasks_created_by ON project_tasks(created_by); CREATE INDEX IF NOT EXISTS idx_project_tasks_assigned_to ON project_tasks(assigned_to); `); } catch (e) { // Index already exists, ignore error } // Migration: Add user tracking columns to notes table try { db.exec(` ALTER TABLE notes ADD COLUMN created_by TEXT; `); } catch (e) { // Column already exists, ignore error } try { db.exec(` ALTER TABLE notes ADD COLUMN is_system INTEGER DEFAULT 0; `); } catch (e) { // Column already exists, ignore error } try { db.exec(` ALTER TABLE projects ADD COLUMN completion_date TEXT; `); } catch (e) { // 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 // DISABLED: This migration was running on every init and converting legitimate // 'pending' tasks back to 'not_started'. The initial migration has been completed. // try { // // First, update all existing 'pending' tasks to 'not_started' // db.exec(` // UPDATE project_tasks SET status = 'not_started' WHERE status = 'pending'; // `); // // Note: CHECK constraint will be applied when recreating the table in future migrations // // For now, we'll rely on application-level validation // } catch (e) { // // Migration already done, ignore error // } // Create indexes for notes user tracking try { db.exec(` CREATE INDEX IF NOT EXISTS idx_notes_created_by ON notes(created_by); CREATE INDEX IF NOT EXISTS idx_notes_project_id ON notes(project_id); CREATE INDEX IF NOT EXISTS idx_notes_task_id ON notes(task_id); `); } catch (e) { // Index already exists, ignore error } // Authorization tables db.exec(` -- Users table CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), name TEXT NOT NULL, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, role TEXT CHECK(role IN ('admin', 'team_lead', 'project_manager', 'user', 'read_only')) DEFAULT 'user', created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP, is_active INTEGER DEFAULT 1, last_login TEXT, failed_login_attempts INTEGER DEFAULT 0, locked_until TEXT ); -- NextAuth.js sessions table (simplified for custom implementation) CREATE TABLE IF NOT EXISTS sessions ( id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), session_token TEXT UNIQUE NOT NULL, user_id TEXT NOT NULL, expires TEXT NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); -- Audit log table for security tracking CREATE TABLE IF NOT EXISTS audit_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT, action TEXT NOT NULL, resource_type TEXT, resource_id TEXT, ip_address TEXT, user_agent TEXT, timestamp TEXT DEFAULT CURRENT_TIMESTAMP, details TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ); -- Create indexes for performance CREATE INDEX IF NOT EXISTS idx_users_username ON users(username); CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(session_token); CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id); CREATE INDEX IF NOT EXISTS idx_audit_user_timestamp ON audit_logs(user_id, timestamp); `); // Migration: Add username column and migrate from email if needed try { // Check if username column exists const columns = db.prepare("PRAGMA table_info(users)").all(); const hasUsername = columns.some(col => col.name === 'username'); const hasEmail = columns.some(col => col.name === 'email'); if (!hasUsername && hasEmail) { // Add username column db.exec(`ALTER TABLE users ADD COLUMN username TEXT;`); // Migrate existing email data to username (for development/testing) // In production, you might want to handle this differently db.exec(`UPDATE users SET username = email WHERE username IS NULL;`); // Create unique index on username db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_users_username_unique ON users(username);`); console.log("✅ Migrated users table from email to username"); } else if (!hasUsername) { // If neither username nor email exists, something is wrong console.warn("⚠️ Users table missing both username and email columns"); } } catch (e) { console.warn("Migration warning:", e.message); } // Migration: Add initial column to users table try { const columns = db.prepare("PRAGMA table_info(users)").all(); const hasInitial = columns.some(col => col.name === 'initial'); if (!hasInitial) { // Add initial column db.exec(`ALTER TABLE users ADD COLUMN initial TEXT;`); // Generate initials from existing names const users = db.prepare('SELECT id, name FROM users WHERE initial IS NULL').all(); const updateStmt = db.prepare('UPDATE users SET initial = ? WHERE id = ?'); users.forEach(user => { if (user.name) { // Generate initials from name (e.g., "John Doe" -> "JD") const nameParts = user.name.trim().split(/\s+/); const initial = nameParts.map(part => part.charAt(0).toUpperCase()).join(''); updateStmt.run(initial, user.id); } }); console.log("✅ Added initial column to users table and generated initials"); } } catch (e) { console.warn("Migration warning:", e.message); } // Migration: Rename project_type to task_category in task_sets try { // Check if the old column exists and rename it const tableInfo = db.prepare("PRAGMA table_info(task_sets)").all(); const hasOldColumn = tableInfo.some(col => col.name === 'project_type'); const hasNewColumn = tableInfo.some(col => col.name === 'task_category'); if (hasOldColumn && !hasNewColumn) { // Rename the column db.exec(` ALTER TABLE task_sets RENAME COLUMN project_type TO task_category; `); console.log("✅ Renamed project_type to task_category in task_sets"); } } catch (e) { console.warn("Migration warning:", e.message); } // Migration: Add task_category column to tasks table try { const tableInfo = db.prepare("PRAGMA table_info(tasks)").all(); const hasTaskCategory = tableInfo.some(col => col.name === 'task_category'); if (!hasTaskCategory) { // Add the task_category column db.exec(` ALTER TABLE tasks ADD COLUMN task_category TEXT CHECK(task_category IN ('design', 'construction')) NOT NULL DEFAULT 'design'; `); console.log("✅ Added task_category column to tasks table"); } } catch (e) { console.warn("Migration warning:", e.message); } // Generic file attachments table db.exec(` CREATE TABLE IF NOT EXISTS file_attachments ( file_id INTEGER PRIMARY KEY AUTOINCREMENT, entity_type TEXT NOT NULL CHECK(entity_type IN ('contract', 'project', 'task')), entity_id INTEGER NOT NULL, original_filename TEXT NOT NULL, stored_filename TEXT NOT NULL, file_path TEXT NOT NULL, file_size INTEGER, mime_type TEXT, description TEXT, uploaded_by TEXT, upload_date TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (uploaded_by) REFERENCES users(id) ); -- Create indexes for file attachments CREATE INDEX IF NOT EXISTS idx_file_attachments_entity ON file_attachments(entity_type, entity_id); CREATE INDEX IF NOT EXISTS idx_file_attachments_uploaded_by ON file_attachments(uploaded_by); -- Generic field change history table CREATE TABLE IF NOT EXISTS field_change_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, table_name TEXT NOT NULL, record_id INTEGER NOT NULL, field_name TEXT NOT NULL, old_value TEXT, new_value TEXT, changed_by INTEGER, changed_at TEXT DEFAULT CURRENT_TIMESTAMP, change_reason TEXT, FOREIGN KEY (changed_by) REFERENCES users(id) ); -- Create indexes for field change history CREATE INDEX IF NOT EXISTS idx_field_history_table_record ON field_change_history(table_name, record_id); CREATE INDEX IF NOT EXISTS idx_field_history_field ON field_change_history(table_name, record_id, field_name); CREATE INDEX IF NOT EXISTS idx_field_history_changed_by ON field_change_history(changed_by); -- Password reset tokens table CREATE TABLE IF NOT EXISTS password_reset_tokens ( id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), user_id TEXT NOT NULL, token TEXT UNIQUE NOT NULL, expires_at TEXT NOT NULL, used INTEGER DEFAULT 0, created_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); -- Create index for password reset tokens CREATE INDEX IF NOT EXISTS idx_password_reset_token ON password_reset_tokens(token); CREATE INDEX IF NOT EXISTS idx_password_reset_user ON password_reset_tokens(user_id); -- Notifications table CREATE TABLE IF NOT EXISTS notifications ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, type TEXT NOT NULL CHECK(type IN ('task_assigned', 'task_status_changed', 'project_updated', 'due_date_reminder', 'system_announcement', 'mention')), title TEXT NOT NULL, message TEXT NOT NULL, resource_type TEXT, resource_id TEXT, is_read INTEGER DEFAULT 0, priority TEXT DEFAULT 'normal' CHECK(priority IN ('low', 'normal', 'high', 'urgent')), created_at TEXT DEFAULT CURRENT_TIMESTAMP, expires_at TEXT, action_url TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); -- Create indexes for notifications CREATE INDEX IF NOT EXISTS idx_notifications_user ON notifications(user_id); CREATE INDEX IF NOT EXISTS idx_notifications_user_read ON notifications(user_id, is_read); CREATE INDEX IF NOT EXISTS idx_notifications_created ON notifications(created_at); CREATE INDEX IF NOT EXISTS idx_notifications_type ON notifications(type); -- Settings table for application configuration CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL, description TEXT, updated_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_by TEXT, FOREIGN KEY (updated_by) REFERENCES users(id) ); -- Insert default settings INSERT OR IGNORE INTO settings (key, value, description) VALUES ('backup_notification_user_id', '', 'User ID to receive backup completion notifications'); `); // Contacts table for managing contact information db.exec(` CREATE TABLE IF NOT EXISTS contacts ( contact_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, phone TEXT, email TEXT, company TEXT, position TEXT, contact_type TEXT CHECK(contact_type IN ('project', 'contractor', 'office', 'supplier', 'other')) DEFAULT 'other', notes TEXT, is_active INTEGER DEFAULT 1, created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP ); -- Project contacts junction table (many-to-many relationship) CREATE TABLE IF NOT EXISTS project_contacts ( project_id INTEGER NOT NULL, contact_id INTEGER NOT NULL, relationship_type TEXT DEFAULT 'general', is_primary INTEGER DEFAULT 0, added_at TEXT DEFAULT CURRENT_TIMESTAMP, added_by TEXT, PRIMARY KEY (project_id, contact_id), FOREIGN KEY (project_id) REFERENCES projects(project_id) ON DELETE CASCADE, FOREIGN KEY (contact_id) REFERENCES contacts(contact_id) ON DELETE CASCADE, FOREIGN KEY (added_by) REFERENCES users(id) ); -- Indexes for contacts CREATE INDEX IF NOT EXISTS idx_contacts_name ON contacts(name); CREATE INDEX IF NOT EXISTS idx_contacts_type ON contacts(contact_type); CREATE INDEX IF NOT EXISTS idx_contacts_active ON contacts(is_active); CREATE INDEX IF NOT EXISTS idx_contacts_phone ON contacts(phone); CREATE INDEX IF NOT EXISTS idx_contacts_email ON contacts(email); CREATE INDEX IF NOT EXISTS idx_project_contacts_project ON project_contacts(project_id); CREATE INDEX IF NOT EXISTS idx_project_contacts_contact ON project_contacts(contact_id); -- Table: docx_templates CREATE TABLE IF NOT EXISTS docx_templates ( template_id INTEGER PRIMARY KEY AUTOINCREMENT, template_name TEXT NOT NULL, description TEXT, original_filename TEXT NOT NULL, stored_filename TEXT NOT NULL, file_path TEXT NOT NULL, file_size INTEGER NOT NULL, mime_type TEXT NOT NULL, is_active INTEGER DEFAULT 1, created_at TEXT DEFAULT CURRENT_TIMESTAMP, created_by TEXT, updated_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (created_by) REFERENCES users(id) ); -- Indexes for templates CREATE INDEX IF NOT EXISTS idx_docx_templates_active ON docx_templates(is_active); CREATE INDEX IF NOT EXISTS idx_docx_templates_created_by ON docx_templates(created_by); `); }