-- ============================================================================ -- Proposal/Quote Builder + Time Tracking -- ============================================================================ -- ── Proposals / Quotes ────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS proposal_statuses ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), name VARCHAR(50) NOT NULL UNIQUE, color VARCHAR(7), sort_order INT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); INSERT INTO proposal_statuses (name, color, sort_order) VALUES ('Draft', '#6366f1', 1), ('Sent', '#eab308', 2), ('Viewed', '#22d3ee', 3), ('Accepted', '#22c55e', 4), ('Declined', '#ef4444', 5), ('Expired', '#a1a1aa', 6) ON CONFLICT (name) DO NOTHING; CREATE TABLE IF NOT EXISTS proposals ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), lead_id UUID REFERENCES leads(id), customer_id UUID REFERENCES customers(id), opportunity_id UUID REFERENCES opportunities(id), title VARCHAR(255) NOT NULL, proposal_number VARCHAR(50) NOT NULL, status_id UUID NOT NULL REFERENCES proposal_statuses(id), content JSONB, subtotal DECIMAL(15,2) NOT NULL DEFAULT 0, tax_percent DECIMAL(5,2) DEFAULT 15, tax_amount DECIMAL(15,2) DEFAULT 0, total_amount DECIMAL(15,2) NOT NULL DEFAULT 0, valid_until DATE, notes TEXT, terms TEXT DEFAULT 'Payment due within 30 days. 50% deposit required to commence work.', created_by UUID NOT NULL REFERENCES users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), deleted_at TIMESTAMPTZ ); CREATE UNIQUE INDEX IF NOT EXISTS uq_proposal_number ON proposals(proposal_number) WHERE deleted_at IS NULL; CREATE INDEX idx_proposals_customer ON proposals(customer_id) WHERE deleted_at IS NULL; CREATE INDEX idx_proposals_lead ON proposals(lead_id) WHERE deleted_at IS NULL; CREATE INDEX idx_proposals_status ON proposals(status_id) WHERE deleted_at IS NULL; CREATE INDEX idx_proposals_created ON proposals(created_at) WHERE deleted_at IS NULL; CREATE TABLE IF NOT EXISTS proposal_items ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), proposal_id UUID NOT NULL REFERENCES proposals(id) ON DELETE CASCADE, product_id UUID REFERENCES products(id), description TEXT NOT NULL, quantity INT NOT NULL DEFAULT 1 CHECK (quantity > 0), unit_price DECIMAL(15,2) NOT NULL CHECK (unit_price >= 0), discount_percent DECIMAL(5,2) NOT NULL DEFAULT 0 CHECK (discount_percent >= 0 AND discount_percent <= 100), total_price DECIMAL(15,2) NOT NULL CHECK (total_price >= 0), sort_order INT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_proposal_items_proposal ON proposal_items(proposal_id); CREATE TABLE IF NOT EXISTS proposal_signatures ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), proposal_id UUID NOT NULL REFERENCES proposals(id) ON DELETE CASCADE, signatory_name VARCHAR(255) NOT NULL, signatory_email VARCHAR(255) NOT NULL, signature_data TEXT, ip_address INET, signed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), is_valid BOOLEAN NOT NULL DEFAULT TRUE ); CREATE INDEX idx_proposal_signatures_proposal ON proposal_signatures(proposal_id); -- ── Time Tracking ────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS time_entries ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES users(id), project_id UUID REFERENCES projects(id), milestone_id UUID REFERENCES project_milestones(id), task_id UUID REFERENCES tasks(id), description TEXT, date DATE NOT NULL DEFAULT CURRENT_DATE, start_time TIMESTAMPTZ, end_time TIMESTAMPTZ, duration_minutes INT NOT NULL CHECK (duration_minutes > 0), billable BOOLEAN NOT NULL DEFAULT TRUE, hourly_rate DECIMAL(15,2), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_time_entries_user ON time_entries(user_id); CREATE INDEX idx_time_entries_project ON time_entries(project_id); CREATE INDEX idx_time_entries_date ON time_entries(date); CREATE INDEX idx_time_entries_billable ON time_entries(billable) WHERE billable = TRUE; -- Seed sample time entry DO $$ DECLARE v_user_id UUID; v_project_id UUID; BEGIN SELECT id INTO v_user_id FROM users LIMIT 1; SELECT id INTO v_project_id FROM projects LIMIT 1; IF v_user_id IS NOT NULL AND v_project_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM time_entries) THEN INSERT INTO time_entries (user_id, project_id, description, date, duration_minutes, billable, hourly_rate) VALUES (v_user_id, v_project_id, 'Initial planning and requirements gathering', CURRENT_DATE - 2, 240, TRUE, 1500), (v_user_id, v_project_id, 'Wireframe design', CURRENT_DATE - 1, 180, TRUE, 1500), (v_user_id, v_project_id, 'Team standup and sprint planning', CURRENT_DATE, 60, FALSE, NULL); END IF; END $$;