docs, css, database: Complete Phase 1 - Foundation & Database Configuration

This commit is contained in:
Luis Gabriel Ramos Robles 2026-06-11 14:46:55 +00:00
parent 34f1a0d3d1
commit f6b8f019cd
3 changed files with 350 additions and 5 deletions

View file

@ -7,14 +7,14 @@
This plan outlines the step-by-step path to construct, test, and host the platform securely.
## Phase 1: Foundation & Database Configuration
* [ ] Initialize Next.js app with TypeScript and `npx` in the repository root.
* [ ] Configure Vanilla CSS design tokens (variables, layout standards, light/dark themes).
* [ ] Define the PostgreSQL schemas in `schema.prisma` including:
* [x] Initialize Next.js app with TypeScript and `npx` in the repository root.
* [x] Configure Vanilla CSS design tokens (variables, layout standards, light/dark themes).
* [x] Define the PostgreSQL schemas in `schema.prisma` including:
* Temporal versioning fields for plans (`version`, `validity_start`, `validity_end`).
* Idempotency and transaction fields for sales imports.
* Adjustment and original reference keys for settlements.
* [ ] Configure **PostgreSQL Row-Level Security (RLS)** policies on the schema for user/hotel data isolation.
* [ ] Execute initial database migration to seed basic structural tables (Regions, Hotels, Roles).
* [x] Configure **PostgreSQL Row-Level Security (RLS)** policies on the schema for user/hotel data isolation.
* [x] Execute initial database migration to seed basic structural tables (Regions, Hotels, Roles).
## Phase 2: Authentication & Security Core
* [ ] Implement secure JWT session cookie-based auth.

213
prisma/rls_and_seed.sql Normal file
View file

