288 lines
9.4 KiB
TypeScript
288 lines
9.4 KiB
TypeScript
import { db } from './db';
|
|
import bcrypt from 'bcryptjs';
|
|
|
|
export async function initSchema() {
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id SERIAL PRIMARY KEY,
|
|
username VARCHAR(50) UNIQUE NOT NULL,
|
|
password_hash VARCHAR(255) NOT NULL,
|
|
role VARCHAR(20) NOT NULL CHECK (role IN ('admin', 'user')),
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS images (
|
|
id SERIAL PRIMARY KEY,
|
|
filename VARCHAR(255) NOT NULL,
|
|
data TEXT NOT NULL,
|
|
uploaded_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS reservations (
|
|
id SERIAL PRIMARY KEY,
|
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
date DATE NOT NULL,
|
|
time TIME NOT NULL,
|
|
guests INTEGER NOT NULL,
|
|
table_name VARCHAR(100) NOT NULL,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS coupons (
|
|
id SERIAL PRIMARY KEY,
|
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
code VARCHAR(50) NOT NULL,
|
|
discount VARCHAR(255) NOT NULL,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS offers (
|
|
id SERIAL PRIMARY KEY,
|
|
title VARCHAR(255) NOT NULL,
|
|
description TEXT NOT NULL,
|
|
active BOOLEAN DEFAULT TRUE,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS benefits (
|
|
id SERIAL PRIMARY KEY,
|
|
text VARCHAR(500) NOT NULL,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS config (
|
|
key VARCHAR(100) PRIMARY KEY,
|
|
value TEXT NOT NULL
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS rooms (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT NOT NULL,
|
|
price NUMERIC(10,2) NOT NULL,
|
|
photos TEXT[] DEFAULT '{}',
|
|
active BOOLEAN DEFAULT TRUE,
|
|
sort_order INTEGER DEFAULT 0,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS menu_items (
|
|
id SERIAL PRIMARY KEY,
|
|
category VARCHAR(100) NOT NULL,
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
price NUMERIC(10,2) NOT NULL,
|
|
is_daily_special BOOLEAN DEFAULT FALSE,
|
|
active BOOLEAN DEFAULT TRUE,
|
|
sort_order INTEGER DEFAULT 0,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS places (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT NOT NULL,
|
|
photo VARCHAR(500),
|
|
distance_km NUMERIC(8,2),
|
|
visit_time VARCHAR(100),
|
|
suggestion TEXT,
|
|
active BOOLEAN DEFAULT TRUE,
|
|
sort_order INTEGER DEFAULT 0,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS reviews (
|
|
id SERIAL PRIMARY KEY,
|
|
author_name VARCHAR(255) NOT NULL,
|
|
rating INTEGER NOT NULL CHECK (rating BETWEEN 1 AND 5),
|
|
text TEXT NOT NULL,
|
|
approved BOOLEAN DEFAULT FALSE,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS slides (
|
|
id SERIAL PRIMARY KEY,
|
|
title VARCHAR(255),
|
|
subtitle TEXT,
|
|
image_id INTEGER REFERENCES images(id) ON DELETE SET NULL,
|
|
image_url VARCHAR(500),
|
|
active BOOLEAN DEFAULT TRUE,
|
|
sort_order INTEGER DEFAULT 0,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS room_comments (
|
|
id SERIAL PRIMARY KEY,
|
|
room_id INTEGER REFERENCES rooms(id) ON DELETE CASCADE,
|
|
photo_url VARCHAR(500),
|
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
first_name VARCHAR(100) NOT NULL,
|
|
last_initial VARCHAR(10),
|
|
text TEXT NOT NULL,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
deleted_by_admin BOOLEAN DEFAULT FALSE
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
ALTER TABLE rooms ADD COLUMN IF NOT EXISTS featured_photo VARCHAR(500);
|
|
`);
|
|
|
|
await db.query(`
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS comments_disabled BOOLEAN DEFAULT FALSE;
|
|
`);
|
|
|
|
await db.query(`
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS first_name VARCHAR(100);
|
|
`);
|
|
|
|
await db.query(`
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS last_name VARCHAR(100);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS user_sessions (
|
|
id SERIAL PRIMARY KEY,
|
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
token TEXT NOT NULL,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
`);
|
|
}
|
|
|
|
export async function seed() {
|
|
const { rows: adminRows } = await db.query(`SELECT id FROM users WHERE username = 'admin'`);
|
|
if (adminRows.length === 0) {
|
|
const adminHash = bcrypt.hashSync('admin', 10);
|
|
const userHash = bcrypt.hashSync('user', 10);
|
|
|
|
await db.query(
|
|
`INSERT INTO users (username, password_hash, role) VALUES ($1, $2, $3), ($4, $5, $6)`,
|
|
['admin', adminHash, 'admin', 'user', userHash, 'user']
|
|
);
|
|
}
|
|
|
|
const { rows: offerRows } = await db.query(`SELECT id FROM offers`);
|
|
if (offerRows.length === 0) {
|
|
await db.query(
|
|
`INSERT INTO offers (title, description) VALUES
|
|
($1, $2), ($3, $4)`,
|
|
[
|
|
'Summer Wine Weekend', 'Complimentary wine tasting with every two-night stay.',
|
|
'Sunset Dinner', 'Three-course menu on the terrace every Friday.',
|
|
]
|
|
);
|
|
}
|
|
|
|
const { rows: benefitRows } = await db.query(`SELECT id FROM benefits`);
|
|
if (benefitRows.length === 0) {
|
|
await db.query(
|
|
`INSERT INTO benefits (text) VALUES
|
|
($1), ($2), ($3), ($4)`,
|
|
[
|
|
'Priority booking for peak weekends',
|
|
'Late checkout on availability',
|
|
'Free welcome cocktail',
|
|
'Member-only events and tastings',
|
|
]
|
|
);
|
|
}
|
|
|
|
const { rows: configRows } = await db.query(`SELECT key FROM config WHERE key = 'wifi_password'`);
|
|
if (configRows.length === 0) {
|
|
await db.query(
|
|
`INSERT INTO config (key, value) VALUES ($1, $2)`,
|
|
['wifi_password', 'LaHuascaGuest2026']
|
|
);
|
|
}
|
|
|
|
const { rows: userResRows } = await db.query(`SELECT id FROM reservations WHERE user_id = (SELECT id FROM users WHERE username = 'user')`);
|
|
if (userResRows.length === 0) {
|
|
await db.query(
|
|
`INSERT INTO reservations (user_id, date, time, guests, table_name)
|
|
SELECT id, $2, $3, $4, $5 FROM users WHERE username = $1`,
|
|
['user', '2026-07-12', '19:00', 4, 'Terrace 3']
|
|
);
|
|
await db.query(
|
|
`INSERT INTO reservations (user_id, date, time, guests, table_name)
|
|
SELECT id, $2, $3, $4, $5 FROM users WHERE username = $1`,
|
|
['user', '2026-07-25', '20:30', 2, 'Barrel 7']
|
|
);
|
|
}
|
|
|
|
const { rows: userCouponRows } = await db.query(`SELECT id FROM coupons WHERE user_id = (SELECT id FROM users WHERE username = 'user')`);
|
|
if (userCouponRows.length === 0) {
|
|
await db.query(
|
|
`INSERT INTO coupons (user_id, code, discount)
|
|
SELECT id, $2, $3 FROM users WHERE username = $1`,
|
|
['user', 'WELCOME20', '20% off your next stay']
|
|
);
|
|
await db.query(
|
|
`INSERT INTO coupons (user_id, code, discount)
|
|
SELECT id, $2, $3 FROM users WHERE username = $1`,
|
|
['user', 'COFFEE5', '$5 off any coffee tasting']
|
|
);
|
|
}
|
|
|
|
const { rows: calendarRows } = await db.query(`SELECT id FROM calendar_events`);
|
|
if (calendarRows.length === 0) {
|
|
const thisYear = new Date().getFullYear();
|
|
await db.query(
|
|
`INSERT INTO calendar_events (date, title, type, description, color) VALUES
|
|
($1, $2, $3, $4, $5), ($6, $7, $8, $9, $10), ($11, $12, $13, $14, $15),
|
|
($16, $17, $18, $19, $20), ($21, $22, $23, $24, $25), ($26, $27, $28, $29, $30),
|
|
($31, $32, $33, $34, $35), ($36, $37, $38, $39, $40), ($41, $42, $43, $44, $45),
|
|
($46, $47, $48, $49, $50), ($51, $52, $53, $54, $55)`,
|
|
[
|
|
`${thisYear}-01-01`, 'New Year', 'holiday', 'Start the year in the Andes.', '#E8A849',
|
|
`${thisYear}-02-14`, 'Valentine', 'holiday', 'Special dinner menu.', '#D94B73',
|
|
`${thisYear}-03-20`, 'Spring Equinox', 'season', 'Blooming highlands.', '#7CB342',
|
|
`${thisYear}-06-21`, 'Summer Solstice', 'season', 'Longest days of the year.', '#E8A849',
|
|
`${thisYear}-09-22`, 'Fall Equinox', 'season', 'Harvest colors.', '#A6683C',
|
|
`${thisYear}-10-31`, 'Halloween', 'holiday', 'Costume dinner party.', '#9C27B0',
|
|
`${thisYear}-11-02`, 'Day of the Dead', 'holiday', 'Traditional altar and meal.', '#FF9800',
|
|
`${thisYear}-12-21`, 'Winter Solstice', 'season', 'Cozy fires and clear skies.', '#4FC3F7',
|
|
`${thisYear}-12-25`, 'Christmas', 'holiday', 'Holiday feast.', '#C62828',
|
|
`${thisYear}-12-31`, 'New Year Eve', 'holiday', 'Countdown dinner.', '#E8A849',
|
|
`${thisYear}-06-28`, 'Sunny 22°C', 'weather', 'Typical Otavalo dry season day.', '#FFD54F',
|
|
]
|
|
);
|
|
}
|
|
|
|
const { rows: siteRows } = await db.query(`SELECT key FROM config WHERE key = 'tagline'`);
|
|
if (siteRows.length === 0) {
|
|
await db.query(`INSERT INTO config (key, value) VALUES ($1, $2)`, ['tagline', 'Hotel · Restaurant · Andean Highlands']);
|
|
}
|
|
}
|
|
|
|
export async function resetDatabase() {
|
|
await db.query('DROP TABLE IF EXISTS room_comments, reviews, places, menu_items, rooms, config, benefits, offers, coupons, reservations, images, users, slides, calendar_events CASCADE');
|
|
await initSchema();
|
|
await seed();
|
|
}
|