refactor: rapor feature'ı — listem, yazdır, sohbet ve actions taşındı (Faz 8)
- app/sonuc/actions.ts → features/rapor/rapor-actions.ts birebir taşındı (kredi harcama/iade idempotency yolları bayt bayt aynı, yalnızca import yolları değişti); liste-uretici/revizyon-kutusu action'ı doğrudan import ediyor - lib/rapor-kaydi.ts rapor-queries.ts'e emildi: kullanicininRaporu aynı tek okuma kapısı; listem sayfasındaki inline drizzle sorgusu sohbetGecmisiGetir oldu; tadimlikSec re-export'u eklendi - listem-govde, sohbet-client, revizyon-kutusu, liste-uretici, listem-bos-cta, rapor-listesi, tadimlik-satiri, yazdir-butonu features/rapor/components/'a - Yeni async ListemIcerik (verifySession + maskeleme + bosDurum dalı içeride; çift await searchParams kalktı) ve RaporYazdir (3 redirect + yazdırılabilir belge); skeleton'lar eski loading.tsx geometrileriyle aynı dosyada - /listem ve /rapor/yazdir senkron: searchParams.then() + sayfada Suspense; maxDuration=300 ve metadata sayfada kaldı; iki loading.tsx silindi - Davranış değişikliği yok; /api/soru el değmedi Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@ import {
|
|||||||
krediBittiIsaretle,
|
krediBittiIsaretle,
|
||||||
} from "@/lib/credits";
|
} from "@/lib/credits";
|
||||||
import { anahtarVar, SORUMLULUK_REDDI } from "@/lib/ai/client";
|
import { anahtarVar, SORUMLULUK_REDDI } from "@/lib/ai/client";
|
||||||
import { kullanicininRaporu } from "@/lib/rapor-kaydi";
|
import { kullanicininRaporu } from "@/features/rapor/rapor-queries";
|
||||||
import { sohbetAkisi } from "@/lib/ai/cagri";
|
import { sohbetAkisi } from "@/lib/ai/cagri";
|
||||||
import { listeOzetiCikar } from "@/lib/ai/rapor";
|
import { listeOzetiCikar } from "@/lib/ai/rapor";
|
||||||
import type { RaporSonuc } from "@/features/rapor/types/rapor";
|
import type { RaporSonuc } from "@/features/rapor/types/rapor";
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import { Skeleton } from "@/components/ui/skeleton";
|
|
||||||
|
|
||||||
export default function ListemLoading() {
|
|
||||||
return (
|
|
||||||
<main className="mx-auto w-full max-w-7xl px-4 py-10">
|
|
||||||
<Skeleton className="h-6 w-40 rounded-full" />
|
|
||||||
<Skeleton className="mt-4 h-9 w-3/4 max-w-xl" />
|
|
||||||
<Skeleton className="mt-3 h-5 w-2/3 max-w-lg" />
|
|
||||||
<div className="mt-8 grid items-start gap-8 lg:grid-cols-[minmax(0,1fr)_340px]">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
{Array.from({ length: 10 }).map((_, i) => (
|
|
||||||
<Skeleton key={i} className="h-16 w-full rounded-xl" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<Skeleton className="h-[480px] w-full rounded-2xl" />
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,21 +1,9 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { asc, eq } from "drizzle-orm";
|
import {
|
||||||
import { redirect } from "next/navigation";
|
ListemIcerik,
|
||||||
import { MessageCircleQuestion, Sparkles } from "lucide-react";
|
ListemIcerikSkeleton,
|
||||||
import { verifySession, getCurrentUser } from "@/lib/session";
|
} from "@/features/rapor/components/listem-icerik";
|
||||||
import { appDb, schema } from "@/lib/appdb";
|
|
||||||
import { kullanicininRaporu } from "@/lib/rapor-kaydi";
|
|
||||||
import { URUNLER, MAX_REVIZYON } from "@/lib/credits";
|
|
||||||
import type { RaporSonuc } from "@/features/rapor/types/rapor";
|
|
||||||
import type { RaporParams } from "@/features/rapor/types/rapor";
|
|
||||||
import { ViewportPortal } from "@/components/viewport-portal";
|
|
||||||
import { SiteFooter } from "@/components/site-footer";
|
|
||||||
import { raporMaskele, ACIK_SATIR } from "@/lib/rapor-maske";
|
|
||||||
import type { MaskeliRapor } from "@/features/rapor/types/rapor";
|
|
||||||
import type { Mesaj } from "./sohbet-client";
|
|
||||||
import { ListemGovde } from "./listem-govde";
|
|
||||||
import { ListemBosCta } from "./listem-bos-cta";
|
|
||||||
import { ListeUretici } from "./liste-uretici";
|
|
||||||
|
|
||||||
// listeRevize aksiyonu 2 AI denemesi yapabiliyor (deneme başına 60 sn abort
|
// listeRevize aksiyonu 2 AI denemesi yapabiliyor (deneme başına 60 sn abort
|
||||||
// sınırı, bkz. lib/ai/cagri.ts) → en kötü ~120 sn. Platform limiti bundan
|
// sınırı, bkz. lib/ai/cagri.ts) → en kötü ~120 sn. Platform limiti bundan
|
||||||
@@ -29,126 +17,16 @@ export const metadata: Metadata = {
|
|||||||
robots: { index: false, follow: false },
|
robots: { index: false, follow: false },
|
||||||
};
|
};
|
||||||
|
|
||||||
const PAKET_FIYATI = (URUNLER.paket.amountKurus / 100).toLocaleString("tr-TR", {
|
export default function ListemPage({
|
||||||
maximumFractionDigits: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default async function ListemPage({
|
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
searchParams: Promise<{ acildi?: string; uret?: string }>;
|
searchParams: Promise<{ acildi?: string; uret?: string }>;
|
||||||
}) {
|
}) {
|
||||||
// Girişsiz gelene callback'i ?uret=1 ile ver: girişten dönüşte bekleyen
|
|
||||||
// seçimlerle üretim kaldığı yerden başlasın.
|
|
||||||
const ilkParams = await searchParams;
|
|
||||||
const geriYol = ilkParams.uret === "1" ? "/listem?uret=1" : "/listem";
|
|
||||||
const session = await verifySession(geriYol);
|
|
||||||
const [user, params, satir, sohbetGecmisi] = await Promise.all([
|
|
||||||
getCurrentUser(),
|
|
||||||
searchParams,
|
|
||||||
kullanicininRaporu(session.user.id),
|
|
||||||
appDb
|
|
||||||
.select({
|
|
||||||
id: schema.chatMessages.id,
|
|
||||||
role: schema.chatMessages.role,
|
|
||||||
content: schema.chatMessages.content,
|
|
||||||
})
|
|
||||||
.from(schema.chatMessages)
|
|
||||||
.where(eq(schema.chatMessages.userId, session.user.id))
|
|
||||||
.orderBy(asc(schema.chatMessages.createdAt))
|
|
||||||
.limit(100),
|
|
||||||
]);
|
|
||||||
if (!user) redirect(`/giris?callback=${encodeURIComponent(geriYol)}`);
|
|
||||||
|
|
||||||
const { acildi } = params;
|
|
||||||
const kilitAcilisi = Boolean(user.hasPaket && acildi === "1");
|
|
||||||
const raporVar = Boolean(satir?.result);
|
|
||||||
|
|
||||||
if (!raporVar) {
|
|
||||||
const bosDurum = (
|
|
||||||
<section className="mx-auto flex max-w-2xl flex-col items-center px-4 py-16 text-center">
|
|
||||||
<Sparkles className="size-10 text-primary" aria-hidden />
|
|
||||||
<h1 className="mt-4 font-heading text-2xl font-bold">
|
|
||||||
Henüz Yapay Zeka listen yok
|
|
||||||
</h1>
|
|
||||||
<ListemBosCta />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<main className="mx-auto w-full max-w-4xl px-4 py-12">
|
|
||||||
{/* ?uret=1: sihirbazdan yönlendirildi — bekleyen seçimlerle üretim
|
|
||||||
burada koşar, animasyon ve hata/kredi durumları burada yaşar. */}
|
|
||||||
{params.uret === "1" ? (
|
|
||||||
<ListeUretici
|
|
||||||
hasPaket={Boolean(user.hasPaket)}
|
|
||||||
bosDurum={bosDurum}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
bosDurum
|
|
||||||
)}
|
|
||||||
</main>
|
|
||||||
<SiteFooter />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rapor: MaskeliRapor = user.hasPaket
|
|
||||||
? (satir!.result as RaporSonuc)
|
|
||||||
: raporMaskele(satir!.result as RaporSonuc);
|
|
||||||
const raporParams = satir!.params as RaporParams;
|
|
||||||
// Danışman özeti sohbetin açılış mesajı olarak gösterilir; sohbet
|
|
||||||
// /api/soru üzerinden zaten liste + seçim bağlamıyla çalışıyor.
|
|
||||||
// Paketsizde rapor zaten maskeli: 1 uyarı açık, kalanının yalnızca sayısı var.
|
|
||||||
const danismanOzeti = rapor.genelDegerlendirme
|
|
||||||
? {
|
|
||||||
metin: rapor.genelDegerlendirme,
|
|
||||||
uyarilar: rapor.uyarilar,
|
|
||||||
kilitli: !user.hasPaket,
|
|
||||||
kilitliUyariSayisi: user.hasPaket ? undefined : rapor.kilitliUyariSayisi,
|
|
||||||
}
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Suspense fallback={<ListemIcerikSkeleton />}>
|
||||||
<main className="mx-auto w-full max-w-7xl px-4 py-12">
|
{searchParams.then(({ acildi, uret }) => (
|
||||||
{kilitAcilisi ? (
|
<ListemIcerik uret={uret === "1"} acildi={acildi === "1"} />
|
||||||
<div
|
))}
|
||||||
role="status"
|
</Suspense>
|
||||||
className="mb-6 flex items-center gap-3 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-800"
|
|
||||||
>
|
|
||||||
<Sparkles className="size-4 shrink-0" aria-hidden />
|
|
||||||
Paketin aktif — listendeki tercihlerin tamamı, gerekçeler, riskler ve
|
|
||||||
PDF artık açık.
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{/* Başlık ve revizyon/PDF aksiyonları listenin kendi başlık satırında
|
|
||||||
(bkz. listem-govde.tsx) — sayfa üstünde ayrı bir header yok. */}
|
|
||||||
<ListemGovde
|
|
||||||
rapor={rapor}
|
|
||||||
adaySira={raporParams.sira}
|
|
||||||
hasPaket={Boolean(user.hasPaket)}
|
|
||||||
kilitAcilisi={kilitAcilisi}
|
|
||||||
kilitliBaslangic={user.hasPaket ? undefined : ACIK_SATIR}
|
|
||||||
kredi={user.creditBalance}
|
|
||||||
sohbetGecmisi={sohbetGecmisi as Mesaj[]}
|
|
||||||
ozet={danismanOzeti}
|
|
||||||
kalanHak={Math.max(0, MAX_REVIZYON - (satir!.revisionCount ?? 0))}
|
|
||||||
paketFiyati={PAKET_FIYATI}
|
|
||||||
paketKredi={URUNLER.paket.credits}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ViewportPortal>
|
|
||||||
<a
|
|
||||||
href="#sohbet"
|
|
||||||
className="fixed bottom-5 right-4 z-40 inline-flex h-11 items-center gap-2 rounded-full bg-primary px-4 text-sm font-semibold text-white shadow-lg transition-transform duration-200 active:scale-[0.97] lg:hidden print:hidden"
|
|
||||||
>
|
|
||||||
<MessageCircleQuestion className="size-4" aria-hidden />
|
|
||||||
Danışmana sor
|
|
||||||
</a>
|
|
||||||
</ViewportPortal>
|
|
||||||
</main>
|
|
||||||
<SiteFooter />
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { eq } from "drizzle-orm";
|
|||||||
import { ArrowRight, CheckCircle2, XCircle } from "lucide-react";
|
import { ArrowRight, CheckCircle2, XCircle } from "lucide-react";
|
||||||
import { verifySession } from "@/lib/session";
|
import { verifySession } from "@/lib/session";
|
||||||
import { appDb, schema } from "@/lib/appdb";
|
import { appDb, schema } from "@/lib/appdb";
|
||||||
import { kullanicininRaporu } from "@/lib/rapor-kaydi";
|
import { kullanicininRaporu } from "@/features/rapor/rapor-queries";
|
||||||
import { odemeyiSonuclandir } from "@/lib/odeme";
|
import { odemeyiSonuclandir } from "@/lib/odeme";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { PagePixelDivider } from "@/components/pixel-decor";
|
import { PagePixelDivider } from "@/components/pixel-decor";
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import { Skeleton } from "@/components/ui/skeleton";
|
|
||||||
|
|
||||||
// /rapor/yazdir rapor sorgusunu bekler — belge düzeninde iskelet.
|
|
||||||
export default function Loading() {
|
|
||||||
return (
|
|
||||||
<main
|
|
||||||
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>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,142 +1,19 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Image from "next/image";
|
import {
|
||||||
import { redirect } from "next/navigation";
|
RaporYazdir,
|
||||||
import { verifySession, getCurrentUser } from "@/lib/session";
|
RaporYazdirSkeleton,
|
||||||
import { kullanicininRaporu } from "@/lib/rapor-kaydi";
|
} from "@/features/rapor/components/rapor-yazdir";
|
||||||
import { PUAN_TURLERI } from "@/types/yokatlas";
|
|
||||||
import type { RaporSonuc } from "@/features/rapor/types/rapor";
|
|
||||||
import type { RaporParams } from "@/features/rapor/types/rapor";
|
|
||||||
import { YazdirButonu } from "./yazdir-butonu";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Tercih Raporu (Yazdır)",
|
title: "Tercih Raporu (Yazdır)",
|
||||||
robots: { index: false, follow: false },
|
robots: { index: false, follow: false },
|
||||||
};
|
};
|
||||||
|
|
||||||
const DILIM_LABEL: Record<string, string> = {
|
export default function YazdirPage() {
|
||||||
hayal: "Hayal (Riskli)",
|
|
||||||
dengeli: "Dengeli (Az riskli)",
|
|
||||||
garanti: "Garanti (Güvenli)",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function YazdirPage() {
|
|
||||||
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;
|
|
||||||
const tarih = new Date(satir.updatedAt).toLocaleDateString("tr-TR", {
|
|
||||||
day: "numeric",
|
|
||||||
month: "long",
|
|
||||||
year: "numeric",
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-3xl bg-white p-8 text-slate-900 print:p-[10mm]">
|
<Suspense fallback={<RaporYazdirSkeleton />}>
|
||||||
<YazdirButonu />
|
<RaporYazdir />
|
||||||
|
</Suspense>
|
||||||
<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} garanti 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">{DILIM_LABEL[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 (2021–2025
|
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import type { Metadata } from "next";
|
|||||||
import { rankWindowFacets, searchByRank } from "@/lib/db";
|
import { rankWindowFacets, searchByRank } from "@/lib/db";
|
||||||
import { PUAN_TURLERI, type PuanTuruKey } from "@/types/yokatlas";
|
import { PUAN_TURLERI, type PuanTuruKey } from "@/types/yokatlas";
|
||||||
import { getSession } from "@/lib/session";
|
import { getSession } from "@/lib/session";
|
||||||
import { kullanicininRaporu } from "@/lib/rapor-kaydi";
|
import { kullanicininRaporu } from "@/features/rapor/rapor-queries";
|
||||||
import { tadimlikSec } from "@/lib/tadimlik-havuzu";
|
import { tadimlikSec } from "@/lib/tadimlik-havuzu";
|
||||||
import { SihirbazBolumu } from "@/features/sihirbaz/components/sihirbaz-cagri-karti";
|
import { SihirbazBolumu } from "@/features/sihirbaz/components/sihirbaz-cagri-karti";
|
||||||
import { TadimlikSatiri } from "./tadimlik-satiri";
|
import { TadimlikSatiri } from "@/features/rapor/components/tadimlik-satiri";
|
||||||
import { ProgramTablosu } from "@/features/liste/components/program-tablosu";
|
import { ProgramTablosu } from "@/features/liste/components/program-tablosu";
|
||||||
import { ManuelHarita } from "@/features/liste/components/manuel-harita";
|
import { ManuelHarita } from "@/features/liste/components/manuel-harita";
|
||||||
import { SiteFooter } from "@/components/site-footer";
|
import { SiteFooter } from "@/components/site-footer";
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { useRouter } from "next/navigation";
|
|||||||
import { ArrowRight, Lock, RefreshCcw } from "lucide-react";
|
import { ArrowRight, Lock, RefreshCcw } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { listeOlustur } from "@/app/sonuc/actions";
|
import { listeOlustur } from "@/features/rapor/rapor-actions";
|
||||||
import { olay, siraKovasi } from "@/lib/analitik";
|
import { olay, siraKovasi } from "@/lib/analitik";
|
||||||
import { sonucHref, type TercihProfili } from "@/features/sihirbaz/sihirbaz-sabitler";
|
import { sonucHref, type TercihProfili } from "@/features/sihirbaz/sihirbaz-sabitler";
|
||||||
import { tercihProfiliOku, tercihProfiliSil } from "@/features/sihirbaz/sihirbaz-profil";
|
import { tercihProfiliOku, tercihProfiliSil } from "@/features/sihirbaz/sihirbaz-profil";
|
||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { RaporListesi } from "@/components/rapor-listesi";
|
import { RaporListesi } from "./rapor-listesi";
|
||||||
import { useGorunumOlayi } from "@/components/use-gorunum-olayi";
|
import { useGorunumOlayi } from "@/components/use-gorunum-olayi";
|
||||||
import { TercihHaritasi } from "@/features/liste/components/tercih-haritasi";
|
import { TercihHaritasi } from "@/features/liste/components/tercih-haritasi";
|
||||||
import { pinleriTuret } from "@/lib/harita-pinler";
|
import { pinleriTuret } from "@/lib/harita-pinler";
|
||||||
150
src/features/rapor/components/listem-icerik.tsx
Normal file
150
src/features/rapor/components/listem-icerik.tsx
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { MessageCircleQuestion, Sparkles } from "lucide-react";
|
||||||
|
import { verifySession, getCurrentUser } from "@/lib/session";
|
||||||
|
import { kullanicininRaporu, sohbetGecmisiGetir } from "../rapor-queries";
|
||||||
|
import { URUNLER, MAX_REVIZYON } from "@/lib/credits";
|
||||||
|
import type {
|
||||||
|
MaskeliRapor,
|
||||||
|
RaporParams,
|
||||||
|
RaporSonuc,
|
||||||
|
} from "../types/rapor";
|
||||||
|
import { ViewportPortal } from "@/components/viewport-portal";
|
||||||
|
import { SiteFooter } from "@/components/site-footer";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { raporMaskele, ACIK_SATIR } from "@/lib/rapor-maske";
|
||||||
|
import type { Mesaj } from "./sohbet-client";
|
||||||
|
import { ListemGovde } from "./listem-govde";
|
||||||
|
import { ListemBosCta } from "./listem-bos-cta";
|
||||||
|
import { ListeUretici } from "./liste-uretici";
|
||||||
|
|
||||||
|
const PAKET_FIYATI = (URUNLER.paket.amountKurus / 100).toLocaleString("tr-TR", {
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function ListemIcerik({
|
||||||
|
uret,
|
||||||
|
acildi,
|
||||||
|
}: {
|
||||||
|
uret: boolean;
|
||||||
|
acildi: boolean;
|
||||||
|
}) {
|
||||||
|
// Girişsiz gelene callback'i ?uret=1 ile ver: girişten dönüşte bekleyen
|
||||||
|
// seçimlerle üretim kaldığı yerden başlasın.
|
||||||
|
const geriYol = uret ? "/listem?uret=1" : "/listem";
|
||||||
|
const session = await verifySession(geriYol);
|
||||||
|
const [user, satir, sohbetGecmisi] = await Promise.all([
|
||||||
|
getCurrentUser(),
|
||||||
|
kullanicininRaporu(session.user.id),
|
||||||
|
sohbetGecmisiGetir(session.user.id),
|
||||||
|
]);
|
||||||
|
if (!user) redirect(`/giris?callback=${encodeURIComponent(geriYol)}`);
|
||||||
|
|
||||||
|
const kilitAcilisi = Boolean(user.hasPaket && acildi);
|
||||||
|
const raporVar = Boolean(satir?.result);
|
||||||
|
|
||||||
|
if (!raporVar) {
|
||||||
|
const bosDurum = (
|
||||||
|
<section className="mx-auto flex max-w-2xl flex-col items-center px-4 py-16 text-center">
|
||||||
|
<Sparkles className="size-10 text-primary" aria-hidden />
|
||||||
|
<h1 className="mt-4 font-heading text-2xl font-bold">
|
||||||
|
Henüz Yapay Zeka listen yok
|
||||||
|
</h1>
|
||||||
|
<ListemBosCta />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<main className="mx-auto w-full max-w-4xl px-4 py-12">
|
||||||
|
{/* ?uret=1: sihirbazdan yönlendirildi — bekleyen seçimlerle üretim
|
||||||
|
burada koşar, animasyon ve hata/kredi durumları burada yaşar. */}
|
||||||
|
{uret ? (
|
||||||
|
<ListeUretici
|
||||||
|
hasPaket={Boolean(user.hasPaket)}
|
||||||
|
bosDurum={bosDurum}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
bosDurum
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
<SiteFooter />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rapor: MaskeliRapor = user.hasPaket
|
||||||
|
? (satir!.result as RaporSonuc)
|
||||||
|
: raporMaskele(satir!.result as RaporSonuc);
|
||||||
|
const raporParams = satir!.params as RaporParams;
|
||||||
|
// Danışman özeti sohbetin açılış mesajı olarak gösterilir; sohbet
|
||||||
|
// /api/soru üzerinden zaten liste + seçim bağlamıyla çalışıyor.
|
||||||
|
// Paketsizde rapor zaten maskeli: 1 uyarı açık, kalanının yalnızca sayısı var.
|
||||||
|
const danismanOzeti = rapor.genelDegerlendirme
|
||||||
|
? {
|
||||||
|
metin: rapor.genelDegerlendirme,
|
||||||
|
uyarilar: rapor.uyarilar,
|
||||||
|
kilitli: !user.hasPaket,
|
||||||
|
kilitliUyariSayisi: user.hasPaket ? undefined : rapor.kilitliUyariSayisi,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<main className="mx-auto w-full max-w-7xl px-4 py-12">
|
||||||
|
{kilitAcilisi ? (
|
||||||
|
<div
|
||||||
|
role="status"
|
||||||
|
className="mb-6 flex items-center gap-3 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-800"
|
||||||
|
>
|
||||||
|
<Sparkles className="size-4 shrink-0" aria-hidden />
|
||||||
|
Paketin aktif — listendeki tercihlerin tamamı, gerekçeler, riskler ve
|
||||||
|
PDF artık açık.
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{/* Başlık ve revizyon/PDF aksiyonları listenin kendi başlık satırında
|
||||||
|
(bkz. listem-govde.tsx) — sayfa üstünde ayrı bir header yok. */}
|
||||||
|
<ListemGovde
|
||||||
|
rapor={rapor}
|
||||||
|
adaySira={raporParams.sira}
|
||||||
|
hasPaket={Boolean(user.hasPaket)}
|
||||||
|
kilitAcilisi={kilitAcilisi}
|
||||||
|
kilitliBaslangic={user.hasPaket ? undefined : ACIK_SATIR}
|
||||||
|
kredi={user.creditBalance}
|
||||||
|
sohbetGecmisi={sohbetGecmisi as Mesaj[]}
|
||||||
|
ozet={danismanOzeti}
|
||||||
|
kalanHak={Math.max(0, MAX_REVIZYON - (satir!.revisionCount ?? 0))}
|
||||||
|
paketFiyati={PAKET_FIYATI}
|
||||||
|
paketKredi={URUNLER.paket.credits}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ViewportPortal>
|
||||||
|
<a
|
||||||
|
href="#sohbet"
|
||||||
|
className="fixed bottom-5 right-4 z-40 inline-flex h-11 items-center gap-2 rounded-full bg-primary px-4 text-sm font-semibold text-white shadow-lg transition-transform duration-200 active:scale-[0.97] lg:hidden print:hidden"
|
||||||
|
>
|
||||||
|
<MessageCircleQuestion className="size-4" aria-hidden />
|
||||||
|
Danışmana sor
|
||||||
|
</a>
|
||||||
|
</ViewportPortal>
|
||||||
|
</main>
|
||||||
|
<SiteFooter />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ListemIcerikSkeleton() {
|
||||||
|
return (
|
||||||
|
<main className="mx-auto w-full max-w-7xl px-4 py-10">
|
||||||
|
<Skeleton className="h-6 w-40 rounded-full" />
|
||||||
|
<Skeleton className="mt-4 h-9 w-3/4 max-w-xl" />
|
||||||
|
<Skeleton className="mt-3 h-5 w-2/3 max-w-lg" />
|
||||||
|
<div className="mt-8 grid items-start gap-8 lg:grid-cols-[minmax(0,1fr)_340px]">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{Array.from({ length: 10 }).map((_, i) => (
|
||||||
|
<Skeleton key={i} className="h-16 w-full rounded-xl" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Skeleton className="h-[480px] w-full rounded-2xl" />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
153
src/features/rapor/components/rapor-yazdir.tsx
Normal file
153
src/features/rapor/components/rapor-yazdir.tsx
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { verifySession, getCurrentUser } from "@/lib/session";
|
||||||
|
import { kullanicininRaporu } from "../rapor-queries";
|
||||||
|
import { PUAN_TURLERI } from "@/types/yokatlas";
|
||||||
|
import type { RaporParams, RaporSonuc } from "../types/rapor";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { YazdirButonu } from "./yazdir-butonu";
|
||||||
|
|
||||||
|
const DILIM_LABEL: Record<string, string> = {
|
||||||
|
hayal: "Hayal (Riskli)",
|
||||||
|
dengeli: "Dengeli (Az riskli)",
|
||||||
|
garanti: "Garanti (Güvenli)",
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
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} garanti 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">{DILIM_LABEL[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 (2021–2025
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import { useRouter } from "next/navigation";
|
|||||||
import { RefreshCcw } from "lucide-react";
|
import { RefreshCcw } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { listeRevize } from "@/app/sonuc/actions";
|
import { listeRevize } from "@/features/rapor/rapor-actions";
|
||||||
import { olay } from "@/lib/analitik";
|
import { olay } from "@/lib/analitik";
|
||||||
|
|
||||||
export function RevizyonKutusu({ kalanHak }: { kalanHak: number }) {
|
export function RevizyonKutusu({ kalanHak }: { kalanHak: number }) {
|
||||||
@@ -14,7 +14,7 @@ import { ArrowRight } from "lucide-react";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { UniLogo } from "@/components/uni-logo";
|
import { UniLogo } from "@/components/uni-logo";
|
||||||
import { SectionEyebrow } from "@/components/pixel-decor";
|
import { SectionEyebrow } from "@/components/pixel-decor";
|
||||||
import { RaporSatirDetayi } from "@/components/rapor-listesi";
|
import { RaporSatirDetayi } from "./rapor-listesi";
|
||||||
import {
|
import {
|
||||||
ProgramSiraGecmisi,
|
ProgramSiraGecmisi,
|
||||||
ProgramTabanTrendi,
|
ProgramTabanTrendi,
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
import { and, eq, sql } from "drizzle-orm";
|
import { and, eq, sql } from "drizzle-orm";
|
||||||
import { getSession, getCurrentUser } from "@/lib/session";
|
import { getSession, getCurrentUser } from "@/lib/session";
|
||||||
import { appDb, schema } from "@/lib/appdb";
|
import { appDb, schema } from "@/lib/appdb";
|
||||||
import { kullanicininRaporu } from "@/lib/rapor-kaydi";
|
import { kullanicininRaporu } from "@/features/rapor/rapor-queries";
|
||||||
import { PUAN_TURLERI, type PuanTuruKey } from "@/types/yokatlas";
|
import { PUAN_TURLERI, type PuanTuruKey } from "@/types/yokatlas";
|
||||||
import {
|
import {
|
||||||
spendCredits,
|
spendCredits,
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
import "server-only";
|
import "server-only";
|
||||||
import { eq } from "drizzle-orm";
|
import { asc, eq } from "drizzle-orm";
|
||||||
import { appDb, schema } from "@/lib/appdb";
|
import { appDb, schema } from "@/lib/appdb";
|
||||||
import { anahtarVar } from "@/lib/ai/client";
|
import { anahtarVar } from "@/lib/ai/client";
|
||||||
import type { RaporSonuc } from "@/features/rapor/types/rapor";
|
import type { RaporSonuc } from "./types/rapor";
|
||||||
|
|
||||||
|
// Rapor feature'ının sorgu kapısı: kayıtlı rapor, sohbet geçmişi ve girişsiz
|
||||||
|
// tadımlık seçimi buradan okunur.
|
||||||
|
|
||||||
|
// Girişsiz tadımlık satırı — batch üretilmiş havuzdan seçilir, LLM'e gitmez.
|
||||||
|
export { tadimlikSec } from "@/lib/tadimlik-havuzu";
|
||||||
|
|
||||||
// Kullanıcının kayıtlı raporunu okuyan TEK kapı. Rapor tablosunu okuyan her
|
// Kullanıcının kayıtlı raporunu okuyan TEK kapı. Rapor tablosunu okuyan her
|
||||||
// yer (sayfalar, action'lar, soru API'si) buradan geçmeli; doğrudan
|
// yer (sayfalar, action'lar, soru API'si) buradan geçmeli; doğrudan
|
||||||
@@ -33,3 +39,17 @@ export async function kullanicininRaporu(userId: string) {
|
|||||||
if (satir?.result && bayatMockMu(satir.result as RaporSonuc)) return null;
|
if (satir?.result && bayatMockMu(satir.result as RaporSonuc)) return null;
|
||||||
return satir ?? null;
|
return satir ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Kullanıcının danışman sohbet geçmişi, eskiden yeniye (en fazla 100 mesaj). */
|
||||||
|
export function sohbetGecmisiGetir(userId: string) {
|
||||||
|
return appDb
|
||||||
|
.select({
|
||||||
|
id: schema.chatMessages.id,
|
||||||
|
role: schema.chatMessages.role,
|
||||||
|
content: schema.chatMessages.content,
|
||||||
|
})
|
||||||
|
.from(schema.chatMessages)
|
||||||
|
.where(eq(schema.chatMessages.userId, userId))
|
||||||
|
.orderBy(asc(schema.chatMessages.createdAt))
|
||||||
|
.limit(100);
|
||||||
|
}
|
||||||
@@ -31,7 +31,7 @@ export type RaporSonuc = {
|
|||||||
uyarilar?: string[];
|
uyarilar?: string[];
|
||||||
kapsam?: RaporKapsam;
|
kapsam?: RaporKapsam;
|
||||||
// Dev mock üretimi işareti: gerçek AI aktifken bayat mock kayıtları
|
// Dev mock üretimi işareti: gerçek AI aktifken bayat mock kayıtları
|
||||||
// ayıklamak için (bkz. src/lib/rapor-kaydi.ts)
|
// ayıklamak için (bkz. features/rapor/rapor-queries.ts)
|
||||||
devMock?: boolean;
|
devMock?: boolean;
|
||||||
// Render için havuzdan zenginleştirilmiş program bilgisi
|
// Render için havuzdan zenginleştirilmiş program bilgisi
|
||||||
// efektifSira: COALESCE(sira2025, sira2024) — eski kayıtlı raporlarda yok
|
// efektifSira: COALESCE(sira2025, sira2024) — eski kayıtlı raporlarda yok
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { appDb, schema } from "@/lib/appdb";
|
import { appDb, schema } from "@/lib/appdb";
|
||||||
import { kullanicininRaporu } from "@/lib/rapor-kaydi";
|
import { kullanicininRaporu } from "@/features/rapor/rapor-queries";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { getCurrentUser } from "@/lib/session";
|
import { getCurrentUser } from "@/lib/session";
|
||||||
import { URUNLER } from "@/lib/credits";
|
import { URUNLER } from "@/lib/credits";
|
||||||
|
|||||||
Reference in New Issue
Block a user