wulf-pulse/app/api/data/contracts/[id]/services/route.ts
lorentz c518eefdb2 feat: Morning NOC Summary adaptive card for Teams
- 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
2026-03-11 09:34:51 -04:00

85 lines
2.7 KiB
TypeScript

/**
* Contract Services Detail API
* GET /api/data/contracts/[id]/services - Returns a contract with all its service lines
*/
import { NextRequest, NextResponse } from 'next/server';
import postgresClient from '@/lib/services/postgres-client';
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const contractId = parseInt(id);
if (isNaN(contractId)) {
return NextResponse.json({ error: 'Invalid contract ID' }, { status: 400 });
}
const contractResult = await postgresClient.query(
`SELECT ct.*, c.company_name
FROM contracts ct
LEFT JOIN companies c ON c.id = ct.company_id
WHERE ct.id = $1 AND ct.is_deleted = false`,
[contractId]
);
if (contractResult.rows.length === 0) {
return NextResponse.json({ error: 'Contract not found' }, { status: 404 });
}
const contract = contractResult.rows[0];
const servicesResult = await postgresClient.query(
`SELECT
cs.id,
cs.service_id,
cs.service_name,
cs.description,
cs.unit_price,
cs.unit_cost,
cs.quantity,
cs.adjusted_price,
cs.period_type,
cs.start_date,
cs.end_date,
s.name AS catalog_name
FROM contract_services cs
LEFT JOIN autotask_services s ON s.id = cs.service_id
WHERE cs.contract_id = $1 AND cs.is_deleted = false
ORDER BY
CASE
WHEN COALESCE(cs.service_name, s.name) ILIKE '%workstation%backup%'
OR COALESCE(cs.service_name, s.name) ILIKE '%w/ backup%'
OR COALESCE(cs.service_name, s.name) ILIKE '%windows server%'
OR COALESCE(cs.service_name, s.name) ILIKE '%server virtual%'
OR COALESCE(cs.service_name, s.name) ILIKE '%server phys%'
OR COALESCE(cs.service_name, s.name) ILIKE '%esxi host%'
THEN 0
ELSE 1
END,
COALESCE(cs.service_name, s.name)`,
[contractId]
);
const periodLabels: Record<number, string> = {
1: 'Monthly',
2: 'Quarterly',
3: 'Semi-Annual',
4: 'Annual',
5: 'One-Time',
};
const services = servicesResult.rows.map((row) => ({
...row,
display_name: row.service_name || row.catalog_name || `Service #${row.service_id}`,
period_label: row.period_type ? (periodLabels[row.period_type] ?? `Type ${row.period_type}`) : null,
}));
return NextResponse.json({ contract, services });
} catch (error) {
console.error('[CONTRACT-SERVICES-API] Error:', error);
return NextResponse.json({ error: 'Failed to fetch contract services' }, { status: 500 });
}
}