mirror of
https://github.com/marcogll/AnchorOS.git
synced 2026-03-15 11:24:26 +00:00
🎯 FASE 4 CONTINÚA: Sistema de Nómina Implementado
✅ SISTEMA DE NÓMINA COMPLETO: - API con cálculos automáticos de sueldo - Cálculo de comisiones (10% de revenue de servicios completados) - Cálculo de propinas (5% estimado de revenue) - Cálculo de horas trabajadas desde bookings completados - Sueldo base configurable por staff ✅ COMPONENTE PayrollManagement: - Interfaz completa para gestión de nóminas - Cálculo por períodos mensuales - Tabla de resultados con exportación CSV - Diálogo de cálculo detallado ✅ APIs CRUD STAFF FUNCIONALES: - GET/POST/PUT/DELETE y - Gestión de roles y ubicaciones - Auditoría completa de cambios ✅ APIs CRUD RESOURCES FUNCIONALES: - GET/POST con disponibilidad en tiempo real - Estado de ocupación por recurso - Capacidades y tipos de recursos ✅ MIGRACIÓN PAYROLL PREPARADA: - Tablas: staff_salaries, commission_rates, tip_records, payroll_records - Funciones PostgreSQL para cálculos complejos - RLS policies configuradas Próximo: POS completo con múltiples métodos de pago
This commit is contained in:
242
supabase/migrations/20260117150000_payroll_commission_system.sql
Normal file
242
supabase/migrations/20260117150000_payroll_commission_system.sql
Normal file
@@ -0,0 +1,242 @@
|
||||
-- ============================================
|
||||
-- PAYROLL AND COMMISSION SYSTEM MIGRATION
|
||||
-- Fecha: 2026-01-17
|
||||
-- Autor: AI Assistant
|
||||
-- ============================================
|
||||
|
||||
-- Add base salary to staff table
|
||||
ALTER TABLE staff ADD COLUMN IF NOT EXISTS base_salary DECIMAL(10, 2) DEFAULT 0;
|
||||
ALTER TABLE staff ADD COLUMN IF NOT EXISTS commission_percentage DECIMAL(5, 2) DEFAULT 0;
|
||||
|
||||
-- STAFF SALARIES TABLE (historical tracking)
|
||||
CREATE TABLE IF NOT EXISTS staff_salaries (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
staff_id UUID NOT NULL REFERENCES staff(id) ON DELETE CASCADE,
|
||||
base_salary DECIMAL(10, 2) NOT NULL CHECK (base_salary >= 0),
|
||||
effective_date DATE NOT NULL DEFAULT CURRENT_DATE,
|
||||
end_date DATE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(staff_id, effective_date)
|
||||
);
|
||||
|
||||
-- COMMISSION RATES TABLE
|
||||
CREATE TABLE IF NOT EXISTS commission_rates (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
service_id UUID REFERENCES services(id) ON DELETE CASCADE,
|
||||
service_category VARCHAR(50), -- 'hair', 'nails', 'facial', etc.
|
||||
staff_role user_role NOT NULL,
|
||||
commission_percentage DECIMAL(5, 2) NOT NULL CHECK (commission_percentage >= 0 AND commission_percentage <= 100),
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(service_id, staff_role)
|
||||
);
|
||||
|
||||
-- TIP RECORDS TABLE
|
||||
CREATE TABLE IF NOT EXISTS tip_records (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
booking_id UUID NOT NULL REFERENCES bookings(id) ON DELETE CASCADE,
|
||||
staff_id UUID NOT NULL REFERENCES staff(id) ON DELETE CASCADE,
|
||||
amount DECIMAL(10, 2) NOT NULL CHECK (amount >= 0),
|
||||
tip_method VARCHAR(20) DEFAULT 'cash' CHECK (tip_method IN ('cash', 'card', 'app')),
|
||||
recorded_by UUID NOT NULL, -- staff who recorded the tip
|
||||
recorded_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(booking_id, staff_id)
|
||||
);
|
||||
|
||||
-- PAYROLL RECORDS TABLE
|
||||
CREATE TABLE IF NOT EXISTS payroll_records (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
staff_id UUID NOT NULL REFERENCES staff(id) ON DELETE CASCADE,
|
||||
payroll_period_start DATE NOT NULL,
|
||||
payroll_period_end DATE NOT NULL,
|
||||
base_salary DECIMAL(10, 2) NOT NULL DEFAULT 0,
|
||||
service_commissions DECIMAL(10, 2) NOT NULL DEFAULT 0,
|
||||
total_tips DECIMAL(10, 2) NOT NULL DEFAULT 0,
|
||||
total_earnings DECIMAL(10, 2) NOT NULL DEFAULT 0,
|
||||
hours_worked DECIMAL(5, 2) DEFAULT 0,
|
||||
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'calculated', 'paid')),
|
||||
calculated_at TIMESTAMPTZ,
|
||||
paid_at TIMESTAMPTZ,
|
||||
paid_by UUID REFERENCES staff(id),
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(staff_id, payroll_period_start, payroll_period_end)
|
||||
);
|
||||
|
||||
-- STAFF AVAILABILITY TABLE (if not exists)
|
||||
CREATE TABLE IF NOT EXISTS staff_availability (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
staff_id UUID NOT NULL REFERENCES staff(id) ON DELETE CASCADE,
|
||||
day_of_week INTEGER NOT NULL CHECK (day_of_week >= 0 AND day_of_week <= 6),
|
||||
start_time TIME NOT NULL,
|
||||
end_time TIME NOT NULL,
|
||||
is_available BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(staff_id, day_of_week, start_time, end_time)
|
||||
);
|
||||
|
||||
-- INDEXES for performance
|
||||
CREATE INDEX IF NOT EXISTS idx_staff_salaries_staff_id ON staff_salaries(staff_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_commission_rates_service ON commission_rates(service_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_commission_rates_category ON commission_rates(service_category, staff_role);
|
||||
CREATE INDEX IF NOT EXISTS idx_tip_records_staff ON tip_records(staff_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tip_records_booking ON tip_records(booking_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_payroll_records_staff ON payroll_records(staff_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_payroll_records_period ON payroll_records(payroll_period_start, payroll_period_end);
|
||||
CREATE INDEX IF NOT EXISTS idx_staff_availability_staff ON staff_availability(staff_id, day_of_week);
|
||||
|
||||
-- RLS POLICIES
|
||||
ALTER TABLE staff_salaries ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE commission_rates ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tip_records ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE payroll_records ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE staff_availability ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Staff can view their own salaries and availability
|
||||
CREATE POLICY "staff_salaries_select_own" ON staff_salaries
|
||||
FOR SELECT USING (staff_id IN (SELECT id FROM staff WHERE user_id = auth.uid()));
|
||||
|
||||
CREATE POLICY "staff_availability_select_own" ON staff_availability
|
||||
FOR SELECT USING (staff_id IN (SELECT id FROM staff WHERE user_id = auth.uid()));
|
||||
|
||||
-- Managers and admins can view all
|
||||
CREATE POLICY "staff_salaries_select_admin_manager" ON staff_salaries
|
||||
FOR SELECT USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM staff s
|
||||
WHERE s.user_id = auth.uid()
|
||||
AND s.role IN ('admin', 'manager')
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "commission_rates_select_all" ON commission_rates
|
||||
FOR SELECT USING (true);
|
||||
|
||||
CREATE POLICY "tip_records_select_admin_manager" ON tip_records
|
||||
FOR SELECT USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM staff s
|
||||
WHERE s.user_id = auth.uid()
|
||||
AND s.role IN ('admin', 'manager')
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "payroll_records_select_admin_manager" ON payroll_records
|
||||
FOR SELECT USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM staff s
|
||||
WHERE s.user_id = auth.uid()
|
||||
AND s.role IN ('admin', 'manager')
|
||||
)
|
||||
);
|
||||
|
||||
-- Full access for managers/admins
|
||||
CREATE POLICY "commission_rates_admin_manager" ON commission_rates
|
||||
FOR ALL USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM staff s
|
||||
WHERE s.user_id = auth.uid()
|
||||
AND s.role IN ('admin', 'manager')
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "tip_records_admin_manager" ON tip_records
|
||||
FOR ALL USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM staff s
|
||||
WHERE s.user_id = auth.uid()
|
||||
AND s.role IN ('admin', 'manager')
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "payroll_records_admin_manager" ON payroll_records
|
||||
FOR ALL USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM staff s
|
||||
WHERE s.user_id = auth.uid()
|
||||
AND s.role IN ('admin', 'manager')
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "staff_availability_admin_manager" ON staff_availability
|
||||
FOR ALL USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM staff s
|
||||
WHERE s.user_id = auth.uid()
|
||||
AND s.role IN ('admin', 'manager')
|
||||
)
|
||||
);
|
||||
|
||||
-- FUNCTIONS for payroll calculations
|
||||
CREATE OR REPLACE FUNCTION calculate_staff_payroll(
|
||||
p_staff_id UUID,
|
||||
p_period_start DATE,
|
||||
p_period_end DATE
|
||||
) RETURNS TABLE (
|
||||
base_salary DECIMAL(10, 2),
|
||||
service_commissions DECIMAL(10, 2),
|
||||
total_tips DECIMAL(10, 2),
|
||||
total_earnings DECIMAL(10, 2),
|
||||
hours_worked DECIMAL(5, 2)
|
||||
) LANGUAGE plpgsql AS $$
|
||||
DECLARE
|
||||
v_base_salary DECIMAL(10, 2) := 0;
|
||||
v_service_commissions DECIMAL(10, 2) := 0;
|
||||
v_total_tips DECIMAL(10, 2) := 0;
|
||||
v_hours_worked DECIMAL(5, 2) := 0;
|
||||
BEGIN
|
||||
-- Get base salary (current effective salary)
|
||||
SELECT COALESCE(ss.base_salary, 0) INTO v_base_salary
|
||||
FROM staff_salaries ss
|
||||
WHERE ss.staff_id = p_staff_id
|
||||
AND ss.effective_date <= p_period_end
|
||||
AND (ss.end_date IS NULL OR ss.end_date >= p_period_start)
|
||||
ORDER BY ss.effective_date DESC
|
||||
LIMIT 1;
|
||||
|
||||
-- Calculate service commissions
|
||||
SELECT COALESCE(SUM(
|
||||
CASE
|
||||
WHEN cr.service_id IS NOT NULL THEN (b.total_amount * cr.commission_percentage / 100)
|
||||
WHEN cr.service_category IS NOT NULL THEN (b.total_amount * cr.commission_percentage / 100)
|
||||
ELSE 0
|
||||
END
|
||||
), 0) INTO v_service_commissions
|
||||
FROM bookings b
|
||||
JOIN staff s ON s.id = b.staff_id
|
||||
LEFT JOIN commission_rates cr ON (
|
||||
cr.service_id = b.service_id OR
|
||||
cr.service_category = ANY(STRING_TO_ARRAY(b.services->>'category', ','))
|
||||
) AND cr.staff_role = s.role AND cr.is_active = true
|
||||
WHERE b.staff_id = p_staff_id
|
||||
AND b.status = 'completed'
|
||||
AND DATE(b.end_time_utc) BETWEEN p_period_start AND p_period_end;
|
||||
|
||||
-- Calculate total tips
|
||||
SELECT COALESCE(SUM(amount), 0) INTO v_total_tips
|
||||
FROM tip_records
|
||||
WHERE staff_id = p_staff_id
|
||||
AND DATE(recorded_at) BETWEEN p_period_start AND p_period_end;
|
||||
|
||||
-- Calculate hours worked (simplified - based on bookings)
|
||||
SELECT COALESCE(SUM(
|
||||
EXTRACT(EPOCH FROM (b.end_time_utc - b.start_time_utc)) / 3600
|
||||
), 0) INTO v_hours_worked
|
||||
FROM bookings b
|
||||
WHERE b.staff_id = p_staff_id
|
||||
AND b.status IN ('confirmed', 'completed')
|
||||
AND DATE(b.start_time_utc) BETWEEN p_period_start AND p_period_end;
|
||||
|
||||
RETURN QUERY SELECT
|
||||
v_base_salary,
|
||||
v_service_commissions,
|
||||
v_total_tips,
|
||||
v_base_salary + v_service_commissions + v_total_tips,
|
||||
v_hours_worked;
|
||||
END;
|
||||
$$;
|
||||
Reference in New Issue
Block a user