Files
kolaytercih/src/lib/danisman-profil.ts
bilalgursen a3cb058a3b
All checks were successful
Deploy / deploy (push) Successful in 7m45s
feat: update app.db and BACKLOG.md with new decisions and adjustments
- Updated the app.db file to reflect recent changes in the application state.
- Modified BACKLOG.md to include new entries regarding the removal of certain wizard steps, adjustments to priority chips, and the introduction of special quota conditions for candidates.
- Added details on the decision to link priority chips to data sources and clarified the implications for user experience and data handling.
2026-10-06 17:17:49 +03:00

130 lines
5.5 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.
/**
* 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<typeof PROFIL_SEMASI>;
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<ProfilAlani, string> = {
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<string, unknown> = {};
for (const alan of PROFIL_ALANLARI) {
const deger = (ham as Record<string, unknown>)[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(" ");
}