- Create dispatch schema with 7 tables - dispatch_boards, unit_statuses, technician_skills, technician_shifts - dispatch_rules, escalation_rules, dispatch_logs - 5 enums for status, shift types, skill levels, channels, actions - 4 views for available units, shifts, expiring skills, activity - RLS policies for multi-tenant isolation - Triggers for updated_at and status change logging Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
504 lines
16 KiB
PL/PgSQL
504 lines
16 KiB
PL/PgSQL
-- ============================================================
|
|
-- 16-dispatch-schema.sql
|
|
-- Mecanicas Diesel - ERP Suite
|
|
--
|
|
-- Dispatch Center Schema (MMD-011)
|
|
-- Centro de despacho para asignacion de incidentes
|
|
-- ============================================================
|
|
|
|
-- Create dispatch schema
|
|
CREATE SCHEMA IF NOT EXISTS dispatch;
|
|
|
|
-- ============================================================
|
|
-- ENUMS
|
|
-- ============================================================
|
|
|
|
-- Unit operational status
|
|
CREATE TYPE dispatch.unit_status_enum AS ENUM (
|
|
'available',
|
|
'assigned',
|
|
'en_route',
|
|
'on_site',
|
|
'returning',
|
|
'offline',
|
|
'maintenance'
|
|
);
|
|
|
|
-- Technician shift types
|
|
CREATE TYPE dispatch.shift_type AS ENUM (
|
|
'morning',
|
|
'afternoon',
|
|
'night',
|
|
'full_day',
|
|
'on_call'
|
|
);
|
|
|
|
-- Skill levels
|
|
CREATE TYPE dispatch.skill_level AS ENUM (
|
|
'basic',
|
|
'intermediate',
|
|
'advanced',
|
|
'expert'
|
|
);
|
|
|
|
-- Escalation notification channels
|
|
CREATE TYPE dispatch.notification_channel AS ENUM (
|
|
'email',
|
|
'sms',
|
|
'whatsapp',
|
|
'push',
|
|
'call'
|
|
);
|
|
|
|
-- Dispatch log actions
|
|
CREATE TYPE dispatch.dispatch_action AS ENUM (
|
|
'created',
|
|
'assigned',
|
|
'reassigned',
|
|
'rejected',
|
|
'escalated',
|
|
'cancelled',
|
|
'acknowledged',
|
|
'completed'
|
|
);
|
|
|
|
-- ============================================================
|
|
-- TABLES
|
|
-- ============================================================
|
|
|
|
-- Dispatch Board Configuration
|
|
CREATE TABLE dispatch.dispatch_boards (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL,
|
|
name VARCHAR(100) NOT NULL,
|
|
description TEXT,
|
|
|
|
-- Map defaults
|
|
default_zoom INTEGER DEFAULT 12,
|
|
center_lat DECIMAL(10, 7) DEFAULT 19.4326,
|
|
center_lng DECIMAL(10, 7) DEFAULT -99.1332,
|
|
|
|
-- Behavior
|
|
refresh_interval_seconds INTEGER DEFAULT 30,
|
|
show_offline_units BOOLEAN DEFAULT true,
|
|
auto_assign_enabled BOOLEAN DEFAULT false,
|
|
max_suggestions INTEGER DEFAULT 5,
|
|
|
|
-- Filters
|
|
default_filters JSONB DEFAULT '{}',
|
|
|
|
is_active BOOLEAN DEFAULT true,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
created_by UUID
|
|
);
|
|
|
|
CREATE INDEX idx_dispatch_boards_tenant ON dispatch.dispatch_boards(tenant_id);
|
|
|
|
-- Unit Status (real-time unit state)
|
|
CREATE TABLE dispatch.unit_statuses (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL,
|
|
|
|
-- Unit reference
|
|
unit_id UUID NOT NULL,
|
|
unit_code VARCHAR(50),
|
|
unit_name VARCHAR(100),
|
|
|
|
-- Status
|
|
status dispatch.unit_status_enum DEFAULT 'offline',
|
|
|
|
-- Current assignment
|
|
current_incident_id UUID,
|
|
current_technician_ids UUID[] DEFAULT '{}',
|
|
|
|
-- Location (cached from GPS)
|
|
last_position_id UUID,
|
|
last_known_lat DECIMAL(10, 7),
|
|
last_known_lng DECIMAL(10, 7),
|
|
last_location_update TIMESTAMPTZ,
|
|
|
|
-- Timing
|
|
last_status_change TIMESTAMPTZ DEFAULT NOW(),
|
|
estimated_available_at TIMESTAMPTZ,
|
|
|
|
-- Capacity and capabilities
|
|
unit_capacity VARCHAR(20) DEFAULT 'light', -- light, medium, heavy
|
|
can_tow BOOLEAN DEFAULT false,
|
|
max_tow_weight_kg INTEGER,
|
|
|
|
notes TEXT,
|
|
metadata JSONB DEFAULT '{}',
|
|
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
CREATE UNIQUE INDEX idx_unit_statuses_tenant_unit ON dispatch.unit_statuses(tenant_id, unit_id);
|
|
CREATE INDEX idx_unit_statuses_status ON dispatch.unit_statuses(tenant_id, status);
|
|
CREATE INDEX idx_unit_statuses_current_incident ON dispatch.unit_statuses(current_incident_id) WHERE current_incident_id IS NOT NULL;
|
|
|
|
-- Technician Skills
|
|
CREATE TABLE dispatch.technician_skills (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL,
|
|
|
|
-- Technician reference
|
|
technician_id UUID NOT NULL,
|
|
|
|
-- Skill definition
|
|
skill_code VARCHAR(50) NOT NULL,
|
|
skill_name VARCHAR(100) NOT NULL,
|
|
skill_description TEXT,
|
|
level dispatch.skill_level DEFAULT 'basic',
|
|
|
|
-- Certification
|
|
certification_number VARCHAR(100),
|
|
certified_at DATE,
|
|
expires_at DATE,
|
|
certification_document_url VARCHAR(500),
|
|
|
|
-- Status
|
|
is_active BOOLEAN DEFAULT true,
|
|
verified_by UUID,
|
|
verified_at TIMESTAMPTZ,
|
|
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
|
|
CONSTRAINT uq_technician_skill UNIQUE(tenant_id, technician_id, skill_code)
|
|
);
|
|
|
|
CREATE INDEX idx_technician_skills_tenant ON dispatch.technician_skills(tenant_id);
|
|
CREATE INDEX idx_technician_skills_technician ON dispatch.technician_skills(tenant_id, technician_id);
|
|
CREATE INDEX idx_technician_skills_code ON dispatch.technician_skills(tenant_id, skill_code);
|
|
CREATE INDEX idx_technician_skills_expiry ON dispatch.technician_skills(expires_at) WHERE expires_at IS NOT NULL;
|
|
|
|
-- Technician Shifts
|
|
CREATE TABLE dispatch.technician_shifts (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL,
|
|
|
|
-- Technician reference
|
|
technician_id UUID NOT NULL,
|
|
|
|
-- Schedule
|
|
shift_date DATE NOT NULL,
|
|
shift_type dispatch.shift_type NOT NULL,
|
|
start_time TIME NOT NULL,
|
|
end_time TIME NOT NULL,
|
|
|
|
-- On-call specifics
|
|
is_on_call BOOLEAN DEFAULT false,
|
|
on_call_priority INTEGER DEFAULT 0,
|
|
|
|
-- Assignment
|
|
assigned_unit_id UUID,
|
|
|
|
-- Status
|
|
actual_start_time TIMESTAMPTZ,
|
|
actual_end_time TIMESTAMPTZ,
|
|
is_absent BOOLEAN DEFAULT false,
|
|
absence_reason TEXT,
|
|
|
|
notes TEXT,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
created_by UUID
|
|
);
|
|
|
|
CREATE INDEX idx_technician_shifts_tenant ON dispatch.technician_shifts(tenant_id);
|
|
CREATE INDEX idx_technician_shifts_technician_date ON dispatch.technician_shifts(tenant_id, technician_id, shift_date);
|
|
CREATE INDEX idx_technician_shifts_date ON dispatch.technician_shifts(tenant_id, shift_date);
|
|
CREATE INDEX idx_technician_shifts_unit ON dispatch.technician_shifts(assigned_unit_id) WHERE assigned_unit_id IS NOT NULL;
|
|
|
|
-- Dispatch Rules (assignment criteria)
|
|
CREATE TABLE dispatch.dispatch_rules (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL,
|
|
|
|
name VARCHAR(100) NOT NULL,
|
|
description TEXT,
|
|
priority INTEGER DEFAULT 0,
|
|
|
|
-- Applicability
|
|
service_type_code VARCHAR(50),
|
|
incident_category VARCHAR(50),
|
|
|
|
-- Conditions (JSON structure for flexibility)
|
|
conditions JSONB NOT NULL DEFAULT '{}',
|
|
/*
|
|
conditions structure:
|
|
{
|
|
"requires_skills": ["DIESEL_ENGINE", "ELECTRICAL"],
|
|
"requires_asset_categories": ["DIAGNOSTIC_TOOL"],
|
|
"min_unit_capacity": "heavy",
|
|
"max_distance_km": 50,
|
|
"zone_codes": ["NORTE", "CENTRO"],
|
|
"min_skill_level": "intermediate",
|
|
"exclude_on_call": false
|
|
}
|
|
*/
|
|
|
|
-- Action
|
|
auto_assign BOOLEAN DEFAULT false,
|
|
assignment_weight INTEGER DEFAULT 100,
|
|
|
|
is_active BOOLEAN DEFAULT true,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
created_by UUID
|
|
);
|
|
|
|
CREATE INDEX idx_dispatch_rules_tenant ON dispatch.dispatch_rules(tenant_id);
|
|
CREATE INDEX idx_dispatch_rules_service_type ON dispatch.dispatch_rules(tenant_id, service_type_code) WHERE service_type_code IS NOT NULL;
|
|
CREATE INDEX idx_dispatch_rules_priority ON dispatch.dispatch_rules(tenant_id, priority DESC);
|
|
|
|
-- Escalation Rules
|
|
CREATE TABLE dispatch.escalation_rules (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL,
|
|
|
|
name VARCHAR(100) NOT NULL,
|
|
description TEXT,
|
|
|
|
-- Trigger conditions
|
|
trigger_after_minutes INTEGER NOT NULL,
|
|
trigger_status VARCHAR(50), -- incident status that triggers
|
|
trigger_priority VARCHAR(20), -- only for specific priorities
|
|
|
|
-- Escalation target
|
|
escalate_to_role VARCHAR(50) NOT NULL,
|
|
escalate_to_user_ids UUID[],
|
|
|
|
-- Notification
|
|
notification_channel dispatch.notification_channel NOT NULL,
|
|
notification_template TEXT,
|
|
notification_data JSONB DEFAULT '{}',
|
|
|
|
-- Repeat
|
|
repeat_interval_minutes INTEGER,
|
|
max_escalations INTEGER DEFAULT 3,
|
|
|
|
is_active BOOLEAN DEFAULT true,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
created_by UUID
|
|
);
|
|
|
|
CREATE INDEX idx_escalation_rules_tenant ON dispatch.escalation_rules(tenant_id);
|
|
CREATE INDEX idx_escalation_rules_trigger ON dispatch.escalation_rules(tenant_id, trigger_after_minutes);
|
|
|
|
-- Dispatch Log (audit trail)
|
|
CREATE TABLE dispatch.dispatch_logs (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL,
|
|
|
|
-- Incident reference
|
|
incident_id UUID NOT NULL,
|
|
|
|
-- Action
|
|
action dispatch.dispatch_action NOT NULL,
|
|
|
|
-- Unit/Technician changes
|
|
from_unit_id UUID,
|
|
to_unit_id UUID,
|
|
from_technician_id UUID,
|
|
to_technician_id UUID,
|
|
|
|
-- Context
|
|
reason TEXT,
|
|
automated BOOLEAN DEFAULT false,
|
|
rule_id UUID, -- dispatch rule that caused action
|
|
escalation_id UUID, -- escalation rule that caused action
|
|
|
|
-- Response times
|
|
response_time_seconds INTEGER,
|
|
|
|
-- Actor
|
|
performed_by UUID,
|
|
performed_at TIMESTAMPTZ DEFAULT NOW(),
|
|
|
|
-- Extra data
|
|
metadata JSONB DEFAULT '{}'
|
|
);
|
|
|
|
CREATE INDEX idx_dispatch_logs_tenant ON dispatch.dispatch_logs(tenant_id);
|
|
CREATE INDEX idx_dispatch_logs_incident ON dispatch.dispatch_logs(tenant_id, incident_id);
|
|
CREATE INDEX idx_dispatch_logs_time ON dispatch.dispatch_logs(tenant_id, performed_at DESC);
|
|
CREATE INDEX idx_dispatch_logs_action ON dispatch.dispatch_logs(tenant_id, action);
|
|
|
|
-- ============================================================
|
|
-- TRIGGERS
|
|
-- ============================================================
|
|
|
|
-- Auto-update updated_at
|
|
CREATE OR REPLACE FUNCTION dispatch.update_updated_at()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.updated_at = NOW();
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TRIGGER trg_dispatch_boards_updated_at
|
|
BEFORE UPDATE ON dispatch.dispatch_boards
|
|
FOR EACH ROW EXECUTE FUNCTION dispatch.update_updated_at();
|
|
|
|
CREATE TRIGGER trg_unit_statuses_updated_at
|
|
BEFORE UPDATE ON dispatch.unit_statuses
|
|
FOR EACH ROW EXECUTE FUNCTION dispatch.update_updated_at();
|
|
|
|
CREATE TRIGGER trg_technician_skills_updated_at
|
|
BEFORE UPDATE ON dispatch.technician_skills
|
|
FOR EACH ROW EXECUTE FUNCTION dispatch.update_updated_at();
|
|
|
|
CREATE TRIGGER trg_technician_shifts_updated_at
|
|
BEFORE UPDATE ON dispatch.technician_shifts
|
|
FOR EACH ROW EXECUTE FUNCTION dispatch.update_updated_at();
|
|
|
|
CREATE TRIGGER trg_dispatch_rules_updated_at
|
|
BEFORE UPDATE ON dispatch.dispatch_rules
|
|
FOR EACH ROW EXECUTE FUNCTION dispatch.update_updated_at();
|
|
|
|
CREATE TRIGGER trg_escalation_rules_updated_at
|
|
BEFORE UPDATE ON dispatch.escalation_rules
|
|
FOR EACH ROW EXECUTE FUNCTION dispatch.update_updated_at();
|
|
|
|
-- Log unit status changes
|
|
CREATE OR REPLACE FUNCTION dispatch.log_unit_status_change()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
IF OLD.status IS DISTINCT FROM NEW.status THEN
|
|
NEW.last_status_change = NOW();
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TRIGGER trg_unit_status_change
|
|
BEFORE UPDATE ON dispatch.unit_statuses
|
|
FOR EACH ROW EXECUTE FUNCTION dispatch.log_unit_status_change();
|
|
|
|
-- ============================================================
|
|
-- VIEWS
|
|
-- ============================================================
|
|
|
|
-- Available units with current position
|
|
CREATE OR REPLACE VIEW dispatch.vw_available_units AS
|
|
SELECT
|
|
us.id,
|
|
us.tenant_id,
|
|
us.unit_id,
|
|
us.unit_code,
|
|
us.unit_name,
|
|
us.status,
|
|
us.last_known_lat,
|
|
us.last_known_lng,
|
|
us.last_location_update,
|
|
us.unit_capacity,
|
|
us.can_tow,
|
|
us.max_tow_weight_kg,
|
|
us.estimated_available_at,
|
|
EXTRACT(EPOCH FROM (NOW() - us.last_location_update)) / 60 AS minutes_since_update
|
|
FROM dispatch.unit_statuses us
|
|
WHERE us.status = 'available';
|
|
|
|
-- Technicians on shift today
|
|
CREATE OR REPLACE VIEW dispatch.vw_technicians_on_shift AS
|
|
SELECT
|
|
ts.id,
|
|
ts.tenant_id,
|
|
ts.technician_id,
|
|
ts.shift_date,
|
|
ts.shift_type,
|
|
ts.start_time,
|
|
ts.end_time,
|
|
ts.is_on_call,
|
|
ts.assigned_unit_id,
|
|
ts.is_absent,
|
|
CASE
|
|
WHEN ts.is_absent THEN 'absent'
|
|
WHEN ts.actual_start_time IS NOT NULL AND ts.actual_end_time IS NULL THEN 'active'
|
|
WHEN ts.actual_start_time IS NULL THEN 'pending'
|
|
ELSE 'completed'
|
|
END AS shift_status
|
|
FROM dispatch.technician_shifts ts
|
|
WHERE ts.shift_date = CURRENT_DATE
|
|
AND ts.is_absent = false;
|
|
|
|
-- Skills about to expire
|
|
CREATE OR REPLACE VIEW dispatch.vw_expiring_skills AS
|
|
SELECT
|
|
tsk.id,
|
|
tsk.tenant_id,
|
|
tsk.technician_id,
|
|
tsk.skill_code,
|
|
tsk.skill_name,
|
|
tsk.level,
|
|
tsk.expires_at,
|
|
tsk.expires_at - CURRENT_DATE AS days_until_expiry
|
|
FROM dispatch.technician_skills tsk
|
|
WHERE tsk.is_active = true
|
|
AND tsk.expires_at IS NOT NULL
|
|
AND tsk.expires_at <= CURRENT_DATE + INTERVAL '30 days';
|
|
|
|
-- Dispatch activity summary
|
|
CREATE OR REPLACE VIEW dispatch.vw_dispatch_activity AS
|
|
SELECT
|
|
dl.tenant_id,
|
|
DATE(dl.performed_at) AS activity_date,
|
|
dl.action,
|
|
COUNT(*) AS count,
|
|
AVG(dl.response_time_seconds) AS avg_response_time_seconds,
|
|
COUNT(CASE WHEN dl.automated THEN 1 END) AS automated_count
|
|
FROM dispatch.dispatch_logs dl
|
|
WHERE dl.performed_at >= CURRENT_DATE - INTERVAL '30 days'
|
|
GROUP BY dl.tenant_id, DATE(dl.performed_at), dl.action;
|
|
|
|
-- ============================================================
|
|
-- RLS POLICIES
|
|
-- ============================================================
|
|
|
|
ALTER TABLE dispatch.dispatch_boards ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE dispatch.unit_statuses ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE dispatch.technician_skills ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE dispatch.technician_shifts ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE dispatch.dispatch_rules ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE dispatch.escalation_rules ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE dispatch.dispatch_logs ENABLE ROW LEVEL SECURITY;
|
|
|
|
-- RLS Policies for each table
|
|
CREATE POLICY tenant_isolation_dispatch_boards ON dispatch.dispatch_boards
|
|
FOR ALL USING (tenant_id = current_setting('app.current_tenant', true)::uuid);
|
|
|
|
CREATE POLICY tenant_isolation_unit_statuses ON dispatch.unit_statuses
|
|
FOR ALL USING (tenant_id = current_setting('app.current_tenant', true)::uuid);
|
|
|
|
CREATE POLICY tenant_isolation_technician_skills ON dispatch.technician_skills
|
|
FOR ALL USING (tenant_id = current_setting('app.current_tenant', true)::uuid);
|
|
|
|
CREATE POLICY tenant_isolation_technician_shifts ON dispatch.technician_shifts
|
|
FOR ALL USING (tenant_id = current_setting('app.current_tenant', true)::uuid);
|
|
|
|
CREATE POLICY tenant_isolation_dispatch_rules ON dispatch.dispatch_rules
|
|
FOR ALL USING (tenant_id = current_setting('app.current_tenant', true)::uuid);
|
|
|
|
CREATE POLICY tenant_isolation_escalation_rules ON dispatch.escalation_rules
|
|
FOR ALL USING (tenant_id = current_setting('app.current_tenant', true)::uuid);
|
|
|
|
CREATE POLICY tenant_isolation_dispatch_logs ON dispatch.dispatch_logs
|
|
FOR ALL USING (tenant_id = current_setting('app.current_tenant', true)::uuid);
|
|
|
|
-- ============================================================
|
|
-- COMMENTS
|
|
-- ============================================================
|
|
|
|
COMMENT ON SCHEMA dispatch IS 'Dispatch Center schema for incident assignment and unit management (MMD-011)';
|
|
COMMENT ON TABLE dispatch.dispatch_boards IS 'Dispatch board configurations for map and assignment views';
|
|
COMMENT ON TABLE dispatch.unit_statuses IS 'Real-time status of fleet units for dispatch';
|
|
COMMENT ON TABLE dispatch.technician_skills IS 'Skills and certifications of technicians';
|
|
COMMENT ON TABLE dispatch.technician_shifts IS 'Shift schedules for technicians';
|
|
COMMENT ON TABLE dispatch.dispatch_rules IS 'Rules for automatic assignment suggestions';
|
|
COMMENT ON TABLE dispatch.escalation_rules IS 'Rules for escalating unresponded incidents';
|
|
COMMENT ON TABLE dispatch.dispatch_logs IS 'Audit log of all dispatch actions';
|