Kanban board, Project Management, Proposals/Quotes, Time Tracking, Custom Fields
Build & Auto-Repair / build (push) Has been cancelled
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:
@@ -0,0 +1,104 @@
|
||||
-- ============================================================================
|
||||
-- Project Management — Post-sale project tracking with milestones
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS project_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,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
customer_id UUID NOT NULL REFERENCES customers(id),
|
||||
opportunity_id UUID REFERENCES opportunities(id),
|
||||
owner_id UUID NOT NULL REFERENCES users(id),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
status_id UUID NOT NULL REFERENCES project_statuses(id),
|
||||
start_date DATE,
|
||||
target_end_date DATE,
|
||||
actual_end_date DATE,
|
||||
budget DECIMAL(15,2),
|
||||
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 INDEX idx_projects_customer ON projects(customer_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_projects_owner ON projects(owner_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_projects_status ON projects(status_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_projects_created ON projects(created_at) WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS project_milestones (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
start_date DATE,
|
||||
due_date DATE,
|
||||
completed_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT chk_milestone_status CHECK (status IN ('pending','in_progress','completed','cancelled'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_milestones_project ON project_milestones(project_id);
|
||||
CREATE INDEX idx_milestones_status ON project_milestones(status);
|
||||
CREATE INDEX idx_milestones_due ON project_milestones(due_date);
|
||||
|
||||
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS project_id UUID REFERENCES projects(id) ON DELETE CASCADE;
|
||||
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS milestone_id UUID REFERENCES project_milestones(id) ON DELETE SET NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id) WHERE project_id IS NOT NULL AND deleted_at IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_milestone ON tasks(milestone_id) WHERE milestone_id IS NOT NULL AND deleted_at IS NULL;
|
||||
|
||||
-- Seed project statuses
|
||||
INSERT INTO project_statuses (name, color, sort_order) VALUES
|
||||
('Planning', '#6366f1', 1),
|
||||
('In Progress', '#22c55e', 2),
|
||||
('Review', '#eab308', 3),
|
||||
('Completed', '#22d3ee', 4),
|
||||
('On Hold', '#f97316', 5),
|
||||
('Cancelled', '#ef4444', 6)
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
-- Seed a sample project from the existing TechCorp opportunity
|
||||
DO $$
|
||||
DECLARE
|
||||
v_customer_id UUID;
|
||||
v_opportunity_id UUID;
|
||||
v_owner_id UUID;
|
||||
v_planning_id UUID;
|
||||
v_progress_id UUID;
|
||||
v_project_id UUID;
|
||||
BEGIN
|
||||
SELECT id INTO v_customer_id FROM customers LIMIT 1;
|
||||
SELECT id INTO v_opportunity_id FROM opportunities LIMIT 1;
|
||||
SELECT u.id INTO v_owner_id FROM users u JOIN user_roles ur ON ur.user_id = u.id JOIN roles r ON r.id = ur.role_id WHERE r.name = 'SALES_USER' LIMIT 1;
|
||||
IF v_owner_id IS NULL THEN SELECT id INTO v_owner_id FROM users LIMIT 1; END IF;
|
||||
SELECT id INTO v_planning_id FROM project_statuses WHERE name = 'Planning';
|
||||
SELECT id INTO v_progress_id FROM project_statuses WHERE name = 'In Progress';
|
||||
|
||||
IF v_customer_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM projects WHERE name = 'TechCorp Website Redesign') THEN
|
||||
INSERT INTO projects (customer_id, opportunity_id, owner_id, name, description, status_id, start_date, target_end_date, budget, created_by)
|
||||
VALUES (v_customer_id, v_opportunity_id, v_owner_id, 'TechCorp Website Redesign',
|
||||
'Full website redesign including homepage, services pages, and contact forms',
|
||||
v_planning_id, CURRENT_DATE, CURRENT_DATE + INTERVAL '60 days', 25000.00, v_owner_id)
|
||||
RETURNING id INTO v_project_id;
|
||||
|
||||
INSERT INTO project_milestones (project_id, name, description, sort_order, status, start_date, due_date) VALUES
|
||||
(v_project_id, 'Requirements & Wireframes', 'Gather requirements, create wireframes for approval', 1, 'in_progress', CURRENT_DATE, CURRENT_DATE + INTERVAL '10 days'),
|
||||
(v_project_id, 'Design Mockups', 'Create visual design mockups for all pages', 2, 'pending', CURRENT_DATE + INTERVAL '10 days', CURRENT_DATE + INTERVAL '20 days'),
|
||||
(v_project_id, 'Frontend Development', 'Build responsive frontend components', 3, 'pending', CURRENT_DATE + INTERVAL '20 days', CURRENT_DATE + INTERVAL '35 days'),
|
||||
(v_project_id, 'Backend Integration', 'Integrate CMS, forms, and email functionality', 4, 'pending', CURRENT_DATE + INTERVAL '35 days', CURRENT_DATE + INTERVAL '45 days'),
|
||||
(v_project_id, 'Testing & QA', 'Cross-browser testing, performance optimization', 5, 'pending', CURRENT_DATE + INTERVAL '45 days', CURRENT_DATE + INTERVAL '55 days'),
|
||||
(v_project_id, 'Launch', 'Deploy to production, final sign-off', 6, 'pending', CURRENT_DATE + INTERVAL '55 days', CURRENT_DATE + INTERVAL '60 days');
|
||||
END IF;
|
||||
END $$;
|
||||
@@ -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 $$;
|
||||
@@ -0,0 +1,43 @@
|
||||
-- ============================================================================
|
||||
-- Custom Fields — Adaptable fields per entity type (leads, customers, etc.)
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS custom_field_definitions (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
entity_type VARCHAR(50) NOT NULL,
|
||||
field_key VARCHAR(100) NOT NULL,
|
||||
field_label VARCHAR(255) NOT NULL,
|
||||
field_type VARCHAR(50) NOT NULL DEFAULT 'text',
|
||||
options JSONB,
|
||||
placeholder TEXT,
|
||||
default_value TEXT,
|
||||
section VARCHAR(100),
|
||||
is_required BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT uq_custom_field_entity_key UNIQUE (entity_type, field_key),
|
||||
CONSTRAINT chk_custom_field_type CHECK (field_type IN ('text','number','date','select','multi_select','boolean','url','email','phone'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_custom_fields_entity ON custom_field_definitions(entity_type, is_active, sort_order);
|
||||
|
||||
ALTER TABLE leads ADD COLUMN IF NOT EXISTS custom_fields JSONB DEFAULT '{}';
|
||||
ALTER TABLE customers ADD COLUMN IF NOT EXISTS custom_fields JSONB DEFAULT '{}';
|
||||
ALTER TABLE opportunities ADD COLUMN IF NOT EXISTS custom_fields JSONB DEFAULT '{}';
|
||||
ALTER TABLE projects ADD COLUMN IF NOT EXISTS custom_fields JSONB DEFAULT '{}';
|
||||
|
||||
-- Seed sample fields
|
||||
INSERT INTO custom_field_definitions (entity_type, field_key, field_label, field_type, options, section, sort_order) VALUES
|
||||
('lead', 'service_type', 'Service Type', 'select', '["Web Development","Mobile App","SEO","Consulting","Retainer","Maintenance"]', 'Details', 1),
|
||||
('lead', 'budget_confirmed', 'Budget Confirmed', 'boolean', NULL, 'Details', 2),
|
||||
('lead', 'referral_source', 'Referral Source', 'text', NULL, 'Details', 3),
|
||||
('customer', 'industry_vertical', 'Industry Vertical', 'select', '["Fintech","Healthcare","E-commerce","Education","Real Estate","Hospitality","Other"]', 'Business', 1),
|
||||
('customer', 'vat_number', 'VAT Number', 'text', NULL, 'Business', 2),
|
||||
('project', 'project_type', 'Project Type', 'select', '["New Build","Redesign","Maintenance","Consulting","Retainer"]', 'Details', 1),
|
||||
('project', 'hosting_provider', 'Hosting Provider', 'text', NULL, 'Technical', 2),
|
||||
('project', 'tech_stack', 'Tech Stack', 'multi_select', '["React","Next.js","Node.js","Python","PHP","WordPress","Shopify","Laravel","Vue","Angular"]', 'Technical', 3),
|
||||
('opportunity', 'competitor', 'Competitor', 'text', NULL, 'Details', 1),
|
||||
('opportunity', 'decision_timeline', 'Decision Timeline', 'select', '["This Week","This Month","This Quarter","Next Quarter","Not Sure"]', 'Details', 2)
|
||||
ON CONFLICT (entity_type, field_key) DO NOTHING;
|
||||
@@ -75,5 +75,15 @@ BEGIN;
|
||||
|
||||
\echo '=== Running 019_allow_null_start_time.sql (Allow Null Start Time) ==='
|
||||
\i 019_allow_null_start_time.sql
|
||||
|
||||
\echo '=== Running 020_projects.sql (Project Management) ==='
|
||||
\i 020_projects.sql
|
||||
|
||||
\echo '=== Running 021_proposals_time.sql (Proposals + Time Tracking) ==='
|
||||
\i 021_proposals_time.sql
|
||||
|
||||
\echo '=== Running 022_custom_fields.sql (Custom Fields + Workflows) ==='
|
||||
\i 022_custom_fields.sql
|
||||
|
||||
\echo '=== Migration Complete ==='
|
||||
COMMIT;
|
||||
|
||||
Reference in New Issue
Block a user