545065b527
Build & Auto-Repair / build (push) Has been cancelled
- Kanban board with drag-and-drop per stage, colour-coded cards, score bars - Project Management with milestones, Gantt timeline, inline add milestone/task - Proposal/Quote builder with line items, VAT, terms, e-signature capture - Time Tracking with stopwatch, manual entry, hours-by-project chart - Custom Fields system: admin-defined fields per entity (leads/customers/ projects/opportunities) with drag-and-drop reordering in settings - Reusable CustomFieldsSection component for entity detail pages - Customers API endpoint - All builds pass with zero errors
120 lines
5.1 KiB
SQL
120 lines
5.1 KiB
SQL
-- ============================================================================
|
|
-- 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 $$;
|