Kanban board, Project Management, Proposals/Quotes, Time Tracking, Custom Fields
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
This commit is contained in:
2026-07-01 11:18:59 +02:00
parent 5d971dc749
commit 545065b527
38 changed files with 3770 additions and 4 deletions
+119
View File
@@ -0,0 +1,119 @@
-- ============================================================================
-- 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 $$;