40292-vm/core/views.py
Flatlogic Bot 60f601a85b OPTEMA AI
2026-06-19 15:42:30 +00:00

689 lines
31 KiB
Python
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 re
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()}..."
MONEY_UNITS = {
"ribu": 1_000,
"rb": 1_000,
"juta": 1_000_000,
"jt": 1_000_000,
"miliar": 1_000_000_000,
"milyar": 1_000_000_000,
}
NUMBER_WORDS = {
"satu": 1,
"dua": 2,
"tiga": 3,
"empat": 4,
"lima": 5,
"enam": 6,
"tujuh": 7,
"delapan": 8,
"sembilan": 9,
"sepuluh": 10,
}
COUNTRY_KEYWORDS = {
"Singapura": ["singapura", "singapore"],
"Jepang": ["jepang", "japan"],
"Indonesia": ["indonesia", "lokal", "dalam negeri"],
"Malaysia": ["malaysia"],
"Australia": ["australia"],
"Korea Selatan": ["korea", "korea selatan"],
}
EDUCATION_COST_ASSUMPTIONS = {
"Jepang": {
"tuition_year": (60_000_000, 130_000_000),
"living_month": (10_000_000, 18_000_000),
"setup": 25_000_000,
"note": "lebih realistis bila digabung beasiswa, kota hemat, dan kerja paruh waktu sesuai aturan visa",
},
"Singapura": {
"tuition_year": (180_000_000, 450_000_000),
"living_month": (18_000_000, 35_000_000),
"setup": 35_000_000,
"note": "biasanya perlu scholarship/subsidy besar agar masuk budget ketat",
},
}
def _format_idr(amount):
if amount is None:
return "Belum terdeteksi"
return f"Rp{int(round(amount)):,.0f}".replace(",", ".")
def _format_idr_range(low, high):
return f"{_format_idr(low)} {_format_idr(high)}"
def _number_value(raw_number):
value = raw_number.lower().replace("rp", "").replace("idr", "").replace(" ", "").strip()
if "," in value and "." in value:
value = value.replace(".", "").replace(",", ".") if value.rfind(",") > value.rfind(".") else value.replace(",", "")
elif "," in value:
value = value.replace(",", ".")
elif "." in value:
parts = value.split(".")
if len(parts) > 1 and len(parts[-1]) == 3 and all(part.isdigit() for part in parts):
value = "".join(parts)
try:
return float(value)
except ValueError:
return None
def _money_to_idr(raw_number, unit=None):
number = _number_value(raw_number)
if number is None:
return None
unit = (unit or "").lower()
if unit in MONEY_UNITS:
return int(number * MONEY_UNITS[unit])
return int(number) if number >= 10_000 else None
def _extract_money_amounts(text):
patterns = [
r"(?:rp\.?|idr)\s*([0-9][0-9.,]*)\s*(miliar|milyar|juta|jt|ribu|rb)?",
r"(?:uang|dana|budget|anggaran|biaya|modal|tabungan|cash|kas|hutang|utang|gaji)\s*(?:hanya|ada|sekitar|kurang lebih|maksimal|max|sebesar|:|=)?\s*(?:rp\.?|idr)?\s*([0-9][0-9.,]*)\s*(miliar|milyar|juta|jt|ribu|rb)?",
r"([0-9][0-9.,]*)\s*(miliar|milyar|juta|jt|ribu|rb)\b",
]
amounts = []
for pattern in patterns:
for match in re.finditer(pattern, text, flags=re.IGNORECASE):
amount = _money_to_idr(match.group(1), match.group(2) if len(match.groups()) > 1 else None)
if amount:
amounts.append(amount)
return sorted(set(amounts))
def _extract_duration_years(text):
match = re.search(r"([0-9]+(?:[,.][0-9]+)?)\s*(tahun|thn|year|years|yr|bulan|bln|month|months|semester)", text, flags=re.IGNORECASE)
if match:
value = _number_value(match.group(1))
unit = match.group(2).lower()
if value:
if unit in {"bulan", "bln", "month", "months"}:
return value / 12
if unit == "semester":
return value / 2
return round(value, 2)
word_pattern = r"(" + "|".join(NUMBER_WORDS.keys()) + r")\s*(tahun|thn|year|years|bulan|bln|semester)"
match = re.search(word_pattern, text, flags=re.IGNORECASE)
if match:
value = NUMBER_WORDS[match.group(1).lower()]
unit = match.group(2).lower()
if unit in {"bulan", "bln"}:
return value / 12
if unit == "semester":
return value / 2
return float(value)
return None
def _format_years(years):
if years is None:
return "Belum terdeteksi"
if years < 1:
months = max(1, int(round(years * 12)))
return f"{months} bulan"
return f"{int(years)} tahun" if float(years).is_integer() else f"{years:.1f} tahun"
def _extract_countries(text):
return [country for country, keywords in COUNTRY_KEYWORDS.items() if any(keyword in text for keyword in keywords)]
def _extract_percentages(text):
percentages = []
for match in re.finditer(r"([0-9]+(?:[,.][0-9]+)?)\s*(%|persen|percent)", text, flags=re.IGNORECASE):
value = _number_value(match.group(1))
if value is not None:
percentages.append(value)
return sorted(set(percentages))
def _extract_case_data(description):
text = description.lower()
money_amounts = _extract_money_amounts(text)
return {
"budget": max(money_amounts) if money_amounts else None,
"money_amounts": money_amounts,
"duration_years": _extract_duration_years(text),
"countries": _extract_countries(text),
"percentages": _extract_percentages(text),
}
def _case_constraint_note(case_data):
parts = []
if case_data.get("budget"):
parts.append(f"dana {_format_idr(case_data['budget'])}")
if case_data.get("duration_years"):
parts.append(f"target {_format_years(case_data['duration_years'])}")
if case_data.get("countries"):
parts.append(f"opsi {', '.join(case_data['countries'])}")
return ", ".join(parts) if parts else "data yang tertulis di pertanyaan"
def _education_cost_rows(case_data, fallback_years=None):
years = case_data.get("duration_years") or fallback_years
if not years:
return []
rows = []
for country in [c for c in case_data.get("countries", []) if c in EDUCATION_COST_ASSUMPTIONS]:
assumption = EDUCATION_COST_ASSUMPTIONS[country]
tuition_low, tuition_high = assumption["tuition_year"]
living_low, living_high = assumption["living_month"]
months = years * 12
total_low = int(round(tuition_low * years + living_low * months + assumption["setup"]))
total_high = int(round(tuition_high * years + living_high * months + assumption["setup"]))
budget = case_data.get("budget")
rows.append({
"country": country,
"total_low": total_low,
"total_high": total_high,
"coverage": round((budget / total_low) * 100, 1) if budget else None,
"shortfall_low": max(0, total_low - budget) if budget else None,
"surplus_low": max(0, budget - total_low) if budget else None,
"note": assumption["note"],
})
return rows
def _build_case_insights(description, category=None):
case_data = _extract_case_data(description)
budget = case_data.get("budget")
years = case_data.get("duration_years")
countries = case_data.get("countries")
percentages = case_data.get("percentages") or []
detected = [
{"label": "Dana/budget", "value": _format_idr(budget) if budget else "Belum disebut", "note": "angka terbesar yang terdeteksi sebagai uang" if budget else "tambahkan nominal agar kalkulasi lebih presisi"},
{"label": "Target waktu", "value": _format_years(years) if years else "Belum disebut", "note": "diambil dari kata tahun/bulan/semester" if years else "tambahkan deadline atau durasi target"},
{"label": "Persentase/rasio", "value": ", ".join(f"{value:g}%" for value in percentages) if percentages else "Belum disebut", "note": "persen yang muncul di pertanyaan" if percentages else "tambahkan rasio seperti turun 30% atau target naik 20%"},
{"label": "Opsi/negara", "value": ", ".join(countries) if countries else "Belum spesifik", "note": "opsi yang muncul di pertanyaan" if countries else "tambahkan opsi yang ingin dibandingkan"},
]
calculations = []
if budget and years:
months = years * 12
reserve = budget * 0.10
usable = budget - reserve
if years >= 1:
calculations.append({"label": "Budget per tahun", "formula": f"{_format_idr(budget)} ÷ {_format_years(years)}", "result": f"{_format_idr(budget / years)} / tahun"})
else:
calculations.append({"label": "Budget untuk periode target", "formula": f"{_format_idr(budget)} untuk {_format_years(years)}", "result": f"{_format_idr(budget)} total periode"})
calculations.extend([
{"label": "Budget per bulan", "formula": f"{_format_idr(budget)} ÷ {int(round(months))} bulan", "result": f"{_format_idr(budget / months)} / bulan"},
{"label": "Dana aman setelah cadangan 10%", "formula": f"{_format_idr(budget)} 10% cadangan ({_format_idr(reserve)})", "result": f"{_format_idr(usable)} total atau {_format_idr(usable / months)} / bulan"},
])
if percentages:
calculations.append({"label": "Persentase dari pertanyaan", "formula": f"Angka rasio terdeteksi: {', '.join(f'{value:g}%' for value in percentages)}", "result": "Tambahkan baseline nominal agar dampak rupiahnya bisa dihitung."})
elif budget:
calculations.append({"label": "Cadangan minimum 10%", "formula": f"{_format_idr(budget)} × 10%", "result": f"{_format_idr(budget * 0.10)} disisihkan sebagai buffer risiko"})
elif years:
calculations.append({"label": "Target waktu terdeteksi", "formula": f"Durasi target = {_format_years(years)}", "result": "Masukkan nominal dana/biaya agar bisa dihitung budget per bulan dan gap biaya."})
comparisons = []
if category == "Pendidikan":
for row in _education_cost_rows(case_data, fallback_years=years):
if budget:
fit = f"Budget menutup ±{row['coverage']}% dari estimasi minimum; gap minimum {_format_idr(row['shortfall_low'])}." if row["shortfall_low"] else f"Budget melewati estimasi minimum; sisa aman sekitar {_format_idr(row['surplus_low'])}."
else:
fit = "Budget belum disebut, jadi gap belum bisa dihitung."
comparisons.append({"option": row["country"], "estimate": _format_idr_range(row["total_low"], row["total_high"]), "budget_fit": fit, "note": row["note"]})
recommendation = ""
if category == "Pendidikan" and comparisons and budget:
rows = _education_cost_rows(case_data, fallback_years=years)
best = min(rows, key=lambda item: item["total_low"])
recommendation = (
f"Secara self-funded, {_format_idr(budget)} belum menutup estimasi minimum {best['country']} ({_format_idr(best['total_low'])}). Opsi paling dekat tetap {best['country']}, tetapi harus dikunci dengan beasiswa/tuition waiver, kota hemat, atau income legal."
if best["shortfall_low"] else
f"Opsi paling aman secara angka awal adalah {best['country']} karena estimasi minimumnya masih masuk budget."
)
elif calculations:
recommendation = f"Analisis disesuaikan dengan {_case_constraint_note(case_data)}; gunakan angka ini sebagai batas saat memilih solusi."
missing = []
if not budget:
missing.append("Nominal dana/biaya/budget belum jelas.")
if not years:
missing.append("Target durasi atau deadline belum jelas.")
if category == "Pendidikan" and not countries:
missing.append("Negara/kampus pembanding belum disebut.")
return {
"detected": detected,
"calculations": calculations,
"comparisons": comparisons,
"recommendation": recommendation,
"missing": missing,
"assumption_note": "Estimasi biaya pendidikan adalah asumsi awal untuk screening; tetap verifikasi biaya resmi kampus, visa, kurs, beasiswa, dan aturan kerja terbaru.",
}
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):
case_data = _extract_case_data(problem.description)
constraint_note = _case_constraint_note(case_data)
budget = case_data.get("budget")
years = case_data.get("duration_years") or 3
budget_text = _format_idr(budget) if budget else "budget yang tersedia"
years_text = _format_years(years)
row_by_country = {row["country"]: row for row in _education_cost_rows(case_data, fallback_years=years)}
japan_gap_text = ""
singapore_gap_text = ""
if budget and row_by_country.get("Jepang"):
gap = row_by_country["Jepang"].get("shortfall_low") or 0
japan_gap_text = f" Estimasi minimum Jepang masih butuh tambahan sekitar {_format_idr(gap)} bila tanpa beasiswa." if gap else " Estimasi minimum Jepang masuk batas budget awal."
if budget and row_by_country.get("Singapura"):
gap = row_by_country["Singapura"].get("shortfall_low") or 0
singapore_gap_text = f" Estimasi minimum Singapura butuh tambahan sekitar {_format_idr(gap)} bila tanpa scholarship besar." if gap else " Estimasi minimum Singapura masuk batas budget awal."
if budget:
budget_math = f"Dengan {budget_text} untuk {years_text}, batas kasar adalah {_format_idr(budget / years)}/tahun atau {_format_idr(budget / (years * 12))}/bulan sebelum cadangan."
else:
budget_math = "Nominal budget belum terbaca, jadi kalkulasi gap harus dilengkapi dengan angka dana/biaya."
cause_profiles = [
("Keterbatasan Dana", 96, f"Pertanyaan menyebut {constraint_note}. {budget_math} Karena itu, keputusan harus dimulai dari total biaya studi, bukan dari nama negara saja."),
("Target Lulus Tepat Waktu", 92, f"Target {years_text} hanya realistis jika program, credit transfer, kalender akademik, dan syarat kelulusan cocok sejak awal."),
("Gap Biaya Negara", 88, f"Perbandingan perlu memakai total tuition + living cost + setup cost. {japan_gap_text}{singapore_gap_text}".strip()),
("Bahasa & Admission", 76, "Risiko gagal masuk atau molor muncul jika syarat IELTS/JLPT, dokumen, deadline, dan kesiapan bahasa belum dipetakan sebelum memilih negara."),
]
options = [
{"title": "Prioritaskan Jepang + Beasiswa/Part-time Legal", "impact": 88, "efficiency": 86, "speed": 72, "low_risk": 78, "success_rate": 84, "rationale": f"Paling dekat dengan batas {budget_text} karena gap estimasi minimum Jepang biasanya lebih kecil daripada Singapura. Tetap wajib cari beasiswa/tuition waiver dan kota hemat; jangan asumsi {budget_text} cukup untuk self-funded penuh."},
{"title": "Pathway Hemat: Mulai Lokal lalu Transfer/Credit Recognition", "impact": 76, "efficiency": 88, "speed": 68, "low_risk": 86, "success_rate": 80, "rationale": f"Menekan cash-out awal sambil mengejar beasiswa dan kesiapan bahasa. Cocok jika hitungan {budget_text} per {years_text} tidak menutup total biaya studi luar negeri penuh."},
{"title": "Singapura Hanya Jika Ada Scholarship/Subsidy Besar", "impact": 82, "efficiency": 58, "speed": 86, "low_risk": 55, "success_rate": 66, "rationale": f"Singapura bisa unggul untuk akses industri dan durasi cepat, tetapi {budget_text} berisiko tidak cukup tanpa bantuan biaya yang jelas sejak awal.{singapore_gap_text}"},
]
steps = [
(1, "Buat batas biaya final", f"Pecah {budget_text} menjadi tuition, living cost, visa, tiket, asuransi, dan dana darurat 10%; coret opsi yang melewati batas aman."),
(2, "Shortlist program sesuai durasi", f"Cari minimal 10 program Jepang dan 3 program Singapura yang durasinya mendekati {years_text}, lalu catat syarat bahasa, deadline, dan total biaya."),
(3, "Hitung gap funding", "Bandingkan estimasi total biaya dengan dana tersedia; targetkan beasiswa, tuition waiver, sponsor, atau income legal untuk menutup gap."),
(4, "Validasi aturan visa", "Cek izin kerja paruh waktu, syarat dokumen finansial, serta risiko jika kurs/biaya hidup naik."),
(5, "Decision gate", "Pilih opsi dengan gap terkecil dan bukti funding paling jelas; Singapura hanya layak jika scholarship/subsidy membuat biaya total 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"]
case_data = _extract_case_data(problem.description)
constraint_note = _case_constraint_note(case_data)
cause_profiles = [
(cause, _clamp(92 - index * 8), f"Input terdeteksi sebagai kategori {kategori} dengan konteks: {constraint_note}. 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 dipilih karena sesuai kategori {kategori}, menargetkan penyebab '{causes[min(index, len(causes) - 1)]}', dan tetap mengikuti batasan pertanyaan: {constraint_note}.",
})
steps = [
(1, "Kunci tujuan dan batasan", f"Kategori utama: {kategori}. Tulis target akhir, batasan dana/waktu, dan indikator sukses yang terukur dari konteks: {constraint_note}."),
(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 12 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()
category = _detect_problem_category(problem.description)
case_insights = _build_case_insights(problem.description, category["kategori"])
return render(request, "core/case_detail.html", {
"problem": problem,
"top_solution": top_solution,
"case_insights": case_insights,
"page_title": f"{problem.title} — Analisis OPTEMA AI",
"meta_description": f"Analisis prioritas, akar masalah, solusi berskor, kalkulasi data, dan action plan untuk {problem.title}.",
})