Files
panel/src/lib/init-db.js

616 lines
20 KiB
JavaScript

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);
`);
}