From bed69aa61c8b9f6a005ce4e6bafd8398c9b7a388 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Feb 2026 03:39:35 +0000 Subject: [PATCH] Add BizDeedz Ops Platform MVP implementation This commit implements the complete BizDeedz Ops Platform for bankruptcy law firm workflow management, based on the PRD specifications. Database (Supabase): - Complete schema with 20+ tables and custom types - Triggers for auto-creating input checklists and conflicts checks - Functions for name normalization and billable creation - Row-level security policies for multi-tenant access - Sample data for testing SQL Views: - v_kpi_dashboard: Real-time KPI calculations - v_ar_aging: Invoice aging buckets with pause triggers - v_firm_performance: Per-firm metrics aggregation - v_delivery_metrics: Weekly/monthly delivery stats - v_matter_queue: Kanban board data with flags - v_conflicts_dashboard: Conflict check overview - Additional views for billing and invoicing Web UI (HTML/CSS/JS): - Dashboard with KPI tiles and work queues - Firms list and detail pages with tabs - Matters kanban board and table views - Matter detail with 6 tabs (Inputs, Conflicts, QC, etc.) - Billing page with AR aging, invoices, retainers - Responsive design with professional styling Documentation: - Automation triggers specification - Make.com scenario blueprints for Stripe/QuickBooks - Email templates for notifications - Error handling and retry strategies https://claude.ai/code/session_011ZCnjpPFVcAQ5BCv7Tma9j --- bizdeedz-ops-platform/database/001_schema.sql | 932 +++++++++++++ bizdeedz-ops-platform/database/002_views.sql | 524 ++++++++ .../docs/automation-triggers.md | 600 +++++++++ bizdeedz-ops-platform/web/billing.html | 726 ++++++++++ bizdeedz-ops-platform/web/dashboard.html | 570 ++++++++ bizdeedz-ops-platform/web/firms.html | 771 +++++++++++ bizdeedz-ops-platform/web/matter-detail.html | 970 ++++++++++++++ bizdeedz-ops-platform/web/matters.html | 771 +++++++++++ bizdeedz-ops-platform/web/styles.css | 1175 +++++++++++++++++ 9 files changed, 7039 insertions(+) create mode 100644 bizdeedz-ops-platform/database/001_schema.sql create mode 100644 bizdeedz-ops-platform/database/002_views.sql create mode 100644 bizdeedz-ops-platform/docs/automation-triggers.md create mode 100644 bizdeedz-ops-platform/web/billing.html create mode 100644 bizdeedz-ops-platform/web/dashboard.html create mode 100644 bizdeedz-ops-platform/web/firms.html create mode 100644 bizdeedz-ops-platform/web/matter-detail.html create mode 100644 bizdeedz-ops-platform/web/matters.html create mode 100644 bizdeedz-ops-platform/web/styles.css diff --git a/bizdeedz-ops-platform/database/001_schema.sql b/bizdeedz-ops-platform/database/001_schema.sql new file mode 100644 index 0000000..fd888f3 --- /dev/null +++ b/bizdeedz-ops-platform/database/001_schema.sql @@ -0,0 +1,932 @@ +-- BizDeedz Ops Platform - Supabase Database Schema +-- Version: 1.0 MVP +-- Description: Complete database schema for bankruptcy law firm workflow management + +-- ============================================================================= +-- EXTENSIONS +-- ============================================================================= +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- For fuzzy matching in conflicts + +-- ============================================================================= +-- CUSTOM TYPES (ENUMS) +-- ============================================================================= + +-- Firm status +CREATE TYPE firm_status AS ENUM ('active', 'on_hold', 'terminated'); + +-- Contact roles +CREATE TYPE contact_role AS ENUM ('attorney', 'staff', 'billing'); + +-- Case types +CREATE TYPE case_type AS ENUM ('ch7', 'ch13', 'amendment'); + +-- Matter stage pipeline +CREATE TYPE matter_stage AS ENUM ( + 'assigned', + 'waiting_inputs', + 'conflicts_review', + 'in_progress', + 'qc', + 'delivered', + 'revision', + 'complete', + 'paused_ar' +); + +-- Inputs status +CREATE TYPE inputs_status AS ENUM ('incomplete', 'complete', 'in_review'); + +-- Conflicts status +CREATE TYPE conflicts_status AS ENUM ('not_run', 'clear', 'potential', 'conflict'); + +-- Conflict check result +CREATE TYPE conflict_result AS ENUM ('clear', 'potential', 'conflict'); + +-- Conflict decision +CREATE TYPE conflict_decision AS ENUM ('approved', 'declined', 'overridden'); + +-- Entity type +CREATE TYPE entity_type AS ENUM ('person', 'business'); + +-- Entity role in matter +CREATE TYPE entity_role AS ENUM ('debtor', 'spouse', 'related_business'); + +-- Deficiency status +CREATE TYPE deficiency_status AS ENUM ('open', 'resolved'); + +-- Deficiency category +CREATE TYPE deficiency_category AS ENUM ( + 'id', + 'income', + 'taxes', + 'banking', + 'assets', + 'debts', + 'household', + 'plan_inputs', + 'other' +); + +-- Delivery type +CREATE TYPE delivery_type AS ENUM ('ch7', 'ch13', 'amendment'); + +-- Billable type +CREATE TYPE billable_type AS ENUM ('retainer', 'ch7', 'ch13', 'amendment', 'rush', 'overage'); + +-- Invoice status +CREATE TYPE invoice_status AS ENUM ('draft', 'sent', 'paid', 'overdue', 'partial'); + +-- User roles +CREATE TYPE user_role AS ENUM ('admin', 'staff', 'client_attorney', 'client_staff'); + +-- ============================================================================= +-- TABLES +-- ============================================================================= + +-- ----------------------------------------------------------------------------- +-- FIRMS +-- Client law firms that BizDeedz works with +-- ----------------------------------------------------------------------------- +CREATE TABLE firms ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + firm_name TEXT NOT NULL, + status firm_status NOT NULL DEFAULT 'active', + retainer_amount NUMERIC(10, 2) DEFAULT 0, + retainer_due_day INTEGER DEFAULT 1 CHECK (retainer_due_day >= 1 AND retainer_due_day <= 28), + invoice_cadence TEXT DEFAULT 'friday', + billing_email TEXT, + active_capacity BOOLEAN DEFAULT false, -- True only when retainer is paid + notes TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Index for quick status filtering +CREATE INDEX idx_firms_status ON firms(status); + +-- ----------------------------------------------------------------------------- +-- CONTACTS +-- People at client firms (attorneys, staff, billing contacts) +-- ----------------------------------------------------------------------------- +CREATE TABLE contacts ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + firm_id UUID NOT NULL REFERENCES firms(id) ON DELETE CASCADE, + name TEXT NOT NULL, + email TEXT, + phone TEXT, + role contact_role NOT NULL DEFAULT 'staff', + is_primary BOOLEAN DEFAULT false, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_contacts_firm ON contacts(firm_id); + +-- ----------------------------------------------------------------------------- +-- MATTERS +-- Individual cases/assignments from client firms +-- ----------------------------------------------------------------------------- +CREATE TABLE matters ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + firm_id UUID NOT NULL REFERENCES firms(id) ON DELETE CASCADE, + matter_name TEXT NOT NULL, -- e.g., "Smith, John – Ch 7" + case_type case_type NOT NULL, + district TEXT, -- Jurisdiction/district + deadline DATE, + rush BOOLEAN DEFAULT false, + status_stage matter_stage NOT NULL DEFAULT 'assigned', + inputs_status inputs_status NOT NULL DEFAULT 'incomplete', + conflicts_status conflicts_status NOT NULL DEFAULT 'not_run', + assigned_to TEXT, -- User/team member name or ID + + -- Timestamps for metrics calculation + assigned_at TIMESTAMPTZ DEFAULT NOW(), + inputs_complete_at TIMESTAMPTZ, + in_progress_at TIMESTAMPTZ, + delivered_at TIMESTAMPTZ, + paused_at TIMESTAMPTZ, + completed_at TIMESTAMPTZ, + + -- Revision tracking + revision_rounds INTEGER DEFAULT 0, + + -- Override notes (when proceeding despite incomplete inputs or potential conflicts) + override_notes TEXT, + + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_matters_firm ON matters(firm_id); +CREATE INDEX idx_matters_stage ON matters(status_stage); +CREATE INDEX idx_matters_deadline ON matters(deadline); + +-- ----------------------------------------------------------------------------- +-- MATTER_INPUTS +-- Checklist of required inputs for each matter +-- ----------------------------------------------------------------------------- +CREATE TABLE matter_inputs ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + matter_id UUID NOT NULL REFERENCES matters(id) ON DELETE CASCADE, + input_type TEXT NOT NULL, -- e.g., "IDs", "Paystubs", "Tax Returns" + input_category deficiency_category, + required BOOLEAN DEFAULT true, + received BOOLEAN DEFAULT false, + received_date DATE, + notes TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_matter_inputs_matter ON matter_inputs(matter_id); + +-- ----------------------------------------------------------------------------- +-- DEFICIENCIES +-- Missing or incomplete items that need client action +-- ----------------------------------------------------------------------------- +CREATE TABLE deficiencies ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + matter_id UUID NOT NULL REFERENCES matters(id) ON DELETE CASCADE, + category deficiency_category DEFAULT 'other', + deficiency_text TEXT NOT NULL, + status deficiency_status NOT NULL DEFAULT 'open', + created_by TEXT, + resolved_at TIMESTAMPTZ, + resolved_by TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_deficiencies_matter ON deficiencies(matter_id); +CREATE INDEX idx_deficiencies_status ON deficiencies(status); + +-- ----------------------------------------------------------------------------- +-- ENTITIES +-- People and businesses for conflicts checking +-- ----------------------------------------------------------------------------- +CREATE TABLE entities ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + entity_type entity_type NOT NULL, + full_name TEXT NOT NULL, + normalized_name TEXT NOT NULL, -- Lowercased, stripped, for matching + aliases TEXT[], -- Array of alternate names + address TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Trigram index for fuzzy matching +CREATE INDEX idx_entities_normalized_trgm ON entities USING gin (normalized_name gin_trgm_ops); +CREATE INDEX idx_entities_full_name ON entities(full_name); + +-- ----------------------------------------------------------------------------- +-- MATTER_ENTITIES +-- Links entities to matters with their role +-- ----------------------------------------------------------------------------- +CREATE TABLE matter_entities ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + matter_id UUID NOT NULL REFERENCES matters(id) ON DELETE CASCADE, + entity_id UUID NOT NULL REFERENCES entities(id) ON DELETE CASCADE, + role entity_role NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + + UNIQUE(matter_id, entity_id, role) +); + +CREATE INDEX idx_matter_entities_matter ON matter_entities(matter_id); +CREATE INDEX idx_matter_entities_entity ON matter_entities(entity_id); + +-- ----------------------------------------------------------------------------- +-- RESTRICTED_ENTITIES +-- Do-not-work entities (internal blocklist) +-- ----------------------------------------------------------------------------- +CREATE TABLE restricted_entities ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + normalized_name TEXT NOT NULL, + display_name TEXT, + reason TEXT, + active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_restricted_normalized ON restricted_entities(normalized_name); +CREATE INDEX idx_restricted_active ON restricted_entities(active) WHERE active = true; + +-- ----------------------------------------------------------------------------- +-- CONFLICT_CHECKS +-- Audit log of all conflicts checks run +-- ----------------------------------------------------------------------------- +CREATE TABLE conflict_checks ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + matter_id UUID NOT NULL REFERENCES matters(id) ON DELETE CASCADE, + run_at TIMESTAMPTZ DEFAULT NOW(), + run_by TEXT, + result conflict_result NOT NULL, + matches JSONB, -- Array of {entity_id, name, match_type, score} + decision conflict_decision, + decision_by TEXT, + decision_notes TEXT, + decision_at TIMESTAMPTZ, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_conflict_checks_matter ON conflict_checks(matter_id); + +-- ----------------------------------------------------------------------------- +-- DELIVERIES +-- Record of all deliverables sent to clients +-- ----------------------------------------------------------------------------- +CREATE TABLE deliveries ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + matter_id UUID NOT NULL REFERENCES matters(id) ON DELETE CASCADE, + delivery_type delivery_type NOT NULL, + delivery_version INTEGER DEFAULT 1, -- 1 = initial, 2+ = revised + delivered_at TIMESTAMPTZ DEFAULT NOW(), + delivered_by TEXT, + qc_completed BOOLEAN DEFAULT false, + qc_completed_by TEXT, + qc_completed_at TIMESTAMPTZ, + notes TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_deliveries_matter ON deliveries(matter_id); + +-- ----------------------------------------------------------------------------- +-- BILLABLES +-- Line items for billing (auto-generated from deliveries + manual adjustments) +-- ----------------------------------------------------------------------------- +CREATE TABLE billables ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + firm_id UUID NOT NULL REFERENCES firms(id) ON DELETE CASCADE, + matter_id UUID REFERENCES matters(id) ON DELETE SET NULL, + billable_type billable_type NOT NULL, + description TEXT, + amount NUMERIC(10, 2) NOT NULL, + quantity NUMERIC(10, 2) DEFAULT 1, + unit TEXT DEFAULT 'each', -- 'each' or 'hour' + approved BOOLEAN DEFAULT true, + billable_date DATE DEFAULT CURRENT_DATE, + invoice_id UUID, -- Set when attached to invoice + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_billables_firm ON billables(firm_id); +CREATE INDEX idx_billables_matter ON billables(matter_id); +CREATE INDEX idx_billables_invoice ON billables(invoice_id); +CREATE INDEX idx_billables_uninvoiced ON billables(invoice_id) WHERE invoice_id IS NULL; + +-- ----------------------------------------------------------------------------- +-- INVOICES +-- Weekly invoices to client firms +-- ----------------------------------------------------------------------------- +CREATE TABLE invoices ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + firm_id UUID NOT NULL REFERENCES firms(id) ON DELETE CASCADE, + invoice_number TEXT, -- Human-readable invoice number + invoice_date DATE DEFAULT CURRENT_DATE, + due_date DATE, + status invoice_status NOT NULL DEFAULT 'draft', + total NUMERIC(10, 2) DEFAULT 0, + stripe_invoice_id TEXT, + sent_at TIMESTAMPTZ, + paid_at TIMESTAMPTZ, + notes TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Add foreign key constraint after invoices table exists +ALTER TABLE billables ADD CONSTRAINT fk_billables_invoice + FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE SET NULL; + +CREATE INDEX idx_invoices_firm ON invoices(firm_id); +CREATE INDEX idx_invoices_status ON invoices(status); +CREATE INDEX idx_invoices_date ON invoices(invoice_date); + +-- ----------------------------------------------------------------------------- +-- PAYMENTS +-- Payment records (mirrors Stripe for reference) +-- ----------------------------------------------------------------------------- +CREATE TABLE payments ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + invoice_id UUID NOT NULL REFERENCES invoices(id) ON DELETE CASCADE, + stripe_payment_intent_id TEXT, + amount NUMERIC(10, 2) NOT NULL, + status TEXT, + paid_at TIMESTAMPTZ, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_payments_invoice ON payments(invoice_id); + +-- ----------------------------------------------------------------------------- +-- TIME_LOGS +-- Time tracking for overage billing +-- ----------------------------------------------------------------------------- +CREATE TABLE time_logs ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + matter_id UUID NOT NULL REFERENCES matters(id) ON DELETE CASCADE, + logged_by TEXT, + minutes INTEGER NOT NULL CHECK (minutes > 0), + work_type TEXT, + description TEXT, + logged_at TIMESTAMPTZ DEFAULT NOW(), + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_time_logs_matter ON time_logs(matter_id); + +-- ----------------------------------------------------------------------------- +-- APP_USERS +-- Application users for RLS (BizDeedz staff + client portal users) +-- ----------------------------------------------------------------------------- +CREATE TABLE app_users ( + id UUID PRIMARY KEY, -- Matches auth.users.id + email TEXT NOT NULL, + name TEXT, + role user_role NOT NULL DEFAULT 'staff', + firm_id UUID REFERENCES firms(id) ON DELETE SET NULL, -- NULL for BizDeedz users + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_app_users_role ON app_users(role); +CREATE INDEX idx_app_users_firm ON app_users(firm_id); + +-- ----------------------------------------------------------------------------- +-- AUDIT_LOGS +-- General audit trail for important actions +-- ----------------------------------------------------------------------------- +CREATE TABLE audit_logs ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + table_name TEXT NOT NULL, + record_id UUID NOT NULL, + action TEXT NOT NULL, -- 'create', 'update', 'delete', 'ai_action', etc. + changes JSONB, + performed_by TEXT, + performed_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_audit_logs_table_record ON audit_logs(table_name, record_id); +CREATE INDEX idx_audit_logs_performed_at ON audit_logs(performed_at); + +-- ----------------------------------------------------------------------------- +-- INPUT_TEMPLATES +-- Default input checklists by case type +-- ----------------------------------------------------------------------------- +CREATE TABLE input_templates ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + case_type case_type NOT NULL, + input_type TEXT NOT NULL, + input_category deficiency_category, + required BOOLEAN DEFAULT true, + sort_order INTEGER DEFAULT 0, + created_at TIMESTAMPTZ DEFAULT NOW(), + + UNIQUE(case_type, input_type) +); + +-- ============================================================================= +-- DEFAULT DATA: Input Templates +-- ============================================================================= + +-- Chapter 7 Required Inputs +INSERT INTO input_templates (case_type, input_type, input_category, required, sort_order) VALUES +-- Identity +('ch7', 'Government ID (front/back)', 'id', true, 1), +('ch7', 'Social Security card or proof of SSN', 'id', true, 2), +-- Income +('ch7', 'Paystubs - last 6 months', 'income', true, 10), +('ch7', 'Proof of other income (SSI/SSDI, child support, unemployment, pension)', 'income', false, 11), +-- Taxes +('ch7', 'Federal tax returns - last 2 years', 'taxes', true, 20), +-- Banking +('ch7', 'Bank statements - last 3 months (all accounts)', 'banking', true, 30), +-- Assets +('ch7', 'Vehicle info: year/make/model/VIN + loan statement', 'assets', false, 40), +('ch7', 'Real property: deed/mortgage statement + valuation', 'assets', false, 41), +('ch7', 'Retirement account statements', 'assets', false, 42), +('ch7', 'Insurance policies (auto/home/renters)', 'assets', false, 43), +('ch7', 'Lawsuits/claims details', 'assets', false, 44), +-- Debts +('ch7', 'Creditor list or credit report', 'debts', true, 50), +('ch7', 'Secured debt statements (auto/mortgage)', 'debts', false, 51), +('ch7', 'Priority claims (tax notices, child support arrears)', 'debts', false, 52), +-- Household +('ch7', 'Household size + dependents info', 'household', true, 60), +('ch7', 'Lease or mortgage statement', 'household', true, 61), +('ch7', 'Utility statements', 'household', false, 62), +-- Prior filings +('ch7', 'Prior bankruptcy case info', 'other', false, 70); + +-- Chapter 13 Required Inputs (includes all Ch7 + additional) +INSERT INTO input_templates (case_type, input_type, input_category, required, sort_order) VALUES +-- Identity +('ch13', 'Government ID (front/back)', 'id', true, 1), +('ch13', 'Social Security card or proof of SSN', 'id', true, 2), +-- Income +('ch13', 'Paystubs - last 6 months', 'income', true, 10), +('ch13', 'Proof of other income (SSI/SSDI, child support, unemployment, pension)', 'income', false, 11), +-- Taxes +('ch13', 'Federal tax returns - last 2 years', 'taxes', true, 20), +-- Banking +('ch13', 'Bank statements - last 3 months (all accounts)', 'banking', true, 30), +-- Assets +('ch13', 'Vehicle info: year/make/model/VIN + loan statement', 'assets', false, 40), +('ch13', 'Real property: deed/mortgage statement + valuation', 'assets', false, 41), +('ch13', 'Retirement account statements', 'assets', false, 42), +('ch13', 'Insurance policies (auto/home/renters)', 'assets', false, 43), +('ch13', 'Lawsuits/claims details', 'assets', false, 44), +-- Debts +('ch13', 'Creditor list or credit report', 'debts', true, 50), +('ch13', 'Secured debt statements (auto/mortgage)', 'debts', false, 51), +('ch13', 'Priority claims (tax notices, child support arrears)', 'debts', false, 52), +-- Household +('ch13', 'Household size + dependents info', 'household', true, 60), +('ch13', 'Lease or mortgage statement', 'household', true, 61), +('ch13', 'Utility statements', 'household', false, 62), +-- Prior filings +('ch13', 'Prior bankruptcy case info', 'other', false, 70), +-- Ch13-specific +('ch13', 'Mortgage arrearage breakdown', 'plan_inputs', true, 80), +('ch13', 'Vehicle payoff + interest rate info', 'plan_inputs', false, 81), +('ch13', 'Monthly budget inputs', 'plan_inputs', true, 82), +('ch13', 'Domestic support obligations', 'plan_inputs', false, 83), +('ch13', 'Tax debt details (years/amounts)', 'plan_inputs', false, 84), +('ch13', 'Attorney plan direction notes', 'plan_inputs', true, 85); + +-- Amendment Required Inputs +INSERT INTO input_templates (case_type, input_type, input_category, required, sort_order) VALUES +('amendment', 'Written instruction: what changed + why', 'other', true, 1), +('amendment', 'Updated supporting documents', 'other', true, 2), +('amendment', 'Impacted schedules/forms identification', 'other', true, 3); + +-- ============================================================================= +-- BILLING RATES CONFIGURATION +-- ============================================================================= +CREATE TABLE billing_rates ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + billable_type billable_type NOT NULL UNIQUE, + default_amount NUMERIC(10, 2) NOT NULL, + unit TEXT DEFAULT 'each', + description TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +INSERT INTO billing_rates (billable_type, default_amount, unit, description) VALUES +('ch7', 200.00, 'each', 'Chapter 7 Petition Preparation'), +('ch13', 350.00, 'each', 'Chapter 13 Petition Preparation'), +('amendment', 250.00, 'each', 'Amendment/Modification'), +('rush', 150.00, 'each', 'Rush Fee (24-48 hours)'), +('overage', 95.00, 'hour', 'Out of Scope Work'), +('retainer', 0.00, 'each', 'Monthly Retainer'); + +-- ============================================================================= +-- FUNCTIONS +-- ============================================================================= + +-- Function to normalize names for conflict matching +CREATE OR REPLACE FUNCTION normalize_name(input_name TEXT) +RETURNS TEXT AS $$ +BEGIN + RETURN LOWER( + REGEXP_REPLACE( + REGEXP_REPLACE( + REGEXP_REPLACE(input_name, '\s+(jr|sr|ii|iii|iv)\.?$', '', 'i'), + '[^\w\s]', '', 'g' + ), + '\s+', ' ', 'g' + ) + ); +END; +$$ LANGUAGE plpgsql IMMUTABLE; + +-- Function to create input checklist from templates +CREATE OR REPLACE FUNCTION create_matter_inputs(p_matter_id UUID, p_case_type case_type) +RETURNS void AS $$ +BEGIN + INSERT INTO matter_inputs (matter_id, input_type, input_category, required) + SELECT p_matter_id, input_type, input_category, required + FROM input_templates + WHERE case_type = p_case_type + ORDER BY sort_order; +END; +$$ LANGUAGE plpgsql; + +-- Function to run conflicts check +CREATE OR REPLACE FUNCTION run_conflicts_check( + p_matter_id UUID, + p_run_by TEXT DEFAULT 'system' +) +RETURNS conflict_result AS $$ +DECLARE + v_result conflict_result := 'clear'; + v_matches JSONB := '[]'::jsonb; + v_entity RECORD; + v_match RECORD; + v_similarity FLOAT; +BEGIN + -- Get all entities for this matter + FOR v_entity IN + SELECT e.id, e.normalized_name, e.full_name + FROM matter_entities me + JOIN entities e ON e.id = me.entity_id + WHERE me.matter_id = p_matter_id + LOOP + -- Check against restricted entities (exact match) + IF EXISTS ( + SELECT 1 FROM restricted_entities + WHERE active = true + AND normalized_name = v_entity.normalized_name + ) THEN + v_result := 'conflict'; + v_matches := v_matches || jsonb_build_object( + 'entity_id', v_entity.id, + 'name', v_entity.full_name, + 'match_type', 'restricted_exact', + 'score', 1.0 + ); + END IF; + + -- Check against other matters (exact match on normalized name) + FOR v_match IN + SELECT DISTINCT e.id, e.full_name, m.matter_name, m.firm_id + FROM matter_entities me + JOIN entities e ON e.id = me.entity_id + JOIN matters m ON m.id = me.matter_id + WHERE me.matter_id != p_matter_id + AND e.normalized_name = v_entity.normalized_name + LOOP + IF v_result != 'conflict' THEN + v_result := 'potential'; + END IF; + v_matches := v_matches || jsonb_build_object( + 'entity_id', v_match.id, + 'name', v_match.full_name, + 'match_type', 'exact', + 'matched_matter', v_match.matter_name, + 'score', 1.0 + ); + END LOOP; + + -- Fuzzy match (similarity >= 0.85) against other entities + FOR v_match IN + SELECT e.id, e.full_name, similarity(e.normalized_name, v_entity.normalized_name) as sim + FROM entities e + WHERE e.id != v_entity.id + AND e.normalized_name % v_entity.normalized_name + AND similarity(e.normalized_name, v_entity.normalized_name) >= 0.85 + LOOP + IF v_result = 'clear' THEN + v_result := 'potential'; + END IF; + v_matches := v_matches || jsonb_build_object( + 'entity_id', v_match.id, + 'name', v_match.full_name, + 'match_type', 'fuzzy', + 'score', v_match.sim + ); + END LOOP; + END LOOP; + + -- Record the check + INSERT INTO conflict_checks (matter_id, run_by, result, matches) + VALUES (p_matter_id, p_run_by, v_result, v_matches); + + -- Update matter status + UPDATE matters + SET conflicts_status = v_result::text::conflicts_status, + updated_at = NOW() + WHERE id = p_matter_id; + + RETURN v_result; +END; +$$ LANGUAGE plpgsql; + +-- Function to create billables from delivery +CREATE OR REPLACE FUNCTION create_delivery_billables( + p_matter_id UUID, + p_delivery_type delivery_type, + p_is_rush BOOLEAN DEFAULT false +) +RETURNS void AS $$ +DECLARE + v_firm_id UUID; + v_base_amount NUMERIC; + v_rush_amount NUMERIC; + v_matter_name TEXT; +BEGIN + -- Get firm and matter info + SELECT firm_id, matter_name INTO v_firm_id, v_matter_name + FROM matters WHERE id = p_matter_id; + + -- Get base rate + SELECT default_amount INTO v_base_amount + FROM billing_rates + WHERE billable_type = p_delivery_type::text::billable_type; + + -- Create base billable + INSERT INTO billables (firm_id, matter_id, billable_type, amount, description, billable_date) + VALUES ( + v_firm_id, + p_matter_id, + p_delivery_type::text::billable_type, + v_base_amount, + p_delivery_type::text || ' Petition Prep – [Matter: ' || v_matter_name || ']', + CURRENT_DATE + ); + + -- Add rush fee if applicable + IF p_is_rush THEN + SELECT default_amount INTO v_rush_amount + FROM billing_rates WHERE billable_type = 'rush'; + + INSERT INTO billables (firm_id, matter_id, billable_type, amount, description, billable_date) + VALUES ( + v_firm_id, + p_matter_id, + 'rush', + v_rush_amount, + 'Rush Fee (24-48 hrs) – [Matter: ' || v_matter_name || '] – Authorized', + CURRENT_DATE + ); + END IF; +END; +$$ LANGUAGE plpgsql; + +-- ============================================================================= +-- TRIGGERS +-- ============================================================================= + +-- Auto-update updated_at timestamp +CREATE OR REPLACE FUNCTION update_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Apply to all tables with updated_at +CREATE TRIGGER trg_firms_updated_at BEFORE UPDATE ON firms + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +CREATE TRIGGER trg_contacts_updated_at BEFORE UPDATE ON contacts + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +CREATE TRIGGER trg_matters_updated_at BEFORE UPDATE ON matters + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +CREATE TRIGGER trg_matter_inputs_updated_at BEFORE UPDATE ON matter_inputs + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +CREATE TRIGGER trg_deficiencies_updated_at BEFORE UPDATE ON deficiencies + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +CREATE TRIGGER trg_entities_updated_at BEFORE UPDATE ON entities + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +CREATE TRIGGER trg_restricted_entities_updated_at BEFORE UPDATE ON restricted_entities + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +CREATE TRIGGER trg_billables_updated_at BEFORE UPDATE ON billables + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +CREATE TRIGGER trg_invoices_updated_at BEFORE UPDATE ON invoices + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +CREATE TRIGGER trg_app_users_updated_at BEFORE UPDATE ON app_users + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +CREATE TRIGGER trg_billing_rates_updated_at BEFORE UPDATE ON billing_rates + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); + +-- Trigger to create input checklist when matter is created +CREATE OR REPLACE FUNCTION on_matter_created() +RETURNS TRIGGER AS $$ +BEGIN + -- Create input checklist from templates + PERFORM create_matter_inputs(NEW.id, NEW.case_type); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trg_matter_created AFTER INSERT ON matters + FOR EACH ROW EXECUTE FUNCTION on_matter_created(); + +-- Trigger to normalize entity names +CREATE OR REPLACE FUNCTION on_entity_insert_update() +RETURNS TRIGGER AS $$ +BEGIN + NEW.normalized_name := normalize_name(NEW.full_name); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trg_entity_normalize BEFORE INSERT OR UPDATE ON entities + FOR EACH ROW EXECUTE FUNCTION on_entity_insert_update(); + +-- ============================================================================= +-- ROW LEVEL SECURITY POLICIES +-- ============================================================================= + +-- Enable RLS on sensitive tables +ALTER TABLE firms ENABLE ROW LEVEL SECURITY; +ALTER TABLE contacts ENABLE ROW LEVEL SECURITY; +ALTER TABLE matters ENABLE ROW LEVEL SECURITY; +ALTER TABLE matter_inputs ENABLE ROW LEVEL SECURITY; +ALTER TABLE deficiencies ENABLE ROW LEVEL SECURITY; +ALTER TABLE deliveries ENABLE ROW LEVEL SECURITY; +ALTER TABLE billables ENABLE ROW LEVEL SECURITY; +ALTER TABLE invoices ENABLE ROW LEVEL SECURITY; + +-- Policy: Admin and staff have full access +CREATE POLICY admin_staff_all ON firms + FOR ALL + USING ( + EXISTS ( + SELECT 1 FROM app_users + WHERE id = auth.uid() + AND role IN ('admin', 'staff') + ) + ); + +CREATE POLICY admin_staff_all ON contacts + FOR ALL + USING ( + EXISTS ( + SELECT 1 FROM app_users + WHERE id = auth.uid() + AND role IN ('admin', 'staff') + ) + ); + +CREATE POLICY admin_staff_all ON matters + FOR ALL + USING ( + EXISTS ( + SELECT 1 FROM app_users + WHERE id = auth.uid() + AND role IN ('admin', 'staff') + ) + ); + +CREATE POLICY admin_staff_all ON matter_inputs + FOR ALL + USING ( + EXISTS ( + SELECT 1 FROM app_users + WHERE id = auth.uid() + AND role IN ('admin', 'staff') + ) + ); + +CREATE POLICY admin_staff_all ON deficiencies + FOR ALL + USING ( + EXISTS ( + SELECT 1 FROM app_users + WHERE id = auth.uid() + AND role IN ('admin', 'staff') + ) + ); + +CREATE POLICY admin_staff_all ON deliveries + FOR ALL + USING ( + EXISTS ( + SELECT 1 FROM app_users + WHERE id = auth.uid() + AND role IN ('admin', 'staff') + ) + ); + +CREATE POLICY admin_staff_all ON billables + FOR ALL + USING ( + EXISTS ( + SELECT 1 FROM app_users + WHERE id = auth.uid() + AND role IN ('admin', 'staff') + ) + ); + +CREATE POLICY admin_staff_all ON invoices + FOR ALL + USING ( + EXISTS ( + SELECT 1 FROM app_users + WHERE id = auth.uid() + AND role IN ('admin', 'staff') + ) + ); + +-- Policy: Client users can only see their firm's data +CREATE POLICY client_firm_read ON matters + FOR SELECT + USING ( + firm_id = ( + SELECT firm_id FROM app_users WHERE id = auth.uid() + ) + ); + +CREATE POLICY client_firm_read ON invoices + FOR SELECT + USING ( + firm_id = ( + SELECT firm_id FROM app_users WHERE id = auth.uid() + ) + ); + +-- ============================================================================= +-- SAMPLE DATA FOR TESTING +-- ============================================================================= + +-- Sample firm +INSERT INTO firms (id, firm_name, status, retainer_amount, billing_email, active_capacity) +VALUES + ('11111111-1111-1111-1111-111111111111', 'Johnson & Associates', 'active', 2500.00, 'billing@johnsonlaw.com', true), + ('22222222-2222-2222-2222-222222222222', 'Smith Legal Group', 'active', 1500.00, 'accounts@smithlegal.com', true), + ('33333333-3333-3333-3333-333333333333', 'Davis Bankruptcy Law', 'on_hold', 2000.00, 'davis@dbllaw.com', false); + +-- Sample contacts +INSERT INTO contacts (firm_id, name, email, phone, role, is_primary) VALUES + ('11111111-1111-1111-1111-111111111111', 'Mark Johnson', 'mjohnson@johnsonlaw.com', '555-0100', 'attorney', true), + ('11111111-1111-1111-1111-111111111111', 'Sarah Williams', 'swilliams@johnsonlaw.com', '555-0101', 'staff', false), + ('22222222-2222-2222-2222-222222222222', 'Linda Smith', 'lsmith@smithlegal.com', '555-0200', 'attorney', true), + ('33333333-3333-3333-3333-333333333333', 'Robert Davis', 'rdavis@dbllaw.com', '555-0300', 'attorney', true); + +-- Sample matters +INSERT INTO matters (id, firm_id, matter_name, case_type, district, deadline, rush, status_stage, inputs_status, conflicts_status, assigned_to) VALUES + ('aaaa1111-1111-1111-1111-111111111111', '11111111-1111-1111-1111-111111111111', 'Adams, Michael – Ch 7', 'ch7', 'N.D. Texas', '2026-02-15', false, 'in_progress', 'complete', 'clear', 'Turea'), + ('aaaa2222-2222-2222-2222-222222222222', '11111111-1111-1111-1111-111111111111', 'Baker, Jennifer – Ch 13', 'ch13', 'N.D. Texas', '2026-02-20', true, 'qc', 'complete', 'clear', 'Turea'), + ('aaaa3333-3333-3333-3333-333333333333', '22222222-2222-2222-2222-222222222222', 'Clark, David – Ch 7', 'ch7', 'S.D. Texas', '2026-02-18', false, 'waiting_inputs', 'incomplete', 'not_run', 'Staff'), + ('aaaa4444-4444-4444-4444-444444444444', '22222222-2222-2222-2222-222222222222', 'Evans, Susan – Amendment', 'amendment', 'S.D. Texas', '2026-02-10', false, 'delivered', 'complete', 'clear', 'Turea'), + ('aaaa5555-5555-5555-5555-555555555555', '33333333-3333-3333-3333-333333333333', 'Foster, William – Ch 7', 'ch7', 'E.D. Texas', '2026-02-25', false, 'paused_ar', 'complete', 'clear', 'Staff'); + +-- Sample entities +INSERT INTO entities (id, entity_type, full_name, address) VALUES + ('eeee1111-1111-1111-1111-111111111111', 'person', 'Michael Adams', '123 Main St, Dallas, TX'), + ('eeee2222-2222-2222-2222-222222222222', 'person', 'Jennifer Baker', '456 Oak Ave, Fort Worth, TX'), + ('eeee3333-3333-3333-3333-333333333333', 'person', 'Thomas Baker', '456 Oak Ave, Fort Worth, TX'), + ('eeee4444-4444-4444-4444-444444444444', 'person', 'David Clark', '789 Elm St, Houston, TX'), + ('eeee5555-5555-5555-5555-555555555555', 'person', 'Susan Evans', '321 Pine Rd, Austin, TX'), + ('eeee6666-6666-6666-6666-666666666666', 'person', 'William Foster', '654 Cedar Ln, San Antonio, TX'); + +-- Link entities to matters +INSERT INTO matter_entities (matter_id, entity_id, role) VALUES + ('aaaa1111-1111-1111-1111-111111111111', 'eeee1111-1111-1111-1111-111111111111', 'debtor'), + ('aaaa2222-2222-2222-2222-222222222222', 'eeee2222-2222-2222-2222-222222222222', 'debtor'), + ('aaaa2222-2222-2222-2222-222222222222', 'eeee3333-3333-3333-3333-333333333333', 'spouse'), + ('aaaa3333-3333-3333-3333-333333333333', 'eeee4444-4444-4444-4444-444444444444', 'debtor'), + ('aaaa4444-4444-4444-4444-444444444444', 'eeee5555-5555-5555-5555-555555555555', 'debtor'), + ('aaaa5555-5555-5555-5555-555555555555', 'eeee6666-6666-6666-6666-666666666666', 'debtor'); + +-- Sample deliveries +INSERT INTO deliveries (matter_id, delivery_type, delivered_at, delivered_by, qc_completed, qc_completed_by) VALUES + ('aaaa4444-4444-4444-4444-444444444444', 'amendment', '2026-02-03 14:30:00', 'Turea', true, 'QC Team'); + +-- Sample billables +INSERT INTO billables (firm_id, matter_id, billable_type, amount, description, billable_date) VALUES + ('22222222-2222-2222-2222-222222222222', 'aaaa4444-4444-4444-4444-444444444444', 'amendment', 250.00, 'Amendment/Modification – [Matter: Evans, Susan – Amendment] – Delivered 2026-02-03', '2026-02-03'), + ('11111111-1111-1111-1111-111111111111', NULL, 'retainer', 2500.00, 'Monthly Retainer - February 2026', '2026-02-01'), + ('22222222-2222-2222-2222-222222222222', NULL, 'retainer', 1500.00, 'Monthly Retainer - February 2026', '2026-02-01'); + +-- Sample invoice +INSERT INTO invoices (id, firm_id, invoice_number, invoice_date, status, total) VALUES + ('iiii1111-1111-1111-1111-111111111111', '33333333-3333-3333-3333-333333333333', 'INV-2026-001', '2026-01-31', 'overdue', 650.00); diff --git a/bizdeedz-ops-platform/database/002_views.sql b/bizdeedz-ops-platform/database/002_views.sql new file mode 100644 index 0000000..2c4e0c3 --- /dev/null +++ b/bizdeedz-ops-platform/database/002_views.sql @@ -0,0 +1,524 @@ +-- BizDeedz Ops Platform - SQL Views for Dashboards +-- Version: 1.0 MVP +-- Description: Views for KPIs, AR aging, metrics, and reporting + +-- ============================================================================= +-- KPI DASHBOARD VIEW +-- High-level metrics for the operator dashboard +-- ============================================================================= + +CREATE OR REPLACE VIEW v_kpi_dashboard AS +SELECT + -- Financial KPIs + (SELECT COUNT(*) FROM invoices WHERE status = 'overdue') as count_overdue_invoices, + (SELECT COALESCE(SUM(total), 0) FROM invoices WHERE status = 'overdue') as total_ar_outstanding, + (SELECT COALESCE(SUM(amount * quantity), 0) FROM billables WHERE invoice_id IS NULL AND approved = true) as unbilled_wip, + (SELECT COUNT(*) FROM firms WHERE active_capacity = true) as firms_with_paid_retainers, + (SELECT COUNT(*) FROM firms WHERE active_capacity = false AND status = 'active') as firms_with_unpaid_retainers, + + -- Operational KPIs - Current Week + (SELECT COUNT(*) FROM matters + WHERE status_stage = 'delivered' + AND delivered_at >= date_trunc('week', CURRENT_DATE)) as delivered_this_week, + + (SELECT COUNT(*) FROM matters + WHERE status_stage IN ('delivered', 'complete') + AND delivered_at >= date_trunc('month', CURRENT_DATE)) as delivered_this_month, + + -- Queue Counts + (SELECT COUNT(*) FROM matters WHERE status_stage = 'assigned') as queue_assigned, + (SELECT COUNT(*) FROM matters WHERE status_stage = 'waiting_inputs') as queue_waiting_inputs, + (SELECT COUNT(*) FROM matters WHERE status_stage = 'conflicts_review') as queue_conflicts, + (SELECT COUNT(*) FROM matters WHERE status_stage = 'in_progress') as queue_in_progress, + (SELECT COUNT(*) FROM matters WHERE status_stage = 'qc') as queue_qc, + (SELECT COUNT(*) FROM matters WHERE status_stage = 'revision') as queue_revision, + (SELECT COUNT(*) FROM matters WHERE status_stage = 'paused_ar') as queue_paused_ar, + + -- Rush matters + (SELECT COUNT(*) FROM matters WHERE rush = true AND status_stage NOT IN ('complete', 'delivered')) as active_rush_matters, + + -- Quality KPIs + (SELECT ROUND(AVG(revision_rounds)::numeric, 2) FROM matters WHERE status_stage = 'complete') as avg_revision_rounds, + + -- Deficiency rate (matters with deficiencies / total delivered) + (SELECT ROUND( + CASE WHEN COUNT(*) > 0 THEN + (COUNT(DISTINCT d.matter_id)::numeric / COUNT(DISTINCT m.id)) * 100 + ELSE 0 END, 1) + FROM matters m + LEFT JOIN deficiencies d ON d.matter_id = m.id AND d.status = 'open' + WHERE m.status_stage = 'delivered') as deficiency_rate_percent, + + -- SLA Metrics (matters delivered before deadline / total delivered) + (SELECT ROUND( + CASE WHEN COUNT(*) > 0 THEN + (COUNT(CASE WHEN delivered_at::date <= deadline THEN 1 END)::numeric / COUNT(*)) * 100 + ELSE 0 END, 1) + FROM matters + WHERE status_stage IN ('delivered', 'complete') + AND delivered_at IS NOT NULL + AND deadline IS NOT NULL + AND delivered_at >= date_trunc('month', CURRENT_DATE)) as sla_hit_rate_percent, + + -- Timestamps + NOW() as calculated_at; + + +-- ============================================================================= +-- AR AGING VIEW +-- Buckets unpaid invoices for AR management and pause triggers +-- ============================================================================= + +CREATE OR REPLACE VIEW v_ar_aging AS +SELECT + f.id as firm_id, + f.firm_name, + i.id as invoice_id, + i.invoice_number, + i.total, + i.invoice_date, + i.sent_at, + i.status, + CURRENT_DATE - i.invoice_date as calendar_days_outstanding, + -- Calculate business days (simplified: exclude weekends) + (SELECT COUNT(*)::int + FROM generate_series(i.invoice_date, CURRENT_DATE - 1, '1 day') d + WHERE EXTRACT(DOW FROM d) NOT IN (0, 6)) as business_days_outstanding, + CASE + WHEN (CURRENT_DATE - i.invoice_date) <= 3 THEN '0-3 Days' + WHEN (CURRENT_DATE - i.invoice_date) BETWEEN 4 AND 7 THEN '4-7 Days' + ELSE '8+ Days' + END as aging_bucket, + CASE + WHEN (SELECT COUNT(*) + FROM generate_series(i.invoice_date, CURRENT_DATE - 1, '1 day') d + WHERE EXTRACT(DOW FROM d) NOT IN (0, 6)) > 3 THEN true + ELSE false + END as should_pause +FROM invoices i +JOIN firms f ON i.firm_id = f.id +WHERE i.status IN ('sent', 'overdue') +ORDER BY f.firm_name, i.invoice_date; + + +-- ============================================================================= +-- AR AGING SUMMARY VIEW +-- Aggregated AR totals by bucket +-- ============================================================================= + +CREATE OR REPLACE VIEW v_ar_aging_summary AS +SELECT + aging_bucket, + COUNT(*) as invoice_count, + SUM(total) as total_outstanding, + COUNT(DISTINCT firm_id) as firms_affected +FROM v_ar_aging +GROUP BY aging_bucket +ORDER BY + CASE aging_bucket + WHEN '0-3 Days' THEN 1 + WHEN '4-7 Days' THEN 2 + ELSE 3 + END; + + +-- ============================================================================= +-- FIRM PERFORMANCE VIEW +-- Aggregated metrics per firm +-- ============================================================================= + +CREATE OR REPLACE VIEW v_firm_performance AS +SELECT + f.id as firm_id, + f.firm_name, + f.status as firm_status, + f.active_capacity, + + -- Matter counts + COUNT(DISTINCT m.id) as total_matters, + COUNT(DISTINCT CASE WHEN m.status_stage NOT IN ('complete', 'paused_ar') THEN m.id END) as active_matters, + COUNT(DISTINCT CASE WHEN m.status_stage = 'complete' THEN m.id END) as completed_matters, + COUNT(DISTINCT CASE WHEN m.status_stage = 'paused_ar' THEN m.id END) as paused_matters, + + -- By case type + COUNT(DISTINCT CASE WHEN m.case_type = 'ch7' THEN m.id END) as ch7_count, + COUNT(DISTINCT CASE WHEN m.case_type = 'ch13' THEN m.id END) as ch13_count, + COUNT(DISTINCT CASE WHEN m.case_type = 'amendment' THEN m.id END) as amendment_count, + + -- Quality metrics + ROUND(AVG(m.revision_rounds)::numeric, 2) as avg_revisions, + + -- Cycle time (average days from assigned to delivered) + ROUND(AVG( + EXTRACT(EPOCH FROM (m.delivered_at - m.assigned_at)) / 86400 + )::numeric, 1) as avg_cycle_time_days, + + -- Financial + COALESCE((SELECT SUM(amount * quantity) FROM billables b WHERE b.firm_id = f.id), 0) as total_billed, + COALESCE((SELECT SUM(total) FROM invoices i WHERE i.firm_id = f.id AND i.status = 'paid'), 0) as total_collected, + COALESCE((SELECT SUM(total) FROM invoices i WHERE i.firm_id = f.id AND i.status IN ('sent', 'overdue')), 0) as ar_outstanding + +FROM firms f +LEFT JOIN matters m ON m.firm_id = f.id +GROUP BY f.id, f.firm_name, f.status, f.active_capacity; + + +-- ============================================================================= +-- DELIVERY METRICS VIEW +-- Delivery statistics for reporting +-- ============================================================================= + +CREATE OR REPLACE VIEW v_delivery_metrics AS +SELECT + date_trunc('week', d.delivered_at)::date as week_start, + date_trunc('month', d.delivered_at)::date as month_start, + d.delivery_type, + COUNT(*) as delivery_count, + COUNT(CASE WHEN m.rush = true THEN 1 END) as rush_count, + + -- Average cycle time for deliveries that week + ROUND(AVG( + EXTRACT(EPOCH FROM (d.delivered_at - m.assigned_at)) / 86400 + )::numeric, 1) as avg_cycle_time_days, + + -- Revenue (based on delivery type) + SUM(CASE + WHEN d.delivery_type = 'ch7' THEN 200 + WHEN d.delivery_type = 'ch13' THEN 350 + WHEN d.delivery_type = 'amendment' THEN 250 + END) as estimated_revenue + +FROM deliveries d +JOIN matters m ON d.matter_id = m.id +WHERE d.delivered_at IS NOT NULL +GROUP BY date_trunc('week', d.delivered_at), date_trunc('month', d.delivered_at), d.delivery_type +ORDER BY week_start DESC, delivery_type; + + +-- ============================================================================= +-- WEEKLY DELIVERY SUMMARY VIEW +-- Summary for Friday invoice preparation +-- ============================================================================= + +CREATE OR REPLACE VIEW v_weekly_delivery_summary AS +SELECT + f.id as firm_id, + f.firm_name, + f.billing_email, + COUNT(DISTINCT d.id) as deliveries_count, + COUNT(DISTINCT CASE WHEN d.delivery_type = 'ch7' THEN d.id END) as ch7_count, + COUNT(DISTINCT CASE WHEN d.delivery_type = 'ch13' THEN d.id END) as ch13_count, + COUNT(DISTINCT CASE WHEN d.delivery_type = 'amendment' THEN d.id END) as amendment_count, + COUNT(DISTINCT CASE WHEN m.rush = true THEN d.id END) as rush_count, + + -- Estimated totals + SUM(CASE + WHEN d.delivery_type = 'ch7' THEN 200 + WHEN d.delivery_type = 'ch13' THEN 350 + WHEN d.delivery_type = 'amendment' THEN 250 + END) + (COUNT(DISTINCT CASE WHEN m.rush = true THEN d.id END) * 150) as estimated_total + +FROM firms f +JOIN matters m ON m.firm_id = f.id +JOIN deliveries d ON d.matter_id = m.id +WHERE d.delivered_at >= date_trunc('week', CURRENT_DATE - INTERVAL '1 week') + AND d.delivered_at < date_trunc('week', CURRENT_DATE) +GROUP BY f.id, f.firm_name, f.billing_email +ORDER BY f.firm_name; + + +-- ============================================================================= +-- MATTER QUEUE VIEW +-- Matters grouped by stage with key info for work boards +-- ============================================================================= + +CREATE OR REPLACE VIEW v_matter_queue AS +SELECT + m.id as matter_id, + m.matter_name, + m.case_type, + m.district, + m.deadline, + m.rush, + m.status_stage, + m.inputs_status, + m.conflicts_status, + m.assigned_to, + m.revision_rounds, + f.id as firm_id, + f.firm_name, + f.active_capacity as firm_active, + + -- Age calculations + CURRENT_DATE - m.assigned_at::date as days_since_assigned, + CASE + WHEN m.deadline IS NOT NULL THEN m.deadline - CURRENT_DATE + ELSE NULL + END as days_until_deadline, + + -- Flags + CASE WHEN m.deadline < CURRENT_DATE AND m.status_stage NOT IN ('complete', 'delivered') THEN true ELSE false END as is_overdue, + CASE WHEN m.deadline <= CURRENT_DATE + 2 AND m.status_stage NOT IN ('complete', 'delivered') THEN true ELSE false END as deadline_soon, + + -- Open deficiencies count + (SELECT COUNT(*) FROM deficiencies d WHERE d.matter_id = m.id AND d.status = 'open') as open_deficiencies, + + m.created_at, + m.updated_at + +FROM matters m +JOIN firms f ON m.firm_id = f.id +ORDER BY + CASE WHEN m.rush = true THEN 0 ELSE 1 END, + m.deadline NULLS LAST, + m.created_at; + + +-- ============================================================================= +-- CONFLICTS DASHBOARD VIEW +-- Overview of conflict check status across all matters +-- ============================================================================= + +CREATE OR REPLACE VIEW v_conflicts_dashboard AS +SELECT + m.id as matter_id, + m.matter_name, + m.conflicts_status, + f.firm_name, + cc.run_at as last_check_at, + cc.run_by as last_check_by, + cc.result as last_check_result, + cc.matches as matches_found, + cc.decision, + cc.decision_by, + cc.decision_notes, + cc.decision_at, + + -- Entity names for this matter + (SELECT array_agg(e.full_name) + FROM matter_entities me + JOIN entities e ON e.id = me.entity_id + WHERE me.matter_id = m.id) as entity_names + +FROM matters m +JOIN firms f ON m.firm_id = f.id +LEFT JOIN LATERAL ( + SELECT * FROM conflict_checks + WHERE matter_id = m.id + ORDER BY run_at DESC + LIMIT 1 +) cc ON true +WHERE m.status_stage NOT IN ('complete') +ORDER BY + CASE m.conflicts_status + WHEN 'conflict' THEN 1 + WHEN 'potential' THEN 2 + WHEN 'not_run' THEN 3 + ELSE 4 + END, + m.created_at DESC; + + +-- ============================================================================= +-- RETAINER STATUS VIEW +-- Track retainer payments and firm capacity status +-- ============================================================================= + +CREATE OR REPLACE VIEW v_retainer_status AS +SELECT + f.id as firm_id, + f.firm_name, + f.status as firm_status, + f.retainer_amount, + f.retainer_due_day, + f.active_capacity, + f.billing_email, + + -- Last retainer billable + (SELECT MAX(billable_date) + FROM billables b + WHERE b.firm_id = f.id AND b.billable_type = 'retainer') as last_retainer_date, + + -- Last retainer invoice paid + (SELECT MAX(i.paid_at) + FROM invoices i + JOIN billables b ON b.invoice_id = i.id + WHERE b.firm_id = f.id AND b.billable_type = 'retainer' AND i.status = 'paid') as last_retainer_paid, + + -- Active matters count + (SELECT COUNT(*) FROM matters m WHERE m.firm_id = f.id AND m.status_stage NOT IN ('complete', 'paused_ar')) as active_matters_count, + + -- Is retainer due this month + CASE + WHEN f.retainer_due_day <= EXTRACT(DAY FROM CURRENT_DATE) + AND NOT EXISTS ( + SELECT 1 FROM billables b + WHERE b.firm_id = f.id + AND b.billable_type = 'retainer' + AND b.billable_date >= date_trunc('month', CURRENT_DATE) + ) + THEN true + ELSE false + END as retainer_due_now + +FROM firms f +WHERE f.status = 'active' +ORDER BY f.active_capacity, f.firm_name; + + +-- ============================================================================= +-- INVOICE LINE ITEMS VIEW +-- Detailed view for invoice generation with proper descriptions +-- ============================================================================= + +CREATE OR REPLACE VIEW v_invoice_line_items AS +SELECT + b.id as billable_id, + b.firm_id, + f.firm_name, + b.matter_id, + m.matter_name, + b.billable_type, + b.amount, + b.quantity, + b.unit, + b.approved, + b.billable_date, + b.invoice_id, + + -- Generate proper Stripe-friendly description + CASE b.billable_type + WHEN 'ch7' THEN 'Ch 7 Petition Prep – [Matter: ' || COALESCE(m.matter_name, 'N/A') || '] – Delivered ' || to_char(b.billable_date, 'YYYY-MM-DD') + WHEN 'ch13' THEN 'Ch 13 Petition Prep – [Matter: ' || COALESCE(m.matter_name, 'N/A') || '] – Delivered ' || to_char(b.billable_date, 'YYYY-MM-DD') + WHEN 'amendment' THEN 'Amendment/Modification – [Matter: ' || COALESCE(m.matter_name, 'N/A') || '] – Delivered ' || to_char(b.billable_date, 'YYYY-MM-DD') + WHEN 'rush' THEN 'Rush Fee (24-48 hrs) – [Matter: ' || COALESCE(m.matter_name, 'N/A') || '] – Authorized' + WHEN 'overage' THEN 'Overage (Out of Scope) – ' || b.quantity || ' hrs @ $' || b.amount || '/hr – [Matter: ' || COALESCE(m.matter_name, 'N/A') || '] – Approved' + WHEN 'retainer' THEN 'Monthly Retainer – ' || to_char(b.billable_date, 'Month YYYY') + ELSE b.description + END as line_description, + + -- Line total + (b.amount * b.quantity) as line_total + +FROM billables b +JOIN firms f ON b.firm_id = f.id +LEFT JOIN matters m ON b.matter_id = m.id +WHERE b.approved = true +ORDER BY b.firm_id, b.billable_date, b.billable_type; + + +-- ============================================================================= +-- UNINVOICED BILLABLES VIEW +-- Billables ready for Friday invoice run +-- ============================================================================= + +CREATE OR REPLACE VIEW v_uninvoiced_billables AS +SELECT + b.firm_id, + f.firm_name, + f.billing_email, + COUNT(*) as line_item_count, + SUM(b.amount * b.quantity) as total_amount, + MIN(b.billable_date) as earliest_date, + MAX(b.billable_date) as latest_date, + array_agg(DISTINCT b.billable_type) as billable_types + +FROM billables b +JOIN firms f ON b.firm_id = f.id +WHERE b.invoice_id IS NULL + AND b.approved = true +GROUP BY b.firm_id, f.firm_name, f.billing_email +HAVING SUM(b.amount * b.quantity) > 0 +ORDER BY f.firm_name; + + +-- ============================================================================= +-- EFFECTIVE HOURLY RATE VIEW +-- Calculate effective hourly rate by matter (requires time logging) +-- ============================================================================= + +CREATE OR REPLACE VIEW v_effective_hourly_rate AS +SELECT + m.id as matter_id, + m.matter_name, + m.case_type, + f.firm_name, + + -- Total time logged + COALESCE(SUM(tl.minutes), 0) as total_minutes, + ROUND(COALESCE(SUM(tl.minutes), 0) / 60.0, 2) as total_hours, + + -- Total billed + COALESCE((SELECT SUM(amount * quantity) FROM billables b WHERE b.matter_id = m.id AND b.approved = true), 0) as total_billed, + + -- Effective hourly rate + CASE + WHEN COALESCE(SUM(tl.minutes), 0) > 0 THEN + ROUND( + (SELECT SUM(amount * quantity) FROM billables b WHERE b.matter_id = m.id AND b.approved = true)::numeric + / (SUM(tl.minutes)::numeric / 60), + 2) + ELSE NULL + END as effective_hourly_rate + +FROM matters m +JOIN firms f ON m.firm_id = f.id +LEFT JOIN time_logs tl ON tl.matter_id = m.id +WHERE m.status_stage = 'complete' +GROUP BY m.id, m.matter_name, m.case_type, f.firm_name +ORDER BY effective_hourly_rate DESC NULLS LAST; + + +-- ============================================================================= +-- DASHBOARD QUEUE COUNTS VIEW +-- Simple counts for dashboard tiles +-- ============================================================================= + +CREATE OR REPLACE VIEW v_dashboard_queue_counts AS +SELECT + status_stage, + COUNT(*) as count, + COUNT(CASE WHEN rush = true THEN 1 END) as rush_count, + COUNT(CASE WHEN deadline < CURRENT_DATE THEN 1 END) as overdue_count +FROM matters +WHERE status_stage NOT IN ('complete') +GROUP BY status_stage +ORDER BY + CASE status_stage + WHEN 'assigned' THEN 1 + WHEN 'waiting_inputs' THEN 2 + WHEN 'conflicts_review' THEN 3 + WHEN 'in_progress' THEN 4 + WHEN 'qc' THEN 5 + WHEN 'delivered' THEN 6 + WHEN 'revision' THEN 7 + WHEN 'paused_ar' THEN 8 + ELSE 9 + END; + + +-- ============================================================================= +-- MONTHLY FINANCIAL SUMMARY VIEW +-- Financial metrics by month +-- ============================================================================= + +CREATE OR REPLACE VIEW v_monthly_financial_summary AS +SELECT + date_trunc('month', i.invoice_date)::date as month, + COUNT(DISTINCT i.id) as invoices_sent, + COUNT(DISTINCT CASE WHEN i.status = 'paid' THEN i.id END) as invoices_paid, + SUM(i.total) as total_invoiced, + SUM(CASE WHEN i.status = 'paid' THEN i.total ELSE 0 END) as total_collected, + SUM(CASE WHEN i.status IN ('sent', 'overdue') THEN i.total ELSE 0 END) as total_outstanding, + + -- Collection rate + ROUND( + CASE WHEN SUM(i.total) > 0 THEN + (SUM(CASE WHEN i.status = 'paid' THEN i.total ELSE 0 END) / SUM(i.total)) * 100 + ELSE 0 END, + 1) as collection_rate_percent + +FROM invoices i +WHERE i.invoice_date >= date_trunc('year', CURRENT_DATE) +GROUP BY date_trunc('month', i.invoice_date) +ORDER BY month DESC; diff --git a/bizdeedz-ops-platform/docs/automation-triggers.md b/bizdeedz-ops-platform/docs/automation-triggers.md new file mode 100644 index 0000000..ec245cd --- /dev/null +++ b/bizdeedz-ops-platform/docs/automation-triggers.md @@ -0,0 +1,600 @@ +# BizDeedz Ops Platform - Automation Triggers & Make.com Scenarios + +## Overview + +This document describes the automation triggers and Make.com (formerly Integromat) scenarios that power the BizDeedz Ops Platform. These automations handle: + +1. **Matter lifecycle events** - Input checklist creation, conflicts checks +2. **Invoice generation** - Friday billing runs, Stripe integration +3. **Payment processing** - Webhook handling, auto-unpause +4. **AR management** - Overdue detection, firm pausing +5. **Notifications** - Email alerts, reminders + +--- + +## Trigger 1: Matter Created + +### When +New row inserted into `matters` table + +### Actions + +1. **Create Input Checklist** + - Call Supabase function `create_matter_inputs(matter_id, case_type)` + - Copies templates from `input_templates` based on case type + +2. **Create Entity Records** + - Insert debtor into `entities` table + - Insert spouse into `entities` table (if provided) + - Link entities to matter via `matter_entities` + +3. **Run Conflicts Check** + - Call Supabase function `run_conflicts_check(matter_id, 'system')` + - Updates `matters.conflicts_status` + - Creates audit record in `conflict_checks` + +4. **Send Notification** (optional) + - Notify assigned team member + - Include matter details and conflicts status + +### Implementation + +```sql +-- Database trigger (already in schema) +CREATE TRIGGER trg_matter_created AFTER INSERT ON matters + FOR EACH ROW EXECUTE FUNCTION on_matter_created(); +``` + +### Make.com Scenario +Not required - handled by Supabase triggers + +--- + +## Trigger 2: Inputs Marked Complete + +### When +`matters.inputs_status` changes to `'complete'` + +### Actions + +1. **Set Timestamp** + - Update `matters.inputs_complete_at = NOW()` + +2. **Update Stage** (if conflicts cleared) + - If `conflicts_status IN ('clear', 'approved')`: + - Move `status_stage` to `'in_progress'` + - Set `in_progress_at = NOW()` + +3. **Start SLA Clock** + - SLA tracking begins from `inputs_complete_at` + +### Implementation + +```sql +-- Supabase Edge Function or Database Trigger +CREATE OR REPLACE FUNCTION on_inputs_complete() +RETURNS TRIGGER AS $$ +BEGIN + IF NEW.inputs_status = 'complete' AND OLD.inputs_status != 'complete' THEN + NEW.inputs_complete_at = NOW(); + + -- Auto-advance if conflicts are clear + IF NEW.conflicts_status IN ('clear') AND NEW.status_stage = 'waiting_inputs' THEN + NEW.status_stage = 'conflicts_review'; + END IF; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; +``` + +--- + +## Trigger 3: Stage Changed to Delivered + +### When +`matters.status_stage` changes to `'delivered'` + +### Preconditions (enforced by UI/API) +- QC must be completed (`deliveries.qc_completed = true` exists) + +### Actions + +1. **Create Delivery Record** + ```sql + INSERT INTO deliveries (matter_id, delivery_type, delivered_by, qc_completed) + VALUES (matter_id, matter.case_type, current_user, true); + ``` + +2. **Create Billables** + - Call `create_delivery_billables(matter_id, case_type, rush)` + - Creates base fee billable + - Creates rush fee billable (if `matter.rush = true`) + +3. **Update Timestamps** + ```sql + UPDATE matters SET delivered_at = NOW() WHERE id = matter_id; + ``` + +4. **Send Notification** + - Email firm contact that delivery is ready + - Include download link (if applicable) + +### Make.com Scenario: Delivery Notification + +``` +Trigger: Supabase Webhook (deliveries INSERT) +│ +├─► HTTP: Get Matter Details +│ └─► Supabase: SELECT from matters, firms, contacts +│ +├─► Email: Send Delivery Notification +│ ├─► To: firm.billing_email +│ ├─► Subject: "BizDeedz Delivery Ready: {matter_name}" +│ └─► Body: Delivery details + next steps +│ +└─► Log: Record notification sent +``` + +--- + +## Trigger 4: Friday Invoice Run + +### When +Scheduled: Every Friday at 5:00 PM CT (configurable) + +### Actions + +1. **Query Unbilled Items** + ```sql + SELECT * FROM v_uninvoiced_billables; + ``` + +2. **Group by Firm** + - Aggregate billables by `firm_id` + - Calculate totals + +3. **Create Invoices** + - For each firm with billables: + - Create `invoices` record with status `'draft'` + - Link billables to invoice via `billables.invoice_id` + +4. **Create Stripe Invoices** (Phase 1.5) + - For each invoice: + - Find or create Stripe Customer + - Create Stripe Invoice Items + - Create and finalize Stripe Invoice + - Update `invoices.stripe_invoice_id` + +5. **Send Invoices** + - Update `invoices.status = 'sent'` + - Set `invoices.sent_at = NOW()` + +### Make.com Scenario: Friday Invoice Generation + +``` +Trigger: Schedule (Every Friday 5:00 PM CT) +│ +├─► Supabase: Query v_uninvoiced_billables +│ └─► Returns: [{firm_id, firm_name, billing_email, total_amount, billables[]}] +│ +├─► Iterator: For each firm +│ │ +│ ├─► Supabase: INSERT into invoices +│ │ └─► Returns: new_invoice_id +│ │ +│ ├─► Supabase: UPDATE billables SET invoice_id = new_invoice_id +│ │ +│ ├─► Stripe: Search Customers by email +│ │ └─► Router: Exists? Use ID : Create Customer +│ │ +│ ├─► Iterator: For each billable +│ │ └─► Stripe: Create Invoice Item +│ │ ├─► Customer: customer_id +│ │ ├─► Amount: billable.amount * 100 (cents) +│ │ └─► Description: From v_invoice_line_items +│ │ +│ ├─► Stripe: Create Invoice +│ │ └─► Auto-advance: false +│ │ +│ ├─► Stripe: Finalize Invoice +│ │ └─► Auto-send: true +│ │ +│ └─► Supabase: UPDATE invoices +│ ├─► stripe_invoice_id = stripe.id +│ ├─► status = 'sent' +│ └─► sent_at = NOW() +│ +└─► Slack/Email: Summary notification to BizDeedz team + └─► "Friday invoices complete: X firms, $Y,YYY.YY total" +``` + +--- + +## Trigger 5: AR Aging Check + +### When +Scheduled: Daily at 9:00 AM CT + +### Actions + +1. **Query Overdue Invoices** + ```sql + SELECT * FROM v_ar_aging WHERE should_pause = true; + ``` + +2. **Update Invoice Status** + ```sql + UPDATE invoices SET status = 'overdue' + WHERE status = 'sent' + AND (SELECT COUNT(*) FROM generate_series...) > 3; + ``` + +3. **Pause Firms** (if policy enabled) + - For invoices > 3 business days: + - Update `firms.status = 'on_hold'` + - Update `matters.status_stage = 'paused_ar'` for firm's active matters + +4. **Send Notifications** + - Email BizDeedz team: "AR Alert: X invoices overdue" + - Email firm (if configured): Payment reminder + +### Make.com Scenario: Daily AR Check + +``` +Trigger: Schedule (Daily 9:00 AM CT) +│ +├─► Supabase: Query v_ar_aging WHERE business_days_outstanding > 3 +│ +├─► Filter: Invoices not already marked overdue +│ +├─► Iterator: For each overdue invoice +│ │ +│ ├─► Supabase: UPDATE invoices SET status = 'overdue' +│ │ +│ ├─► Router: Days > 3 (Warning) vs Days > 7 (Pause) +│ │ │ +│ │ ├─► [3-7 days] Email: Send payment reminder to firm +│ │ │ +│ │ └─► [8+ days] +│ │ ├─► Supabase: UPDATE firms SET status = 'on_hold' +│ │ ├─► Supabase: UPDATE matters SET status_stage = 'paused_ar' +│ │ └─► Email: Notify BizDeedz team +│ │ +│ └─► Log: Record action taken +│ +└─► Slack: Daily AR summary +``` + +--- + +## Trigger 6: Stripe Payment Webhook + +### When +Stripe webhook event: `invoice.payment_succeeded` + +### Actions + +1. **Find Invoice** + ```sql + SELECT * FROM invoices WHERE stripe_invoice_id = event.data.object.id; + ``` + +2. **Update Invoice Status** + ```sql + UPDATE invoices + SET status = 'paid', paid_at = NOW() + WHERE stripe_invoice_id = stripe_id; + ``` + +3. **Create Payment Record** (optional) + ```sql + INSERT INTO payments (invoice_id, stripe_payment_intent_id, amount, status, paid_at) + VALUES (...); + ``` + +4. **Check Firm AR Status** + ```sql + SELECT COUNT(*) FROM invoices + WHERE firm_id = X AND status IN ('sent', 'overdue'); + ``` + +5. **Unpause Firm** (if no other overdue) + - If count = 0: + - Update `firms.status = 'active'` + - Update `matters.status_stage` from `'paused_ar'` to previous state + +6. **Send Confirmation** + - Email BizDeedz: "Payment received: {firm} - ${amount}" + +### Make.com Scenario: Stripe Payment Handler + +``` +Trigger: Stripe Webhook (invoice.payment_succeeded) +│ +├─► Supabase: SELECT from invoices WHERE stripe_invoice_id = event.id +│ └─► Returns: invoice record +│ +├─► Supabase: UPDATE invoices SET status = 'paid', paid_at = NOW() +│ +├─► Supabase: INSERT into payments (audit record) +│ +├─► Supabase: SELECT COUNT(*) overdue invoices for firm +│ +└─► Router: Has other overdue? + │ + ├─► [No] Unpause Firm + │ ├─► Supabase: UPDATE firms SET status = 'active' + │ ├─► Supabase: UPDATE matters SET status_stage = (previous) + │ └─► Email: Notify team "Firm unpause: {firm_name}" + │ + └─► [Yes] Do nothing (firm stays paused) +``` + +--- + +## Trigger 7: Deficiency List Generated + +### When +User clicks "Generate Deficiency List" for a matter + +### Actions + +1. **Query Missing Inputs** + ```sql + SELECT * FROM matter_inputs + WHERE matter_id = X AND required = true AND received = false; + ``` + +2. **Create Deficiency Records** + ```sql + INSERT INTO deficiencies (matter_id, category, deficiency_text, created_by) + SELECT matter_id, input_category, input_type, current_user + FROM matter_inputs WHERE ...; + ``` + +3. **Generate Email/Document** + - Format deficiency list for client + - Include matter reference + - List each missing item by category + +4. **Send to Client** (optional, with approval) + - Email to firm contact + - Or generate PDF for manual sending + +### Make.com Scenario (optional): Deficiency Email + +``` +Trigger: Supabase Webhook (deficiencies bulk INSERT) +│ +├─► Aggregate: Group deficiencies by matter_id +│ +├─► HTTP: Get matter and firm details +│ +├─► Text: Format deficiency list +│ └─► Template with categories and items +│ +└─► Email: Send to firm contact + ├─► Subject: "Action Required: Missing Documents - {matter_name}" + └─► Body: Formatted deficiency list +``` + +--- + +## Email Templates + +### 1. Delivery Notification + +**Subject:** `BizDeedz Delivery Ready: {matter_name}` + +**Body:** +``` +Dear {contact_name}, + +Your matter has been completed and is ready for review. + +Matter: {matter_name} +Case Type: {case_type} +Delivered: {delivered_at} + +Please review the deliverables at your earliest convenience. If you have any revisions, please consolidate all feedback into a single response. + +Thank you for your business. + +Best regards, +BizDeedz Team +``` + +### 2. Payment Reminder (4-7 days) + +**Subject:** `Payment Reminder: Invoice {invoice_number}` + +**Body:** +``` +Dear {contact_name}, + +This is a friendly reminder that your invoice is now {days} days outstanding. + +Invoice: {invoice_number} +Amount: ${total} +Due: On receipt + +Please remit payment at your earliest convenience via the Stripe payment link below. + +[Pay Now: {stripe_invoice_url}] + +Thank you. + +BizDeedz Team +``` + +### 3. Work Paused Notice (8+ days) + +**Subject:** `Important: Work Paused - Payment Required` + +**Body:** +``` +Dear {contact_name}, + +Due to outstanding payment, work on your matters has been temporarily paused. + +Outstanding Invoice: {invoice_number} +Amount: ${total} +Days Outstanding: {days} + +To resume work, please remit payment immediately: +[Pay Now: {stripe_invoice_url}] + +Once payment is received, work will resume automatically. + +If you have questions about this invoice, please contact us. + +BizDeedz Team +``` + +### 4. Deficiency List + +**Subject:** `Action Required: Missing Documents - {matter_name}` + +**Body:** +``` +Dear {contact_name}, + +To proceed with {matter_name}, we need the following documents: + +**Identity:** +- [ ] Government ID (front/back) + +**Income:** +- [ ] Paystubs - last 6 months + +**Banking:** +- [ ] Bank statements - last 3 months + +Please provide these items at your earliest convenience. The deadline for this matter is {deadline}. + +Thank you. + +BizDeedz Team +``` + +--- + +## Integration Endpoints + +### Stripe + +- **API Version:** 2023-10-16 +- **Endpoints Used:** + - `POST /v1/customers` - Create customer + - `GET /v1/customers/search` - Find by email + - `POST /v1/invoiceitems` - Create invoice item + - `POST /v1/invoices` - Create invoice + - `POST /v1/invoices/{id}/finalize` - Finalize invoice + - `POST /v1/invoices/{id}/send` - Send invoice + +### QuickBooks (Phase 1.5) + +- **Endpoints Used:** + - `POST /v3/company/{realmId}/customer` - Create customer + - `POST /v3/company/{realmId}/invoice` - Create invoice + - `POST /v3/company/{realmId}/payment` - Record payment + +### Supabase + +- **Webhooks:** POST to Make.com on table changes +- **Edge Functions:** For complex business logic +- **RPC:** For stored procedures + +--- + +## Error Handling + +### Retry Strategy + +1. **Network Errors:** Retry up to 4 times with exponential backoff (2s, 4s, 8s, 16s) +2. **Rate Limits:** Honor `Retry-After` header +3. **Validation Errors:** Log and alert team, do not retry + +### Monitoring + +- Log all automation runs to `audit_logs` table +- Slack alerts for: + - Failed automation runs + - Stripe webhook failures + - AR threshold breaches + +### Fallback Procedures + +1. **Invoice Generation Fails:** + - Alert team via Slack + - Manual generation available in UI + +2. **Payment Webhook Fails:** + - Invoice remains in "sent" status + - Manual "Mark Paid" available in UI + +3. **Email Delivery Fails:** + - Retry with backup email provider + - Log failure for manual follow-up + +--- + +## Configuration + +### Environment Variables + +``` +# Supabase +SUPABASE_URL=https://xxx.supabase.co +SUPABASE_SERVICE_KEY=eyJ... + +# Stripe +STRIPE_SECRET_KEY=sk_live_... +STRIPE_WEBHOOK_SECRET=whsec_... + +# Email (SendGrid or similar) +SENDGRID_API_KEY=SG.xxx +FROM_EMAIL=billing@bizdeedz.com + +# Slack (optional) +SLACK_WEBHOOK_URL=https://hooks.slack.com/... + +# QuickBooks (Phase 1.5) +QBO_CLIENT_ID=xxx +QBO_CLIENT_SECRET=xxx +QBO_REALM_ID=xxx +``` + +### Scheduling + +| Automation | Schedule | Timezone | +|------------|----------|----------| +| Friday Invoices | Friday 5:00 PM | America/Chicago | +| AR Aging Check | Daily 9:00 AM | America/Chicago | +| Retainer Reminders | 1st of month 9:00 AM | America/Chicago | + +--- + +## Testing + +### Test Scenarios + +1. **Matter Creation Flow** + - Create matter → verify inputs created → verify conflicts run + +2. **Invoice Flow** + - Create billables → run Friday invoices → verify Stripe invoice created + +3. **Payment Flow** + - Simulate Stripe webhook → verify invoice marked paid → verify firm unpause + +4. **AR Pause Flow** + - Create overdue invoice → run AR check → verify firm paused + +### Test Data + +Use the sample data in `001_schema.sql` for testing. Test firm IDs: +- Johnson & Associates: `11111111-1111-1111-1111-111111111111` +- Smith Legal Group: `22222222-2222-2222-2222-222222222222` +- Davis Bankruptcy Law: `33333333-3333-3333-3333-333333333333` diff --git a/bizdeedz-ops-platform/web/billing.html b/bizdeedz-ops-platform/web/billing.html new file mode 100644 index 0000000..605a5d0 --- /dev/null +++ b/bizdeedz-ops-platform/web/billing.html @@ -0,0 +1,726 @@ + + + + + + Billing - BizDeedz Ops Platform + + + + + +
+ + + + +
+ + +
+ +
+
+
AR Aging
+
Invoices
+
Retainers
+
Friday Invoice Run
+
+ + +
+ +
+
+
0-3 Days
+
$1,200.00
+
2 invoices
+
+
+
4-7 Days
+
$550.00
+
1 invoice
+
+
+
8+ Days (PAUSE)
+
$650.00
+
1 invoice
+
+
+ + +
+ + + + + +
+
Action Required: Pause Firm
+ Davis Bankruptcy Law has an invoice 8+ days overdue. Consider pausing their work. +
+
+ + +
+
+

