/** * Danışman profili — sohbetten çıkarılan öğrenci tercihleri. KAPALI LİSTE: * yalnız burada tanımlı alanlar yazılabilir, serbest metin alanı yoktur. * Neden: model sohbetten din, sağlık, aile gibi özel nitelikli veriyi profile * taşıyamasın (KVKK md. 6); öğrenci her alanı görür ve tek tıkla siler * (docs/arastirma/2026-09-25-kisisellestirme/README.md §8). * * Sorulmayanlar: cinsiyet, sağlık/engel, gelir. Ücret üst sınırı sorulur, gelir * sorulmaz. Konum il düzeyindedir (adres/koordinat değil). Özel kontenjan * durumu (okul birincisi, şehit-gazi yakını, depremzede, 34 yaş üstü kadın) * ÖSYM kontenjan hakkının beyanıdır, cinsiyet alanı değildir (6 Eki 2026). * * KULLANIM SINIRI (karar, 25 Eyl 2026): profil yalnız liste üretimi * (rapor-havuzu) ve danışman aramalarında kullanılır; pazarlama, e-posta, * segmentleme ya da analitik değerinde KULLANILMAZ — kullanılırsa açık rıza * konusu olur (hukuk-ve-rakipler.md C.3/13). Profil boşken hizmet tam çalışır. * * Saf modül: client, server ve scripts aynı dosyayı okur. */ import { z } from "zod"; import { OZEL_DURUMLAR, OZEL_DURUM_KONTENJAN_ADI, ozelDurumDogrula, type OzelDurum } from "./ozel-durum"; export const PROFIL_SECENEKLERI = { burs: { "yalniz-burslu": "yalnız burslu", yuzde50: "%50 burslu da olur", "ucretli-olur": "ücretli de olur", }, dil: { turkce: "Türkçe", ingilizce: "İngilizce", farketmez: "fark etmez", }, ogretimTuru: { "yalniz-orgun": "yalnız örgün", "uzaktan-olur": "uzaktan/açık da olur", }, kampus: { sakin: "sakin, küçük", kalabalik: "kalabalık, büyük", farketmez: "fark etmez", }, } as const; export const PROFIL_SEMASI = z .object({ /** Yaşadığı il — DB'deki il adı biçimi (büyük harf, Türkçe). */ evIl: z.string().min(2).max(40).optional(), /** Evden en fazla kaç km (il merkezleri arası kuş uçuşu). */ maxKm: z.number().int().min(50).max(1500).optional(), /** Vakıfta yıllık ücret üst sınırı (TL). */ ucretUst: z.number().int().min(50_000).max(5_000_000).optional(), burs: z.enum(["yalniz-burslu", "yuzde50", "ucretli-olur"]).optional(), dil: z.enum(["turkce", "ingilizce", "farketmez"]).optional(), ogretimTuru: z.enum(["yalniz-orgun", "uzaktan-olur"]).optional(), kampus: z.enum(["sakin", "kalabalik", "farketmez"]).optional(), /** Özel kontenjan durumu (lib/ozel-durum.ts); sihirbazdaki alanla birleşir. */ ozelDurum: z.array(z.enum(OZEL_DURUMLAR)).max(OZEL_DURUMLAR.length).optional(), }) .strict(); export type DanismanProfili = z.infer; export type ProfilAlani = keyof DanismanProfili; export const PROFIL_ALANLARI = Object.keys(PROFIL_SEMASI.shape) as ProfilAlani[]; /** Alanın öğrenciye görünen kısa açıklaması (çip ve araç şeması aynı metni kullanır). */ export const PROFIL_ALAN_ACIKLAMA: Record = { evIl: "Yaşadığı il (evden uzaklık için)", maxKm: "Evden en fazla kaç km uzakta okumak istediği", ucretUst: "Vakıfta yıllık ücret üst sınırı (TL)", burs: "Vakıfta burs beklentisi", dil: "Öğretim dili tercihi", ogretimTuru: "Uzaktan/açık öğretim kabul mü", kampus: "Kampüs büyüklüğü tercihi", ozelDurum: "Özel kontenjan durumu (okul-birincisi, sehit-gazi-yakini, depremzede, kadin-34; virgülle birden çok)", }; /** Bilinmeyen anahtarları atarak profili güvenli okur; bozuk değer alanı düşürür. */ export function profilTemizle(ham: unknown): DanismanProfili { if (!ham || typeof ham !== "object") return {}; const sonuc: Record = {}; for (const alan of PROFIL_ALANLARI) { const deger = (ham as Record)[alan]; if (deger === undefined || deger === null) continue; const tek = PROFIL_SEMASI.shape[alan].safeParse(deger); if (tek.success) sonuc[alan] = tek.data; } return sonuc as DanismanProfili; } /** Tek alanı öğrencinin okuyacağı cümleye çevirir: "Yıllık ücret en fazla 300.000 TL". */ export function profilAlaniMetni(alan: ProfilAlani, deger: unknown): string { switch (alan) { case "evIl": return `Yaşadığı il: ${ilBaslik(String(deger))}`; case "maxKm": return `Evden en fazla ${Number(deger).toLocaleString("tr-TR")} km`; case "ucretUst": return `Yıllık ücret en fazla ${Number(deger).toLocaleString("tr-TR")} TL`; case "burs": return `Vakıfta ${PROFIL_SECENEKLERI.burs[deger as keyof typeof PROFIL_SECENEKLERI.burs]}`; case "dil": return `Öğretim dili: ${PROFIL_SECENEKLERI.dil[deger as keyof typeof PROFIL_SECENEKLERI.dil]}`; case "ogretimTuru": return deger === "yalniz-orgun" ? "Yalnız örgün öğretim" : "Uzaktan/açık öğretim de olur"; case "kampus": return `Kampüs: ${PROFIL_SECENEKLERI.kampus[deger as keyof typeof PROFIL_SECENEKLERI.kampus]}`; case "ozelDurum": return `Özel kontenjan: ${ozelDurumDogrula(deger) .map((d: OzelDurum) => OZEL_DURUM_KONTENJAN_ADI[d].replace(/ kontenjanı$/, "")) .join(", ")}`; } } /** Profilin tamamı, dolu alanlar sırayla. */ export function profilSatirlari(p: DanismanProfili): { alan: ProfilAlani; metin: string }[] { return PROFIL_ALANLARI.flatMap((alan) => p[alan] === undefined ? [] : [{ alan, metin: profilAlaniMetni(alan, p[alan]) }] ); } function ilBaslik(il: string): string { return il .toLocaleLowerCase("tr-TR") .split(" ") .map((k) => k.charAt(0).toLocaleUpperCase("tr-TR") + k.slice(1)) .join(" "); }