|
| 1 | +import { implement } from "@orpc/server"; |
| 2 | +import { apiContract } from "../contracts/index.js"; |
| 3 | +import { sql } from "../services/db/sql-tag.js"; |
| 4 | + |
| 5 | +const os = implement(apiContract); |
| 6 | + |
| 7 | +export const analysisRouter = { |
| 8 | + getCoalitionAlignment: os.analysis.getCoalitionAlignment.handler( |
| 9 | + async ({ input }) => { |
| 10 | + const period = input?.period ?? "all"; |
| 11 | + |
| 12 | + const results = await sql<{ 'fractie1Id': string | null; 'fractie2Id': string | null; 'fractie1Name': string | null; 'fractie2Name': string | null; alignmentPct: string; sameVotes: string | null; totalVotes: string }>` |
| 13 | + SELECT |
| 14 | + plpm.fractie1_id AS "fractie1Id", |
| 15 | + plpm.fractie2_id AS "fractie2Id", |
| 16 | + f1.afkorting AS "fractie1Name", |
| 17 | + f2.afkorting AS "fractie2Name", |
| 18 | + ROUND(AVG(CASE WHEN same_vote THEN 1.0 ELSE 0.0 END) * 100, 2) AS "alignmentPct", |
| 19 | + SUM(CASE WHEN same_vote THEN 1 ELSE 0 END) AS "sameVotes", |
| 20 | + COUNT(*) AS "totalVotes" |
| 21 | + FROM party_likeness_per_motion plpm |
| 22 | + JOIN fracties f1 ON plpm.fractie1_id = f1.id |
| 23 | + JOIN fracties f2 ON plpm.fractie2_id = f2.id |
| 24 | + WHERE (${period} = 'all' OR EXTRACT(YEAR FROM plpm.gestart_op)::text = ${period}) |
| 25 | + GROUP BY plpm.fractie1_id, plpm.fractie2_id, f1.afkorting, f2.afkorting |
| 26 | + HAVING COUNT(*) >= 10 |
| 27 | + ORDER BY "alignmentPct" DESC |
| 28 | + `; |
| 29 | + |
| 30 | + return results |
| 31 | + .filter((r) => r.fractie1Id && r.fractie2Id && r.fractie1Name && r.fractie2Name) |
| 32 | + .map((r) => ({ |
| 33 | + fractie1Id: r.fractie1Id!, |
| 34 | + fractie2Id: r.fractie2Id!, |
| 35 | + fractie1Name: r.fractie1Name!, |
| 36 | + fractie2Name: r.fractie2Name!, |
| 37 | + alignmentPct: Number(r.alignmentPct), |
| 38 | + sameVotes: Number(r.sameVotes), |
| 39 | + totalVotes: Number(r.totalVotes), |
| 40 | + })); |
| 41 | + }, |
| 42 | + ), |
| 43 | + |
| 44 | + getMPDeviations: os.analysis.getMPDeviations.handler(async ({ input }) => { |
| 45 | + const period = input?.period ?? "all"; |
| 46 | + const limit = input?.limit ?? 50; |
| 47 | + |
| 48 | + const results = await sql<{ persoonId: string | null; fractieId: string | null; persoonNaam: string | null; fractieNaam: string | null; deviationPct: string; deviationCount: string | null; totalVotes: string }>` |
| 49 | + WITH party_majority AS ( |
| 50 | + SELECT |
| 51 | + b.id as besluit_id, |
| 52 | + s.fractie_id, |
| 53 | + s.soort as majority_vote |
| 54 | + FROM stemmingen s |
| 55 | + JOIN besluiten b ON s.besluit_id = b.id |
| 56 | + JOIN zaken z ON b.zaak_id = z.id |
| 57 | + WHERE s.fractie_id IS NOT NULL |
| 58 | + AND s.soort IN ('Voor', 'Tegen') |
| 59 | + AND z.soort = 'Motie' |
| 60 | + AND (${period} = 'all' OR EXTRACT(YEAR FROM z.gestart_op)::text = ${period}) |
| 61 | + ), |
| 62 | + individual_votes AS ( |
| 63 | + SELECT |
| 64 | + s.persoon_id, |
| 65 | + s.fractie_id, |
| 66 | + b.id as besluit_id, |
| 67 | + s.soort as vote |
| 68 | + FROM stemmingen s |
| 69 | + JOIN besluiten b ON s.besluit_id = b.id |
| 70 | + JOIN zaken z ON b.zaak_id = z.id |
| 71 | + WHERE s.persoon_id IS NOT NULL |
| 72 | + AND s.fractie_id IS NOT NULL |
| 73 | + AND s.soort IN ('Voor', 'Tegen') |
| 74 | + AND z.soort = 'Motie' |
| 75 | + AND (${period} = 'all' OR EXTRACT(YEAR FROM z.gestart_op)::text = ${period}) |
| 76 | + ) |
| 77 | + SELECT |
| 78 | + iv.persoon_id AS "persoonId", |
| 79 | + iv.fractie_id AS "fractieId", |
| 80 | + COALESCE(p.roepnaam, p.voornamen) || ' ' || COALESCE(p.tussenvoegsel || ' ', '') || p.achternaam AS "persoonNaam", |
| 81 | + f.afkorting AS "fractieNaam", |
| 82 | + ROUND(AVG(CASE WHEN iv.vote != pm.majority_vote THEN 1.0 ELSE 0.0 END) * 100, 2) AS "deviationPct", |
| 83 | + SUM(CASE WHEN iv.vote != pm.majority_vote THEN 1 ELSE 0 END) AS "deviationCount", |
| 84 | + COUNT(*) AS "totalVotes" |
| 85 | + FROM individual_votes iv |
| 86 | + JOIN party_majority pm ON iv.besluit_id = pm.besluit_id AND iv.fractie_id = pm.fractie_id |
| 87 | + JOIN personen p ON iv.persoon_id = p.id |
| 88 | + JOIN fracties f ON iv.fractie_id = f.id |
| 89 | + GROUP BY iv.persoon_id, iv.fractie_id, p.roepnaam, p.voornamen, p.tussenvoegsel, p.achternaam, f.afkorting |
| 90 | + HAVING COUNT(*) >= 20 |
| 91 | + ORDER BY "deviationPct" DESC |
| 92 | + LIMIT ${limit} |
| 93 | + `; |
| 94 | + |
| 95 | + return results |
| 96 | + .filter((r) => r.persoonId && r.fractieId && r.persoonNaam && r.fractieNaam) |
| 97 | + .map((r) => ({ |
| 98 | + persoonId: r.persoonId!, |
| 99 | + fractieId: r.fractieId!, |
| 100 | + persoonNaam: r.persoonNaam!, |
| 101 | + fractieNaam: r.fractieNaam!, |
| 102 | + deviationPct: Number(r.deviationPct), |
| 103 | + deviationCount: Number(r.deviationCount), |
| 104 | + totalVotes: Number(r.totalVotes), |
| 105 | + })); |
| 106 | + }), |
| 107 | + |
| 108 | + getTopicTrends: os.analysis.getTopicTrends.handler(async ({ input }) => { |
| 109 | + const period = input?.period ?? "all"; |
| 110 | + |
| 111 | + const results = await sql<{ |
| 112 | + categoryId: string; |
| 113 | + categoryName: string; |
| 114 | + motionCount: string; |
| 115 | + acceptedCount: string; |
| 116 | + rejectedCount: string; |
| 117 | + }>` |
| 118 | + SELECT |
| 119 | + zc.category_id AS "categoryId", |
| 120 | + mc.name AS "categoryName", |
| 121 | + COUNT(DISTINCT z.id) AS "motionCount", |
| 122 | + COUNT(DISTINCT CASE WHEN b.status = 'Aangenomen' THEN z.id END) AS "acceptedCount", |
| 123 | + COUNT(DISTINCT CASE WHEN b.status = 'Verworpen' THEN z.id END) AS "rejectedCount" |
| 124 | + FROM zaak_categories zc |
| 125 | + JOIN zaken z ON zc.zaak_id = z.id |
| 126 | + JOIN motion_categories mc ON zc.category_id = mc.id |
| 127 | + LEFT JOIN besluiten b ON b.zaak_id = z.id |
| 128 | + WHERE z.soort = 'Motie' |
| 129 | + AND (${period} = 'all' OR EXTRACT(YEAR FROM z.gestart_op)::text = ${period}) |
| 130 | + GROUP BY zc.category_id, mc.name |
| 131 | + ORDER BY "motionCount" DESC |
| 132 | + `; |
| 133 | + |
| 134 | + return results.map((r) => ({ |
| 135 | + categoryId: r.categoryId, |
| 136 | + categoryName: r.categoryName, |
| 137 | + motionCount: Number(r.motionCount), |
| 138 | + acceptedCount: Number(r.acceptedCount), |
| 139 | + rejectedCount: Number(r.rejectedCount), |
| 140 | + })); |
| 141 | + }), |
| 142 | + |
| 143 | + getPartyTopicVoting: os.analysis.getPartyTopicVoting.handler( |
| 144 | + async ({ input }) => { |
| 145 | + const period = input?.period ?? "all"; |
| 146 | + const fractieId = input?.fractieId ?? ""; |
| 147 | + |
| 148 | + const results = await sql<{ fractieId: string | null; categoryId: string; fractieNaam: string | null; categoryName: string; votesFor: string | null; votesAgainst: string | null; totalVotes: string; forPct: string }>` |
| 149 | + SELECT |
| 150 | + mv.fractie_id AS "fractieId", |
| 151 | + zc.category_id AS "categoryId", |
| 152 | + f.afkorting AS "fractieNaam", |
| 153 | + mc.name AS "categoryName", |
| 154 | + SUM(CASE WHEN mv.vote_type = 'Voor' THEN 1 ELSE 0 END) AS "votesFor", |
| 155 | + SUM(CASE WHEN mv.vote_type = 'Tegen' THEN 1 ELSE 0 END) AS "votesAgainst", |
| 156 | + COUNT(*) AS "totalVotes", |
| 157 | + ROUND(AVG(CASE WHEN mv.vote_type = 'Voor' THEN 1.0 ELSE 0.0 END) * 100, 2) AS "forPct" |
| 158 | + FROM majority_party_votes mv |
| 159 | + JOIN zaak_categories zc ON mv.zaak_id = zc.zaak_id |
| 160 | + JOIN motion_categories mc ON zc.category_id = mc.id |
| 161 | + JOIN fracties f ON mv.fractie_id = f.id |
| 162 | + WHERE (${fractieId} = '' OR mv.fractie_id = ${fractieId}) |
| 163 | + AND (${period} = 'all' OR EXTRACT(YEAR FROM mv.gestart_op)::text = ${period}) |
| 164 | + GROUP BY mv.fractie_id, zc.category_id, f.afkorting, mc.name |
| 165 | + HAVING COUNT(*) >= 5 |
| 166 | + ORDER BY "totalVotes" DESC |
| 167 | + `; |
| 168 | + |
| 169 | + return results |
| 170 | + .filter((r) => r.fractieId && r.fractieNaam) |
| 171 | + .map((r) => ({ |
| 172 | + fractieId: r.fractieId!, |
| 173 | + categoryId: r.categoryId, |
| 174 | + fractieNaam: r.fractieNaam!, |
| 175 | + categoryName: r.categoryName, |
| 176 | + votesFor: Number(r.votesFor), |
| 177 | + votesAgainst: Number(r.votesAgainst), |
| 178 | + totalVotes: Number(r.totalVotes), |
| 179 | + forPct: Number(r.forPct), |
| 180 | + })); |
| 181 | + }, |
| 182 | + ), |
| 183 | +}; |
0 commit comments