Outstanding Invoices

+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FirmInvoice #AmountSentDays OutBucketActions
+ Davis Bankruptcy Law +
davis@dbllaw.com
+
INV-2026-001$650.00Jan 25, 2026108+ Days + +
+ Smith Legal Group +
accounts@smithlegal.com
+
INV-2026-003$550.00Jan 30, 202654-7 Days + +
+ Johnson & Associates +
billing@johnsonlaw.com
+
INV-2026-006$750.00Feb 2, 202620-3 Days + +
+ Smith Legal Group +
accounts@smithlegal.com
+
INV-2026-007$450.00Feb 3, 202610-3 Days + +
+
+
+ + +
+
+

All Invoices

+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Invoice #FirmDateAmountStatusPaid DateActions
INV-2026-007Smith Legal GroupFeb 3, 2026$450.00Sent- + +
INV-2026-006Johnson & AssociatesFeb 2, 2026$750.00Sent- + +
INV-2026-005Johnson & AssociatesJan 31, 2026$3,200.00Feb 1, 2026 + +
INV-2026-003Smith Legal GroupJan 30, 2026$550.004-7 Days- + +
INV-2026-002Johnson & AssociatesJan 24, 2026$1,850.00Jan 25, 2026 + +
INV-2026-001Davis Bankruptcy LawJan 25, 2026$650.00Overdue- + +
+
+
+ + +
+
+

