import { wingPool } from '../db/wingDb.js'; interface PredictionAnalysis { acdntSn: number; acdntNm: string; occurredAt: string; analysisDate: string; requestor: string; duration: string; oilType: string; volume: number | null; location: string; lat: number | null; lon: number | null; kospsStatus: string; poseidonStatus: string; opendriftStatus: string; backtrackStatus: string; analyst: string; officeName: string; } interface PredictionDetail { acdnt: { acdntSn: number; acdntNm: string; occurredAt: string; lat: number | null; lon: number | null; location: string; analyst: string; officeName: string; }; spill: { oilType: string; volume: number | null; unit: string; fcstHr: number | null; } | null; vessels: Array<{ vesselInfoSn: number; imoNo: string; vesselNm: string; vesselTp: string; loaM: number | null; breadthM: number | null; draftM: number | null; gt: number | null; dwt: number | null; builtYr: number | null; flagCd: string; callsign: string; engineDc: string; insuranceData: unknown; }>; weather: Array<{ obsDtm: string; locNm: string; temp: string; weatherDc: string; wind: string; wave: string; humid: string; vis: string; sst: string; }>; } interface BacktrackResult { backtrackSn: number; acdntSn: number; estSpilDtm: string | null; anlysRange: string | null; lon: number | null; lat: number | null; srchRadiusNm: number | null; totalVessels: number | null; execSttsCd: string; rsltData: unknown; regDtm: string; } interface CreateBacktrackInput { acdntSn: number; lat: number; lon: number; estSpilDtm?: string; anlysRange?: string; srchRadiusNm?: number; } interface SaveBoomLineInput { acdntSn: number; boomNm: string; priorityOrd?: number; geojson: unknown; lengthM?: number; efficiencyPct?: number; } interface BoomLineItem { boomLineSn: number; acdntSn: number; boomNm: string; priorityOrd: number; geom: unknown; lengthM: number | null; efficiencyPct: number | null; sttsCd: string; regDtm: string; } interface ListAnalysesInput { search?: string; } export async function listAnalyses(input: ListAnalysesInput): Promise { const params: unknown[] = []; const conditions: string[] = ["A.USE_YN = 'Y'"]; if (input.search) { params.push(`%${input.search}%`); conditions.push(`(A.ACDNT_NM ILIKE $${params.length} OR A.LOC_DC ILIKE $${params.length})`); } const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; const sql = ` SELECT A.ACDNT_SN, A.ACDNT_NM, A.OCCRN_DTM, A.LAT, A.LNG, A.LOC_DC, A.ANALYST_NM, A.OFFICE_NM, A.REGION_NM, S.OIL_TP_CD, S.SPIL_QTY, S.SPIL_UNIT_CD, S.FCST_HR, P.KOSPS_STATUS, P.POSEIDON_STATUS, P.OPENDRIFT_STATUS, B.BACKTRACK_STATUS FROM ACDNT A LEFT JOIN SPIL_DATA S ON S.ACDNT_SN = A.ACDNT_SN LEFT JOIN ( SELECT ACDNT_SN, MAX(CASE WHEN ALGO_CD = 'KOSPS' THEN EXEC_STTS_CD END) AS KOSPS_STATUS, MAX(CASE WHEN ALGO_CD = 'POSEIDON' THEN EXEC_STTS_CD END) AS POSEIDON_STATUS, MAX(CASE WHEN ALGO_CD = 'OPENDRIFT' THEN EXEC_STTS_CD END) AS OPENDRIFT_STATUS FROM PRED_EXEC GROUP BY ACDNT_SN ) P ON P.ACDNT_SN = A.ACDNT_SN LEFT JOIN ( SELECT ACDNT_SN, MAX(CASE WHEN B.EXEC_STTS_CD IS NOT NULL THEN B.EXEC_STTS_CD ELSE 'pending' END) AS BACKTRACK_STATUS FROM BACKTRACK B GROUP BY ACDNT_SN ) B ON B.ACDNT_SN = A.ACDNT_SN ${whereClause} ORDER BY A.OCCRN_DTM DESC `; const { rows } = await wingPool.query(sql, params); return rows.map((row: Record) => ({ acdntSn: Number(row['acdnt_sn']), acdntNm: String(row['acdnt_nm'] ?? ''), occurredAt: row['occrn_dtm'] ? String(row['occrn_dtm']) : '', analysisDate: row['occrn_dtm'] ? String(row['occrn_dtm']) : '', requestor: String(row['analyst_nm'] ?? ''), duration: row['fcst_hr'] != null ? `${row['fcst_hr']}hr` : '', oilType: String(row['oil_tp_cd'] ?? ''), volume: row['spil_qty'] != null ? parseFloat(String(row['spil_qty'])) : null, location: String(row['loc_dc'] ?? ''), lat: row['lat'] != null ? parseFloat(String(row['lat'])) : null, lon: row['lng'] != null ? parseFloat(String(row['lng'])) : null, kospsStatus: String(row['kosps_status'] ?? 'pending').toLowerCase(), poseidonStatus: String(row['poseidon_status'] ?? 'pending').toLowerCase(), opendriftStatus: String(row['opendrift_status'] ?? 'pending').toLowerCase(), backtrackStatus: String(row['backtrack_status'] ?? 'pending').toLowerCase(), analyst: String(row['analyst_nm'] ?? ''), officeName: String(row['office_nm'] ?? ''), })); } export async function getAnalysisDetail(acdntSn: number): Promise { const acdntSql = ` SELECT A.ACDNT_SN, A.ACDNT_NM, A.OCCRN_DTM, A.LAT, A.LNG, A.LOC_DC, A.ANALYST_NM, A.OFFICE_NM FROM ACDNT A WHERE A.ACDNT_SN = $1 AND A.USE_YN = 'Y' `; const { rows: acdntRows } = await wingPool.query(acdntSql, [acdntSn]); if (acdntRows.length === 0) return null; const a = acdntRows[0] as Record; const spillSql = ` SELECT OIL_TP_CD, SPIL_QTY, SPIL_UNIT_CD, FCST_HR FROM SPIL_DATA WHERE ACDNT_SN = $1 ORDER BY SPIL_DATA_SN ASC LIMIT 1 `; const { rows: spillRows } = await wingPool.query(spillSql, [acdntSn]); const vesselSql = ` SELECT VESSEL_INFO_SN, IMO_NO, VESSEL_NM, VESSEL_TP, LOA_M, BREADTH_M, DRAFT_M, GT, DWT, BUILT_YR, FLAG_CD, CALLSIGN, ENGINE_DC, INSURANCE_DATA FROM VESSEL_INFO WHERE ACDNT_SN = $1 ORDER BY VESSEL_INFO_SN ASC `; const { rows: vesselRows } = await wingPool.query(vesselSql, [acdntSn]); const weatherSql = ` SELECT OBS_DTM, LOC_NM, TEMP, WEATHER_DC, WIND, WAVE, HUMID, VIS, SST FROM ACDNT_WEATHER WHERE ACDNT_SN = $1 ORDER BY OBS_DTM ASC `; const { rows: weatherRows } = await wingPool.query(weatherSql, [acdntSn]); const spill = spillRows.length > 0 ? (() => { const s = spillRows[0] as Record; return { oilType: String(s['oil_tp_cd'] ?? ''), volume: s['spil_qty'] != null ? parseFloat(String(s['spil_qty'])) : null, unit: String(s['spil_unit_cd'] ?? ''), fcstHr: s['fcst_hr'] != null ? parseFloat(String(s['fcst_hr'])) : null, }; })() : null; const vessels = vesselRows.map((v: Record) => ({ vesselInfoSn: Number(v['vessel_info_sn']), imoNo: String(v['imo_no'] ?? ''), vesselNm: String(v['vessel_nm'] ?? ''), vesselTp: String(v['vessel_tp'] ?? ''), loaM: v['loa_m'] != null ? parseFloat(String(v['loa_m'])) : null, breadthM: v['breadth_m'] != null ? parseFloat(String(v['breadth_m'])) : null, draftM: v['draft_m'] != null ? parseFloat(String(v['draft_m'])) : null, gt: v['gt'] != null ? parseFloat(String(v['gt'])) : null, dwt: v['dwt'] != null ? parseFloat(String(v['dwt'])) : null, builtYr: v['built_yr'] != null ? Number(v['built_yr']) : null, flagCd: String(v['flag_cd'] ?? ''), callsign: String(v['callsign'] ?? ''), engineDc: String(v['engine_dc'] ?? ''), insuranceData: v['insurance_data'] ?? null, })); const weather = weatherRows.map((w: Record) => ({ obsDtm: w['obs_dtm'] ? String(w['obs_dtm']) : '', locNm: String(w['loc_nm'] ?? ''), temp: String(w['temp'] ?? ''), weatherDc: String(w['weather_dc'] ?? ''), wind: String(w['wind'] ?? ''), wave: String(w['wave'] ?? ''), humid: String(w['humid'] ?? ''), vis: String(w['vis'] ?? ''), sst: String(w['sst'] ?? ''), })); return { acdnt: { acdntSn: Number(a['acdnt_sn']), acdntNm: String(a['acdnt_nm'] ?? ''), occurredAt: a['occrn_dtm'] ? String(a['occrn_dtm']) : '', lat: a['lat'] != null ? parseFloat(String(a['lat'])) : null, lon: a['lng'] != null ? parseFloat(String(a['lng'])) : null, location: String(a['loc_dc'] ?? ''), analyst: String(a['analyst_nm'] ?? ''), officeName: String(a['office_nm'] ?? ''), }, spill, vessels, weather, }; } export async function getBacktrack(sn: number): Promise { const sql = ` SELECT BACKTRACK_SN, ACDNT_SN, EST_SPIL_DTM, ANLYS_RANGE, LON, LAT, SRCH_RADIUS_NM, TOTAL_VESSELS, EXEC_STTS_CD, RSLT_DATA, REG_DTM FROM BACKTRACK WHERE BACKTRACK_SN = $1 AND USE_YN = 'Y' `; const { rows } = await wingPool.query(sql, [sn]); if (rows.length === 0) return null; return rowToBacktrack(rows[0] as Record); } export async function listBacktracksByAcdnt(acdntSn: number): Promise { const sql = ` SELECT BACKTRACK_SN, ACDNT_SN, EST_SPIL_DTM, ANLYS_RANGE, LON, LAT, SRCH_RADIUS_NM, TOTAL_VESSELS, EXEC_STTS_CD, RSLT_DATA, REG_DTM FROM BACKTRACK WHERE ACDNT_SN = $1 AND USE_YN = 'Y' ORDER BY REG_DTM DESC `; const { rows } = await wingPool.query(sql, [acdntSn]); return rows.map((r: Record) => rowToBacktrack(r)); } function rowToBacktrack(r: Record): BacktrackResult { return { backtrackSn: Number(r['backtrack_sn']), acdntSn: Number(r['acdnt_sn']), estSpilDtm: r['est_spil_dtm'] ? String(r['est_spil_dtm']) : null, anlysRange: r['anlys_range'] ? String(r['anlys_range']) : null, lon: r['lon'] != null ? parseFloat(String(r['lon'])) : null, lat: r['lat'] != null ? parseFloat(String(r['lat'])) : null, srchRadiusNm: r['srch_radius_nm'] != null ? parseFloat(String(r['srch_radius_nm'])) : null, totalVessels: r['total_vessels'] != null ? Number(r['total_vessels']) : null, execSttsCd: String(r['exec_stts_cd'] ?? ''), rsltData: r['rslt_data'] ?? null, regDtm: String(r['reg_dtm'] ?? ''), }; } export async function createBacktrack( input: CreateBacktrackInput, ): Promise<{ backtrackSn: number }> { const { acdntSn, lat, lon, estSpilDtm, anlysRange, srchRadiusNm } = input; const sql = ` INSERT INTO BACKTRACK (ACDNT_SN, LAT, LON, GEOM, LOC_DC, EST_SPIL_DTM, ANLYS_RANGE, SRCH_RADIUS_NM, EXEC_STTS_CD) VALUES ( $1, $2, $3, ST_SetSRID(ST_MakePoint($3::float, $2::float), 4326), $3 || ' + ' || $2, $4, $5, $6, 'PENDING' ) RETURNING BACKTRACK_SN `; const { rows } = await wingPool.query(sql, [ acdntSn, lat, lon, estSpilDtm || null, anlysRange || null, srchRadiusNm || null, ]); return { backtrackSn: Number((rows[0] as Record)['backtrack_sn']) }; } export async function saveBoomLine(input: SaveBoomLineInput): Promise<{ boomLineSn: number }> { const { acdntSn, boomNm, priorityOrd = 0, geojson, lengthM, efficiencyPct } = input; const sql = ` INSERT INTO BOOM_LINE (ACDNT_SN, BOOM_NM, PRIORITY_ORD, GEOM, LENGTH_M, EFFICIENCY_PCT) VALUES ($1, $2, $3, ST_GeomFromGeoJSON($4), $5, $6) RETURNING BOOM_LINE_SN `; const { rows } = await wingPool.query(sql, [ acdntSn, boomNm, priorityOrd, JSON.stringify(geojson), lengthM || null, efficiencyPct || null, ]); return { boomLineSn: Number((rows[0] as Record)['boom_line_sn']) }; } export async function listBoomLines(acdntSn: number): Promise { const sql = ` SELECT BOOM_LINE_SN, ACDNT_SN, BOOM_NM, PRIORITY_ORD, ST_AsGeoJSON(GEOM) AS GEOM, LENGTH_M, EFFICIENCY_PCT, STTS_CD, REG_DTM FROM BOOM_LINE WHERE ACDNT_SN = $1 AND USE_YN = 'Y' ORDER BY PRIORITY_ORD ASC `; const { rows } = await wingPool.query(sql, [acdntSn]); return rows.map((r: Record) => ({ boomLineSn: Number(r['boom_line_sn']), acdntSn: Number(r['acdnt_sn']), boomNm: String(r['boom_nm'] ?? ''), priorityOrd: Number(r['priority_ord'] ?? 0), geom: r['geom'] != null ? JSON.parse(String(r['geom'])) : null, lengthM: r['length_m'] != null ? parseFloat(String(r['length_m'])) : null, efficiencyPct: r['efficiency_pct'] != null ? parseFloat(String(r['efficiency_pct'])) : null, sttsCd: String(r['stts_cd'] ?? 'PLANNED'), regDtm: String(r['reg_dtm'] ?? ''), })); }