468 lines
16 KiB
JavaScript
468 lines
16 KiB
JavaScript
|
|
const mysql = require('mysql2/promise');
|
|
const { Pool } = require('pg');
|
|
const bcrypt = require('bcryptjs');
|
|
require('dotenv').config();
|
|
|
|
const DB_CLIENT = process.env.DB_CLIENT || 'mysql'; // 'mysql' or 'postgres'
|
|
|
|
// DB Configuration
|
|
const dbConfig = {
|
|
host: process.env.DB_HOST || 'localhost',
|
|
user: process.env.DB_USER || 'root',
|
|
password: process.env.DB_PASS || '',
|
|
database: process.env.DB_NAME || 'condominio',
|
|
port: process.env.DB_PORT || (DB_CLIENT === 'postgres' ? 5432 : 3306),
|
|
};
|
|
|
|
let mysqlPool = null;
|
|
let pgPool = null;
|
|
|
|
if (DB_CLIENT === 'postgres') {
|
|
pgPool = new Pool(dbConfig);
|
|
} else {
|
|
mysqlPool = mysql.createPool({
|
|
...dbConfig,
|
|
waitForConnections: true,
|
|
connectionLimit: 10,
|
|
queueLimit: 0
|
|
});
|
|
}
|
|
|
|
const executeQuery = async (sql, params = []) => {
|
|
if (DB_CLIENT === 'postgres') {
|
|
let paramIndex = 1;
|
|
const pgSql = sql.replace(/\?/g, () => `$${paramIndex++}`);
|
|
const result = await pgPool.query(pgSql, params);
|
|
return [result.rows, result.fields];
|
|
} else {
|
|
return await mysqlPool.query(sql, params);
|
|
}
|
|
};
|
|
|
|
const dbInterface = {
|
|
query: executeQuery,
|
|
getConnection: async () => {
|
|
if (DB_CLIENT === 'postgres') {
|
|
return {
|
|
query: executeQuery,
|
|
release: () => {},
|
|
beginTransaction: async () => {},
|
|
commit: async () => {},
|
|
rollback: async () => {}
|
|
};
|
|
} else {
|
|
return await mysqlPool.getConnection();
|
|
}
|
|
}
|
|
};
|
|
|
|
const initDb = async () => {
|
|
try {
|
|
const connection = await dbInterface.getConnection();
|
|
console.log(`Database connected successfully using ${DB_CLIENT}.`);
|
|
|
|
const TIMESTAMP_TYPE = 'TIMESTAMP';
|
|
const LONG_TEXT_TYPE = DB_CLIENT === 'postgres' ? 'TEXT' : 'LONGTEXT'; // For base64 files
|
|
const JSON_TYPE = 'JSON';
|
|
|
|
// --- 0. SETTINGS TABLE ---
|
|
// Definizione completa per nuova installazione
|
|
await connection.query(`
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
id INT PRIMARY KEY,
|
|
current_year INT,
|
|
smtp_config ${JSON_TYPE},
|
|
features ${JSON_TYPE},
|
|
storage_config ${JSON_TYPE},
|
|
branding ${JSON_TYPE}
|
|
)
|
|
`);
|
|
|
|
// MIGRATION: Controllo e aggiunta colonne mancanti per installazioni esistenti
|
|
try {
|
|
let cols = [];
|
|
if (DB_CLIENT === 'postgres') {
|
|
const [res] = await connection.query("SELECT column_name FROM information_schema.columns WHERE table_name='settings'");
|
|
cols = res.map(c => c.column_name);
|
|
} else {
|
|
const [res] = await connection.query("SHOW COLUMNS FROM settings");
|
|
cols = res.map(c => c.Field);
|
|
}
|
|
|
|
if (!cols.includes('features')) {
|
|
console.log('Migrating: Adding features to settings...');
|
|
await connection.query("ALTER TABLE settings ADD COLUMN features JSON");
|
|
}
|
|
if (!cols.includes('storage_config')) {
|
|
console.log('Migrating: Adding storage_config to settings...');
|
|
await connection.query("ALTER TABLE settings ADD COLUMN storage_config JSON");
|
|
}
|
|
if (!cols.includes('branding')) {
|
|
console.log('Migrating: Adding branding to settings...');
|
|
await connection.query("ALTER TABLE settings ADD COLUMN branding JSON");
|
|
}
|
|
} catch(e) { console.warn("Settings migration warning:", e.message); }
|
|
|
|
// --- 1. CONDOS TABLE ---
|
|
await connection.query(`
|
|
CREATE TABLE IF NOT EXISTS condos (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
name VARCHAR(255) NOT NULL,
|
|
address VARCHAR(255),
|
|
street_number VARCHAR(20),
|
|
city VARCHAR(100),
|
|
province VARCHAR(100),
|
|
zip_code VARCHAR(20),
|
|
notes TEXT,
|
|
iban VARCHAR(50),
|
|
paypal_client_id VARCHAR(255),
|
|
default_monthly_quota DECIMAL(10, 2) DEFAULT 100.00,
|
|
due_day INT DEFAULT 10,
|
|
image VARCHAR(255),
|
|
created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
`);
|
|
|
|
// Migration condos
|
|
try {
|
|
let cols = [];
|
|
if (DB_CLIENT === 'postgres') {
|
|
const [res] = await connection.query("SELECT column_name FROM information_schema.columns WHERE table_name='condos'");
|
|
cols = res.map(c => c.column_name);
|
|
} else {
|
|
const [res] = await connection.query("SHOW COLUMNS FROM condos");
|
|
cols = res.map(c => c.Field);
|
|
}
|
|
|
|
if (!cols.includes('due_day')) {
|
|
console.log('Migrating: Adding due_day to condos...');
|
|
await connection.query("ALTER TABLE condos ADD COLUMN due_day INT DEFAULT 10");
|
|
}
|
|
} catch(e) { console.warn("Condo migration warning:", e.message); }
|
|
|
|
// --- 2. FAMILIES TABLE ---
|
|
await connection.query(`
|
|
CREATE TABLE IF NOT EXISTS families (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
condo_id VARCHAR(36),
|
|
name VARCHAR(255) NOT NULL,
|
|
unit_number VARCHAR(50),
|
|
stair VARCHAR(50),
|
|
floor VARCHAR(50),
|
|
notes TEXT,
|
|
contact_email VARCHAR(255),
|
|
custom_monthly_quota DECIMAL(10, 2) NULL,
|
|
created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (condo_id) REFERENCES condos(id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
|
|
// Migration families
|
|
try {
|
|
let cols = [];
|
|
if (DB_CLIENT === 'postgres') {
|
|
const [res] = await connection.query("SELECT column_name FROM information_schema.columns WHERE table_name='families'");
|
|
cols = res.map(c => c.column_name);
|
|
} else {
|
|
const [res] = await connection.query("SHOW COLUMNS FROM families");
|
|
cols = res.map(c => c.Field);
|
|
}
|
|
|
|
if (!cols.includes('custom_monthly_quota')) {
|
|
console.log('Migrating: Adding custom_monthly_quota to families...');
|
|
await connection.query("ALTER TABLE families ADD COLUMN custom_monthly_quota DECIMAL(10, 2) NULL");
|
|
}
|
|
} catch(e) { console.warn("Family migration warning:", e.message); }
|
|
|
|
// --- 3. PAYMENTS TABLE ---
|
|
await connection.query(`
|
|
CREATE TABLE IF NOT EXISTS payments (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
family_id VARCHAR(36) NOT NULL,
|
|
extraordinary_expense_id VARCHAR(36) NULL,
|
|
amount DECIMAL(10, 2) NOT NULL,
|
|
date_paid ${TIMESTAMP_TYPE} NOT NULL,
|
|
for_month INT NOT NULL,
|
|
for_year INT NOT NULL,
|
|
notes TEXT,
|
|
created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (family_id) REFERENCES families(id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
|
|
// --- 4. USERS TABLE ---
|
|
await connection.query(`
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
email VARCHAR(255) UNIQUE NOT NULL,
|
|
password_hash VARCHAR(255) NOT NULL,
|
|
name VARCHAR(255),
|
|
role VARCHAR(20) DEFAULT 'user',
|
|
phone VARCHAR(20),
|
|
family_id VARCHAR(36) NULL,
|
|
receive_alerts BOOLEAN DEFAULT TRUE,
|
|
created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (family_id) REFERENCES families(id) ON DELETE SET NULL
|
|
)
|
|
`);
|
|
|
|
// --- 5. ALERTS TABLE ---
|
|
await connection.query(`
|
|
CREATE TABLE IF NOT EXISTS alerts (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
condo_id VARCHAR(36) NULL,
|
|
subject VARCHAR(255) NOT NULL,
|
|
body TEXT,
|
|
days_offset INT DEFAULT 1,
|
|
offset_type VARCHAR(50) DEFAULT 'before_next_month',
|
|
send_hour INT DEFAULT 9,
|
|
active BOOLEAN DEFAULT TRUE,
|
|
last_sent ${TIMESTAMP_TYPE} NULL,
|
|
created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (condo_id) REFERENCES condos(id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
|
|
// --- 6. NOTICES TABLE ---
|
|
await connection.query(`
|
|
CREATE TABLE IF NOT EXISTS notices (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
condo_id VARCHAR(36) NOT NULL,
|
|
title VARCHAR(255) NOT NULL,
|
|
content TEXT,
|
|
type VARCHAR(50) DEFAULT 'info',
|
|
link VARCHAR(255),
|
|
date ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP,
|
|
active BOOLEAN DEFAULT TRUE,
|
|
target_families ${JSON_TYPE},
|
|
created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (condo_id) REFERENCES condos(id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
|
|
// Migration notices
|
|
try {
|
|
let cols = [];
|
|
if (DB_CLIENT === 'postgres') {
|
|
const [res] = await connection.query("SELECT column_name FROM information_schema.columns WHERE table_name='notices'");
|
|
cols = res.map(c => c.column_name);
|
|
} else {
|
|
const [res] = await connection.query("SHOW COLUMNS FROM notices");
|
|
cols = res.map(c => c.Field);
|
|
}
|
|
|
|
if (!cols.includes('target_families')) {
|
|
console.log('Migrating: Adding target_families to notices...');
|
|
await connection.query("ALTER TABLE notices ADD COLUMN target_families JSON");
|
|
}
|
|
} catch(e) { console.warn("Notices migration warning:", e.message); }
|
|
|
|
// --- 7. NOTICE READS ---
|
|
await connection.query(`
|
|
CREATE TABLE IF NOT EXISTS notice_reads (
|
|
user_id VARCHAR(36),
|
|
notice_id VARCHAR(36),
|
|
read_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (user_id, notice_id),
|
|
FOREIGN KEY (notice_id) REFERENCES notices(id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
|
|
// --- 8. TICKETS TABLE ---
|
|
await connection.query(`
|
|
CREATE TABLE IF NOT EXISTS tickets (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
condo_id VARCHAR(36) NOT NULL,
|
|
user_id VARCHAR(36) NOT NULL,
|
|
title VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
status VARCHAR(20) DEFAULT 'OPEN',
|
|
priority VARCHAR(20) DEFAULT 'MEDIUM',
|
|
category VARCHAR(20) DEFAULT 'OTHER',
|
|
created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (condo_id) REFERENCES condos(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
|
|
// --- 9. TICKET ATTACHMENTS ---
|
|
await connection.query(`
|
|
CREATE TABLE IF NOT EXISTS ticket_attachments (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
ticket_id VARCHAR(36) NOT NULL,
|
|
file_name VARCHAR(255) NOT NULL,
|
|
file_type VARCHAR(100),
|
|
data ${LONG_TEXT_TYPE},
|
|
created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (ticket_id) REFERENCES tickets(id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
|
|
// --- 10. TICKET COMMENTS ---
|
|
await connection.query(`
|
|
CREATE TABLE IF NOT EXISTS ticket_comments (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
ticket_id VARCHAR(36) NOT NULL,
|
|
user_id VARCHAR(36) NOT NULL,
|
|
text TEXT NOT NULL,
|
|
created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (ticket_id) REFERENCES tickets(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
|
|
// --- 11. EXTRAORDINARY EXPENSES ---
|
|
await connection.query(`
|
|
CREATE TABLE IF NOT EXISTS extraordinary_expenses (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
condo_id VARCHAR(36) NOT NULL,
|
|
title VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
start_date ${TIMESTAMP_TYPE},
|
|
end_date ${TIMESTAMP_TYPE},
|
|
contractor_name VARCHAR(255),
|
|
total_amount DECIMAL(15, 2) DEFAULT 0,
|
|
created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (condo_id) REFERENCES condos(id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
|
|
await connection.query(`
|
|
CREATE TABLE IF NOT EXISTS expense_items (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
expense_id VARCHAR(36) NOT NULL,
|
|
description VARCHAR(255),
|
|
amount DECIMAL(15, 2) NOT NULL,
|
|
FOREIGN KEY (expense_id) REFERENCES extraordinary_expenses(id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
|
|
await connection.query(`
|
|
CREATE TABLE IF NOT EXISTS expense_shares (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
expense_id VARCHAR(36) NOT NULL,
|
|
family_id VARCHAR(36) NOT NULL,
|
|
percentage DECIMAL(5, 2) NOT NULL,
|
|
amount_due DECIMAL(15, 2) NOT NULL,
|
|
amount_paid DECIMAL(15, 2) DEFAULT 0,
|
|
status VARCHAR(20) DEFAULT 'UNPAID',
|
|
FOREIGN KEY (expense_id) REFERENCES extraordinary_expenses(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (family_id) REFERENCES families(id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
|
|
await connection.query(`
|
|
CREATE TABLE IF NOT EXISTS expense_attachments (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
expense_id VARCHAR(36) NOT NULL,
|
|
file_name VARCHAR(255) NOT NULL,
|
|
file_type VARCHAR(100),
|
|
data ${LONG_TEXT_TYPE},
|
|
created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (expense_id) REFERENCES extraordinary_expenses(id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
|
|
// --- 12. CONDO ORDINARY EXPENSES (USCITE) ---
|
|
await connection.query(`
|
|
CREATE TABLE IF NOT EXISTS condo_expenses (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
condo_id VARCHAR(36) NOT NULL,
|
|
description VARCHAR(255) NOT NULL,
|
|
supplier_name VARCHAR(255),
|
|
amount DECIMAL(10, 2) NOT NULL,
|
|
payment_date ${TIMESTAMP_TYPE} NULL,
|
|
status VARCHAR(20) DEFAULT 'UNPAID',
|
|
payment_method VARCHAR(50),
|
|
invoice_number VARCHAR(100),
|
|
notes TEXT,
|
|
created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (condo_id) REFERENCES condos(id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
|
|
await connection.query(`
|
|
CREATE TABLE IF NOT EXISTS condo_expense_attachments (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
condo_expense_id VARCHAR(36) NOT NULL,
|
|
file_name VARCHAR(255) NOT NULL,
|
|
file_type VARCHAR(100),
|
|
data ${LONG_TEXT_TYPE},
|
|
created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (condo_expense_id) REFERENCES condo_expenses(id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
|
|
// --- 13. DOCUMENTS (Cloud/Local) ---
|
|
await connection.query(`
|
|
CREATE TABLE IF NOT EXISTS documents (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
condo_id VARCHAR(36) NOT NULL,
|
|
title VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
file_name VARCHAR(255) NOT NULL,
|
|
file_type VARCHAR(100),
|
|
file_size INT,
|
|
tags ${JSON_TYPE},
|
|
storage_provider VARCHAR(50) DEFAULT 'local_db',
|
|
file_data ${LONG_TEXT_TYPE},
|
|
upload_date ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (condo_id) REFERENCES condos(id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
|
|
// --- SEEDING ---
|
|
const [rows] = await connection.query('SELECT * FROM settings WHERE id = 1');
|
|
const defaultFeatures = {
|
|
multiCondo: true,
|
|
tickets: true,
|
|
payPal: true,
|
|
notices: true,
|
|
reports: true,
|
|
extraordinaryExpenses: true,
|
|
condoFinancialsView: false,
|
|
documents: true
|
|
};
|
|
|
|
const defaultStorage = {
|
|
provider: 'local_db',
|
|
apiKey: '',
|
|
apiSecret: '',
|
|
bucket: '',
|
|
region: ''
|
|
};
|
|
|
|
const defaultBranding = {
|
|
appName: 'CondoPay',
|
|
primaryColor: 'blue',
|
|
logoUrl: '',
|
|
loginBackgroundUrl: ''
|
|
};
|
|
|
|
if (rows.length === 0) {
|
|
const currentYear = new Date().getFullYear();
|
|
await connection.query(
|
|
'INSERT INTO settings (id, current_year, features, storage_config, branding) VALUES (1, ?, ?, ?, ?)',
|
|
[currentYear, JSON.stringify(defaultFeatures), JSON.stringify(defaultStorage), JSON.stringify(defaultBranding)]
|
|
);
|
|
|
|
const hash = await bcrypt.hash('admin', 10);
|
|
await connection.query(
|
|
'INSERT INTO users (id, email, password_hash, name, role, receive_alerts) VALUES (?, ?, ?, ?, ?, ?)',
|
|
['admin-id', 'admin@condo.it', hash, 'Amministratore', 'admin', true]
|
|
);
|
|
console.log("Database initialized with seed data.");
|
|
}
|
|
|
|
if (DB_CLIENT !== 'postgres') {
|
|
connection.release();
|
|
}
|
|
} catch (e) {
|
|
console.error("Database init error:", e);
|
|
}
|
|
};
|
|
|
|
module.exports = { pool: DB_CLIENT === 'postgres' ? pgPool : mysqlPool, initDb };
|