Retainer Status

+ +
+ +
+
+

Johnson & Associates

+
$2,500.00/month • Due: 1st
+
+
+ + Paid Feb 1, 2026 +
+
+ +
+
+

Smith Legal Group

+
$1,500.00/month • Due: 1st
+
+
+ + Paid Feb 1, 2026 +
+
+ +
+
+

Davis Bankruptcy Law

+
$2,000.00/month • Due: 1st
+
+
+ Overdue + +
+
+ +
+ + + + + +
+ Retainers are due on the 1st of each month. Firms with unpaid retainers have their "Active Capacity" disabled until paid. +
+
+
+ + +
+
+

Friday Invoice Preview

+
Week of Jan 27 - Feb 2, 2026
+
+ +
+ + + + + +
+ Preview of invoices that will be generated. Review before clicking "Generate Invoices". +
+
+ + +
+
+
+
Johnson & Associates
+
billing@johnsonlaw.com
+
+
+
Invoice Total
+
$750.00
+
+
+
+
+ Ch 7 Petition Prep – [Matter: Adams, Michael] – Delivered 2026-02-01 + $200.00 +
+
+ Ch 13 Petition Prep – [Matter: Baker, Jennifer] – Delivered 2026-02-02 + $350.00 +
+
+ Rush Fee (24-48 hrs) – [Matter: Baker, Jennifer] – Authorized + $150.00 +
+
+ Total (3 line items) + $700.00 +
+
+
+ + +
+
+ + +
+
+
+
Smith Legal Group
+
accounts@smithlegal.com
+
+
+
Invoice Total
+
$450.00
+
+
+
+
+ Amendment/Modification – [Matter: Evans, Susan] – Delivered 2026-02-03 + $250.00 +
+
+ Ch 7 Petition Prep – [Matter: Thompson, Lisa] – Delivered 2026-02-03 + $200.00 +
+
+ Total (2 line items) + $450.00 +
+
+
+ + +
+
+ + +
+
+
+
+
Total to Invoice: $1,150.00
+
2 firms • 5 line items
+
+ +
+
+
+
+
+
+
+
+ + + + diff --git a/bizdeedz-ops-platform/web/dashboard.html b/bizdeedz-ops-platform/web/dashboard.html new file mode 100644 index 0000000..206cbc6 --- /dev/null +++ b/bizdeedz-ops-platform/web/dashboard.html @@ -0,0 +1,570 @@ + + + + + + Dashboard - BizDeedz Ops Platform + + + + +
+ + + + +
+ + +
+ +
+ +
+ + +
+
+
Delivered This Week
+
8
+
+ + + + +3 from last week +
+
+
+
SLA Hit Rate
+
92
+
Target: 85%
+
+
+
AR Outstanding
+
4,250
+
2 invoices overdue
+
+
+
Unbilled WIP
+
1,850
+
Friday invoice run
+
+
+
Active Rush
+
2
+
24-48hr deadline
+
+
+
Avg Revisions
+
1.2
+
Target: ≤1.5
+
+
+ + +

