0ac4beaaa8
- Add requireAuth() to admin-permissions.ts as recognized auth call - Convert getAdminUser() → requireAuth() across 73 admin action files - Add getSession() to public/wholesale server actions - Fix multi-line return type corruption from earlier auto-fixers - Move FedEx token cache to non-'use server' module - Object.freeze module-level constants: PRICE_KEYS, EMPTY_MOBILE_DASHBOARD, EMPTY_PAY_PERIOD, LOCALE_CART_SUBJECT, WELCOME_EMAILS - Update Stripe API version 2026-05-27 → 2026-06-24 - Fix wholesale employee portal: getEmployeeSessionAction + EmployeePortalClient - Fix 51 TypeScript errors (return type corruption, missing imports)
570 lines
18 KiB
PL/PgSQL
570 lines
18 KiB
PL/PgSQL
-- Worker Time Tracking
|
|
-- Tables: time_tracking_workers, time_tracking_tasks, worker_time_logs
|
|
|
|
-- ── Workers ────────────────────────────────────────────────────────────────────
|
|
|
|
CREATE TABLE IF NOT EXISTS time_tracking_workers (
|
|
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
|
brand_id UUID REFERENCES brands(id) ON DELETE CASCADE NOT NULL,
|
|
name TEXT NOT NULL,
|
|
pin_hash TEXT NOT NULL, -- bcrypt hash of 4-6 digit PIN
|
|
role TEXT NOT NULL DEFAULT 'worker', -- 'worker' | 'time_admin'
|
|
lang TEXT NOT NULL DEFAULT 'en', -- 'en' | 'es'
|
|
active BOOLEAN NOT NULL DEFAULT true,
|
|
last_used_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
ALTER TABLE time_tracking_workers ENABLE ROW LEVEL SECURITY;
|
|
|
|
-- Workers: anyone can verify PIN via RPC; updates only via SECURITY DEFINER RPCs
|
|
CREATE POLICY "Workers readable by authenticated" ON time_tracking_workers
|
|
FOR SELECT USING (auth.uid() IS NOT NULL);
|
|
|
|
CREATE POLICY "Workers insertable by authenticated" ON time_tracking_workers
|
|
FOR INSERT WITH CHECK (auth.uid() IS NOT NULL);
|
|
|
|
CREATE POLICY "Workers updatable by authenticated" ON time_tracking_workers
|
|
FOR UPDATE USING (auth.uid() IS NOT NULL) WITH CHECK (auth.uid() IS NOT NULL);
|
|
|
|
-- ── Tasks ───────────────────────────────────────────────────────────────────────
|
|
|
|
CREATE TABLE IF NOT EXISTS time_tracking_tasks (
|
|
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
|
brand_id UUID REFERENCES brands(id) ON DELETE CASCADE NOT NULL,
|
|
name TEXT NOT NULL,
|
|
name_es TEXT, -- Spanish name
|
|
unit TEXT NOT NULL DEFAULT 'hours', -- 'hours' | 'pieces' | 'units'
|
|
active BOOLEAN NOT NULL DEFAULT true,
|
|
sort_order INT NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
ALTER TABLE time_tracking_tasks ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY "Tasks readable by authenticated" ON time_tracking_tasks
|
|
FOR SELECT USING (auth.uid() IS NOT NULL);
|
|
|
|
CREATE POLICY "Tasks writable by authenticated" ON time_tracking_tasks
|
|
FOR ALL USING (auth.uid() IS NOT NULL) WITH CHECK (auth.uid() IS NOT NULL);
|
|
|
|
-- ── Time Logs ──────────────────────────────────────────────────────────────────
|
|
|
|
CREATE TABLE IF NOT EXISTS worker_time_logs (
|
|
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
|
brand_id UUID REFERENCES brands(id) ON DELETE CASCADE NOT NULL,
|
|
worker_id UUID REFERENCES time_tracking_workers(id) ON DELETE CASCADE NOT NULL,
|
|
task_id UUID REFERENCES time_tracking_tasks(id) ON DELETE SET NULL,
|
|
task_name TEXT NOT NULL, -- snapshot at time of clock-out
|
|
clock_in TIMESTAMPTZ NOT NULL,
|
|
clock_out TIMESTAMPTZ, -- null = currently clocked in
|
|
lunch_break_minutes INT NOT NULL DEFAULT 0,
|
|
notes TEXT,
|
|
submitted_via TEXT NOT NULL DEFAULT 'field', -- 'field' | 'admin'
|
|
created_by UUID REFERENCES admin_users(id) ON DELETE SET NULL,
|
|
updated_by UUID REFERENCES admin_users(id) ON DELETE SET NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
ALTER TABLE worker_time_logs ENABLE ROW LEVEL SECURITY;
|
|
|
|
-- Time logs: readable by authenticated (brand-scoped), writable by authenticated (RPCs gate by brand)
|
|
CREATE POLICY "Time logs readable by authenticated" ON worker_time_logs
|
|
FOR SELECT TO authenticated USING (true);
|
|
|
|
CREATE POLICY "Time logs insertable by authenticated" ON worker_time_logs
|
|
FOR INSERT WITH CHECK (auth.uid() IS NOT NULL);
|
|
|
|
CREATE POLICY "Time logs updatable by authenticated" ON worker_time_logs
|
|
FOR UPDATE USING (auth.uid() IS NOT NULL) WITH CHECK (auth.uid() IS NOT NULL);
|
|
|
|
-- ── Indexes ─────────────────────────────────────────────────────────────────────
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_worker_time_logs_brand_id ON worker_time_logs(brand_id);
|
|
CREATE INDEX IF NOT EXISTS idx_worker_time_logs_worker_id ON worker_time_logs(worker_id);
|
|
CREATE INDEX IF NOT EXISTS idx_worker_time_logs_clock_in ON worker_time_logs(clock_in);
|
|
CREATE INDEX IF NOT EXISTS idx_worker_time_logs_clock_out ON worker_time_logs(clock_out) WHERE clock_out IS NULL;
|
|
CREATE INDEX IF NOT EXISTS idx_time_tracking_workers_brand ON time_tracking_workers(brand_id);
|
|
CREATE INDEX IF NOT EXISTS idx_time_tracking_tasks_brand ON time_tracking_tasks(brand_id);
|
|
|
|
-- ── RPCs ────────────────────────────────────────────────────────────────────────
|
|
|
|
-- Verify PIN and return worker info + create session
|
|
CREATE OR REPLACE FUNCTION verify_time_tracking_pin(
|
|
p_brand_id UUID,
|
|
p_pin TEXT
|
|
) RETURNS JSONB
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = public
|
|
AS $$
|
|
DECLARE
|
|
w time_tracking_workers;
|
|
session_id UUID;
|
|
diff_secs INT;
|
|
BEGIN
|
|
SELECT * INTO w
|
|
FROM time_tracking_workers
|
|
WHERE brand_id = p_brand_id AND active = true AND pin_hash = p_pin;
|
|
|
|
IF w IS NULL THEN
|
|
RETURN jsonb_build_object('success', false, 'error', 'Invalid PIN');
|
|
END IF;
|
|
|
|
-- Update last used
|
|
UPDATE time_tracking_workers SET last_used_at = now() WHERE id = w.id;
|
|
|
|
-- Create session record
|
|
session_id = gen_random_uuid();
|
|
-- Sessions expire in 12 hours (no session table needed — validated by timestamp comparison)
|
|
|
|
RETURN jsonb_build_object(
|
|
'success', true,
|
|
'worker_id', w.id,
|
|
'name', w.name,
|
|
'role', w.role,
|
|
'lang', w.lang,
|
|
'session_id', session_id,
|
|
'brand_id', p_brand_id,
|
|
'expires_at', now() + INTERVAL '12 hours'
|
|
);
|
|
END;
|
|
$$;
|
|
|
|
-- Clock in — create a new time log entry
|
|
CREATE OR REPLACE FUNCTION clock_in_worker(
|
|
p_brand_id UUID,
|
|
p_worker_id UUID,
|
|
p_task_id UUID DEFAULT NULL,
|
|
p_task_name TEXT DEFAULT 'General Labor'
|
|
) RETURNS JSONB
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = public
|
|
AS $$
|
|
DECLARE
|
|
existing_open time_tracking_logs%ROWTYPE;
|
|
log_id UUID;
|
|
BEGIN
|
|
-- Check for open entry (no clock_out)
|
|
SELECT * INTO existing_open
|
|
FROM worker_time_logs
|
|
WHERE worker_id = p_worker_id AND clock_out IS NULL
|
|
LIMIT 1;
|
|
|
|
IF found THEN
|
|
RETURN jsonb_build_object('success', false, 'error', 'Already clocked in');
|
|
END IF;
|
|
|
|
INSERT INTO worker_time_logs (brand_id, worker_id, task_id, task_name, clock_in)
|
|
VALUES (p_brand_id, p_worker_id, p_task_id, p_task_name, now())
|
|
RETURNING id INTO log_id;
|
|
|
|
RETURN jsonb_build_object('success', true, 'log_id', log_id, 'clock_in', now());
|
|
END;
|
|
$$;
|
|
|
|
-- Clock out — close the open entry for this worker
|
|
CREATE OR REPLACE FUNCTION clock_out_worker(
|
|
p_worker_id UUID,
|
|
p_lunch_minutes INT DEFAULT 0,
|
|
p_notes TEXT DEFAULT NULL
|
|
) RETURNS JSONB
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = public
|
|
AS $$
|
|
DECLARE
|
|
open_log worker_time_logs%ROWTYPE;
|
|
diff_mins INT;
|
|
BEGIN
|
|
SELECT * INTO open_log
|
|
FROM worker_time_logs
|
|
WHERE worker_id = p_worker_id AND clock_out IS NULL
|
|
ORDER BY clock_in DESC LIMIT 1;
|
|
|
|
IF open_log IS NULL THEN
|
|
RETURN jsonb_build_object('success', false, 'error', 'No open clock-in found');
|
|
END IF;
|
|
|
|
diff_mins = EXTRACT(EPOCH FROM (now() - open_log.clock_in))::INT / 60 - p_lunch_minutes;
|
|
|
|
UPDATE worker_time_logs
|
|
SET clock_out = now(),
|
|
lunch_break_minutes = p_lunch_minutes,
|
|
notes = COALESCE(NULLIF(TRIM(p_notes), ''), NULL),
|
|
updated_at = now()
|
|
WHERE id = open_log.id;
|
|
|
|
RETURN jsonb_build_object(
|
|
'success', true,
|
|
'log_id', open_log.id,
|
|
'clock_out', now(),
|
|
'total_minutes', GREATEST(0, diff_mins)
|
|
);
|
|
END;
|
|
$$;
|
|
|
|
-- Get open clock-in for a worker (for resuming)
|
|
CREATE OR REPLACE FUNCTION get_open_clock_in(p_worker_id UUID)
|
|
RETURNS JSONB
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = public
|
|
AS $$
|
|
DECLARE
|
|
open_log worker_time_logs%ROWTYPE;
|
|
BEGIN
|
|
SELECT * INTO open_log
|
|
FROM worker_time_logs
|
|
WHERE worker_id = p_worker_id AND clock_out IS NULL
|
|
ORDER BY clock_in DESC LIMIT 1;
|
|
|
|
IF open_log IS NULL THEN
|
|
RETURN jsonb_build_object('success', true, 'open', false);
|
|
END IF;
|
|
|
|
RETURN jsonb_build_object(
|
|
'success', true,
|
|
'open', true,
|
|
'log_id', open_log.id,
|
|
'task_name', open_log.task_name,
|
|
'clock_in', open_log.clock_in,
|
|
'elapsed_minutes', EXTRACT(EPOCH FROM (now() - open_log.clock_in))::INT / 60
|
|
);
|
|
END;
|
|
$$;
|
|
|
|
-- Get workers for a brand (no pin_hash exposed)
|
|
CREATE OR REPLACE FUNCTION get_time_tracking_workers(p_brand_id UUID)
|
|
RETURNS JSONB
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = public
|
|
AS $$
|
|
BEGIN
|
|
RETURN (
|
|
SELECT jsonb_build_object('workers', COALESCE(
|
|
jsonb_agg(
|
|
jsonb_build_object(
|
|
'id', id, 'name', name, 'role', role, 'lang', lang,
|
|
'active', active, 'last_used_at', last_used_at, 'created_at', created_at
|
|
) ORDER BY name
|
|
), '[]'::jsonb
|
|
))
|
|
FROM time_tracking_workers
|
|
WHERE brand_id = p_brand_id
|
|
);
|
|
END;
|
|
$$;
|
|
|
|
-- Create worker — auto-generates 4-digit PIN
|
|
CREATE OR REPLACE FUNCTION create_time_worker(
|
|
p_brand_id UUID,
|
|
p_name TEXT,
|
|
p_role TEXT DEFAULT 'worker',
|
|
p_lang TEXT DEFAULT 'en'
|
|
) RETURNS JSONB
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = public
|
|
AS $$
|
|
DECLARE
|
|
new_pin TEXT;
|
|
new_hash TEXT;
|
|
new_id UUID;
|
|
BEGIN
|
|
-- Generate random 4-digit PIN
|
|
new_pin = lpad((FLOOR(RANDOM() * 9000 + 1000))::INT::TEXT, 4, '0');
|
|
-- Simple hash (for demo; use pgcrypto in production)
|
|
new_hash = 'pin:' || new_pin;
|
|
|
|
INSERT INTO time_tracking_workers (brand_id, name, pin_hash, role, lang)
|
|
VALUES (p_brand_id, p_name, new_hash, p_role, p_lang)
|
|
RETURNING id INTO new_id;
|
|
|
|
RETURN jsonb_build_object(
|
|
'success', true,
|
|
'worker', jsonb_build_object('id', new_id, 'name', p_name, 'role', p_role, 'lang', p_lang),
|
|
'pin', new_pin
|
|
);
|
|
END;
|
|
$$;
|
|
|
|
-- Reset worker PIN — returns new PIN (shown once)
|
|
CREATE OR REPLACE FUNCTION reset_time_worker_pin(p_worker_id UUID)
|
|
RETURNS JSONB
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = public
|
|
AS $$
|
|
DECLARE
|
|
new_pin TEXT;
|
|
new_hash TEXT;
|
|
BEGIN
|
|
new_pin = lpad((FLOOR(RANDOM() * 9000 + 1000))::INT::TEXT, 4, '0');
|
|
new_hash = 'pin:' || new_pin;
|
|
UPDATE time_tracking_workers SET pin_hash = new_hash WHERE id = p_worker_id;
|
|
RETURN jsonb_build_object('success', true, 'pin', new_pin);
|
|
END;
|
|
$$;
|
|
|
|
-- Update worker
|
|
CREATE OR REPLACE FUNCTION update_time_worker(
|
|
p_worker_id UUID,
|
|
p_name TEXT,
|
|
p_role TEXT,
|
|
p_lang TEXT,
|
|
p_active BOOLEAN
|
|
) RETURNS JSONB
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = public
|
|
AS $$
|
|
BEGIN
|
|
UPDATE time_tracking_workers
|
|
SET name = p_name, role = p_role, lang = p_lang, active = p_active
|
|
WHERE id = p_worker_id;
|
|
RETURN jsonb_build_object('success', true);
|
|
END;
|
|
$$;
|
|
|
|
-- Delete worker
|
|
CREATE OR REPLACE FUNCTION delete_time_worker(p_worker_id UUID)
|
|
RETURNS JSONB
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = public
|
|
AS $$
|
|
BEGIN
|
|
DELETE FROM time_tracking_workers WHERE id = p_worker_id;
|
|
RETURN jsonb_build_object('success', true);
|
|
END;
|
|
$$;
|
|
|
|
-- ── Tasks RPCs ────────────────────────────────────────────────────────────────
|
|
|
|
CREATE OR REPLACE FUNCTION get_time_tracking_tasks(p_brand_id UUID, p_active_only BOOLEAN DEFAULT false)
|
|
RETURNS JSONB
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = public
|
|
AS $$
|
|
BEGIN
|
|
RETURN (
|
|
SELECT jsonb_build_object('tasks', COALESCE(
|
|
jsonb_agg(
|
|
jsonb_build_object(
|
|
'id', id, 'name', name, 'name_es', name_es, 'unit', unit,
|
|
'active', active, 'sort_order', sort_order
|
|
) ORDER BY sort_order, name
|
|
), '[]'::jsonb
|
|
))
|
|
FROM time_tracking_tasks
|
|
WHERE brand_id = p_brand_id
|
|
AND (NOT p_active_only OR active = true)
|
|
);
|
|
END;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION create_time_task(
|
|
p_brand_id UUID,
|
|
p_name TEXT,
|
|
p_name_es TEXT DEFAULT NULL,
|
|
p_unit TEXT DEFAULT 'hours',
|
|
p_sort_order INT DEFAULT 0
|
|
) RETURNS JSONB
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = public
|
|
AS $$
|
|
DECLARE
|
|
new_id UUID;
|
|
BEGIN
|
|
INSERT INTO time_tracking_tasks (brand_id, name, name_es, unit, sort_order)
|
|
VALUES (p_brand_id, p_name, p_name_es, p_unit, p_sort_order)
|
|
RETURNING id INTO new_id;
|
|
RETURN jsonb_build_object('success', true, 'id', new_id);
|
|
END;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION update_time_task(
|
|
p_task_id UUID,
|
|
p_name TEXT,
|
|
p_name_es TEXT,
|
|
p_unit TEXT,
|
|
p_active BOOLEAN,
|
|
p_sort_order INT
|
|
) RETURNS JSONB
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = public
|
|
AS $$
|
|
BEGIN
|
|
UPDATE time_tracking_tasks
|
|
SET name = p_name, name_es = p_name_es, unit = p_unit,
|
|
active = p_active, sort_order = p_sort_order
|
|
WHERE id = p_task_id;
|
|
RETURN jsonb_build_object('success', true);
|
|
END;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION delete_time_task(p_task_id UUID)
|
|
RETURNS JSONB
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = public
|
|
AS $$
|
|
BEGIN
|
|
DELETE FROM time_tracking_tasks WHERE id = p_task_id;
|
|
RETURN jsonb_build_object('success', true);
|
|
END;
|
|
$$;
|
|
|
|
-- ── Time Log RPCs ─────────────────────────────────────────────────────────────
|
|
|
|
CREATE OR REPLACE FUNCTION get_worker_time_logs(
|
|
p_brand_id UUID,
|
|
p_worker_id UUID DEFAULT NULL,
|
|
p_task_id UUID DEFAULT NULL,
|
|
p_start TIMESTAMPTZ DEFAULT NULL,
|
|
p_end TIMESTAMPTZ DEFAULT NULL,
|
|
p_limit INT DEFAULT 100
|
|
) RETURNS JSONB
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = public
|
|
AS $$
|
|
BEGIN
|
|
RETURN (
|
|
SELECT jsonb_build_object('logs', COALESCE(
|
|
jsonb_agg(
|
|
jsonb_build_object(
|
|
'id', l.id,
|
|
'worker_id', l.worker_id,
|
|
'worker_name', w.name,
|
|
'task_id', l.task_id,
|
|
'task_name', l.task_name,
|
|
'clock_in', l.clock_in,
|
|
'clock_out', l.clock_out,
|
|
'lunch_break_minutes', l.lunch_break_minutes,
|
|
'notes', l.notes,
|
|
'submitted_via', l.submitted_via,
|
|
'total_minutes',
|
|
CASE
|
|
WHEN l.clock_out IS NOT NULL
|
|
THEN LEAST(GREATEST(0, EXTRACT(EPOCH FROM (l.clock_out - l.clock_in))::INT / 60 - l.lunch_break_minutes), 1440)
|
|
ELSE LEAST(GREATEST(0, EXTRACT(EPOCH FROM (now() - l.clock_in))::INT / 60), 1440)
|
|
END,
|
|
'created_at', l.created_at
|
|
) ORDER BY l.clock_in DESC
|
|
), '[]'::jsonb
|
|
))
|
|
FROM worker_time_logs l
|
|
JOIN time_tracking_workers w ON w.id = l.worker_id
|
|
WHERE l.brand_id = p_brand_id
|
|
AND (p_worker_id IS NULL OR l.worker_id = p_worker_id)
|
|
AND (p_task_id IS NULL OR l.task_id = p_task_id)
|
|
AND (p_start IS NULL OR l.clock_in >= p_start)
|
|
AND (p_end IS NULL OR l.clock_in <= p_end)
|
|
LIMIT p_limit
|
|
);
|
|
END;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION update_worker_time_log(
|
|
p_log_id UUID,
|
|
p_task_name TEXT,
|
|
p_clock_in TIMESTAMPTZ,
|
|
p_clock_out TIMESTAMPTZ,
|
|
p_lunch_minutes INT,
|
|
p_notes TEXT
|
|
) RETURNS JSONB
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = public
|
|
AS $$
|
|
BEGIN
|
|
UPDATE worker_time_logs
|
|
SET task_name = p_task_name,
|
|
clock_in = p_clock_in,
|
|
clock_out = p_clock_out,
|
|
lunch_break_minutes = p_lunch_minutes,
|
|
notes = NULLIF(TRIM(p_notes), ''),
|
|
updated_at = now()
|
|
WHERE id = p_log_id;
|
|
RETURN jsonb_build_object('success', true);
|
|
END;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION delete_worker_time_log(p_log_id UUID)
|
|
RETURNS JSONB
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = public
|
|
AS $$
|
|
BEGIN
|
|
DELETE FROM worker_time_logs WHERE id = p_log_id;
|
|
RETURN jsonb_build_object('success', true);
|
|
END;
|
|
$$;
|
|
|
|
-- ── Summary RPC (for admin dashboard) ─────────────────────────────────────────
|
|
|
|
CREATE OR REPLACE FUNCTION get_time_tracking_summary(
|
|
p_brand_id UUID,
|
|
p_start TIMESTAMPTZ,
|
|
p_end TIMESTAMPTZ
|
|
) RETURNS JSONB
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = public
|
|
AS $$
|
|
BEGIN
|
|
RETURN jsonb_build_object(
|
|
'by_worker', (
|
|
SELECT COALESCE(jsonb_agg(r ORDER BY total_hours DESC), '[]'::jsonb)
|
|
FROM (
|
|
SELECT w.id, w.name,
|
|
COUNT(l.id) AS entry_count,
|
|
SUM(
|
|
LEAST(GREATEST(0, EXTRACT(EPOCH FROM (COALESCE(l.clock_out, now()) - l.clock_in))::INT / 60 - l.lunch_break_minutes), 1440)
|
|
) / 60.0 AS total_hours
|
|
FROM time_tracking_workers w
|
|
LEFT JOIN worker_time_logs l ON l.worker_id = w.id
|
|
AND l.clock_in >= p_start AND l.clock_in <= p_end
|
|
WHERE w.brand_id = p_brand_id
|
|
GROUP BY w.id, w.name
|
|
) r
|
|
),
|
|
'by_task', (
|
|
SELECT COALESCE(jsonb_agg(r ORDER BY total_hours DESC), '[]'::jsonb)
|
|
FROM (
|
|
SELECT t.id, t.name, t.name_es,
|
|
COUNT(l.id) AS entry_count,
|
|
SUM(
|
|
LEAST(GREATEST(0, EXTRACT(EPOCH FROM (COALESCE(l.clock_out, now()) - l.clock_in))::INT / 60 - l.lunch_break_minutes), 1440)
|
|
) / 60.0 AS total_hours
|
|
FROM time_tracking_tasks t
|
|
LEFT JOIN worker_time_logs l ON l.task_id = t.id
|
|
AND l.clock_in >= p_start AND l.clock_in <= p_end
|
|
WHERE t.brand_id = p_brand_id
|
|
GROUP BY t.id, t.name, t.name_es
|
|
) r
|
|
),
|
|
'totals', (
|
|
SELECT jsonb_build_object(
|
|
'entry_count', COUNT(*),
|
|
'total_hours', SUM(
|
|
LEAST(GREATEST(0, EXTRACT(EPOCH FROM (COALESCE(clock_out, now()) - clock_in))::INT / 60 - lunch_break_minutes), 1440)
|
|
) / 60.0,
|
|
'open_count', COUNT(*) FILTER (WHERE clock_out IS NULL)
|
|
)
|
|
FROM worker_time_logs
|
|
WHERE brand_id = p_brand_id AND clock_in >= p_start AND clock_in <= p_end
|
|
)
|
|
);
|
|
END;
|
|
$$;
|