diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 88e2843..cfc83f0 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -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. diff --git a/prisma/rls_and_seed.sql b/prisma/rls_and_seed.sql new file mode 100644 index 0000000..291ce45 --- /dev/null +++ b/prisma/rls_and_seed.sql @@ -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')); diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 0000000..4646f15 --- /dev/null +++ b/src/app/globals.css @@ -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; +}