Work Queues

+
+ +
+
+
+ + Waiting Inputs +
+
3
+
+
+
+
+
Clark, David – Ch 7
+
Smith Legal • 5 days
+
+ 5 days +
+
+
+
Garcia, Maria – Ch 13
+
Johnson & Associates • 2 days
+
+
+
+
+
Lee, Robert – Ch 7
+
Davis Bankruptcy • 1 day
+
+ Rush +
+
+
+ + +
+
+
+ + Conflicts Review +
+
1
+
+
+
+
+
Wilson, James – Ch 7
+
Potential match found
+
+ Potential +
+
+
+ + +
+
+
+ + QC Ready +
+
2
+
+
+
+
+
Baker, Jennifer – Ch 13
+
Johnson & Associates
+
+ Rush +
+
+
+
Thompson, Lisa – Amendment
+
Smith Legal
+
+
+
+
+ + +
+
+
+ + Paused – AR +
+
1
+
+
+
+
+
Foster, William – Ch 7
+
Davis Bankruptcy • Invoice 8+ days
+
+ Paused +
+
+
+
+ + +
+
+

Recent Deliveries

+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MatterFirmTypeDeliveredStatusActions
Evans, Susan – AmendmentSmith Legal GroupAmendmentFeb 3, 2026Delivered + +
Martinez, Carlos – Ch 7Johnson & AssociatesCh 7Feb 2, 2026Complete + +
Brown, Angela – Ch 13Smith Legal GroupCh 13Feb 1, 2026Revision + +
+
+ + +
+
+

