- 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
85 lines
2.7 KiB
TypeScript
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 });
|
|
}
|
|
}
|