Files
kolaytercih/src/features/rapor/components/rapor-yazdir.tsx
2026-09-21 17:05:14 +03:00

175 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Image from "next/image";
import { redirect } from "next/navigation";
import { verifySession, getCurrentUser } from "@/lib/session";
import { kullanicininRaporu } from "../rapor-queries";
import { PUAN_TURLERI, type DilimKey } from "@/types/yokatlas";
import { DILIM_ETIKET, RISK_ETIKET, dilimdenRisk } from "@/lib/risk";
import type { RaporParams, RaporSonuc } from "../types/rapor";
import { Skeleton } from "@/components/ui/skeleton";
import { YazdirButonu } from "./yazdir-butonu";
import { ILK_YIL, VERI_ARALIGI } from "@/lib/veri-yillari";
// Dilim sütunu: dilim adı + varsayılan riski. Üçüncü dilimde ikisi aynı
// kelime ("Güvenli") olduğu için parantez düşer — "Güvenli (Güvenli)" yazmaz.
function dilimLabel(dilim: DilimKey): string {
const ad = DILIM_ETIKET[dilim] ?? dilim;
const risk = RISK_ETIKET[dilimdenRisk(dilim)];
return ad === risk ? ad : `${ad} (${risk})`;
}
export async function RaporYazdir() {
const session = await verifySession("/rapor/yazdir");
// Kullanıcı satırı ile rapor sorgusu bağımsız — paralel çöz
const [user, satir] = await Promise.all([
getCurrentUser(),
kullanicininRaporu(session.user.id),
]);
if (!user) redirect("/giris");
if (!user.hasPaket) redirect("/paket");
if (!satir?.result) redirect("/sonuc");
const rapor = satir.result as RaporSonuc;
const params = satir.params as RaporParams;
// Dipnottaki yıl aralığı raporun ÜRETİLDİĞİ veri penceresidir: eski kayıtlı
// rapor (5 yıllık geçmiş) güncel "2021–2026"yı iddia etmesin.
const gecmisler = Object.values(rapor.programlar).filter(
(p) => p.siraGecmisi && p.siraGecmisi.length > 0,
);
const veriAraligi =
gecmisler.length > 0
? (() => {
const ilk = Math.min(...gecmisler.map((p) => p.ilkYil ?? ILK_YIL));
const son = Math.max(
...gecmisler.map(
(p) => (p.ilkYil ?? ILK_YIL) + p.siraGecmisi!.length - 1,
),
);
return `${ilk}–${son}`;
})()
: VERI_ARALIGI;
const tarih = new Date(satir.updatedAt).toLocaleDateString("tr-TR", {
day: "numeric",
month: "long",
year: "numeric",
});
return (
<div className="mx-auto max-w-3xl bg-white p-8 text-slate-900 print:p-[10mm]">
<YazdirButonu />
<header className="border-b-2 border-slate-900 pb-4">
<h1 className="font-heading text-2xl font-bold">
{rapor.tercihler.length} Tercihlik Kişisel Liste ve Risk Raporu
</h1>
<p className="mt-1 text-sm text-slate-600">
Başarı sıralaması: {params.sira.toLocaleString("tr-TR")} · Puan
türü: {PUAN_TURLERI[params.tur]}
{params.il ? ` · İl: ${params.il}` : ""} · {tarih}
</p>
{rapor.kapsam?.uyarlandi ? (
<p className="mt-1 text-sm text-slate-600">
Not: Tercihlerine göre {rapor.kapsam.toplam} uygun program bulundu;
liste {rapor.kapsam.dagilim.hayal} hayal ·{" "}
{rapor.kapsam.dagilim.dengeli} dengeli ·{" "}
{rapor.kapsam.dagilim.garanti} güvenli olarak kuruldu.
</p>
) : null}
</header>
<section className="mt-5">
<h2 className="font-heading text-base font-bold">
Genel değerlendirme
</h2>
<p className="mt-1 whitespace-pre-line text-sm leading-relaxed">
{rapor.genelDegerlendirme}
</p>
</section>
<table className="mt-5 w-full border-collapse text-xs">
<thead>
<tr className="border-b-2 border-slate-900 text-left">
<th className="py-2 pr-2">#</th>
<th className="py-2 pr-2">Program / Üniversite</th>
<th className="py-2 pr-2">Dilim</th>
<th className="py-2 pr-2">Gerekçe</th>
<th className="py-2 pr-2">Risk notu</th>
<th className="py-2">Trend</th>
</tr>
</thead>
<tbody>
{rapor.tercihler.map((t) => {
const p = rapor.programlar[t.programId];
return (
<tr
key={t.sira}
className="break-inside-avoid border-b border-slate-200 align-top"
>
<td className="py-2 pr-2 font-bold">{t.sira}</td>
<td className="py-2 pr-2">
<span className="font-semibold">{p?.isim}</span>
<br />
<span className="text-slate-600">
{p?.universite} · {p?.il ?? "—"}
</span>
</td>
<td className="py-2 pr-2">{dilimLabel(t.dilim)}</td>
<td className="py-2 pr-2">{t.gerekce}</td>
<td className="py-2 pr-2">{t.riskNotu}</td>
<td className="py-2">{t.trendOzeti}</td>
</tr>
);
})}
</tbody>
</table>
<footer className="mt-6 border-t border-slate-300 pt-3 text-[10px] leading-relaxed text-slate-500">
<p>
Bu rapor KolayTercih tarafından resmî YÖK Atlas verisi ({veriAraligi}{" "}
taban sıralamaları, kontenjan ve yerleşme istatistikleri) kullanılarak
üretilmiştir. Taban sıralamaları her yıl değişir; bu rapor yerleşme
garantisi vermez. Tercih listesinin ÖSYM sistemine girilmesi ve nihai
sorumluluğu adaya aittir.
</p>
<div className="mt-4 flex break-inside-avoid items-center justify-between">
<div className="flex items-center font-bricolage text-slate-900">
<Image
src="/kolay-tercih-mark.svg"
alt=""
width={44}
height={44}
aria-hidden="true"
className="size-11 shrink-0"
/>
<span>
<span className="font-light text-3xl tracking-tighter">Kolay</span>
<span className="font-semibold text-3xl tracking-tighter">
Tercih
</span>
</span>
</div>
<span className="font-bricolage text-sm font-medium text-slate-900">
kolaytercih.com
</span>
</div>
</footer>
</div>
);
}
// Rapor sorgusu beklenirken belge düzeninde iskelet.
export function RaporYazdirSkeleton() {
return (
<div aria-live="polite" className="mx-auto w-full max-w-3xl bg-white p-8">
<div className="border-b-2 border-slate-200 pb-4">
<Skeleton className="h-8 w-64" />
<Skeleton className="mt-2 h-4 w-40" />
</div>
<div className="mt-6 space-y-3">
{Array.from({ length: 8 }).map((_, i) => (
<Skeleton key={i} className="h-14 w-full rounded-lg" />
))}
</div>
</div>
);
}