AR Aging

+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FirmInvoice #AmountDays OutstandingAging BucketActions
Davis Bankruptcy LawINV-2026-001$650.0088+ Days + +
Smith Legal GroupINV-2026-003$550.0054-7 Days + +
+
+
+
+
+ + + + + + + diff --git a/bizdeedz-ops-platform/web/firms.html b/bizdeedz-ops-platform/web/firms.html new file mode 100644 index 0000000..ff274fb --- /dev/null +++ b/bizdeedz-ops-platform/web/firms.html @@ -0,0 +1,771 @@ + + + + + + Firms - BizDeedz Ops Platform + + + + + +
+ + + + +
+ + +
+ +
+
+
+

All Firms

+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Firm NameStatusCapacityRetainerActive MattersAR OutstandingActions
+ Johnson & Associates +
billing@johnsonlaw.com
+
Active + + + Active + + $2,500.004$0.00 + +
+ Smith Legal Group +
accounts@smithlegal.com
+
Active + + + Active + + $1,500.003$550.00 + +
+ Davis Bankruptcy Law +
davis@dbllaw.com
+
On Hold + + + Inactive + + $2,000.001 (paused)$650.00 + +
+
+
+ + + +
+
+
+ + + + + + + diff --git a/bizdeedz-ops-platform/web/matter-detail.html b/bizdeedz-ops-platform/web/matter-detail.html new file mode 100644 index 0000000..8074994 --- /dev/null +++ b/bizdeedz-ops-platform/web/matter-detail.html @@ -0,0 +1,970 @@ + + + + + + Matter Detail - BizDeedz Ops Platform + + + + + +
+ + + + +
+ + +
+ +
+
+
+

