448 lines
18 KiB
Python
448 lines
18 KiB
Python
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}.",
|
||
})
|