@ -0,0 +1,213 @@
-- RLS Policies and Seeding Script for Hoteles Estelar Variable Remuneration System
-- ==========================================
-- 1. SEED DATA
-- ==========================================
-- Seed Regions
INSERT INTO "regions" ("name", "code") VALUES
('Bogotá', 'BOG'),
('Antioquia', 'ANT'),
('Caribe', 'CAR')
ON CONFLICT ("code") DO NOTHING;
-- Seed Hotels
INSERT INTO "hotels" ("name", "code", "region_id", "status") VALUES
('Estelar Parque de la 93', 'EST-P93', (SELECT id FROM regions WHERE code = 'BOG'), 'ACTIVE'),
('Estelar Medellin', 'EST-MDE', (SELECT id FROM regions WHERE code = 'ANT'), 'ACTIVE'),
('Estelar Cartagena', 'EST-CTG', (SELECT id FROM regions WHERE code = 'CAR'), 'ACTIVE')
ON CONFLICT ("code") DO NOTHING;
-- Seed Users
-- password_hash is bcrypt hash of 'password123'
INSERT INTO "users" ("username", "email", "password_hash", "role", "hotel_id", "area", "status", "created_at") VALUES
('admin', 'admin@estelar.com', '$2b$10$EpjJNrk.wU5YzB3sJsmJg.35A5EJUPhq31O7p8HwS5zM4pU1fT26G', 'ADMIN', (SELECT id FROM hotels WHERE code = 'EST-P93'), 'Sistemas', 'ACTIVE', NOW()),
('director', 'director@estelar.com', '$2b$10$EpjJNrk.wU5YzB3sJsmJg.35A5EJUPhq31O7p8HwS5zM4pU1fT26G', 'DIRECTOR', (SELECT id FROM hotels WHERE code = 'EST-P93'), 'Comercial', 'ACTIVE', NOW()),
('gerente_mde', 'gerente.mde@estelar.com', '$2b$10$EpjJNrk.wU5YzB3sJsmJg.35A5EJUPhq31O7p8HwS5zM4pU1fT26G', 'GERENTE', (SELECT id FROM hotels WHERE code = 'EST-MDE'), 'Administracion', 'ACTIVE', NOW()),
('lider_ctg', 'lider.ctg@estelar.com', '$2b$10$EpjJNrk.wU5YzB3sJsmJg.35A5EJUPhq31O7p8HwS5zM4pU1fT26G', 'LIDER', (SELECT id FROM hotels WHERE code = 'EST-CTG'), 'Ventas', 'ACTIVE', NOW()),
('analista', 'analista@estelar.com', '$2b$10$EpjJNrk.wU5YzB3sJsmJg.35A5EJUPhq31O7p8HwS5zM4pU1fT26G', 'ANALISTA', (SELECT id FROM hotels WHERE code = 'EST-P93'), 'Finanzas', 'ACTIVE', NOW()),
('consulta', 'consulta@estelar.com', '$2b$10$EpjJNrk.wU5YzB3sJsmJg.35A5EJUPhq31O7p8HwS5zM4pU1fT26G', 'CONSULTA', (SELECT id FROM hotels WHERE code = 'EST-P93'), 'Auditoria', 'ACTIVE', NOW()),
('colaborador_mde', 'colaborador.mde@estelar.com', '$2b$10$EpjJNrk.wU5YzB3sJsmJg.35A5EJUPhq31O7p8HwS5zM4pU1fT26G', 'COLABORADOR', (SELECT id FROM hotels WHERE code = 'EST-MDE'), 'Ventas', 'ACTIVE', NOW())
ON CONFLICT ("username") DO NOTHING;
-- ==========================================
-- 2. ENABLE ROW-LEVEL SECURITY
-- ==========================================
ALTER TABLE "regions" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "hotels" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "users" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "goals" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "sales_results" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "settlements" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "audit_logs" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "regions" FORCE ROW LEVEL SECURITY;
ALTER TABLE "hotels" FORCE ROW LEVEL SECURITY;
ALTER TABLE "users" FORCE ROW LEVEL SECURITY;
ALTER TABLE "goals" FORCE ROW LEVEL SECURITY;
ALTER TABLE "sales_results" FORCE ROW LEVEL SECURITY;
ALTER TABLE "settlements" FORCE ROW LEVEL SECURITY;
ALTER TABLE "audit_logs" FORCE ROW LEVEL SECURITY;
-- ==========================================
-- 3. CLEAN UP OLD POLICIES
-- ==========================================
DROP POLICY IF EXISTS audit_logs_insert_policy ON "audit_logs";
DROP POLICY IF EXISTS audit_logs_select_policy ON "audit_logs";
DROP POLICY IF EXISTS regions_select_policy ON "regions";
DROP POLICY IF EXISTS regions_modify_policy ON "regions";
DROP POLICY IF EXISTS hotels_select_policy ON "hotels";
DROP POLICY IF EXISTS hotels_modify_policy ON "hotels";
DROP POLICY IF EXISTS users_select_policy ON "users";
DROP POLICY IF EXISTS users_modify_policy ON "users";
DROP POLICY IF EXISTS goals_select_policy ON "goals";
DROP POLICY IF EXISTS goals_modify_policy ON "goals";
DROP POLICY IF EXISTS sales_results_select_policy ON "sales_results";
DROP POLICY IF EXISTS sales_results_modify_policy ON "sales_results";
DROP POLICY IF EXISTS settlements_select_policy ON "settlements";
DROP POLICY IF EXISTS settlements_modify_policy ON "settlements";
DROP POLICY IF EXISTS plans_select_policy ON "compensation_plans";
DROP POLICY IF EXISTS plans_modify_policy ON "compensation_plans";
DROP POLICY IF EXISTS rules_select_policy ON "calculation_rules";
DROP POLICY IF EXISTS rules_modify_policy ON "calculation_rules";
-- ==========================================
-- 4. CREATE RLS POLICIES
-- ==========================================
-- A. Audit Logs Policies (Insert allowed for all, Select only for Admin/Analista, No Updates/Deletes)
CREATE POLICY audit_logs_insert_policy ON "audit_logs"
FOR INSERT WITH CHECK (true);
CREATE POLICY audit_logs_select_policy ON "audit_logs"
FOR SELECT USING (current_setting('app.current_user_role', true) IN ('ADMIN', 'ANALISTA'));
-- B. Regions Policies
CREATE POLICY regions_select_policy ON "regions"
FOR SELECT USING (
current_setting('app.current_user_role', true) IN ('ADMIN', 'ANALISTA', 'DIRECTOR')
OR id = NULLIF(current_setting('app.current_region_id', true), '')::integer
);
CREATE POLICY regions_modify_policy ON "regions"
FOR ALL USING (current_setting('app.current_user_role', true) IN ('ADMIN', 'DIRECTOR'));
-- C. Hotels Policies
CREATE POLICY hotels_select_policy ON "hotels"
FOR SELECT USING (
current_setting('app.current_user_role', true) IN ('ADMIN', 'ANALISTA', 'DIRECTOR')
OR region_id = NULLIF(current_setting('app.current_region_id', true), '')::integer
OR id = NULLIF(current_setting('app.current_hotel_id', true), '')::integer
);
CREATE POLICY hotels_modify_policy ON "hotels"
FOR ALL USING (current_setting('app.current_user_role', true) IN ('ADMIN', 'DIRECTOR'));
-- D. Users Policies
CREATE POLICY users_select_policy ON "users"
FOR SELECT USING (
current_setting('app.current_user_role', true) IN ('ADMIN', 'ANALISTA', 'DIRECTOR')
OR hotel_id = NULLIF(current_setting('app.current_hotel_id', true), '')::integer
OR hotel_id IN (SELECT id FROM hotels WHERE region_id = NULLIF(current_setting('app.current_region_id', true), '')::integer)
OR id = NULLIF(current_setting('app.current_user_id', true), '')::integer
);
CREATE POLICY users_modify_policy ON "users"
FOR ALL USING (current_setting('app.current_user_role', true) = 'ADMIN');
-- E. Goals Policies
CREATE POLICY goals_select_policy ON "goals"
FOR SELECT USING (
current_setting('app.current_user_role', true) IN ('ADMIN', 'ANALISTA', 'DIRECTOR')
OR target_id = NULLIF(current_setting('app.current_user_id', true), '')::integer
OR target_id IN (
SELECT u.id FROM users u WHERE u.hotel_id = NULLIF(current_setting('app.current_hotel_id', true), '')::integer
)
OR target_id IN (
SELECT u.id FROM users u JOIN hotels h ON u.hotel_id = h.id
WHERE h.region_id = NULLIF(current_setting('app.current_region_id', true), '')::integer
)
);
CREATE POLICY goals_modify_policy ON "goals"
FOR ALL USING (current_setting('app.current_user_role', true) IN ('ADMIN', 'DIRECTOR'));
-- F. Sales Results Policies
CREATE POLICY sales_results_select_policy ON "sales_results"
FOR SELECT USING (
current_setting('app.current_user_role', true) IN ('ADMIN', 'ANALISTA', 'DIRECTOR')
OR user_id = NULLIF(current_setting('app.current_user_id', true), '')::integer
OR hotel_id = NULLIF(current_setting('app.current_hotel_id', true), '')::integer
OR hotel_id IN (
SELECT id FROM hotels WHERE region_id = NULLIF(current_setting('app.current_region_id', true), '')::integer
)
);
CREATE POLICY sales_results_modify_policy ON "sales_results"
FOR ALL USING (
current_setting('app.current_user_role', true) IN ('ADMIN', 'ANALISTA')
OR (
current_setting('app.current_user_role', true) = 'LIDER'
AND hotel_id IN (
SELECT id FROM hotels WHERE region_id = NULLIF(current_setting('app.current_region_id', true), '')::integer
)
)
);
-- G. Settlements Policies
CREATE POLICY settlements_select_policy ON "settlements"
FOR SELECT USING (
current_setting('app.current_user_role', true) IN ('ADMIN', 'ANALISTA', 'DIRECTOR')
OR user_id = NULLIF(current_setting('app.current_user_id', true), '')::integer
OR user_id IN (
SELECT u.id FROM users u WHERE u.hotel_id = NULLIF(current_setting('app.current_hotel_id', true), '')::integer
)
OR user_id IN (
SELECT u.id FROM users u JOIN hotels h ON u.hotel_id = h.id
WHERE h.region_id = NULLIF(current_setting('app.current_region_id', true), '')::integer
)
);
CREATE POLICY settlements_modify_policy ON "settlements"
FOR ALL USING (
current_setting('app.current_user_role', true) IN ('ADMIN', 'ANALISTA')
OR (
current_setting('app.current_user_role', true) = 'LIDER'
AND user_id IN (
SELECT u.id FROM users u JOIN hotels h ON u.hotel_id = h.id
WHERE h.region_id = NULLIF(current_setting('app.current_region_id', true), '')::integer
)
)
);
-- H. Compensation Plans Policies
ALTER TABLE "compensation_plans" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "compensation_plans" FORCE ROW LEVEL SECURITY;
CREATE POLICY plans_select_policy ON "compensation_plans"
FOR SELECT USING (true);
CREATE POLICY plans_modify_policy ON "compensation_plans"
FOR ALL USING (current_setting('app.current_user_role', true) IN ('ADMIN', 'DIRECTOR', 'ANALISTA'));
-- I. Calculation Rules Policies
ALTER TABLE "calculation_rules" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "calculation_rules" FORCE ROW LEVEL SECURITY;
CREATE POLICY rules_select_policy ON "calculation_rules"
FOR SELECT USING (true);
CREATE POLICY rules_modify_policy ON "calculation_rules"
FOR ALL USING (current_setting('app.current_user_role', true) IN ('ADMIN', 'DIRECTOR', 'ANALISTA'));