Baker, Jennifer – Ch 13

+
+
+ + + + Johnson & Associates +
+
+ + + + + + N.D. Texas +
+
+ + + + + + + Due: Feb 20, 2026 +
+
+ + + + + Assigned: Turea +
+
+
+
+ Ch 13 + Rush +
+
+
+
+
Stage
+ QC +
+
+
Inputs
+ Complete +
+
+
Conflicts
+ Clear +
+
+
Revisions
+ 0 +
+
+
+ +
+ +
+
+
+
Inputs
+
Conflicts
+
Tasks/QC
+
Deliveries
+
Billables
+
Audit
+
+ + +
+
+ + +
+ +
+
+
+
+
15 of 20 items received
+
+ +

Identity

+
    +
  • + + Government ID (front/back) + Required +
  • +
  • + + Social Security card or proof of SSN + Required +
  • +
+ +

Income

+
    +
  • + + Paystubs - last 6 months + Required +
  • +
  • + + Proof of other income (SSI/SSDI, child support, unemployment, pension) +
  • +
+ +

Taxes

+
    +
  • + + Federal tax returns - last 2 years + Required +
  • +
+ +

Banking

+
    +
  • + + Bank statements - last 3 months (all accounts) + Required +
  • +
+ +

Plan Inputs (Ch 13)

