from decimal import Decimal from django.contrib import messages from django.db import transaction from django.db.models import Prefetch from django.shortcuts import get_object_or_404, redirect, render from .forms import ProblemCaseForm from .models import ActionPlanStep, ProblemCase, RootCause, SolutionOption def _clamp(value, minimum=1, maximum=100): return max(minimum, min(maximum, int(value))) def _has_any(text, keywords): return any(keyword in text for keyword in keywords) def _decision_score(impact, efficiency, speed, low_risk): return Decimal(str(round((impact * 0.40) + (efficiency * 0.30) + (speed * 0.20) + (low_risk * 0.10), 2))) def _case_title_from_description(description): first_line = " ".join(description.strip().split()) if len(first_line) <= 78: return first_line return f"{first_line[:75].rstrip()}..." ANALYSIS_DATABASE = [ { "kategori": "Pendidikan", "keyword": [ "kuliah", "studi", "study", "kampus", "universitas", "university", "beasiswa", "scholarship", "s1", "s2", "sarjana", "bachelor", "master", "singapura", "singapore", "jepang", "japan", "jlpt", "ielts", ], "penyebab": [ "Budget belum cukup untuk total biaya studi", "Pilihan negara/kampus belum dibandingkan dari total cost of study", "Target lulus 3 tahun butuh validasi kurikulum, bahasa, dan admission", ], "solusi": [ "Prioritaskan Jepang dengan beasiswa dan kota hemat", "Pilih Singapura hanya jika ada scholarship/subsidy besar", "Buat shortlist program 3 tahun dan simulasi biaya lengkap", ], "impact_label": "Budget ketat", "priority": 50, }, { "kategori": "Bisnis", "keyword": ["jualan", "penjualan", "produk", "usaha", "bisnis", "toko"], "penyebab": [ "Target pasar tidak tepat", "Promosi kurang efektif", "Produk belum sesuai kebutuhan pasar", ], "solusi": [ "Riset pelanggan", "Perbaiki produk", "Optimasi pemasaran", "Bangun channel penjualan", ], "impact_label": "Besar", "priority": 10, }, { "kategori": "Keuangan", "keyword": ["uang", "hutang", "utang", "gaji", "tabungan", "biaya"], "penyebab": [ "Tidak ada kontrol keuangan", "Pengeluaran terlalu besar", "Pemasukan kurang optimal", ], "solusi": [ "Buat anggaran", "Kurangi biaya tidak penting", "Cari sumber pemasukan baru", ], "impact_label": "Cashflow/budget", "priority": 10, }, { "kategori": "Karier", "keyword": ["kerja", "pekerjaan", "cv", "skill", "karir", "karier"], "penyebab": [ "Skill tidak sesuai kebutuhan", "Kurang pengalaman", "Portofolio belum kuat", ], "solusi": [ "Upgrade skill", "Buat portofolio", "Cari peluang kerja sesuai kemampuan", ], "impact_label": "Dampak karier", "priority": 10, }, { "kategori": "Logistik", "keyword": ["kirim", "pengiriman", "barang", "gudang", "rute"], "penyebab": [ "Rute tidak optimal", "Monitoring kurang", "Proses distribusi lambat", ], "solusi": [ "Optimasi rute", "Tracking barang", "Perbaiki sistem distribusi", ], "impact_label": "Operasional", "priority": 10, }, { "kategori": "Teknologi", "keyword": ["aplikasi", "website", "coding", "program", "error", "bug"], "penyebab": [ "Sistem belum optimal", "Arsitektur kurang tepat", "Bug perangkat lunak", ], "solusi": [ "Audit sistem", "Perbaiki kode", "Optimasi teknologi", ], "impact_label": "Teknis/operasional", "priority": 10, }, ] def _detect_problem_category(description): text = description.lower() best_item = None best_score = -1 for item in ANALYSIS_DATABASE: matches = sum(1 for keyword in item["keyword"] if keyword in text) if matches == 0: continue score = matches * 10 + item.get("priority", 0) if item["kategori"] == "Pendidikan" and _has_any(text, ["kuliah", "studi", "kampus", "universitas"]): score += 35 if item["kategori"] == "Pendidikan" and _has_any(text, ["singapura", "singapore", "jepang", "japan"]): score += 20 if score > best_score: best_item = item best_score = score return best_item or next(item for item in ANALYSIS_DATABASE if item["kategori"] == "Bisnis") def _is_education_decision(description): text = description.lower() education_keywords = [ "kuliah", "studi", "study", "kampus", "universitas", "university", "beasiswa", "scholarship", "s1", "s2", "sarjana", "bachelor", "master", "singapura", "singapore", "jepang", "japan", "jlpt", "ielts", ] constraint_keywords = [ "dana", "budget", "biaya", "uang", "200 juta", "200jt", "3 tahun", "tiga tahun", "kelar", "lulus", "visa", ] return _has_any(text, education_keywords) and _has_any(text, constraint_keywords) def _infer_business_area(description): text = description.lower() kategori = _detect_problem_category(description)["kategori"] if kategori == "Pendidikan": return ProblemCase.AREA_OTHER if kategori == "Keuangan": return ProblemCase.AREA_FINANCE if kategori == "Karier": return ProblemCase.AREA_PEOPLE if kategori in {"Logistik", "Teknologi"}: return ProblemCase.AREA_OPERATIONS if _has_any(text, ["iklan", "marketing", "konten", "campaign", "kampanye", "promosi"]): return ProblemCase.AREA_MARKETING if _has_any(text, ["produk", "layanan", "fitur", "kualitas"]): return ProblemCase.AREA_PRODUCT return ProblemCase.AREA_SALES def _write_analysis_records(problem, financial_impact, cause_profiles, options, steps): problem.root_causes.all().delete() problem.solutions.all().delete() problem.priority_score = _clamp(80 + problem.urgency * 3) problem.financial_impact = financial_impact problem.status = ProblemCase.STATUS_ANALYZED problem.save(update_fields=["priority_score", "financial_impact", "status", "updated_at"]) RootCause.objects.bulk_create([ RootCause( problem=problem, factor=factor, contribution_score=_clamp(score), why_chain=why_chain, ) for factor, score, why_chain in cause_profiles ]) scored = [] for option in options: scored.append({ **option, "decision_score": _decision_score( option["impact"], option["efficiency"], option["speed"], option["low_risk"], ), }) scored.sort(key=lambda item: item["decision_score"], reverse=True) created_solutions = SolutionOption.objects.bulk_create([ SolutionOption( problem=problem, title=option["title"], impact=option["impact"], efficiency=option["efficiency"], speed=option["speed"], low_risk=option["low_risk"], decision_score=option["decision_score"], success_rate=option["success_rate"], rank=rank, rationale=option["rationale"], ) for rank, option in enumerate(scored, start=1) ]) top_solution = created_solutions[0] ActionPlanStep.objects.bulk_create([ ActionPlanStep(solution=top_solution, day_index=day, title=title, task=task) for day, title, task in steps ]) def _build_education_analysis(problem): cause_profiles = [ ( "Keterbatasan Dana", 96, "Budget perlu menutup tuition, biaya hidup, visa, tiket, asuransi, dan dana darurat; jadi pilihan negara/kampus harus disaring dari batas biaya dulu.", ), ( "Target Lulus 3 Tahun", 92, "Target selesai cepat hanya realistis jika program, credit transfer, kalender akademik, dan syarat kelulusan cocok sejak awal.", ), ( "Biaya Hidup Negara", 88, "Perbandingan Singapura vs Jepang harus dihitung dari total cost of study, bukan hanya uang kuliah; kota, tempat tinggal, dan transport sangat menentukan.", ), ( "Bahasa & Admission", 76, "Risiko gagal masuk atau molor muncul jika syarat IELTS/JLPT, dokumen, deadline, dan kesiapan bahasa belum dipetakan.", ), ] options = [ { "title": "Prioritaskan Jepang + Beasiswa/Part-time Legal", "impact": 88, "efficiency": 86, "speed": 72, "low_risk": 78, "success_rate": 84, "rationale": "Lebih masuk akal untuk budget ketat bila shortlist difokuskan ke kampus/program yang punya beasiswa, kota yang lebih hemat, dan opsi kerja paruh waktu sesuai aturan visa.", }, { "title": "Singapura Hanya Jika Ada Scholarship/Subsidy Besar", "impact": 82, "efficiency": 58, "speed": 86, "low_risk": 55, "success_rate": 66, "rationale": "Singapura bisa unggul untuk akses industri dan durasi cepat, tetapi budget 200 juta berisiko tidak cukup tanpa bantuan biaya yang jelas sejak awal.", }, { "title": "Pathway Hemat: Mulai Lokal lalu Transfer/Credit Recognition", "impact": 76, "efficiency": 88, "speed": 68, "low_risk": 86, "success_rate": 80, "rationale": "Menekan cash-out awal sambil mengejar beasiswa dan kesiapan bahasa, tetapi harus dipastikan kredit bisa diakui agar target total 3 tahun tidak mundur.", }, ] steps = [ (1, "Buat batas biaya final", "Pecah dana 200 juta menjadi tuition, living cost, visa, tiket, asuransi, dan dana darurat; coret opsi yang melewati batas aman."), (2, "Shortlist program 3 tahun", "Cari minimal 10 program Jepang dan 3 program Singapura yang durasinya cocok, lalu tandai syarat bahasa, deadline, dan total biaya."), (3, "Kejar funding", "Daftar beasiswa, tuition waiver, atau sponsor; jangan memilih Singapura kecuali kekurangan biaya sudah tertutup jelas."), (4, "Validasi aturan visa", "Cek izin kerja paruh waktu, syarat dokumen finansial, serta risiko jika kurs/biaya hidup naik."), (5, "Decision gate", "Pilih Jepang jika total biaya paling aman; pilih Singapura hanya jika scholarship/subsidy membuat biaya 3 tahun masuk budget."), ] _write_analysis_records(problem, "Budget ketat", cause_profiles, options, steps) def _build_category_analysis(problem, category): kategori = category["kategori"] causes = category["penyebab"] solutions = category["solusi"] cause_profiles = [ ( cause, _clamp(92 - index * 8), f"Input terdeteksi sebagai kategori {kategori}. Penyebab ini perlu divalidasi karena dapat langsung memengaruhi target, batasan, dan keputusan berikutnya.", ) for index, cause in enumerate(causes) ] options = [] for index, solution in enumerate(solutions): options.append({ "title": solution, "impact": _clamp(88 - index * 4), "efficiency": _clamp(82 + (index % 2) * 5 - index * 2), "speed": _clamp(84 - index * 5), "low_risk": _clamp(74 + index * 4), "success_rate": _clamp(82 - index * 3), "rationale": f"Solusi ini cocok untuk kategori {kategori} karena langsung menargetkan penyebab utama: {causes[min(index, len(causes) - 1)]}.", }) steps = [ (1, "Kunci tujuan dan batasan", f"Kategori utama: {kategori}. Tulis target akhir, batasan dana/waktu, dan indikator sukses yang terukur."), (2, "Validasi penyebab utama", f"Cek apakah penyebab paling dominan adalah: {causes[0]}. Kumpulkan bukti sederhana sebelum memilih solusi."), (3, "Bandingkan opsi solusi", f"Bandingkan opsi: {', '.join(solutions[:3])}. Pilih yang paling sesuai dengan budget, waktu, dan risiko."), (4, "Jalankan langkah kecil", "Mulai dari eksperimen paling kecil selama 1-3 hari agar cepat terlihat apakah arah solusi benar."), (5, "Evaluasi dan putuskan", "Bandingkan hasil dengan indikator sukses, lalu pilih lanjutkan, ubah strategi, atau hentikan opsi yang tidak efektif."), ] _write_analysis_records(problem, category.get("impact_label", "Sedang"), cause_profiles, options, steps) def _build_business_analysis(problem): cause_profiles = [ ("Marketing", 95, "Akuisisi dan pesan penjualan belum cukup tajam untuk menjangkau segmen yang paling siap membeli."), ("Kompetitor", 88, "Penawaran kompetitor terlihat lebih kuat sehingga pelanggan membandingkan harga, bonus, dan bukti hasil."), ("Harga", 85, "Persepsi value belum seimbang dengan harga; bundling, bonus, atau bukti manfaat perlu diperjelas."), ("Produk", 70, "Produk/layanan perlu validasi ulang dari feedback pelanggan agar lebih relevan dengan kebutuhan saat ini."), ] options = [ { "title": "Optimasi Iklan Digital", "impact": 90, "efficiency": 70, "speed": 85, "low_risk": 60, "success_rate": 85, "rationale": "Perbaiki targeting, materi iklan, dan pesan promosi agar trafik yang masuk lebih relevan dan cepat terukur.", }, { "title": "Program Reseller & Agen", "impact": 85, "efficiency": 80, "speed": 60, "low_risk": 75, "success_rate": 92, "rationale": "Bangun kanal distribusi rendah biaya melalui mitra yang sudah punya relasi dan kepercayaan dengan calon pembeli.", }, { "title": "Diskon Bundling Akhir Bulan", "impact": 70, "efficiency": 60, "speed": 95, "low_risk": 90, "success_rate": 70, "rationale": "Dorong keputusan pembelian cepat dengan paket bernilai tinggi, periode terbatas, dan risiko operasional rendah.", }, ] steps = [ (1, "Audit data cepat", "Kumpulkan angka penjualan, biaya iklan, channel, produk terlaris, dan keluhan pelanggan 90 hari terakhir."), (2, "Validasi akar masalah", "Bandingkan temuan dengan faktor Marketing, Kompetitor, Harga, dan Produk; pilih 1–2 penyebab paling dominan."), (3, "Luncurkan eksperimen", "Jalankan eksperimen kecil untuk solusi teratas dengan target metrik, owner, dan batas anggaran yang jelas."), (4, "Pantau respon pasar", "Review metrik harian: leads, conversion rate, biaya per transaksi, repeat order, dan risiko operasional."), (5, "Putuskan scale/stop", "Bandingkan hasil dengan baseline lalu pilih: scale, iterasi pesan/offer, atau hentikan eksperimen."), ] _write_analysis_records(problem, "Besar", cause_profiles, options, steps) def build_case_analysis(problem): """Create deterministic MVP analysis records with category-aware rules.""" category = _detect_problem_category(problem.description) if category["kategori"] == "Pendidikan" and _is_education_decision(problem.description): _build_education_analysis(problem) return _build_category_analysis(problem, category) def home(request): form = ProblemCaseForm(request.POST or None) if request.method == "POST": if form.is_valid(): with transaction.atomic(): problem = form.save(commit=False) problem.title = _case_title_from_description(problem.description) problem.business_area = _infer_business_area(problem.description) problem.status = ProblemCase.STATUS_DRAFT problem.save() build_case_analysis(problem) messages.success(request, "✅ Analisis berhasil disimulasikan dan disimpan sebagai case.") return redirect(problem.get_absolute_url()) messages.error(request, "Mohon periksa input. Deskripsi masalah perlu cukup lengkap agar analisis akurat.") recent_cases = ProblemCase.objects.all()[:4] context = { "form": form, "recent_cases": recent_cases, "page_title": "OPTEMA AI — MVP Decision Intelligence", "meta_description": "OPTEMA AI membantu mengubah masalah bisnis, keuangan, karier, logistik, teknologi, dan pendidikan menjadi problem detection, root-cause analysis, decision scoring, dan action plan.", } return render(request, "core/index.html", context) def case_list(request): cases = ProblemCase.objects.all() return render(request, "core/case_list.html", { "cases": cases, "page_title": "Daftar Kasus — OPTEMA AI", "meta_description": "Lihat histori kasus yang sudah dianalisis oleh OPTEMA AI.", }) def case_detail(request, pk): action_steps = Prefetch("solutions__action_steps", queryset=ActionPlanStep.objects.all()) problem = get_object_or_404( ProblemCase.objects.prefetch_related("root_causes", "solutions", action_steps), pk=pk, ) top_solution = problem.solutions.first() return render(request, "core/case_detail.html", { "problem": problem, "top_solution": top_solution, "page_title": f"{problem.title} — Analisis OPTEMA AI", "meta_description": f"Analisis prioritas, akar masalah, solusi berskor, dan action plan untuk {problem.title}.", })