- Add MorningSummaryService with Zabbix aggregation and adaptive card builder - Add webhook delivery system with Teams incoming webhooks - Add admin UI at /admin/morning-summary for webhook/config management - Add API routes: /send, /test, /webhooks, /webhooks/[id], /config, /history - Register morning-summary cron job in SyncScheduler (Mon-Fri 6:30 AM) - Add outages_only filter (Unavailable triggers only) - Fix host resolution: use getTriggerEnabledHosts to exclude disabled hosts - Fix resolved events: event.get value:1 scoped to window with r_eventid filter - Remove emojis from fact rows and section headers in card - Remove Open Zabbix button (duplicate of View Problems) - Add migrations: morning_summary_config + morning_summaries tables - Add outages_only column to morning_summary_config
181 lines
6.9 KiB
TypeScript
181 lines
6.9 KiB
TypeScript
/**
|
|
* Contract Coverage API
|
|
* GET /api/veeam/contract-coverage
|
|
* Returns per-company contracted vs deployed counts for Servers, Workstations, M365
|
|
* plus the individual contract service lines for the expanded view.
|
|
*/
|
|
|
|
import { NextResponse } from 'next/server';
|
|
import postgresClient from '@/lib/services/postgres-client';
|
|
|
|
export async function GET() {
|
|
try {
|
|
const result = await postgresClient.query(`
|
|
WITH
|
|
|
|
-- All active contract service lines with category classification
|
|
cs_lines AS (
|
|
SELECT
|
|
ct.company_id,
|
|
ct.id AS contract_id,
|
|
ct.contract_name,
|
|
cs.id AS cs_id,
|
|
cs.service_name AS line_name,
|
|
cs.unit_price,
|
|
cs.unit_cost,
|
|
CASE
|
|
WHEN cs.service_name ILIKE '%windows server%'
|
|
OR cs.service_name ILIKE '%server virtual%'
|
|
OR cs.service_name ILIKE '%server phys%'
|
|
OR cs.service_name ILIKE '%esxi host%'
|
|
OR cs.service_name ILIKE '%wulf 365 it complete server%'
|
|
OR cs.service_name ILIKE '%wulf it complete (server)%'
|
|
THEN 'server'
|
|
WHEN cs.service_name ILIKE '%workstation%backup%'
|
|
OR cs.service_name ILIKE '%w/ backup%'
|
|
OR cs.service_name ILIKE '%wulf 365 it complete endpoint%'
|
|
OR cs.service_name ILIKE '%wulf it complete (endpoint)%'
|
|
THEN 'workstation'
|
|
WHEN cs.service_name ILIKE '%microsoft 365%'
|
|
OR cs.service_name ILIKE '%office 365%'
|
|
OR cs.service_name ILIKE '%exchange online%'
|
|
OR cs.service_name ILIKE '%m365%'
|
|
OR cs.service_name ILIKE '%veeam backup for microsoft office 365%'
|
|
THEN 'm365'
|
|
ELSE 'other'
|
|
END AS category
|
|
FROM contract_services cs
|
|
JOIN contracts ct ON ct.id = cs.contract_id
|
|
WHERE cs.is_deleted = false
|
|
AND ct.is_deleted = false
|
|
AND ct.status = 1
|
|
),
|
|
|
|
-- Summarise contracted counts per company
|
|
contracted AS (
|
|
SELECT
|
|
company_id,
|
|
COUNT(*) FILTER (WHERE category = 'server') AS contracted_servers,
|
|
COUNT(*) FILTER (WHERE category = 'workstation') AS contracted_workstations,
|
|
COUNT(*) FILTER (WHERE category = 'm365') AS contracted_m365,
|
|
COUNT(*) FILTER (WHERE category = 'other') AS contracted_other,
|
|
COUNT(*) AS contracted_total
|
|
FROM cs_lines
|
|
GROUP BY company_id
|
|
),
|
|
|
|
-- Deployed counts from Veeam agent jobs
|
|
deployed AS (
|
|
SELECT
|
|
o.company_id,
|
|
COUNT(*) FILTER (WHERE j.operation_mode = 'Server') AS deployed_servers,
|
|
COUNT(*) FILTER (WHERE j.operation_mode = 'Workstation') AS deployed_workstations,
|
|
COUNT(*) FILTER (WHERE j.operation_mode NOT IN ('Server','Workstation')) AS deployed_other
|
|
FROM veeam_backup_agent_jobs j
|
|
JOIN veeam_organizations o ON o.instance_uid = j.organization_uid
|
|
WHERE j.is_enabled = true
|
|
GROUP BY o.company_id
|
|
),
|
|
|
|
-- All companies that appear in either side
|
|
all_companies AS (
|
|
SELECT company_id FROM contracted
|
|
UNION
|
|
SELECT company_id FROM deployed
|
|
)
|
|
|
|
SELECT
|
|
ac.company_id,
|
|
c.company_name,
|
|
COALESCE(ct.contracted_servers, 0) AS contracted_servers,
|
|
COALESCE(ct.contracted_workstations, 0) AS contracted_workstations,
|
|
COALESCE(ct.contracted_m365, 0) AS contracted_m365,
|
|
COALESCE(ct.contracted_other, 0) AS contracted_other,
|
|
COALESCE(d.deployed_servers, 0) AS deployed_servers,
|
|
COALESCE(d.deployed_workstations, 0) AS deployed_workstations,
|
|
COALESCE(d.deployed_other, 0) AS deployed_other
|
|
FROM all_companies ac
|
|
JOIN companies c ON c.id = ac.company_id
|
|
LEFT JOIN contracted ct ON ct.company_id = ac.company_id
|
|
LEFT JOIN deployed d ON d.company_id = ac.company_id
|
|
ORDER BY c.company_name
|
|
`);
|
|
|
|
// Build per-company service lines map
|
|
const linesResult = await postgresClient.query(`
|
|
SELECT
|
|
ct.company_id,
|
|
ct.id AS contract_id,
|
|
ct.contract_name,
|
|
cs.id AS cs_id,
|
|
cs.service_name AS line_name,
|
|
cs.unit_price,
|
|
cs.unit_cost,
|
|
CASE
|
|
WHEN cs.service_name ILIKE '%windows server%'
|
|
OR cs.service_name ILIKE '%server virtual%'
|
|
OR cs.service_name ILIKE '%server phys%'
|
|
OR cs.service_name ILIKE '%esxi host%'
|
|
OR cs.service_name ILIKE '%wulf 365 it complete server%'
|
|
OR cs.service_name ILIKE '%wulf it complete (server)%'
|
|
THEN 'server'
|
|
WHEN cs.service_name ILIKE '%workstation%backup%'
|
|
OR cs.service_name ILIKE '%w/ backup%'
|
|
OR cs.service_name ILIKE '%wulf 365 it complete endpoint%'
|
|
OR cs.service_name ILIKE '%wulf it complete (endpoint)%'
|
|
THEN 'workstation'
|
|
WHEN cs.service_name ILIKE '%microsoft 365%'
|
|
OR cs.service_name ILIKE '%office 365%'
|
|
OR cs.service_name ILIKE '%exchange online%'
|
|
OR cs.service_name ILIKE '%m365%'
|
|
OR cs.service_name ILIKE '%veeam backup for microsoft office 365%'
|
|
THEN 'm365'
|
|
ELSE 'other'
|
|
END AS category
|
|
FROM contract_services cs
|
|
JOIN contracts ct ON ct.id = cs.contract_id
|
|
WHERE cs.is_deleted = false
|
|
AND ct.is_deleted = false
|
|
AND ct.status = 1
|
|
ORDER BY ct.company_id, ct.contract_name, cs.service_name
|
|
`);
|
|
|
|
// Group lines by company_id
|
|
const linesByCompany: Record<number, typeof linesResult.rows> = {};
|
|
for (const row of linesResult.rows) {
|
|
const cid = row.company_id;
|
|
if (!linesByCompany[cid]) linesByCompany[cid] = [];
|
|
linesByCompany[cid].push(row);
|
|
}
|
|
|
|
const rows = result.rows.map((r) => ({
|
|
company_id: r.company_id,
|
|
company_name: r.company_name,
|
|
contracted: {
|
|
servers: Number(r.contracted_servers),
|
|
workstations: Number(r.contracted_workstations),
|
|
m365: Number(r.contracted_m365),
|
|
other: Number(r.contracted_other),
|
|
},
|
|
deployed: {
|
|
servers: Number(r.deployed_servers),
|
|
workstations: Number(r.deployed_workstations),
|
|
other: Number(r.deployed_other),
|
|
},
|
|
lines: (linesByCompany[r.company_id] || []).map((l) => ({
|
|
cs_id: l.cs_id,
|
|
contract_id: l.contract_id,
|
|
contract_name: l.contract_name,
|
|
line_name: l.line_name,
|
|
unit_price: l.unit_price != null ? Number(l.unit_price) : null,
|
|
unit_cost: l.unit_cost != null ? Number(l.unit_cost) : null,
|
|
category: l.category,
|
|
})),
|
|
}));
|
|
|
|
return NextResponse.json({ rows });
|
|
} catch (error) {
|
|
console.error('[contract-coverage] error:', error);
|
|
return NextResponse.json({ rows: [] });
|
|
}
|
|
}
|