+
    +
  • + + Mortgage arrearage breakdown + Required +
  • +
  • + + Vehicle payoff + interest rate info +
  • +
  • + + Monthly budget inputs + Required +
  • +
  • + + Attorney plan direction notes + Required +
  • +
+
+ + +
+
+ +
+ +
+ + + + +
+
Conflicts Status: Clear
+ No matches found against existing matters or restricted entities. +
+
+ +

Entities Checked

+ + + + + + + + + + + + + + + + + + + + +
NameRoleResult
Jennifer BakerDebtorClear
Thomas BakerSpouseClear
+ +

Check History

+
+
+
+
+ Conflicts Check Passed
+ Run by: System (auto) +
+
Jan 28, 2026 10:30 AM
+
+
+ + + +
+ + +
+
+ +
+ +

QC Checklist

+
    +
  • + + All schedules completed +
  • +
  • + + Means test calculations verified +
  • +
  • + + Creditor matrix complete +
  • +
  • + + Plan payment calculations verified +
  • +
  • + + Client name/SSN verified against source docs +
  • +
  • + + District-specific requirements checked +
  • +
+ +
+ + + + + +
+ Complete all QC items before marking as delivered. +
+
+
+ + +
+
+ + + + + +
+ No deliveries yet. Complete QC and mark as delivered to create a delivery record. +
+
+ +

Delivery History

+

Deliveries will appear here after the matter is marked as delivered.

+ + +
+ + +
+
+ +
+ +
+
+

Billable Items

+
+
+
+ Ch 13 Petition Prep +
Standard rate
+
+
$350.00
+
+
+
+ Rush Fee (24-48 hrs) +
Authorized
+
+
$150.00
+
+
+ Total (pending delivery) + $500.00 +
+
+ +

+ Billables are created automatically when the matter is delivered. + They will be included in the next Friday invoice run. +

+
+ + +
+
+ +
+ +

Activity Timeline

+
+
+
+
+ Stage Changed to QC
+ By: Turea +
+
Feb 3, 2026 4:15 PM
+
+
+
+
+ Inputs Marked Complete
+ All required documents received +
+
Feb 2, 2026 11:30 AM
+
+
+
+
+ Conflicts Check Passed
+ No matches found +
+
Jan 28, 2026 10:30 AM
+
+
+
+
+ Stage Changed to In Progress
+ By: Turea +
+
Jan 28, 2026 10:32 AM
+
+
+
+
+ Matter Created
+ Case Type: Ch 13 | Rush: Yes +
+
Jan 28, 2026 9:15 AM
+
+
+
+
+
+ + +
+ +
+
+

Quick Actions

+
+
+ + + + +
+
+ + +
+
+

Entities

+
+
+
+ Debtor + Jennifer Baker +
+
+ Spouse + Thomas Baker +
+ +
+
+ + +
+
+

Key Dates

+
+
+
+ Assigned + Jan 28, 2026 +
+
+ Inputs Complete + Feb 2, 2026 +
+
+ In Progress + Jan 28, 2026 +
+
+ Deadline + Feb 20, 2026 +
+
+
+ + +
+
+

Notes

+
+
+

Rush requested by client. Attorney confirmed plan direction.