132
src/app/globals.css Normal file
View file

@ -0,0 +1,132 @@
:root {
/* Color Palette - HSL values for premium dark/light mode */
--primary-h: 220;
--primary-s: 85%;
--primary-l: 57%;
--primary: hsl(var(--primary-h), var(--primary-s), var(--primary-l));
--secondary-h: 260;
--secondary-s: 70%;
--secondary-l: 50%;
--secondary: hsl(var(--secondary-h), var(--secondary-s), var(--secondary-l));
--background-h: 0;
--background-s: 0%;
--background-l: 100%;
--background: hsl(var(--background-h), var(--background-s), var(--background-l));
--foreground-h: 220;
--foreground-s: 40%;
--foreground-l: 10%;
--foreground: hsl(var(--foreground-h), var(--foreground-s), var(--foreground-l));
--card: hsl(0, 0%, 97%);
--border: hsl(220, 20%, 90%);
/* Spacing Grid (8pt Grid System) */
--space-1: 0.25rem; /* 4px */
--space-2: 0.5rem; /* 8px */
--space-3: 0.75rem; /* 12px */
--space-4: 1rem; /* 16px */
--space-6: 1.5rem; /* 24px */
--space-8: 2rem; /* 32px */
--space-12: 3rem; /* 48px */
/* Typography Scale */
--font-sans: 'Inter', system-ui, -apple-system, sans-serif;
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--text-xl: 1.25rem;
--text-2xl: 1.5rem;
--text-3xl: 1.875rem;
--text-4xl: 2.25rem;
--weight-normal: 400;
--weight-medium: 500;
--weight-semibold: 600;
--weight-bold: 700;
/* Border Radius & Shadow Tokens */
--radius-sm: 0.375rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
--radius-xl: 1rem;
--radius-full: 9999px;
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
--shadow-glow: 0 0 15px 2px hsla(var(--primary-h), var(--primary-s), var(--primary-l), 0.15);
/* Transitions */
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-normal: 250ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-slow: 350ms cubic-bezier(0.4, 0, 0.2, 1);
}
/* Dark mode overrides (Activated via html[data-theme='dark'] or prefers-color-scheme) */
[data-theme='dark'] {
--background-h: 220;
--background-s: 40%;
--background-l: 6%;
--background: hsl(var(--background-h), var(--background-s), var(--background-l));
--foreground-h: 220;
--foreground-s: 15%;
--foreground-l: 90%;
--foreground: hsl(var(--foreground-h), var(--foreground-s), var(--foreground-l));
--card: hsl(220, 30%, 11%);
--border: hsl(220, 20%, 18%);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme='light']) {
--background-h: 220;
--background-s: 40%;
--background-l: 6%;
--background: hsl(var(--background-h), var(--background-s), var(--background-l));
--foreground-h: 220;
--foreground-s: 15%;
--foreground-l: 90%;
--foreground: hsl(var(--foreground-h), var(--foreground-s), var(--foreground-l));
--card: hsl(220, 30%, 11%);
--border: hsl(220, 20%, 18%);
}
}
html {
height: 100%;
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
}
body {
min-height: 100%;
display: flex;
flex-direction: column;
color: var(--foreground);
background: var(--background);
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
a {
color: inherit;
text-decoration: none;
}