+ +
+
+
+
+
+
+
+ + + + diff --git a/bizdeedz-ops-platform/web/matters.html b/bizdeedz-ops-platform/web/matters.html new file mode 100644 index 0000000..f79e571 --- /dev/null +++ b/bizdeedz-ops-platform/web/matters.html @@ -0,0 +1,771 @@ + + + + + + Matters - BizDeedz Ops Platform + + + + + +
+ + + + +
+ + +
+ +
+ + + + + +
+ + +
+
+ +
+
+ Assigned + 2 +
+
+
+
Harris, Thomas – Ch 7
+
+ Johnson & Associates + + N.D. Texas +
+
+ Ch 7 + Due Feb 25 +
+
+
+
White, Emily – Ch 13
+
+ Smith Legal + + S.D. Texas +
+
+ Ch 13 + Rush + Due Feb 8 +
+
+
+
+ + +
+
+ Waiting Inputs + 3 +
+
+
+
Clark, David – Ch 7
+
+ Smith Legal + + 5 days waiting +
+
+ Ch 7 + Due Feb 18 +
+
+
+
Garcia, Maria – Ch 13
+
+ Johnson & Associates + + 2 days waiting +
+
+ Ch 13 +
+
+
+
Lee, Robert – Ch 7
+
+ Davis Bankruptcy + + 1 day waiting +
+
+ Ch 7 + Rush +
+
+
+
+ + +
+
+ Conflicts Review + 1 +
+
+
+
Wilson, James – Ch 7
+
+ Johnson & Associates + + Potential Match +
+
+ Ch 7 + Potential +
+
+
+
+ + +
+
+ In Progress + 3 +
+
+
+
Adams, Michael – Ch 7
+
+ Johnson & Associates + + Assigned: Turea +
+
+ Ch 7 + Due Feb 15 +
+
+
+
Brown, Angela – Ch 13
+
+ Smith Legal + + Assigned: Staff +
+
+ Ch 13 +
+
+
+
Taylor, Susan – Amendment
+
+ Johnson & Associates + + Assigned: Turea +
+
+ Amendment +
+
+
+
+ + +
+
+ QC + 2 +
+
+
+
Baker, Jennifer – Ch 13
+
+ Johnson & Associates + + Ready for QC +
+
+ Ch 13 + Rush +
+
+
+
Thompson, Lisa – Amendment
+
+ Smith Legal + + Ready for QC +
+
+ Amendment +
+
+
+
+ + +
+
+ Revision + 1 +
+
+
+
Martinez, Carlos – Ch 7
+
+ Smith Legal + + Round 2 +
+
+ Ch 7 + Rev 2 +
+
+
+
+ + +
+
+ Paused – AR + 1 +
+
+
+
Foster, William – Ch 7
+
+ Davis Bankruptcy + + Invoice 8+ days +
+
+ Ch 7 + Paused +
+
+
+
+
+
+ + + +
+
+
+ + + + + + + diff --git a/bizdeedz-ops-platform/web/styles.css b/bizdeedz-ops-platform/web/styles.css new file mode 100644 index 0000000..eaffb9b --- /dev/null +++ b/bizdeedz-ops-platform/web/styles.css @@ -0,0 +1,1175 @@ +/* BizDeedz Ops Platform - Shared Styles + * Version: 1.0 MVP + * Design System: Professional legal ops with clear hierarchy + */ + +/* ============================================================================= + CSS VARIABLES / DESIGN TOKENS + ============================================================================= */ + +:root { + /* Brand Colors */ + --primary: #2563eb; + --primary-dark: #1d4ed8; + --primary-light: #3b82f6; + + /* Status Colors */ + --success: #059669; + --success-light: #d1fae5; + --warning: #d97706; + --warning-light: #fef3c7; + --danger: #dc2626; + --danger-light: #fee2e2; + --info: #0891b2; + --info-light: #cffafe; + + /* Neutral Colors */ + --gray-50: #f9fafb; + --gray-100: #f3f4f6; + --gray-200: #e5e7eb; + --gray-300: #d1d5db; + --gray-400: #9ca3af; + --gray-500: #6b7280; + --gray-600: #4b5563; + --gray-700: #374151; + --gray-800: #1f2937; + --gray-900: #111827; + + /* Stage Colors */ + --stage-assigned: #8b5cf6; + --stage-waiting-inputs: #f59e0b; + --stage-conflicts: #ef4444; + --stage-in-progress: #3b82f6; + --stage-qc: #06b6d4; + --stage-delivered: #10b981; + --stage-revision: #f97316; + --stage-complete: #22c55e; + --stage-paused: #ef4444; + + /* Typography */ + --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + --font-mono: 'JetBrains Mono', 'Fira Code', 'SF Mono', Monaco, monospace; + + /* Spacing */ + --space-1: 0.25rem; + --space-2: 0.5rem; + --space-3: 0.75rem; + --space-4: 1rem; + --space-5: 1.25rem; + --space-6: 1.5rem; + --space-8: 2rem; + --space-10: 2.5rem; + --space-12: 3rem; + + /* Border Radius */ + --radius-sm: 0.25rem; + --radius-md: 0.375rem; + --radius-lg: 0.5rem; + --radius-xl: 0.75rem; + + /* Shadows */ + --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + + /* Transitions */ + --transition-fast: 150ms ease; + --transition-normal: 200ms ease; +} + +/* ============================================================================= + BASE STYLES + ============================================================================= */ + +*, *::before, *::after { + box-sizing: border-box; +} + +html { + font-size: 16px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + margin: 0; + font-family: var(--font-sans); + font-size: 0.875rem; + line-height: 1.5; + color: var(--gray-800); + background-color: var(--gray-50); +} + +h1, h2, h3, h4, h5, h6 { + margin: 0; + font-weight: 600; + line-height: 1.25; + color: var(--gray-900); +} + +h1 { font-size: 1.875rem; } +h2 { font-size: 1.5rem; } +h3 { font-size: 1.25rem; } +h4 { font-size: 1.125rem; } + +p { margin: 0 0 1rem; } + +a { + color: var(--primary); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +/* ============================================================================= + LAYOUT + ============================================================================= */ + +.app-container { + display: flex; + min-height: 100vh; +} + +.sidebar { + width: 240px; + background: var(--gray-900); + color: white; + display: flex; + flex-direction: column; + position: fixed; + height: 100vh; + z-index: 100; +} + +.sidebar-header { + padding: var(--space-5); + border-bottom: 1px solid var(--gray-700); +} + +.sidebar-logo { + font-size: 1.25rem; + font-weight: 700; + color: white; + display: flex; + align-items: center; + gap: var(--space-2); +} + +.sidebar-nav { + flex: 1; + padding: var(--space-4); + overflow-y: auto; +} + +.nav-section { + margin-bottom: var(--space-6); +} + +.nav-section-title { + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--gray-400); + margin-bottom: var(--space-2); +} + +.nav-link { + display: flex; + align-items: center; + gap: var(--space-3); + padding: var(--space-2) var(--space-3); + border-radius: var(--radius-md); + color: var(--gray-300); + font-size: 0.875rem; + transition: var(--transition-fast); +} + +.nav-link:hover { + background: var(--gray-800); + color: white; + text-decoration: none; +} + +.nav-link.active { + background: var(--primary); + color: white; +} + +.nav-link-icon { + width: 18px; + height: 18px; + opacity: 0.7; +} + +.nav-link.active .nav-link-icon { + opacity: 1; +} + +.nav-badge { + margin-left: auto; + background: var(--danger); + color: white; + font-size: 0.75rem; + font-weight: 600; + padding: 0.125rem 0.5rem; + border-radius: 9999px; +} + +.main-content { + flex: 1; + margin-left: 240px; + display: flex; + flex-direction: column; +} + +.page-header { + background: white; + border-bottom: 1px solid var(--gray-200); + padding: var(--space-4) var(--space-6); + display: flex; + align-items: center; + justify-content: space-between; + position: sticky; + top: 0; + z-index: 50; +} + +.page-title { + font-size: 1.25rem; + font-weight: 600; +} + +.page-actions { + display: flex; + gap: var(--space-3); +} + +.page-body { + flex: 1; + padding: var(--space-6); +} + +/* ============================================================================= + KPI CARDS + ============================================================================= */ + +.kpi-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: var(--space-4); + margin-bottom: var(--space-6); +} + +.kpi-card { + background: white; + border-radius: var(--radius-lg); + padding: var(--space-5); + box-shadow: var(--shadow-sm); + border: 1px solid var(--gray-200); +} + +.kpi-card.warning { + border-left: 4px solid var(--warning); +} + +.kpi-card.danger { + border-left: 4px solid var(--danger); +} + +.kpi-card.success { + border-left: 4px solid var(--success); +} + +.kpi-label { + font-size: 0.75rem; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--gray-500); + margin-bottom: var(--space-1); +} + +.kpi-value { + font-size: 1.75rem; + font-weight: 700; + color: var(--gray-900); + line-height: 1; +} + +.kpi-value.money::before { + content: '$'; +} + +.kpi-value.percent::after { + content: '%'; +} + +.kpi-change { + font-size: 0.75rem; + margin-top: var(--space-2); + display: flex; + align-items: center; + gap: var(--space-1); +} + +.kpi-change.positive { color: var(--success); } +.kpi-change.negative { color: var(--danger); } + +/* ============================================================================= + QUEUE CARDS + ============================================================================= */ + +.queue-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: var(--space-4); + margin-bottom: var(--space-6); +} + +.queue-card { + background: white; + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); + border: 1px solid var(--gray-200); + overflow: hidden; +} + +.queue-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-4); + border-bottom: 1px solid var(--gray-100); +} + +.queue-title { + font-size: 0.875rem; + font-weight: 600; + display: flex; + align-items: center; + gap: var(--space-2); +} + +.queue-dot { + width: 8px; + height: 8px; + border-radius: 50%; +} + +.queue-dot.assigned { background: var(--stage-assigned); } +.queue-dot.waiting-inputs { background: var(--stage-waiting-inputs); } +.queue-dot.conflicts { background: var(--stage-conflicts); } +.queue-dot.in-progress { background: var(--stage-in-progress); } +.queue-dot.qc { background: var(--stage-qc); } +.queue-dot.revision { background: var(--stage-revision); } +.queue-dot.paused { background: var(--stage-paused); } + +.queue-count { + font-size: 1.25rem; + font-weight: 700; + color: var(--gray-900); +} + +.queue-list { + max-height: 300px; + overflow-y: auto; +} + +.queue-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-3) var(--space-4); + border-bottom: 1px solid var(--gray-100); + cursor: pointer; + transition: var(--transition-fast); +} + +.queue-item:last-child { + border-bottom: none; +} + +.queue-item:hover { + background: var(--gray-50); +} + +.queue-item-name { + font-size: 0.875rem; + font-weight: 500; + color: var(--gray-800); +} + +.queue-item-meta { + font-size: 0.75rem; + color: var(--gray-500); + margin-top: 2px; +} + +.queue-item-badge { + font-size: 0.75rem; + padding: 2px 8px; + border-radius: 9999px; + font-weight: 500; +} + +.queue-item-badge.rush { + background: var(--danger-light); + color: var(--danger); +} + +.queue-item-badge.overdue { + background: var(--warning-light); + color: var(--warning); +} + +/* ============================================================================= + DATA TABLES + ============================================================================= */ + +.data-table-container { + background: white; + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); + border: 1px solid var(--gray-200); + overflow: hidden; + margin-bottom: var(--space-6); +} + +.data-table-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-4); + border-bottom: 1px solid var(--gray-200); +} + +.data-table-title { + font-size: 1rem; + font-weight: 600; +} + +.data-table-actions { + display: flex; + gap: var(--space-2); +} + +.data-table { + width: 100%; + border-collapse: collapse; +} + +.data-table th, +.data-table td { + padding: var(--space-3) var(--space-4); + text-align: left; + border-bottom: 1px solid var(--gray-100); +} + +.data-table th { + background: var(--gray-50); + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--gray-600); +} + +.data-table tbody tr:hover { + background: var(--gray-50); +} + +.data-table tbody tr:last-child td { + border-bottom: none; +} + +/* ============================================================================= + STATUS BADGES + ============================================================================= */ + +.status-badge { + display: inline-flex; + align-items: center; + padding: 2px 10px; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 500; +} + +.status-badge.assigned { + background: #ede9fe; + color: var(--stage-assigned); +} + +.status-badge.waiting-inputs { + background: #fef3c7; + color: var(--stage-waiting-inputs); +} + +.status-badge.conflicts-review { + background: #fee2e2; + color: var(--stage-conflicts); +} + +.status-badge.in-progress { + background: #dbeafe; + color: var(--stage-in-progress); +} + +.status-badge.qc { + background: #cffafe; + color: var(--stage-qc); +} + +.status-badge.delivered { + background: var(--success-light); + color: var(--success); +} + +.status-badge.revision { + background: #ffedd5; + color: var(--stage-revision); +} + +.status-badge.complete { + background: #dcfce7; + color: var(--stage-complete); +} + +.status-badge.paused-ar { + background: var(--danger-light); + color: var(--danger); +} + +/* Invoice status */ +.status-badge.draft { background: var(--gray-100); color: var(--gray-600); } +.status-badge.sent { background: var(--info-light); color: var(--info); } +.status-badge.paid { background: var(--success-light); color: var(--success); } +.status-badge.overdue { background: var(--danger-light); color: var(--danger); } +.status-badge.partial { background: var(--warning-light); color: var(--warning); } + +/* Conflicts status */ +.status-badge.clear { background: var(--success-light); color: var(--success); } +.status-badge.potential { background: var(--warning-light); color: var(--warning); } +.status-badge.conflict { background: var(--danger-light); color: var(--danger); } +.status-badge.not-run { background: var(--gray-100); color: var(--gray-500); } + +/* ============================================================================= + BUTTONS + ============================================================================= */ + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-2); + padding: var(--space-2) var(--space-4); + border-radius: var(--radius-md); + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: var(--transition-fast); + border: none; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-primary { + background: var(--primary); + color: white; +} + +.btn-primary:hover:not(:disabled) { + background: var(--primary-dark); +} + +.btn-secondary { + background: var(--gray-100); + color: var(--gray-700); + border: 1px solid var(--gray-300); +} + +.btn-secondary:hover:not(:disabled) { + background: var(--gray-200); +} + +.btn-success { + background: var(--success); + color: white; +} + +.btn-success:hover:not(:disabled) { + background: #047857; +} + +.btn-danger { + background: var(--danger); + color: white; +} + +.btn-danger:hover:not(:disabled) { + background: #b91c1c; +} + +.btn-icon { + padding: var(--space-2); +} + +.btn-sm { + padding: var(--space-1) var(--space-2); + font-size: 0.75rem; +} + +.btn-lg { + padding: var(--space-3) var(--space-6); + font-size: 1rem; +} + +/* ============================================================================= + FORMS + ============================================================================= */ + +.form-group { + margin-bottom: var(--space-4); +} + +.form-label { + display: block; + font-size: 0.875rem; + font-weight: 500; + color: var(--gray-700); + margin-bottom: var(--space-1); +} + +.form-input, +.form-select, +.form-textarea { + width: 100%; + padding: var(--space-2) var(--space-3); + border: 1px solid var(--gray-300); + border-radius: var(--radius-md); + font-size: 0.875rem; + transition: var(--transition-fast); +} + +.form-input:focus, +.form-select:focus, +.form-textarea:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); +} + +.form-input.error, +.form-select.error { + border-color: var(--danger); +} + +.form-help { + font-size: 0.75rem; + color: var(--gray-500); + margin-top: var(--space-1); +} + +.form-error { + font-size: 0.75rem; + color: var(--danger); + margin-top: var(--space-1); +} + +.form-checkbox { + display: flex; + align-items: center; + gap: var(--space-2); +} + +.form-checkbox input[type="checkbox"] { + width: 16px; + height: 16px; + accent-color: var(--primary); +} + +/* ============================================================================= + CARDS & PANELS + ============================================================================= */ + +.card { + background: white; + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); + border: 1px solid var(--gray-200); +} + +.card-header { + padding: var(--space-4); + border-bottom: 1px solid var(--gray-200); + display: flex; + align-items: center; + justify-content: space-between; +} + +.card-title { + font-size: 1rem; + font-weight: 600; +} + +.card-body { + padding: var(--space-4); +} + +.card-footer { + padding: var(--space-4); + border-top: 1px solid var(--gray-200); + background: var(--gray-50); +} + +/* ============================================================================= + TABS + ============================================================================= */ + +.tabs { + border-bottom: 1px solid var(--gray-200); + display: flex; + gap: var(--space-1); +} + +.tab { + padding: var(--space-3) var(--space-4); + font-size: 0.875rem; + font-weight: 500; + color: var(--gray-500); + border-bottom: 2px solid transparent; + cursor: pointer; + transition: var(--transition-fast); +} + +.tab:hover { + color: var(--gray-700); +} + +.tab.active { + color: var(--primary); + border-bottom-color: var(--primary); +} + +.tab-content { + display: none; + padding: var(--space-4); +} + +.tab-content.active { + display: block; +} + +/* ============================================================================= + MODALS + ============================================================================= */ + +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + display: none; + align-items: center; + justify-content: center; + z-index: 200; +} + +.modal-overlay.active { + display: flex; +} + +.modal { + background: white; + border-radius: var(--radius-xl); + box-shadow: var(--shadow-lg); + max-width: 500px; + width: 90%; + max-height: 90vh; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.modal-header { + padding: var(--space-4) var(--space-5); + border-bottom: 1px solid var(--gray-200); + display: flex; + align-items: center; + justify-content: space-between; +} + +.modal-title { + font-size: 1.125rem; + font-weight: 600; +} + +.modal-close { + background: none; + border: none; + padding: var(--space-1); + cursor: pointer; + color: var(--gray-400); + font-size: 1.5rem; + line-height: 1; +} + +.modal-close:hover { + color: var(--gray-600); +} + +.modal-body { + padding: var(--space-5); + overflow-y: auto; +} + +.modal-footer { + padding: var(--space-4) var(--space-5); + border-top: 1px solid var(--gray-200); + display: flex; + justify-content: flex-end; + gap: var(--space-3); +} + +/* ============================================================================= + ALERTS & NOTIFICATIONS + ============================================================================= */ + +.alert { + padding: var(--space-3) var(--space-4); + border-radius: var(--radius-md); + display: flex; + align-items: flex-start; + gap: var(--space-3); + margin-bottom: var(--space-4); +} + +.alert-icon { + flex-shrink: 0; + width: 20px; + height: 20px; +} + +.alert-content { + flex: 1; +} + +.alert-title { + font-weight: 600; + margin-bottom: var(--space-1); +} + +.alert-info { + background: var(--info-light); + color: var(--info); + border: 1px solid #a5f3fc; +} + +.alert-success { + background: var(--success-light); + color: var(--success); + border: 1px solid #a7f3d0; +} + +.alert-warning { + background: var(--warning-light); + color: var(--warning); + border: 1px solid #fde68a; +} + +.alert-danger { + background: var(--danger-light); + color: var(--danger); + border: 1px solid #fecaca; +} + +/* ============================================================================= + KANBAN BOARD + ============================================================================= */ + +.kanban-board { + display: flex; + gap: var(--space-4); + overflow-x: auto; + padding-bottom: var(--space-4); +} + +.kanban-column { + min-width: 280px; + max-width: 320px; + flex-shrink: 0; + background: var(--gray-100); + border-radius: var(--radius-lg); + display: flex; + flex-direction: column; + max-height: calc(100vh - 200px); +} + +.kanban-column-header { + padding: var(--space-3) var(--space-4); + font-weight: 600; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 2px solid transparent; +} + +.kanban-column.assigned .kanban-column-header { border-color: var(--stage-assigned); } +.kanban-column.waiting-inputs .kanban-column-header { border-color: var(--stage-waiting-inputs); } +.kanban-column.conflicts-review .kanban-column-header { border-color: var(--stage-conflicts); } +.kanban-column.in-progress .kanban-column-header { border-color: var(--stage-in-progress); } +.kanban-column.qc .kanban-column-header { border-color: var(--stage-qc); } +.kanban-column.delivered .kanban-column-header { border-color: var(--stage-delivered); } +.kanban-column.revision .kanban-column-header { border-color: var(--stage-revision); } +.kanban-column.paused-ar .kanban-column-header { border-color: var(--stage-paused); } + +.kanban-column-count { + background: var(--gray-200); + padding: 2px 8px; + border-radius: 9999px; + font-size: 0.75rem; +} + +.kanban-cards { + flex: 1; + overflow-y: auto; + padding: var(--space-3); +} + +.kanban-card { + background: white; + border-radius: var(--radius-md); + padding: var(--space-3); + margin-bottom: var(--space-2); + box-shadow: var(--shadow-sm); + border: 1px solid var(--gray-200); + cursor: pointer; + transition: var(--transition-fast); +} + +.kanban-card:hover { + box-shadow: var(--shadow-md); + border-color: var(--gray-300); +} + +.kanban-card-title { + font-weight: 500; + margin-bottom: var(--space-2); +} + +.kanban-card-meta { + font-size: 0.75rem; + color: var(--gray-500); + display: flex; + flex-wrap: wrap; + gap: var(--space-2); +} + +.kanban-card-tags { + display: flex; + gap: var(--space-1); + margin-top: var(--space-2); +} + +.kanban-tag { + font-size: 0.625rem; + padding: 2px 6px; + border-radius: var(--radius-sm); + font-weight: 500; +} + +.kanban-tag.rush { + background: var(--danger-light); + color: var(--danger); +} + +.kanban-tag.ch7 { + background: #dbeafe; + color: #1d4ed8; +} + +.kanban-tag.ch13 { + background: #fae8ff; + color: #a21caf; +} + +.kanban-tag.amendment { + background: #dcfce7; + color: #15803d; +} + +/* ============================================================================= + CHECKLIST + ============================================================================= */ + +.checklist { + list-style: none; + padding: 0; + margin: 0; +} + +.checklist-item { + display: flex; + align-items: center; + gap: var(--space-3); + padding: var(--space-2) 0; + border-bottom: 1px solid var(--gray-100); +} + +.checklist-item:last-child { + border-bottom: none; +} + +.checklist-checkbox { + width: 18px; + height: 18px; + accent-color: var(--success); +} + +.checklist-label { + flex: 1; +} + +.checklist-label.completed { + text-decoration: line-through; + color: var(--gray-400); +} + +.checklist-required { + color: var(--danger); + font-size: 0.75rem; +} + +/* ============================================================================= + TIMELINE / AUDIT LOG + ============================================================================= */ + +.timeline { + position: relative; + padding-left: var(--space-6); +} + +.timeline::before { + content: ''; + position: absolute; + left: 7px; + top: 4px; + bottom: 4px; + width: 2px; + background: var(--gray-200); +} + +.timeline-item { + position: relative; + padding-bottom: var(--space-4); +} + +.timeline-item:last-child { + padding-bottom: 0; +} + +.timeline-dot { + position: absolute; + left: calc(-1 * var(--space-6) + 3px); + top: 4px; + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--gray-300); + border: 2px solid white; +} + +.timeline-item.success .timeline-dot { background: var(--success); } +.timeline-item.warning .timeline-dot { background: var(--warning); } +.timeline-item.danger .timeline-dot { background: var(--danger); } +.timeline-item.info .timeline-dot { background: var(--info); } + +.timeline-content { + font-size: 0.875rem; +} + +.timeline-time { + font-size: 0.75rem; + color: var(--gray-500); + margin-top: var(--space-1); +} + +/* ============================================================================= + UTILITIES + ============================================================================= */ + +.text-muted { color: var(--gray-500); } +.text-success { color: var(--success); } +.text-warning { color: var(--warning); } +.text-danger { color: var(--danger); } + +.font-mono { font-family: var(--font-mono); } +.font-bold { font-weight: 700; } +.font-medium { font-weight: 500; } + +.text-sm { font-size: 0.75rem; } +.text-lg { font-size: 1.125rem; } +.text-xl { font-size: 1.25rem; } + +.text-right { text-align: right; } +.text-center { text-align: center; } + +.mb-0 { margin-bottom: 0; } +.mb-2 { margin-bottom: var(--space-2); } +.mb-4 { margin-bottom: var(--space-4); } +.mb-6 { margin-bottom: var(--space-6); } + +.mt-2 { margin-top: var(--space-2); } +.mt-4 { margin-top: var(--space-4); } + +.flex { display: flex; } +.flex-col { flex-direction: column; } +.items-center { align-items: center; } +.justify-between { justify-content: space-between; } +.gap-2 { gap: var(--space-2); } +.gap-4 { gap: var(--space-4); } + +.w-full { width: 100%; } +.hidden { display: none; } + +/* ============================================================================= + RESPONSIVE + ============================================================================= */ + +@media (max-width: 768px) { + .sidebar { + transform: translateX(-100%); + transition: var(--transition-normal); + } + + .sidebar.open { + transform: translateX(0); + } + + .main-content { + margin-left: 0; + } + + .kpi-grid { + grid-template-columns: repeat(2, 1fr); + } + + .queue-grid { + grid-template-columns: 1fr; + } + + .kanban-board { + flex-direction: column; + } + + .kanban-column { + min-width: 100%; + max-width: 100%; + max-height: none; + } +}