2377 lines
90 KiB
TypeScript
2377 lines
90 KiB
TypeScript
import {
|
||
mdiBrain,
|
||
mdiChartTimelineVariant,
|
||
mdiClipboardTextClockOutline,
|
||
mdiLightbulbOnOutline,
|
||
mdiPlus,
|
||
} from '@mdi/js';
|
||
import axios from 'axios';
|
||
import Head from 'next/head';
|
||
import React, { ReactElement, useEffect, useMemo, useState } from 'react';
|
||
import BaseButton from '../components/BaseButton';
|
||
import BaseIcon from '../components/BaseIcon';
|
||
import CardBox from '../components/CardBox';
|
||
import SectionMain from '../components/SectionMain';
|
||
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
|
||
import { getPageTitle } from '../config';
|
||
import LayoutAuthenticated from '../layouts/Authenticated';
|
||
import { useAppSelector } from '../stores/hooks';
|
||
|
||
type Cause = {
|
||
id: string;
|
||
label: string;
|
||
category:
|
||
| 'people'
|
||
| 'process'
|
||
| 'technology'
|
||
| 'policy'
|
||
| 'environment'
|
||
| 'materials'
|
||
| 'measurement'
|
||
| 'other';
|
||
contribution: number;
|
||
description: string;
|
||
};
|
||
|
||
type DataSource = {
|
||
id: string;
|
||
title: string;
|
||
category: 'input' | 'metric' | 'pattern';
|
||
summary: string;
|
||
confidence: number;
|
||
};
|
||
|
||
type Solution = {
|
||
id: string;
|
||
title: string;
|
||
description: string;
|
||
rationale: string;
|
||
executionSteps: string[];
|
||
kpis: string[];
|
||
risks: string[];
|
||
resources: string[];
|
||
type:
|
||
| 'marketing'
|
||
| 'sales'
|
||
| 'operations'
|
||
| 'finance'
|
||
| 'product'
|
||
| 'hr'
|
||
| 'technology'
|
||
| 'other';
|
||
impact: number;
|
||
efficiency: number;
|
||
speed: number;
|
||
lowRisk: number;
|
||
score: number;
|
||
success: number;
|
||
rootCauseId: string;
|
||
duration: number;
|
||
};
|
||
|
||
type PlanTask = {
|
||
id: string;
|
||
day: number;
|
||
title: string;
|
||
description: string;
|
||
};
|
||
|
||
type Run = {
|
||
id: string;
|
||
caseId: string;
|
||
planId: string;
|
||
createdAt: string;
|
||
title: string;
|
||
problem: string;
|
||
urgency: number;
|
||
financialImpact: 'low' | 'medium' | 'high' | 'unknown';
|
||
priority: 'low' | 'medium' | 'high' | 'critical';
|
||
priorityScore: number;
|
||
causes: Cause[];
|
||
dataSources: DataSource[];
|
||
solutions: Solution[];
|
||
selectedSolution: Solution;
|
||
tasks: PlanTask[];
|
||
conclusion: string;
|
||
};
|
||
|
||
type ProblemKnowledge = {
|
||
key: 'pengangguran' | 'kemiskinan' | 'keluarga_hukum_waris';
|
||
label: string;
|
||
keywords: string[];
|
||
severity: 'Tinggi' | 'Kritis';
|
||
penyebab: string[];
|
||
hipotesis: string[];
|
||
solusi: string[];
|
||
kesimpulan: string;
|
||
causeCategories: Cause['category'][];
|
||
};
|
||
|
||
type SolutionDraft = Omit<
|
||
Solution,
|
||
'id' | 'score' | 'success' | 'rootCauseId'
|
||
> & {
|
||
rootCauseId?: string;
|
||
};
|
||
|
||
type QuestionSolutionFit = {
|
||
focus: string;
|
||
keywords: string[];
|
||
};
|
||
|
||
const STORAGE_KEY = 'optema-ai-runs';
|
||
const MAX_DATA_SOURCES = 3;
|
||
|
||
const PROBLEM_KNOWLEDGE: ProblemKnowledge[] = [
|
||
{
|
||
key: 'pengangguran',
|
||
label: 'Pengangguran',
|
||
keywords: [
|
||
'pengangguran',
|
||
'menganggur',
|
||
'nganggur',
|
||
'pencari kerja',
|
||
'cari kerja',
|
||
'pekerjaan',
|
||
'lamaran',
|
||
'cv',
|
||
'interview',
|
||
'wawancara',
|
||
'lowongan kerja',
|
||
'lapangan kerja',
|
||
'jobless',
|
||
],
|
||
severity: 'Tinggi',
|
||
penyebab: [
|
||
'Kurangnya lapangan kerja',
|
||
'Skill tidak sesuai kebutuhan industri',
|
||
'Kurangnya pengalaman',
|
||
'Pendidikan rendah',
|
||
'Otomatisasi pekerjaan',
|
||
'Pertumbuhan ekonomi lambat',
|
||
'Kurangnya informasi lowongan',
|
||
'Mobilitas rendah',
|
||
'Kurangnya sertifikasi',
|
||
'Persaingan tinggi',
|
||
],
|
||
hipotesis: [
|
||
'Skill tidak relevan',
|
||
'Lokasi pekerjaan terbatas',
|
||
'Kurangnya pelatihan',
|
||
'Kurangnya akses informasi',
|
||
'Ekonomi melambat',
|
||
'Perusahaan mengurangi perekrutan',
|
||
'Kurangnya jaringan profesional',
|
||
'Kurangnya pengalaman kerja',
|
||
'Teknologi menggantikan pekerjaan',
|
||
'Sertifikasi belum memadai',
|
||
],
|
||
solusi: [
|
||
'Pelatihan keterampilan',
|
||
'Mengikuti sertifikasi',
|
||
'Membuat CV profesional',
|
||
'Magang',
|
||
'Networking',
|
||
'Pelatihan digital',
|
||
'Job matching',
|
||
'Wirausaha',
|
||
'Peningkatan pendidikan',
|
||
'Pencarian kerja aktif',
|
||
],
|
||
kesimpulan:
|
||
'Pengangguran dapat dikurangi dengan peningkatan kompetensi, akses informasi kerja, dan penciptaan lapangan kerja.',
|
||
causeCategories: ['policy', 'people', 'process', 'technology'],
|
||
},
|
||
{
|
||
key: 'kemiskinan',
|
||
label: 'Kemiskinan',
|
||
keywords: [
|
||
'kemiskinan',
|
||
'miskin',
|
||
'pendapatan rendah',
|
||
'kredit mikro',
|
||
'modal usaha',
|
||
'bantuan modal',
|
||
'usaha kecil',
|
||
'umkm',
|
||
'ekonomi keluarga',
|
||
],
|
||
severity: 'Kritis',
|
||
penyebab: [
|
||
'Pendapatan rendah',
|
||
'Pendidikan rendah',
|
||
'Pengangguran',
|
||
'Kurangnya modal',
|
||
'Akses layanan terbatas',
|
||
'Produktivitas rendah',
|
||
'Ketimpangan ekonomi',
|
||
'Inflasi',
|
||
'Beban keluarga tinggi',
|
||
'Kurangnya keterampilan',
|
||
],
|
||
hipotesis: [
|
||
'Kurangnya peluang ekonomi',
|
||
'Kurangnya akses pendidikan',
|
||
'Tidak memiliki aset produktif',
|
||
'Akses modal sulit',
|
||
'Kesehatan buruk',
|
||
'Kondisi wilayah tertinggal',
|
||
'Kurangnya pelatihan',
|
||
'Harga kebutuhan meningkat',
|
||
'Pendapatan tidak stabil',
|
||
'Kesempatan kerja terbatas',
|
||
],
|
||
solusi: [
|
||
'Pelatihan kerja',
|
||
'Bantuan modal usaha',
|
||
'Pendidikan vokasi',
|
||
'Pendampingan UMKM',
|
||
'Program padat karya',
|
||
'Akses kredit mikro',
|
||
'Peningkatan produktivitas',
|
||
'Digitalisasi usaha',
|
||
'Kemitraan usaha',
|
||
'Penciptaan lapangan kerja',
|
||
],
|
||
kesimpulan:
|
||
'Kemiskinan membutuhkan kombinasi peningkatan pendapatan, pendidikan, keterampilan, dan akses ekonomi.',
|
||
causeCategories: ['environment', 'people', 'policy', 'process'],
|
||
},
|
||
{
|
||
key: 'keluarga_hukum_waris',
|
||
label: 'Hukum keluarga, perceraian, dan warisan',
|
||
keywords: [
|
||
'selingkuh',
|
||
'perselingkuhan',
|
||
'cerai',
|
||
'perceraian',
|
||
'warisan',
|
||
'waris',
|
||
'ahli waris',
|
||
'harta waris',
|
||
'harta bersama',
|
||
'harta gono gini',
|
||
'gono gini',
|
||
'gono-gini',
|
||
'hak asuh',
|
||
'nafkah anak',
|
||
'hak anak',
|
||
'sengketa keluarga',
|
||
'rumah tangga',
|
||
'suami',
|
||
'istri',
|
||
],
|
||
severity: 'Kritis',
|
||
penyebab: [
|
||
'Konflik rumah tangga dan dugaan perselingkuhan',
|
||
'Perceraian, hak asuh, dan nafkah anak belum tertata',
|
||
'Pembagian harta bersama dan warisan belum jelas',
|
||
'Hak dan kebutuhan 10 anak berisiko terabaikan',
|
||
'Dokumen keluarga, aset, dan ahli waris belum diinventarisasi',
|
||
'Komunikasi keluarga rentan memanas tanpa mediator netral',
|
||
'Keputusan emosional berisiko memperburuk sengketa hukum',
|
||
'Belum ada rencana perlindungan anak jangka pendek',
|
||
'Potensi pengalihan aset sebelum status hukum jelas',
|
||
'Kebutuhan pendamping hukum dan psikososial belum dipenuhi',
|
||
],
|
||
hipotesis: [
|
||
'Isu perceraian, warisan, harta bersama, nafkah, dan hak anak masih tercampur menjadi satu masalah',
|
||
'Keluarga belum memiliki kronologi, bukti, dan dokumen pendukung yang rapi',
|
||
'Ahli waris, aset, utang, dan status harta perlu dipetakan sebelum mengambil keputusan',
|
||
'Mediasi awal dibutuhkan agar konflik tidak langsung merugikan anak-anak',
|
||
'Konsultasi advokat, pos bantuan hukum, mediator, atau notaris perlu dilakukan sebelum membuat kesepakatan',
|
||
'Anak-anak membutuhkan rencana sementara untuk biaya hidup, sekolah, kesehatan, dan tempat tinggal',
|
||
'Kesepakatan lisan berisiko lemah jika tidak dituangkan dalam dokumen yang benar',
|
||
'Ada risiko aset keluarga dipindahkan atau dipakai tanpa persetujuan pihak yang berhak',
|
||
'Perlu pemisahan antara masalah moral/perselingkuhan dan hak perdata keluarga',
|
||
'Keputusan paling aman adalah langkah hukum bertahap berbasis dokumen',
|
||
],
|
||
solusi: [
|
||
'Konsultasi hukum keluarga dan waris',
|
||
'Mediasi keluarga dengan pihak netral',
|
||
'Inventarisasi aset, utang, dan ahli waris',
|
||
'Rencana hak asuh dan nafkah anak',
|
||
'Pengamanan dokumen keluarga dan aset',
|
||
'Pendampingan psikososial anak',
|
||
'Penyusunan kronologi dan bukti',
|
||
'Opsi penyelesaian damai tertulis',
|
||
'Rujukan advokat atau pos bantuan hukum',
|
||
'Koordinasi notaris/PPAT untuk urusan aset bila diperlukan',
|
||
],
|
||
kesimpulan:
|
||
'Kasus perselingkuhan, perceraian, warisan, dan banyak anak harus dipisahkan menjadi isu hukum keluarga, harta/waris, dan perlindungan anak; langkah aman adalah dokumentasi, mediasi, dan konsultasi hukum profesional.',
|
||
causeCategories: ['people', 'policy', 'process', 'other'],
|
||
},
|
||
];
|
||
|
||
const uuid = () =>
|
||
typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
||
? crypto.randomUUID()
|
||
: `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||
|
||
const clamp = (value: number) => Math.max(1, Math.min(100, Math.round(value)));
|
||
|
||
const scoreSolution = (
|
||
impact: number,
|
||
efficiency: number,
|
||
speed: number,
|
||
lowRisk: number,
|
||
) => Math.round(impact * 0.4 + efficiency * 0.3 + speed * 0.2 + lowRisk * 0.1);
|
||
|
||
const titleFromProblem = (problem: string) => {
|
||
const clean = problem.trim().replace(/\s+/g, ' ');
|
||
if (!clean) return 'Kasus keputusan baru';
|
||
return clean.length > 64 ? `${clean.slice(0, 61)}...` : clean;
|
||
};
|
||
|
||
const shortList = (items: string[], limit = 3) => {
|
||
const selected = items.slice(0, limit);
|
||
|
||
return selected.length ? selected.join(', ') : 'data belum tersedia';
|
||
};
|
||
|
||
const cleanText = (value: string) => value.trim().replace(/\s+/g, ' ');
|
||
|
||
const problemSnippet = (problem: string, maxLength = 150) => {
|
||
const clean = cleanText(problem);
|
||
|
||
return clean.length > maxLength ? `${clean.slice(0, maxLength - 3)}...` : clean;
|
||
};
|
||
|
||
const trimSentenceEnd = (value: string) =>
|
||
cleanText(value).replace(/[.!?]+$/u, '');
|
||
|
||
const keywordFitScore = (problem: string, keywords: string[]) => {
|
||
const text = cleanText(problem).toLowerCase();
|
||
|
||
return keywords.reduce((score, keyword) => {
|
||
const normalizedKeyword = cleanText(keyword).toLowerCase();
|
||
|
||
if (!normalizedKeyword || !text.includes(normalizedKeyword)) {
|
||
return score;
|
||
}
|
||
|
||
return score + (normalizedKeyword.includes(' ') ? 3 : 1);
|
||
}, 0);
|
||
};
|
||
|
||
const applyQuestionFitToDrafts = (
|
||
drafts: SolutionDraft[],
|
||
fits: QuestionSolutionFit[],
|
||
problem: string,
|
||
): SolutionDraft[] => {
|
||
const scores = fits.map((fit) => keywordFitScore(problem, fit.keywords));
|
||
const maxScore = Math.max(...scores, 0);
|
||
const hasExplicitFit = maxScore > 0;
|
||
const snippet = problemSnippet(problem);
|
||
|
||
return drafts.map((draft, index) => {
|
||
const fit = fits[index];
|
||
const fitScore = scores[index] || 0;
|
||
const isBestFit = hasExplicitFit && fitScore === maxScore && fitScore > 0;
|
||
const scoreBoost = isBestFit
|
||
? 10 + Math.min(fitScore, 8)
|
||
: fitScore > 0
|
||
? 4 + Math.min(fitScore, 4)
|
||
: hasExplicitFit
|
||
? -3
|
||
: 0;
|
||
const questionContext = fit
|
||
? fitScore > 0
|
||
? `Kesesuaian pertanyaan: solusi ini menjawab fokus “${fit.focus}” yang muncul pada input pengguna: “${snippet}”.`
|
||
: hasExplicitFit
|
||
? 'Catatan kesesuaian: solusi ini hanya alternatif pendukung, bukan jawaban paling langsung dari pertanyaan pengguna.'
|
||
: `Kesesuaian pertanyaan: solusi ini disusun dari pola masalah yang ditulis pengguna: “${snippet}”.`
|
||
: '';
|
||
|
||
return {
|
||
...draft,
|
||
title: isBestFit ? `${draft.title} — paling sesuai pertanyaan` : draft.title,
|
||
rationale: [questionContext, draft.rationale].filter(Boolean).join(' '),
|
||
impact: draft.impact + scoreBoost,
|
||
efficiency: draft.efficiency + (isBestFit ? 4 : fitScore > 0 ? 2 : 0),
|
||
speed: draft.speed + (isBestFit ? 3 : fitScore > 0 ? 1 : 0),
|
||
lowRisk: draft.lowRisk + (isBestFit ? 2 : 0),
|
||
};
|
||
});
|
||
};
|
||
|
||
const findProblemKnowledge = (problem: string) => {
|
||
const scoredKnowledge = PROBLEM_KNOWLEDGE.map((knowledge) => ({
|
||
knowledge,
|
||
score: keywordFitScore(problem, knowledge.keywords),
|
||
}))
|
||
.filter((item) => item.score > 0)
|
||
.sort((a, b) => b.score - a.score);
|
||
|
||
return scoredKnowledge[0]?.knowledge;
|
||
};
|
||
|
||
const buildKnowledgeCauses = (
|
||
knowledge: ProblemKnowledge,
|
||
urgency: number,
|
||
financialImpact: Run['financialImpact'],
|
||
): Cause[] => {
|
||
const impactBoost =
|
||
financialImpact === 'high' ? 9 : financialImpact === 'medium' ? 5 : 2;
|
||
const severityBoost = knowledge.severity === 'Kritis' ? 10 : 6;
|
||
const base = 56 + urgency * 5 + impactBoost + severityBoost;
|
||
|
||
return knowledge.penyebab.slice(0, 4).map((cause, index) => ({
|
||
id: uuid(),
|
||
label: cause,
|
||
category: knowledge.causeCategories[index] || 'other',
|
||
contribution: clamp(base + 14 - index * 7),
|
||
description: `Penyebab dari knowledge base ${knowledge.label}. Hipotesis pendukung: ${
|
||
knowledge.hipotesis[index] || knowledge.hipotesis[0]
|
||
}.`,
|
||
}));
|
||
};
|
||
|
||
const finalizeSolutions = (
|
||
raw: SolutionDraft[],
|
||
causes: Cause[],
|
||
): Solution[] =>
|
||
raw
|
||
.map((solution) => {
|
||
const score = scoreSolution(
|
||
solution.impact,
|
||
solution.efficiency,
|
||
solution.speed,
|
||
solution.lowRisk,
|
||
);
|
||
|
||
return {
|
||
...solution,
|
||
id: uuid(),
|
||
rootCauseId: solution.rootCauseId || causes[0]?.id || uuid(),
|
||
impact: clamp(solution.impact),
|
||
efficiency: clamp(solution.efficiency),
|
||
speed: clamp(solution.speed),
|
||
lowRisk: clamp(solution.lowRisk),
|
||
score,
|
||
success: clamp(score + (solution.lowRisk - 70) * 0.25),
|
||
} as Solution;
|
||
})
|
||
.sort((a, b) => b.score - a.score);
|
||
|
||
const inferCauses = (
|
||
problem: string,
|
||
urgency: number,
|
||
financialImpact: Run['financialImpact'],
|
||
): Cause[] => {
|
||
const matchedKnowledge = findProblemKnowledge(problem);
|
||
|
||
if (matchedKnowledge) {
|
||
return buildKnowledgeCauses(matchedKnowledge, urgency, financialImpact);
|
||
}
|
||
|
||
const text = problem.toLowerCase();
|
||
const impactBoost =
|
||
financialImpact === 'high' ? 8 : financialImpact === 'medium' ? 4 : 0;
|
||
const base = 58 + urgency * 5 + impactBoost;
|
||
|
||
const candidates: Cause[] = [
|
||
{
|
||
id: uuid(),
|
||
label: text.match(/jual|sales|revenue|omzet|penjualan|conversion/)
|
||
? 'Akuisisi & konversi penjualan melemah'
|
||
: 'Sinyal performa utama belum stabil',
|
||
category: 'sales' as Cause['category'],
|
||
contribution: clamp(base + 16),
|
||
description:
|
||
'Indikasi utama datang dari funnel permintaan, kualitas prospek, atau kemampuan tim mengubah peluang menjadi transaksi.',
|
||
},
|
||
{
|
||
id: uuid(),
|
||
label: text.match(/iklan|marketing|campaign|brand|traffic/)
|
||
? 'Pesan marketing kurang tepat sasaran'
|
||
: 'Eksperimen marketing belum cukup terukur',
|
||
category: 'measurement',
|
||
contribution: clamp(base + 9),
|
||
description:
|
||
'Channel, pesan, dan metrik evaluasi perlu dipisahkan agar keputusan tidak hanya berdasarkan asumsi.',
|
||
},
|
||
{
|
||
id: uuid(),
|
||
label: text.match(/harga|margin|biaya|cash|modal/)
|
||
? 'Tekanan harga, margin, atau biaya'
|
||
: 'Prioritas operasional belum selaras dengan target',
|
||
category: 'process',
|
||
contribution: clamp(base + 3),
|
||
description:
|
||
'Ada peluang menata ulang proses, prioritas kerja, dan batasan biaya sebelum menjalankan solusi besar.',
|
||
},
|
||
{
|
||
id: uuid(),
|
||
label: text.match(/produk|fitur|quality|kualitas|stok/)
|
||
? 'Kesesuaian produk dan pengalaman pelanggan perlu divalidasi'
|
||
: 'Risiko eksekusi perlu dikendalikan sejak awal',
|
||
category: 'product' as Cause['category'],
|
||
contribution: clamp(base - 5),
|
||
description:
|
||
'Solusi harus diuji cepat agar tim tidak menghabiskan biaya pada arah yang belum terbukti.',
|
||
},
|
||
];
|
||
|
||
return candidates
|
||
.map((cause) => ({
|
||
...cause,
|
||
category:
|
||
cause.category === 'sales' || cause.category === 'product'
|
||
? 'other'
|
||
: cause.category,
|
||
}))
|
||
.sort((a, b) => b.contribution - a.contribution);
|
||
};
|
||
|
||
const buildDataSources = (
|
||
problem: string,
|
||
urgency: number,
|
||
financialImpact: Run['financialImpact'],
|
||
causes: Cause[],
|
||
): DataSource[] => {
|
||
const cleanProblem = problem.trim().replace(/\s+/g, ' ');
|
||
const financialLabel =
|
||
financialImpact === 'high'
|
||
? 'besar'
|
||
: financialImpact === 'medium'
|
||
? 'sedang'
|
||
: financialImpact === 'low'
|
||
? 'rendah'
|
||
: 'belum diketahui';
|
||
const topCauses = causes
|
||
.slice(0, 2)
|
||
.map((cause) => cause.label.toLowerCase())
|
||
.join(' dan ');
|
||
const matchedKnowledge = findProblemKnowledge(problem);
|
||
|
||
if (matchedKnowledge) {
|
||
const sources: DataSource[] = [
|
||
{
|
||
id: uuid(),
|
||
title: 'Input masalah dari pengguna',
|
||
category: 'input',
|
||
summary: `Narasi utama terdeteksi sebagai topik ${matchedKnowledge.label}: “${
|
||
cleanProblem.length > 132
|
||
? `${cleanProblem.slice(0, 129)}...`
|
||
: cleanProblem
|
||
}”.`,
|
||
confidence: clamp(90 + Math.min(cleanProblem.length / 50, 6)),
|
||
},
|
||
{
|
||
id: uuid(),
|
||
title: `Knowledge base ${matchedKnowledge.label}`,
|
||
category: 'pattern',
|
||
summary: `Severity ${matchedKnowledge.severity}. Penyebab dominan: ${shortList(
|
||
matchedKnowledge.penyebab,
|
||
MAX_DATA_SOURCES,
|
||
)}.`,
|
||
confidence: matchedKnowledge.severity === 'Kritis' ? 94 : 91,
|
||
},
|
||
{
|
||
id: uuid(),
|
||
title: 'Hipotesis dan prioritas eksekusi',
|
||
category: 'metric',
|
||
summary: `Hipotesis utama: ${shortList(
|
||
matchedKnowledge.hipotesis,
|
||
MAX_DATA_SOURCES,
|
||
)}. Urgensi ${urgency}/5 dan dampak finansial ${financialLabel}.`,
|
||
confidence: clamp(84 + urgency * 2),
|
||
},
|
||
];
|
||
|
||
return sources.slice(0, MAX_DATA_SOURCES);
|
||
}
|
||
|
||
const sources: DataSource[] = [
|
||
{
|
||
id: uuid(),
|
||
title: 'Input masalah dari pengguna',
|
||
category: 'input',
|
||
summary: `Narasi utama yang dianalisis: “${cleanProblem.length > 132 ? `${cleanProblem.slice(0, 129)}...` : cleanProblem}”.`,
|
||
confidence: clamp(88 + Math.min(cleanProblem.length / 40, 8)),
|
||
},
|
||
{
|
||
id: uuid(),
|
||
title: 'Parameter prioritas bisnis',
|
||
category: 'metric',
|
||
summary: `Urgensi berada di level ${urgency}/5 dengan dampak finansial ${financialLabel}; parameter ini menaikkan bobot prioritas dan kecepatan eksekusi.`,
|
||
confidence: clamp(82 + urgency * 2),
|
||
},
|
||
{
|
||
id: uuid(),
|
||
title: 'Pola akar masalah terdeteksi',
|
||
category: 'pattern',
|
||
summary: topCauses
|
||
? `OPTEMA membaca pola dominan pada ${topCauses}; pola ini menjadi dasar pemilihan solusi dan action plan.`
|
||
: 'OPTEMA belum menemukan pola dominan yang kuat; solusi diarahkan ke validasi cepat agar data berikutnya lebih presisi.',
|
||
confidence: clamp(causes[0]?.contribution || 72),
|
||
},
|
||
];
|
||
|
||
return sources.slice(0, MAX_DATA_SOURCES);
|
||
};
|
||
|
||
const buildKnowledgeSolutionDrafts = (
|
||
problem: string,
|
||
knowledge: ProblemKnowledge,
|
||
urgency: number,
|
||
causes: Cause[],
|
||
): SolutionDraft[] => {
|
||
const allSolutionOptions = shortList(knowledge.solusi, 10);
|
||
const topHypotheses = shortList(knowledge.hipotesis, MAX_DATA_SOURCES);
|
||
const topCauses = shortList(knowledge.penyebab, MAX_DATA_SOURCES);
|
||
const withQuestionFit = (
|
||
drafts: SolutionDraft[],
|
||
fits: QuestionSolutionFit[],
|
||
) => applyQuestionFitToDrafts(drafts, fits, problem);
|
||
|
||
if (knowledge.key === 'pengangguran') {
|
||
return withQuestionFit([
|
||
{
|
||
title: 'Pelatihan keterampilan dan sertifikasi kerja terarah',
|
||
description:
|
||
'Membangun jalur peningkatan kompetensi yang langsung dihubungkan dengan kebutuhan industri, sertifikasi, dan target penempatan kerja.',
|
||
rationale: `Diprioritaskan karena penyebab utama adalah ${topCauses}. Hipotesis yang diuji: ${topHypotheses}. Paket solusi dasar: ${allSolutionOptions}.`,
|
||
executionSteps: [
|
||
'Petakan 3–5 sektor industri yang masih membuka lowongan dan daftar skill yang paling sering diminta.',
|
||
'Kelompokkan pencari kerja berdasarkan level skill, pengalaman, pendidikan, dan kesiapan mengikuti sertifikasi.',
|
||
'Jalankan pelatihan digital/vokasi pendek berbasis proyek agar peserta punya portofolio nyata.',
|
||
'Fasilitasi sertifikasi yang relevan dengan lowongan prioritas, bukan sertifikasi umum yang sulit dikonversi menjadi pekerjaan.',
|
||
'Hubungkan peserta tersertifikasi ke job matching, magang, dan interview terjadwal dengan perusahaan/UMKM lokal.',
|
||
'Review hasil setiap 2 minggu: peserta lulus, lamaran terkirim, interview, penempatan, dan gap skill yang tersisa.',
|
||
],
|
||
kpis: [
|
||
'Jumlah peserta menyelesaikan pelatihan',
|
||
'Persentase peserta memperoleh sertifikasi',
|
||
'Jumlah peserta masuk interview/magang',
|
||
'Tingkat penempatan kerja setelah 30–60 hari',
|
||
],
|
||
risks: [
|
||
'Pelatihan tidak sesuai kebutuhan industri; mitigasi: validasi kurikulum dengan lowongan aktif dan HR/perusahaan lokal.',
|
||
'Peserta berhenti di tengah program; mitigasi: gunakan kelas pendek, mentor, dan target portofolio mingguan.',
|
||
'Sertifikasi tidak meningkatkan peluang kerja; mitigasi: pilih sertifikasi yang disebut eksplisit dalam lowongan prioritas.',
|
||
],
|
||
resources: [
|
||
'Data lowongan aktif',
|
||
'Trainer vokasi/digital',
|
||
'Mitra sertifikasi',
|
||
'PIC job matching',
|
||
],
|
||
type: 'hr',
|
||
impact: 92 + urgency,
|
||
efficiency: 86,
|
||
speed: 78,
|
||
lowRisk: 82,
|
||
duration: 30,
|
||
rootCauseId: causes[1]?.id || causes[0]?.id,
|
||
},
|
||
{
|
||
title: 'Job matching, CV profesional, dan pencarian kerja aktif',
|
||
description:
|
||
'Memperbaiki akses informasi kerja, kualitas CV, kesiapan interview, dan ritme lamaran agar peluang kerja meningkat cepat.',
|
||
rationale:
|
||
'Cocok untuk mengatasi kurangnya informasi lowongan, jaringan profesional terbatas, dan rendahnya pengalaman mencari kerja secara terstruktur.',
|
||
executionSteps: [
|
||
'Bangun database lowongan yang dikurasi berdasarkan lokasi, skill minimum, gaji, dan peluang entry-level.',
|
||
'Buat template CV profesional, LinkedIn/profil digital, dan surat lamaran yang disesuaikan per jenis posisi.',
|
||
'Latih simulasi interview, komunikasi profesional, dan cara menjelaskan pengalaman nonformal atau proyek portofolio.',
|
||
'Tetapkan target pencarian kerja aktif: jumlah lamaran, follow-up, networking, dan evaluasi penolakan per minggu.',
|
||
'Pasangkan kandidat dengan mentor atau alumni agar mereka mendapat referensi dan informasi lowongan lebih cepat.',
|
||
],
|
||
kpis: [
|
||
'Jumlah CV/profil diperbaiki',
|
||
'Jumlah lamaran berkualitas per minggu',
|
||
'Rasio panggilan interview',
|
||
'Jumlah kandidat mendapat offer/magang',
|
||
],
|
||
risks: [
|
||
'Lamaran banyak tetapi tidak relevan; mitigasi: pakai daftar lowongan terkurasi dan scoring kecocokan skill.',
|
||
'Kandidat kurang percaya diri saat interview; mitigasi: lakukan simulasi dan feedback sebelum interview nyata.',
|
||
],
|
||
resources: [
|
||
'Database lowongan',
|
||
'Template CV dan profil digital',
|
||
'Mentor karier',
|
||
'Kemitraan perusahaan/HR',
|
||
],
|
||
type: 'hr',
|
||
impact: 87,
|
||
efficiency: 89,
|
||
speed: 92,
|
||
lowRisk: 78,
|
||
duration: 14,
|
||
rootCauseId: causes[2]?.id || causes[0]?.id,
|
||
},
|
||
{
|
||
title: 'Magang, networking, dan wirausaha mikro',
|
||
description:
|
||
'Membuka jalur pengalaman kerja melalui magang, jejaring profesional, dan opsi wirausaha kecil untuk mengurangi ketergantungan pada lowongan formal.',
|
||
rationale:
|
||
'Menjawab hambatan kurang pengalaman, persaingan tinggi, dan keterbatasan lapangan kerja dengan jalur pengalaman serta pendapatan alternatif.',
|
||
executionSteps: [
|
||
'Identifikasi UMKM/perusahaan lokal yang bisa menerima magang pendek berbasis proyek dan target output jelas.',
|
||
'Bentuk komunitas networking pencari kerja, mentor, pelaku usaha, dan perusahaan lokal.',
|
||
'Siapkan proyek mikro: jasa digital, produksi kecil, reseller, atau layanan lokal yang bisa menghasilkan portofolio dan pendapatan awal.',
|
||
'Berikan pendampingan dasar keuangan, pemasaran, dan layanan pelanggan untuk peserta yang memilih wirausaha.',
|
||
'Dokumentasikan pengalaman peserta menjadi portofolio agar tetap berguna untuk melamar kerja formal.',
|
||
],
|
||
kpis: [
|
||
'Jumlah slot magang aktif',
|
||
'Jumlah proyek portofolio selesai',
|
||
'Pendapatan awal dari usaha mikro',
|
||
'Jumlah koneksi profesional baru',
|
||
],
|
||
risks: [
|
||
'Magang tidak berkualitas; mitigasi: tetapkan output, mentor, dan evaluasi mingguan.',
|
||
'Usaha mikro tidak berlanjut; mitigasi: mulai kecil, validasi permintaan, dan batasi modal awal.',
|
||
],
|
||
resources: [
|
||
'Mitra UMKM/perusahaan',
|
||
'Mentor bisnis/karier',
|
||
'Modul wirausaha mikro',
|
||
'Komunitas alumni/pencari kerja',
|
||
],
|
||
type: 'hr',
|
||
impact: 82,
|
||
efficiency: 76,
|
||
speed: 84,
|
||
lowRisk: 74,
|
||
duration: 21,
|
||
rootCauseId: causes[3]?.id || causes[0]?.id,
|
||
},
|
||
], [
|
||
{
|
||
focus: 'peningkatan skill, pelatihan, sertifikasi, dan kesiapan industri',
|
||
keywords: [
|
||
'skill',
|
||
'keterampilan',
|
||
'kompetensi',
|
||
'sertifikasi',
|
||
'pelatihan',
|
||
'pendidikan',
|
||
'industri',
|
||
'otomatisasi',
|
||
'digital',
|
||
],
|
||
},
|
||
{
|
||
focus: 'akses informasi lowongan, CV, lamaran, interview, dan job matching',
|
||
keywords: [
|
||
'lowongan',
|
||
'informasi',
|
||
'cv',
|
||
'lamaran',
|
||
'interview',
|
||
'wawancara',
|
||
'job matching',
|
||
'pencarian kerja',
|
||
'pencari kerja',
|
||
'lokasi',
|
||
],
|
||
},
|
||
{
|
||
focus: 'pengalaman kerja, magang, networking, portofolio, atau wirausaha',
|
||
keywords: [
|
||
'pengalaman',
|
||
'magang',
|
||
'networking',
|
||
'jaringan',
|
||
'wirausaha',
|
||
'usaha',
|
||
'umkm',
|
||
'lapangan kerja',
|
||
'modal',
|
||
'portofolio',
|
||
],
|
||
},
|
||
]);
|
||
}
|
||
|
||
if (knowledge.key === 'keluarga_hukum_waris') {
|
||
return withQuestionFit([
|
||
{
|
||
title: 'Konsultasi hukum keluarga dan waris terpadu',
|
||
description:
|
||
'Memisahkan isu perselingkuhan, perceraian, hak anak, harta bersama, dan warisan agar keluarga punya langkah hukum yang aman dan tidak merugikan 10 anak.',
|
||
rationale: `Diprioritaskan karena penyebab utama adalah ${topCauses}. Hipotesis yang diuji: ${topHypotheses}. Paket solusi dasar: ${allSolutionOptions}. Rekomendasi ini bersifat arahan awal dan perlu divalidasi dengan advokat, pos bantuan hukum, mediator, notaris, atau pejabat berwenang.`,
|
||
executionSteps: [
|
||
'Tulis kronologi singkat kejadian perselingkuhan, konflik rumah tangga, proses cerai, isu warisan, dan posisi 10 anak tanpa menyebarkan tuduhan ke pihak luar.',
|
||
'Kumpulkan dokumen inti: buku nikah/akta cerai bila ada, kartu keluarga, akta kelahiran anak, identitas pihak terkait, sertifikat aset, rekening, surat utang, dan dokumen waris.',
|
||
'Pisahkan masalah menjadi 4 jalur: status perkawinan/perceraian, hak asuh dan nafkah anak, harta bersama, serta warisan/ahli waris.',
|
||
'Buat daftar ahli waris, aset, utang, dan pihak yang menguasai aset saat ini agar pembahasan tidak bercampur dengan emosi konflik.',
|
||
'Jadwalkan konsultasi dengan advokat/pos bantuan hukum/mediator keluarga dan bawa dokumen serta pertanyaan tertulis.',
|
||
'Tentukan langkah aman berikutnya: mediasi tertulis, perlindungan hak anak sementara, pengamanan dokumen, atau jalur pengadilan bila damai tidak memungkinkan.',
|
||
],
|
||
kpis: [
|
||
'Kronologi dan dokumen inti terkumpul',
|
||
'Daftar aset, utang, ahli waris, dan kebutuhan 10 anak tersusun',
|
||
'Jadwal konsultasi hukum atau mediasi ditetapkan',
|
||
'Rencana sementara hak asuh, nafkah, sekolah, kesehatan, dan tempat tinggal anak tersedia',
|
||
],
|
||
risks: [
|
||
'Konflik keluarga memanas; mitigasi: gunakan mediator netral dan hindari keputusan sepihak saat emosi tinggi.',
|
||
'Dokumen atau aset hilang/dialihkan; mitigasi: buat salinan dokumen, catat penguasaan aset, dan konsultasikan langkah pengamanan hukum.',
|
||
'Hak anak terabaikan; mitigasi: dahulukan biaya hidup, sekolah, kesehatan, dan tempat tinggal anak sebelum sengketa harta diperluas.',
|
||
],
|
||
resources: [
|
||
'Dokumen keluarga dan aset',
|
||
'Advokat atau pos bantuan hukum',
|
||
'Mediator keluarga',
|
||
'Notaris/PPAT bila menyangkut aset',
|
||
'Pendamping psikososial anak',
|
||
],
|
||
type: 'other',
|
||
impact: 94 + urgency,
|
||
efficiency: 86,
|
||
speed: 82,
|
||
lowRisk: 78,
|
||
duration: 7,
|
||
rootCauseId: causes[0]?.id,
|
||
},
|
||
{
|
||
title: 'Mediasi keluarga dan rencana perlindungan anak',
|
||
description:
|
||
'Menurunkan eskalasi konflik dengan forum mediasi yang fokus pada keselamatan, kebutuhan, hak asuh, nafkah, dan stabilitas 10 anak.',
|
||
rationale:
|
||
'Cocok saat konflik rumah tangga dan perceraian mulai memengaruhi anak-anak; mediasi membantu membuat kesepakatan sementara sebelum proses hukum berjalan lebih jauh.',
|
||
executionSteps: [
|
||
'Tentukan pihak yang wajib hadir dan pihak netral yang dipercaya, seperti mediator keluarga, tokoh yang disepakati, konselor, atau pendamping hukum.',
|
||
'Buat agenda mediasi terbatas: kebutuhan anak, jadwal pengasuhan, nafkah sementara, akses sekolah/kesehatan, dan larangan mengalihkan aset tanpa persetujuan.',
|
||
'Pisahkan pembahasan dugaan perselingkuhan dari pembahasan kebutuhan anak agar anak tidak menjadi alat tekanan konflik orang tua.',
|
||
'Tuliskan hasil mediasi sementara secara jelas: siapa membayar apa, kapan, bukti pembayaran, akses komunikasi anak, dan tanggal review.',
|
||
'Jika mediasi gagal atau ada ancaman/kekerasan, eskalasi ke pendamping hukum atau lembaga perlindungan yang berwenang.',
|
||
],
|
||
kpis: [
|
||
'Agenda mediasi dan pihak netral disepakati',
|
||
'Kesepakatan sementara nafkah dan pengasuhan anak tertulis',
|
||
'Kebutuhan sekolah, kesehatan, dan tempat tinggal anak terpetakan',
|
||
'Risiko konflik atau ancaman berkurang',
|
||
],
|
||
risks: [
|
||
'Salah satu pihak menolak hadir; mitigasi: minta mediator mengirim undangan tertulis dan siapkan opsi konsultasi hukum.',
|
||
'Anak terseret konflik; mitigasi: batasi pembicaraan orang dewasa dan libatkan pendamping anak bila perlu.',
|
||
'Kesepakatan tidak dipatuhi; mitigasi: dokumentasikan pelanggaran dan konsultasikan penguatan formalnya.',
|
||
],
|
||
resources: [
|
||
'Mediator keluarga',
|
||
'Daftar kebutuhan anak',
|
||
'Catatan komunikasi para pihak',
|
||
'Pendamping hukum atau konselor',
|
||
],
|
||
type: 'other',
|
||
impact: 90,
|
||
efficiency: 84,
|
||
speed: 88,
|
||
lowRisk: 82,
|
||
duration: 5,
|
||
rootCauseId: causes[1]?.id || causes[0]?.id,
|
||
},
|
||
{
|
||
title: 'Inventarisasi harta bersama, warisan, dan ahli waris',
|
||
description:
|
||
'Membuat peta aset, utang, status kepemilikan, ahli waris, dan dokumen pendukung sebelum ada pembagian atau keputusan atas warisan.',
|
||
rationale:
|
||
'Cocok ketika perceraian dan warisan bercampur; inventarisasi mencegah aset diperebutkan tanpa data dan membantu menentukan isu mana yang perlu notaris, mediator, atau pengadilan.',
|
||
executionSteps: [
|
||
'Buat tabel aset: tanah, rumah, kendaraan, tabungan, usaha, barang berharga, piutang, dan utang beserta bukti dokumennya.',
|
||
'Tandai status setiap aset: milik pribadi, harta bersama, harta warisan, masih sengketa, atau belum jelas.',
|
||
'Susun daftar calon ahli waris dan hubungan keluarga berdasarkan dokumen resmi, lalu catat pihak yang masih perlu diverifikasi.',
|
||
'Amankan salinan dokumen dan bukti penguasaan aset agar tidak hilang ketika konflik meningkat.',
|
||
'Konsultasikan tabel aset dan ahli waris ke notaris/PPAT atau advokat agar langkah pembagian tidak salah prosedur.',
|
||
],
|
||
kpis: [
|
||
'Tabel aset dan utang selesai',
|
||
'Status harta bersama dan warisan dipisahkan',
|
||
'Daftar ahli waris awal tersusun',
|
||
'Dokumen aset tersalin dan aman',
|
||
],
|
||
risks: [
|
||
'Data aset disembunyikan; mitigasi: kumpulkan bukti dari dokumen resmi, saksi, catatan transaksi, dan konsultasi hukum.',
|
||
'Pembagian dilakukan sebelum status jelas; mitigasi: tahan keputusan pembagian sampai ada validasi pihak berwenang.',
|
||
'Utang tidak dihitung; mitigasi: masukkan semua kewajiban sebelum menghitung bagian bersih.',
|
||
],
|
||
resources: [
|
||
'Sertifikat dan bukti aset',
|
||
'Dokumen ahli waris',
|
||
'Catatan utang/piutang',
|
||
'Notaris/PPAT atau advokat',
|
||
],
|
||
type: 'other',
|
||
impact: 86,
|
||
efficiency: 82,
|
||
speed: 76,
|
||
lowRisk: 86,
|
||
duration: 10,
|
||
rootCauseId: causes[2]?.id || causes[0]?.id,
|
||
},
|
||
], [
|
||
{
|
||
focus: 'perselingkuhan, perceraian, warisan, hak anak, harta bersama, dan perlindungan 10 anak',
|
||
keywords: [
|
||
'selingkuh',
|
||
'perselingkuhan',
|
||
'cerai',
|
||
'perceraian',
|
||
'warisan',
|
||
'waris',
|
||
'10 anak',
|
||
'anak',
|
||
'hak anak',
|
||
'hak asuh',
|
||
'nafkah',
|
||
'nafkah anak',
|
||
'harta bersama',
|
||
'harta gono gini',
|
||
'gono gini',
|
||
'gono-gini',
|
||
'ahli waris',
|
||
],
|
||
},
|
||
{
|
||
focus: 'mediasi keluarga, konflik rumah tangga, pengasuhan, nafkah, dan stabilitas anak',
|
||
keywords: [
|
||
'mediasi',
|
||
'konflik',
|
||
'keluarga',
|
||
'rumah tangga',
|
||
'suami',
|
||
'istri',
|
||
'anak',
|
||
'hak asuh',
|
||
'nafkah',
|
||
'damai',
|
||
'konselor',
|
||
],
|
||
},
|
||
{
|
||
focus: 'inventarisasi aset, utang, harta warisan, ahli waris, sertifikat, dan prosedur notaris/pengadilan',
|
||
keywords: [
|
||
'aset',
|
||
'harta',
|
||
'warisan',
|
||
'waris',
|
||
'ahli waris',
|
||
'sertifikat',
|
||
'tanah',
|
||
'rumah',
|
||
'utang',
|
||
'notaris',
|
||
'ppat',
|
||
'pengadilan',
|
||
],
|
||
},
|
||
]);
|
||
}
|
||
|
||
return withQuestionFit([
|
||
{
|
||
title: 'Pendampingan UMKM, modal mikro, dan digitalisasi usaha',
|
||
description:
|
||
'Menaikkan pendapatan keluarga melalui akses modal produktif, pendampingan usaha, pencatatan keuangan, dan kanal penjualan digital.',
|
||
rationale: `Diprioritaskan karena penyebab utama adalah ${topCauses}. Hipotesis yang diuji: ${topHypotheses}. Paket solusi dasar: ${allSolutionOptions}.`,
|
||
executionSteps: [
|
||
'Identifikasi keluarga/UMKM sasaran berdasarkan pendapatan, aset produktif, jenis usaha, dan kesiapan menjalankan usaha.',
|
||
'Tentukan kebutuhan modal produktif yang spesifik: stok, alat kerja, bahan baku, atau akses distribusi, bukan bantuan konsumtif umum.',
|
||
'Berikan kredit mikro/bantuan modal bertahap dengan pendampingan pencatatan cash-in, cash-out, margin, dan stok.',
|
||
'Digitalisasikan usaha secara sederhana: katalog WhatsApp, marketplace lokal, pembayaran digital, dan promosi komunitas.',
|
||
'Pasangkan UMKM dengan mentor dan kemitraan penjualan agar modal berubah menjadi omzet, bukan hanya tambahan utang.',
|
||
'Evaluasi setiap 2 minggu: omzet, margin, repeat order, arus kas, dan kemampuan membayar kembali modal.',
|
||
],
|
||
kpis: [
|
||
'Kenaikan pendapatan bersih keluarga',
|
||
'Jumlah UMKM aktif setelah pendampingan',
|
||
'Rasio modal menjadi omzet',
|
||
'Jumlah transaksi digital/kemitraan penjualan',
|
||
],
|
||
risks: [
|
||
'Modal dipakai untuk konsumsi; mitigasi: salurkan bertahap berdasarkan rencana usaha dan bukti pembelian produktif.',
|
||
'Usaha tidak punya pasar; mitigasi: validasi pembeli sebelum modal besar diberikan.',
|
||
'Pencatatan keuangan tidak disiplin; mitigasi: gunakan format harian sangat sederhana dan review mingguan.',
|
||
],
|
||
resources: [
|
||
'Dana/kredit mikro',
|
||
'Mentor UMKM',
|
||
'Template pencatatan keuangan',
|
||
'Kanal penjualan digital',
|
||
],
|
||
type: 'finance',
|
||
impact: 94 + urgency,
|
||
efficiency: 84,
|
||
speed: 76,
|
||
lowRisk: 76,
|
||
duration: 45,
|
||
rootCauseId: causes[3]?.id || causes[0]?.id,
|
||
},
|
||
{
|
||
title: 'Pelatihan kerja, pendidikan vokasi, dan program padat karya',
|
||
description:
|
||
'Menghubungkan keluarga berpendapatan rendah dengan keterampilan kerja cepat, pekerjaan sementara, dan jalur vokasi menuju pekerjaan lebih stabil.',
|
||
rationale:
|
||
'Menjawab pengangguran, pendidikan rendah, keterampilan terbatas, dan pendapatan tidak stabil dengan kombinasi income cepat dan skill jangka menengah.',
|
||
executionSteps: [
|
||
'Pilih skill vokasi yang cepat diserap pasar lokal: konstruksi ringan, kuliner, perawatan, administrasi, digital dasar, atau logistik.',
|
||
'Jalankan program padat karya untuk pendapatan cepat sambil peserta mengikuti pelatihan terjadwal.',
|
||
'Hubungkan peserta dengan sertifikasi sederhana, magang, atau kontrak kerja lokal setelah pelatihan.',
|
||
'Sediakan dukungan dasar seperti transportasi, jadwal fleksibel, atau childcare bila menjadi penghalang partisipasi.',
|
||
'Monitor transisi dari padat karya ke kerja tetap/usaha produktif agar bantuan tidak berhenti sebagai program sementara.',
|
||
],
|
||
kpis: [
|
||
'Jumlah peserta mendapat pendapatan cepat',
|
||
'Jumlah peserta menyelesaikan vokasi',
|
||
'Jumlah penempatan kerja/magang',
|
||
'Kenaikan pendapatan 30–90 hari',
|
||
],
|
||
risks: [
|
||
'Program padat karya berhenti tanpa transisi; mitigasi: sejak awal pasangkan dengan pelatihan dan job matching.',
|
||
'Peserta tidak bisa hadir konsisten; mitigasi: jadwal fleksibel dan dukungan transportasi/keluarga.',
|
||
],
|
||
resources: [
|
||
'Mitra pelatihan vokasi',
|
||
'Anggaran padat karya',
|
||
'Instruktur/mentor',
|
||
'Mitra penempatan kerja',
|
||
],
|
||
type: 'hr',
|
||
impact: 90,
|
||
efficiency: 80,
|
||
speed: 86,
|
||
lowRisk: 78,
|
||
duration: 30,
|
||
rootCauseId: causes[1]?.id || causes[0]?.id,
|
||
},
|
||
{
|
||
title: 'Kemitraan usaha dan peningkatan produktivitas komunitas',
|
||
description:
|
||
'Membangun akses ekonomi melalui kemitraan pemasok, koperasi/kelompok usaha, peningkatan produktivitas, dan akses layanan pendukung.',
|
||
rationale:
|
||
'Cocok untuk wilayah dengan akses layanan terbatas, produktivitas rendah, dan ketimpangan ekonomi yang tidak selesai hanya dengan bantuan tunai.',
|
||
executionSteps: [
|
||
'Petakan komoditas, jasa, atau keterampilan lokal yang bisa dikembangkan sebagai produk komunitas.',
|
||
'Bentuk kelompok usaha/koperasi kecil untuk pembelian bahan baku, produksi, pemasaran, dan negosiasi harga bersama.',
|
||
'Cari kemitraan dengan off-taker, toko lokal, BUMDes, komunitas, atau marketplace agar ada permintaan yang jelas.',
|
||
'Perbaiki produktivitas melalui alat sederhana, standar kualitas, jadwal produksi, dan pembagian peran.',
|
||
'Hubungkan keluarga miskin dengan layanan pendukung: kesehatan dasar, administrasi, pendidikan, dan akses modal lanjutan.',
|
||
],
|
||
kpis: [
|
||
'Jumlah kemitraan/off-taker aktif',
|
||
'Produktivitas per kelompok usaha',
|
||
'Jumlah keluarga mendapat akses layanan pendukung',
|
||
'Kenaikan margin/pendapatan komunitas',
|
||
],
|
||
risks: [
|
||
'Kelompok usaha konflik internal; mitigasi: aturan peran, pembukuan transparan, dan fasilitator netral.',
|
||
'Permintaan pasar tidak stabil; mitigasi: mulai dengan kontrak kecil/off-taker yang sudah teridentifikasi.',
|
||
],
|
||
resources: [
|
||
'Fasilitator komunitas',
|
||
'Mitra off-taker',
|
||
'Alat produktivitas sederhana',
|
||
'Akses layanan sosial/ekonomi',
|
||
],
|
||
type: 'operations',
|
||
impact: 84,
|
||
efficiency: 78,
|
||
speed: 70,
|
||
lowRisk: 82,
|
||
duration: 60,
|
||
rootCauseId: causes[2]?.id || causes[0]?.id,
|
||
},
|
||
], [
|
||
{
|
||
focus: 'modal usaha, kredit mikro, UMKM, dan digitalisasi pendapatan',
|
||
keywords: [
|
||
'modal',
|
||
'modal usaha',
|
||
'kredit mikro',
|
||
'bantuan modal',
|
||
'umkm',
|
||
'usaha',
|
||
'pendapatan',
|
||
'ekonomi keluarga',
|
||
'digitalisasi',
|
||
'produktif',
|
||
],
|
||
},
|
||
{
|
||
focus: 'pelatihan kerja, pendidikan vokasi, pekerjaan cepat, dan padat karya',
|
||
keywords: [
|
||
'pelatihan',
|
||
'kerja',
|
||
'pendidikan',
|
||
'vokasi',
|
||
'padat karya',
|
||
'skill',
|
||
'keterampilan',
|
||
'pengangguran',
|
||
'pekerjaan',
|
||
'pendapatan tidak stabil',
|
||
],
|
||
},
|
||
{
|
||
focus: 'kemitraan usaha, produktivitas komunitas, akses layanan, dan pasar',
|
||
keywords: [
|
||
'kemitraan',
|
||
'produktifitas',
|
||
'produktivitas',
|
||
'komunitas',
|
||
'koperasi',
|
||
'pasar',
|
||
'akses layanan',
|
||
'ketimpangan',
|
||
'wilayah tertinggal',
|
||
'aset produktif',
|
||
],
|
||
},
|
||
]);
|
||
};
|
||
|
||
const buildSolutions = (
|
||
problem: string,
|
||
urgency: number,
|
||
causes: Cause[],
|
||
): Solution[] => {
|
||
const matchedKnowledge = findProblemKnowledge(problem);
|
||
|
||
if (matchedKnowledge) {
|
||
return finalizeSolutions(
|
||
buildKnowledgeSolutionDrafts(problem, matchedKnowledge, urgency, causes),
|
||
causes,
|
||
);
|
||
}
|
||
|
||
const text = problem.toLowerCase();
|
||
const isSales = Boolean(
|
||
text.match(/jual|sales|omzet|penjualan|conversion|revenue/),
|
||
);
|
||
const isOps = Boolean(
|
||
text.match(/operasi|stok|logistik|produksi|delivery|proses/),
|
||
);
|
||
const isCost = Boolean(text.match(/biaya|cash|margin|modal|cost|budget/));
|
||
|
||
const raw = [
|
||
{
|
||
title: isSales
|
||
? 'Optimasi funnel penjualan 7 hari'
|
||
: 'Sprint validasi peluang tertinggi',
|
||
description:
|
||
'Audit cepat data, segmentasi pelanggan, perbaiki pesan utama, lalu jalankan eksperimen kecil dengan metrik harian.',
|
||
rationale: isSales
|
||
? 'Dipilih karena masalah penjualan biasanya paling cepat diperbaiki dengan memperjelas segmen, memperketat follow-up, dan mengukur conversion rate harian.'
|
||
: 'Dipilih karena membantu tim menguji peluang dengan biaya kecil sebelum mengambil keputusan yang lebih besar.',
|
||
executionSteps: [
|
||
'Tarik data 30–90 hari terakhir: sumber lead, penawaran, conversion, nilai transaksi, dan alasan gagal closing.',
|
||
'Kelompokkan pelanggan/lead berdasarkan segmen paling bernilai, lalu pilih satu segmen prioritas untuk eksperimen cepat.',
|
||
'Perbaiki value proposition, CTA, skrip follow-up, dan paket penawaran agar hambatan utama langsung dijawab.',
|
||
'Jalankan eksperimen 7 hari dengan dashboard harian untuk melihat lead masuk, conversion, biaya, dan omzet tambahan.',
|
||
],
|
||
kpis: [
|
||
'Conversion rate harian',
|
||
'Cost per qualified lead',
|
||
'Omzet tambahan',
|
||
'Jumlah follow-up selesai',
|
||
],
|
||
risks: [
|
||
'Data penjualan tidak lengkap; mitigasi: gunakan baseline sederhana dan catat manual selama 7 hari.',
|
||
'Tim terlalu banyak mencoba kanal; mitigasi: fokus hanya pada 1–2 kanal utama dulu.',
|
||
],
|
||
resources: [
|
||
'1 PIC sales/marketing',
|
||
'Data transaksi dan lead',
|
||
'Template pesan/skrip follow-up',
|
||
'Budget eksperimen kecil',
|
||
],
|
||
type: isSales ? 'sales' : 'operations',
|
||
impact: 88 + urgency,
|
||
efficiency: 76,
|
||
speed: 90,
|
||
lowRisk: 72,
|
||
duration: 7,
|
||
rootCauseId: causes[0]?.id,
|
||
},
|
||
{
|
||
title: isCost
|
||
? 'Kontrol biaya dan prioritas cash-flow'
|
||
: 'Program reseller/kemitraan mikro',
|
||
description: isCost
|
||
? 'Pisahkan biaya wajib dan variabel, hentikan pengeluaran rendah ROI, lalu alihkan anggaran ke kanal yang terukur.'
|
||
: 'Bangun penawaran sederhana untuk partner, siapkan materi jual, dan ukur performa tiap calon kanal baru.',
|
||
rationale: isCost
|
||
? 'Dipilih untuk menjaga ruang napas bisnis: arus kas diamankan lebih dulu, lalu biaya dialihkan ke aktivitas yang dampaknya bisa dibuktikan.'
|
||
: 'Dipilih karena kemitraan kecil bisa memperluas distribusi tanpa menambah beban fixed cost besar di awal.',
|
||
executionSteps: isCost
|
||
? [
|
||
'Klasifikasikan biaya menjadi wajib, pendukung pertumbuhan, eksperimen, dan biaya yang bisa dihentikan sementara.',
|
||
'Tentukan batas biaya mingguan dan daftar pengeluaran yang harus disetujui sebelum berjalan.',
|
||
'Alihkan budget dari aktivitas rendah ROI ke kanal/produk yang memiliki bukti penjualan terbaik.',
|
||
'Review cash-in dan cash-out setiap 3 hari untuk memastikan keputusan biaya langsung terlihat efeknya.',
|
||
]
|
||
: [
|
||
'Definisikan profil partner ideal: reseller, komunitas, kreator, atau bisnis komplementer dengan audiens relevan.',
|
||
'Siapkan paket kemitraan: margin, materi promosi, contoh pesan, aturan komisi, dan target minimum.',
|
||
'Rekrut 5–10 partner mikro, mulai dari jaringan terdekat yang sudah dipercaya pelanggan.',
|
||
'Bandingkan performa partner berdasarkan leads, transaksi, repeat order, dan biaya insentif.',
|
||
],
|
||
kpis: isCost
|
||
? [
|
||
'Cash runway',
|
||
'Biaya rendah ROI yang dihentikan',
|
||
'Margin kontribusi',
|
||
'ROI kanal prioritas',
|
||
]
|
||
: [
|
||
'Jumlah partner aktif',
|
||
'Lead dari partner',
|
||
'Penjualan per partner',
|
||
'Biaya komisi per transaksi',
|
||
],
|
||
risks: isCost
|
||
? [
|
||
'Pemotongan biaya mengganggu kualitas layanan; mitigasi: jangan hentikan biaya yang langsung menjaga pengalaman pelanggan.',
|
||
'Tim kehilangan momentum growth; mitigasi: sisakan budget eksperimen kecil yang terukur.',
|
||
]
|
||
: [
|
||
'Partner tidak aktif setelah onboarding; mitigasi: berikan target 7 hari dan materi siap pakai.',
|
||
'Komisi terlalu besar menekan margin; mitigasi: batasi promo pada produk/margin tertentu.',
|
||
],
|
||
resources: isCost
|
||
? [
|
||
'Laporan biaya',
|
||
'Data margin produk',
|
||
'PIC finance/owner',
|
||
'Dashboard cash-flow sederhana',
|
||
]
|
||
: [
|
||
'Materi penawaran',
|
||
'Kode/referral partner',
|
||
'PIC partner relations',
|
||
'Template pelaporan mingguan',
|
||
],
|
||
type: isCost ? 'finance' : 'marketing',
|
||
impact: 84,
|
||
efficiency: 84,
|
||
speed: 68,
|
||
lowRisk: 80,
|
||
duration: 14,
|
||
rootCauseId: causes[1]?.id,
|
||
},
|
||
{
|
||
title: isOps
|
||
? 'Perbaikan proses operasional kritis'
|
||
: 'Promo bundling taktis berbasis data',
|
||
description: isOps
|
||
? 'Petakan bottleneck, tetapkan SLA harian, dan buat ritme monitoring singkat agar hambatan terlihat lebih cepat.'
|
||
: 'Uji bundling dengan batas waktu, stok, dan target margin yang jelas agar efek terhadap omzet dan profit bisa dibandingkan.',
|
||
rationale: isOps
|
||
? 'Dipilih karena bottleneck operasional sering membuat biaya naik, pelanggan kecewa, dan peluang penjualan hilang meski demand ada.'
|
||
: 'Dipilih untuk menghasilkan sinyal pasar cepat tanpa mengubah produk inti; cocok saat bisnis butuh dorongan omzet jangka pendek.',
|
||
executionSteps: isOps
|
||
? [
|
||
'Petakan proses dari permintaan masuk sampai delivery/penyelesaian, lalu tandai titik tunggu dan rework terbesar.',
|
||
'Tetapkan SLA harian untuk titik kritis dan buat papan monitoring sederhana yang terlihat oleh semua PIC.',
|
||
'Hilangkan satu hambatan terbesar terlebih dahulu melalui perubahan SOP, pembagian tugas, atau checklist kualitas.',
|
||
'Lakukan review singkat setiap akhir hari untuk memutuskan lanjut, koreksi, atau eskalasi.',
|
||
]
|
||
: [
|
||
'Pilih produk/jasa dengan margin sehat dan produk pelengkap yang menaikkan nilai transaksi.',
|
||
'Buat 2 variasi bundling: satu fokus volume cepat dan satu fokus margin/upsell.',
|
||
'Jalankan promo terbatas 5 hari dengan batas stok, batas waktu, dan pesan manfaat yang jelas.',
|
||
'Bandingkan hasil bundling dengan penjualan normal dari sisi omzet, margin, dan repeat interest.',
|
||
],
|
||
kpis: isOps
|
||
? [
|
||
'Lead time proses',
|
||
'Jumlah bottleneck terselesaikan',
|
||
'SLA harian tercapai',
|
||
'Keluhan pelanggan',
|
||
]
|
||
: [
|
||
'Average order value',
|
||
'Margin per bundle',
|
||
'Jumlah transaksi promo',
|
||
'Repeat inquiry',
|
||
],
|
||
risks: isOps
|
||
? [
|
||
'SOP baru tidak dipakai konsisten; mitigasi: jadikan checklist harian dan tunjuk satu owner.',
|
||
'Perubahan terlalu banyak sekaligus; mitigasi: pilih satu bottleneck paling kritis dulu.',
|
||
]
|
||
: [
|
||
'Diskon menurunkan persepsi nilai; mitigasi: tekankan bonus/manfaat, bukan sekadar potongan harga.',
|
||
'Stok tidak cukup; mitigasi: tetapkan kuota dan alternatif produk.',
|
||
],
|
||
resources: isOps
|
||
? [
|
||
'Peta proses',
|
||
'Checklist SLA',
|
||
'PIC operasional',
|
||
'Data komplain/lead time',
|
||
]
|
||
: [
|
||
'Data produk dan margin',
|
||
'Materi promo',
|
||
'Stok/kapasitas layanan',
|
||
'Kanal komunikasi pelanggan',
|
||
],
|
||
type: isOps ? 'operations' : 'sales',
|
||
impact: 76,
|
||
efficiency: 69,
|
||
speed: 95,
|
||
lowRisk: 88,
|
||
duration: 5,
|
||
rootCauseId: causes[2]?.id,
|
||
},
|
||
] as SolutionDraft[];
|
||
|
||
const genericFits: QuestionSolutionFit[] = [
|
||
{
|
||
focus: isSales
|
||
? 'penjualan, omzet, funnel, pelanggan, conversion, dan revenue'
|
||
: 'validasi peluang atau keputusan paling penting dari masalah pengguna',
|
||
keywords: isSales
|
||
? [
|
||
'jual',
|
||
'sales',
|
||
'omzet',
|
||
'penjualan',
|
||
'conversion',
|
||
'revenue',
|
||
'pelanggan',
|
||
'lead',
|
||
'toko',
|
||
'promosi',
|
||
]
|
||
: [
|
||
'validasi',
|
||
'peluang',
|
||
'target',
|
||
'strategi',
|
||
'keputusan',
|
||
'prioritas',
|
||
'masalah',
|
||
],
|
||
},
|
||
{
|
||
focus: isCost
|
||
? 'biaya, modal, budget, margin, cash-flow, dan kondisi keuangan'
|
||
: 'kemitraan, reseller, partner, distribusi, dan kanal baru',
|
||
keywords: isCost
|
||
? [
|
||
'biaya',
|
||
'cash',
|
||
'cash-flow',
|
||
'arus kas',
|
||
'margin',
|
||
'modal',
|
||
'cost',
|
||
'budget',
|
||
'uang',
|
||
'keuangan',
|
||
]
|
||
: [
|
||
'reseller',
|
||
'kemitraan',
|
||
'partner',
|
||
'distribusi',
|
||
'kanal',
|
||
'channel',
|
||
'komunitas',
|
||
'affiliate',
|
||
],
|
||
},
|
||
{
|
||
focus: isOps
|
||
? 'operasi, stok, logistik, produksi, delivery, SOP, dan bottleneck proses'
|
||
: 'promo, bundling, penawaran, paket, dan dorongan transaksi cepat',
|
||
keywords: isOps
|
||
? [
|
||
'operasi',
|
||
'stok',
|
||
'stock',
|
||
'logistik',
|
||
'produksi',
|
||
'delivery',
|
||
'pengiriman',
|
||
'proses',
|
||
'sop',
|
||
'bottleneck',
|
||
]
|
||
: [
|
||
'promo',
|
||
'bundling',
|
||
'diskon',
|
||
'penawaran',
|
||
'paket',
|
||
'transaksi',
|
||
'stok',
|
||
'jualan',
|
||
],
|
||
},
|
||
];
|
||
|
||
return finalizeSolutions(applyQuestionFitToDrafts(raw, genericFits, problem), causes);
|
||
};
|
||
|
||
const buildTasks = (solution: Solution): PlanTask[] => {
|
||
const steps = solution.executionSteps || [];
|
||
const kpis = solution.kpis || [];
|
||
const risks = solution.risks || [];
|
||
const resources = solution.resources || [];
|
||
const firstStep = trimSentenceEnd(steps[0] || solution.description);
|
||
const secondStep = trimSentenceEnd(
|
||
steps[1] || steps[0] || solution.description,
|
||
);
|
||
const mainRisk = trimSentenceEnd(
|
||
risks[0] || 'risiko eksekusi yang muncul selama uji coba',
|
||
);
|
||
const isLegalFamilyCase = Boolean(
|
||
`${solution.title} ${solution.description}`.match(
|
||
/hukum|waris|cerai|perceraian|hak asuh|nafkah|mediasi keluarga|anak/i,
|
||
),
|
||
);
|
||
|
||
if (isLegalFamilyCase) {
|
||
return [
|
||
{
|
||
id: uuid(),
|
||
day: 1,
|
||
title: 'Amankan fakta dan dokumen inti',
|
||
description: `Kumpulkan fakta dan dokumen awal untuk solusi “${solution.title}”: ${resources.slice(0, 3).join(', ') || 'dokumen keluarga, dokumen aset, dan daftar pihak terkait'}.`,
|
||
},
|
||
{
|
||
id: uuid(),
|
||
day: 2,
|
||
title: 'Pisahkan isu hukum utama',
|
||
description: `Turunkan kasus menjadi langkah hukum pertama yang jelas: ${firstStep}. Pisahkan isu perceraian, warisan, harta, dan hak anak agar tidak bercampur.`,
|
||
},
|
||
{
|
||
id: uuid(),
|
||
day: 3,
|
||
title: 'Mulai konsultasi atau mediasi',
|
||
description: `Jalankan langkah awal yang aman: ${secondStep}. Catat posisi tiap pihak, dokumen yang kurang, dan risiko untuk anak.`,
|
||
},
|
||
{
|
||
id: uuid(),
|
||
day: 4,
|
||
title: 'Cek perlindungan anak dan aset',
|
||
description: `Bandingkan progress dengan indikator utama: ${kpis.slice(0, 3).join(', ') || 'dokumen, mediasi, perlindungan anak, dan status aset'}. Koreksi jika ada hak anak atau aset yang belum terlindungi.`,
|
||
},
|
||
{
|
||
id: uuid(),
|
||
day: 5,
|
||
title: 'Putuskan jalur damai atau hukum formal',
|
||
description: `Pilih langkah lanjut berdasarkan dokumen, hasil konsultasi/mediasi, dan risiko utama: ${mainRisk}.`,
|
||
},
|
||
];
|
||
}
|
||
|
||
return [
|
||
{
|
||
id: uuid(),
|
||
day: 1,
|
||
title: 'Audit fakta dan baseline',
|
||
description:
|
||
`Kumpulkan data awal yang langsung terkait solusi “${solution.title}”: ${resources.slice(0, 3).join(', ') || 'data performa, PIC, dan baseline masalah'}.`,
|
||
},
|
||
{
|
||
id: uuid(),
|
||
day: 2,
|
||
title: 'Susun eksperimen solusi sesuai pertanyaan',
|
||
description: `Turunkan strategi “${solution.title}” menjadi langkah pertama yang spesifik: ${firstStep}.`,
|
||
},
|
||
{
|
||
id: uuid(),
|
||
day: 3,
|
||
title: 'Eksekusi batch pertama',
|
||
description:
|
||
`Jalankan batch kecil sesuai langkah implementasi: ${secondStep}. Catat hasil dan hambatan.`,
|
||
},
|
||
{
|
||
id: uuid(),
|
||
day: 4,
|
||
title: 'Monitoring dan koreksi cepat',
|
||
description:
|
||
`Bandingkan hasil dengan KPI utama: ${kpis.slice(0, 3).join(', ') || 'progress, dampak, dan risiko'}. Koreksi aktivitas yang tidak menjawab masalah.`,
|
||
},
|
||
{
|
||
id: uuid(),
|
||
day: 5,
|
||
title: 'Review keputusan lanjut',
|
||
description:
|
||
`Putuskan scale, iterate, atau stop berdasarkan KPI dan risiko utama: ${mainRisk}.`,
|
||
},
|
||
];
|
||
};
|
||
|
||
const buildConclusion = (
|
||
selectedSolution: Solution,
|
||
priority: Run['priority'],
|
||
priorityScore: number,
|
||
causes: Cause[],
|
||
dataSources: DataSource[],
|
||
knowledge?: ProblemKnowledge,
|
||
) => {
|
||
const mainCause = causes[0]?.label || 'akar masalah utama belum tervalidasi';
|
||
const evidenceText =
|
||
dataSources.length > 0
|
||
? `berdasarkan ${Math.min(dataSources.length, MAX_DATA_SOURCES)} sumber data ringkas yang tersedia`
|
||
: 'berdasarkan sinyal awal yang tersedia';
|
||
const knowledgeConclusion = knowledge
|
||
? ` Knowledge base ${knowledge.label} memiliki severity ${knowledge.severity} dan kesimpulan domain: ${knowledge.kesimpulan}`
|
||
: '';
|
||
|
||
if (knowledge?.key === 'keluarga_hukum_waris') {
|
||
return `Kesimpulan: kasus ini berada pada prioritas ${priority} (${priorityScore}/100) dan perlu ditangani dengan pendampingan hukum/mediasi bertahap, bukan keputusan emosional atau sepihak. ${evidenceText}, akar masalah paling dominan adalah ${mainCause.toLowerCase()}.${knowledgeConclusion} Rekomendasi utama adalah “${selectedSolution.title}” karena memiliki Decision Score ${selectedSolution.score}/100, estimasi success rate ${selectedSolution.success}%, dan dapat mulai dieksekusi dalam ${selectedSolution.duration} hari. Fokus 5 hari pertama adalah mengamankan dokumen, memetakan perceraian/warisan/harta/hak anak, konsultasi dengan pihak profesional, lalu memilih jalur damai tertulis atau hukum formal.`;
|
||
}
|
||
|
||
return `Kesimpulan: kasus ini berada pada prioritas ${priority} (${priorityScore}/100) dan perlu ditangani dengan eksperimen terukur, bukan keputusan besar tanpa validasi. ${evidenceText}, akar masalah paling dominan adalah ${mainCause.toLowerCase()}.${knowledgeConclusion} Rekomendasi utama adalah “${selectedSolution.title}” karena memiliki Decision Score ${selectedSolution.score}/100, estimasi success rate ${selectedSolution.success}%, dan dapat mulai dieksekusi dalam ${selectedSolution.duration} hari. Fokus 5 hari pertama adalah mengunci baseline, menjalankan eksperimen kecil, mengukur KPI harian, lalu memutuskan scale, iterate, atau stop.`;
|
||
};
|
||
|
||
const createRun = (
|
||
problem: string,
|
||
urgency: number,
|
||
financialImpact: Run['financialImpact'],
|
||
): Run => {
|
||
const matchedKnowledge = findProblemKnowledge(problem);
|
||
const causes = inferCauses(problem, urgency, financialImpact);
|
||
const dataSources = buildDataSources(
|
||
problem,
|
||
urgency,
|
||
financialImpact,
|
||
causes,
|
||
);
|
||
const solutions = buildSolutions(problem, urgency, causes);
|
||
const selectedSolution = solutions[0];
|
||
const impactBoost =
|
||
financialImpact === 'high'
|
||
? 18
|
||
: financialImpact === 'medium'
|
||
? 10
|
||
: financialImpact === 'low'
|
||
? 3
|
||
: 6;
|
||
const knowledgeBoost = matchedKnowledge
|
||
? matchedKnowledge.severity === 'Kritis'
|
||
? 10
|
||
: 6
|
||
: 0;
|
||
const priorityScore = clamp(
|
||
52 +
|
||
urgency * 7 +
|
||
impactBoost +
|
||
knowledgeBoost +
|
||
Math.min(problem.length / 18, 8),
|
||
);
|
||
const priority: Run['priority'] =
|
||
priorityScore >= 88
|
||
? 'critical'
|
||
: priorityScore >= 76
|
||
? 'high'
|
||
: priorityScore >= 58
|
||
? 'medium'
|
||
: 'low';
|
||
const conclusion = buildConclusion(
|
||
selectedSolution,
|
||
priority,
|
||
priorityScore,
|
||
causes,
|
||
dataSources,
|
||
matchedKnowledge,
|
||
);
|
||
|
||
return {
|
||
id: uuid(),
|
||
caseId: uuid(),
|
||
planId: uuid(),
|
||
createdAt: new Date().toISOString(),
|
||
title: titleFromProblem(problem),
|
||
problem,
|
||
urgency,
|
||
financialImpact,
|
||
priority,
|
||
priorityScore,
|
||
causes,
|
||
dataSources,
|
||
solutions,
|
||
selectedSolution,
|
||
tasks: buildTasks(selectedSolution),
|
||
conclusion,
|
||
};
|
||
};
|
||
|
||
const saveRunToBackend = async (run: Run, currentUserId?: string) => {
|
||
await axios.post('cases', {
|
||
data: {
|
||
id: run.caseId,
|
||
title: run.title,
|
||
problem_statement: run.problem,
|
||
status: 'ready',
|
||
priority: run.priority,
|
||
urgency_level: run.urgency,
|
||
priority_score: run.priorityScore,
|
||
financial_impact_level: run.financialImpact,
|
||
currency: 'IDR',
|
||
opened_at: run.createdAt,
|
||
notes: [
|
||
'Generated from OPTEMA AI Decision Lab MVP workflow.',
|
||
run.conclusion,
|
||
`Sumber data: ${(run.dataSources || [])
|
||
.slice(0, MAX_DATA_SOURCES)
|
||
.map((source) => source.title)
|
||
.join(', ')}`,
|
||
].join('\n'),
|
||
created_by_user: currentUserId || null,
|
||
},
|
||
});
|
||
|
||
await Promise.all(
|
||
run.causes.map((cause) =>
|
||
axios.post('root_cause_nodes', {
|
||
data: {
|
||
id: cause.id,
|
||
label: cause.label,
|
||
description: cause.description,
|
||
analysis_method: 'mixed',
|
||
category: cause.category,
|
||
depth_level: 1,
|
||
contribution_score: cause.contribution,
|
||
validated: false,
|
||
case: run.caseId,
|
||
},
|
||
}),
|
||
),
|
||
);
|
||
|
||
await Promise.all(
|
||
run.solutions.map((solution) =>
|
||
axios.post('solutions', {
|
||
data: {
|
||
id: solution.id,
|
||
title: solution.title,
|
||
description: [
|
||
solution.description,
|
||
`Alasan: ${solution.rationale || '-'}`,
|
||
`Langkah utama: ${(solution.executionSteps || []).join(' | ')}`,
|
||
`KPI: ${(solution.kpis || []).join(', ')}`,
|
||
`Risiko/mitigasi: ${(solution.risks || []).join(' | ')}`,
|
||
`Resource: ${(solution.resources || []).join(', ')}`,
|
||
].join('\n'),
|
||
solution_type: solution.type,
|
||
impact_score: solution.impact,
|
||
efficiency_score: solution.efficiency,
|
||
speed_score: solution.speed,
|
||
low_risk_score: solution.lowRisk,
|
||
decision_score: solution.score,
|
||
success_probability: solution.success,
|
||
status:
|
||
solution.id === run.selectedSolution.id ? 'selected' : 'proposed',
|
||
expected_duration_days: solution.duration,
|
||
case: run.caseId,
|
||
linked_root_cause: solution.rootCauseId,
|
||
},
|
||
}),
|
||
),
|
||
);
|
||
|
||
await axios.post('action_plans', {
|
||
data: {
|
||
id: run.planId,
|
||
title: `Rencana aksi: ${run.selectedSolution.title}`,
|
||
objective: `Menjalankan solusi terpilih untuk kasus “${run.title}” dengan ritme 5 hari kerja. ${run.conclusion}`,
|
||
status: 'active',
|
||
start_at: run.createdAt,
|
||
budget_currency: 'IDR',
|
||
solution: run.selectedSolution.id,
|
||
owner_user: currentUserId || null,
|
||
},
|
||
});
|
||
|
||
await Promise.all(
|
||
run.tasks.map((task) =>
|
||
axios.post('action_tasks', {
|
||
data: {
|
||
id: task.id,
|
||
title: task.title,
|
||
description: task.description,
|
||
status: task.day === 1 ? 'doing' : 'todo',
|
||
priority: task.day <= 2 ? 'high' : 'medium',
|
||
day_number: task.day,
|
||
estimated_hours: task.day === 1 ? 3 : 2,
|
||
action_plan: run.planId,
|
||
},
|
||
}),
|
||
),
|
||
);
|
||
};
|
||
|
||
const loadRuns = (): Run[] => {
|
||
if (typeof window === 'undefined') return [];
|
||
try {
|
||
const raw = window.localStorage.getItem(STORAGE_KEY);
|
||
return raw ? (JSON.parse(raw) as Run[]) : [];
|
||
} catch (error) {
|
||
console.error('Failed to load OPTEMA AI local history', error);
|
||
return [];
|
||
}
|
||
};
|
||
|
||
const persistRuns = (runs: Run[]) => {
|
||
if (typeof window === 'undefined') return;
|
||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(runs.slice(0, 8)));
|
||
};
|
||
|
||
const Pill = ({
|
||
children,
|
||
tone = 'blue',
|
||
}: {
|
||
children: React.ReactNode;
|
||
tone?: 'blue' | 'green' | 'orange' | 'slate';
|
||
}) => {
|
||
const tones = {
|
||
blue: 'bg-blue-50 text-blue-700 ring-blue-200 dark:bg-blue-900/30 dark:text-blue-200',
|
||
green:
|
||
'bg-emerald-50 text-emerald-700 ring-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-200',
|
||
orange:
|
||
'bg-orange-50 text-orange-700 ring-orange-200 dark:bg-orange-900/30 dark:text-orange-200',
|
||
slate:
|
||
'bg-slate-100 text-slate-700 ring-slate-200 dark:bg-slate-800 dark:text-slate-200',
|
||
};
|
||
return (
|
||
<span
|
||
className={`inline-flex rounded-full px-3 py-1 text-xs font-semibold ring-1 ${tones[tone]}`}
|
||
>
|
||
{children}
|
||
</span>
|
||
);
|
||
};
|
||
|
||
const OptemaAiPage = () => {
|
||
const { currentUser } = useAppSelector((state) => state.auth);
|
||
const [problem, setProblem] = useState(
|
||
'Pengangguran di wilayah saya meningkat karena skill warga tidak sesuai kebutuhan industri dan informasi lowongan sulit diakses.',
|
||
);
|
||
const [urgency, setUrgency] = useState(3);
|
||
const [financialImpact, setFinancialImpact] =
|
||
useState<Run['financialImpact']>('high');
|
||
const [runs, setRuns] = useState<Run[]>([]);
|
||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||
const [isSaving, setIsSaving] = useState(false);
|
||
const [notice, setNotice] = useState<{
|
||
type: 'success' | 'error';
|
||
message: string;
|
||
} | null>(null);
|
||
|
||
useEffect(() => {
|
||
const stored = loadRuns();
|
||
setRuns(stored);
|
||
setSelectedId(stored[0]?.id || null);
|
||
}, []);
|
||
|
||
const selectedRun = useMemo(
|
||
() => runs.find((run) => run.id === selectedId) || runs[0],
|
||
[runs, selectedId],
|
||
);
|
||
const selectedDataSources = useMemo(() => {
|
||
if (!selectedRun) return [];
|
||
const sources = selectedRun.dataSources?.length
|
||
? selectedRun.dataSources
|
||
: buildDataSources(
|
||
selectedRun.problem,
|
||
selectedRun.urgency,
|
||
selectedRun.financialImpact,
|
||
selectedRun.causes || [],
|
||
);
|
||
|
||
return sources.slice(0, MAX_DATA_SOURCES);
|
||
}, [selectedRun]);
|
||
const selectedConclusion = useMemo(() => {
|
||
if (!selectedRun) return '';
|
||
|
||
return (
|
||
selectedRun.conclusion ||
|
||
buildConclusion(
|
||
selectedRun.selectedSolution,
|
||
selectedRun.priority,
|
||
selectedRun.priorityScore,
|
||
selectedRun.causes || [],
|
||
selectedDataSources,
|
||
findProblemKnowledge(selectedRun.problem),
|
||
)
|
||
);
|
||
}, [selectedRun, selectedDataSources]);
|
||
|
||
const handleAnalyze = async () => {
|
||
setNotice(null);
|
||
if (problem.trim().length < 20) {
|
||
setNotice({
|
||
type: 'error',
|
||
message:
|
||
'Tuliskan masalah minimal 20 karakter agar analisis lebih bermakna.',
|
||
});
|
||
return;
|
||
}
|
||
|
||
const run = createRun(problem, urgency, financialImpact);
|
||
setIsSaving(true);
|
||
|
||
try {
|
||
await saveRunToBackend(run, currentUser?.id);
|
||
const nextRuns = [run, ...runs].slice(0, 8);
|
||
setRuns(nextRuns);
|
||
setSelectedId(run.id);
|
||
persistRuns(nextRuns);
|
||
setNotice({
|
||
type: 'success',
|
||
message:
|
||
'Analisis berhasil dibuat dan disimpan sebagai Case, Solutions, Root Causes, Action Plan, dan Tasks.',
|
||
});
|
||
} catch (error) {
|
||
console.error('OPTEMA AI workflow failed', error);
|
||
setNotice({
|
||
type: 'error',
|
||
message:
|
||
'Analisis gagal disimpan. Pastikan akun Anda memiliki izin membuat Cases, Solutions, Root Causes, Action Plans, dan Tasks.',
|
||
});
|
||
} finally {
|
||
setIsSaving(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<>
|
||
<Head>
|
||
<title>{getPageTitle('OPTEMA AI Decision Lab')}</title>
|
||
</Head>
|
||
<SectionMain>
|
||
<SectionTitleLineWithButton
|
||
icon={mdiBrain}
|
||
title='OPTEMA AI Decision Lab'
|
||
main
|
||
>
|
||
<BaseButton
|
||
href='/cases/cases-list'
|
||
label='Buka Cases'
|
||
color='whiteDark'
|
||
small
|
||
/>
|
||
</SectionTitleLineWithButton>
|
||
|
||
<div className='grid gap-6 lg:grid-cols-[0.9fr_1.4fr]'>
|
||
<div className='space-y-6'>
|
||
<CardBox
|
||
className='overflow-hidden border-0 bg-gradient-to-br from-[#08111F] via-[#102A43] to-[#13B8A6] text-white shadow-xl'
|
||
cardBoxClassName='p-6'
|
||
>
|
||
<div className='mb-5 flex items-center gap-3'>
|
||
<div className='rounded-2xl bg-white/15 p-3 ring-1 ring-white/20'>
|
||
<BaseIcon path={mdiChartTimelineVariant} size={26} />
|
||
</div>
|
||
<div>
|
||
<p className='text-sm font-semibold text-cyan-100'>
|
||
Problem Intake
|
||
</p>
|
||
<h2 className='text-2xl font-bold'>
|
||
Ubah masalah jadi keputusan.
|
||
</h2>
|
||
</div>
|
||
</div>
|
||
|
||
<label
|
||
className='mb-2 block text-sm font-semibold text-cyan-50'
|
||
htmlFor='problem'
|
||
>
|
||
Deskripsi masalah bisnis
|
||
</label>
|
||
<textarea
|
||
id='problem'
|
||
value={problem}
|
||
onChange={(event) => setProblem(event.target.value)}
|
||
className='min-h-44 w-full rounded-2xl border border-white/20 bg-white/10 p-4 text-sm text-white placeholder:text-white/50 outline-none ring-cyan-200/40 transition focus:ring-4'
|
||
placeholder='Contoh: Penjualan toko saya turun 40% dalam 3 bulan terakhir.'
|
||
/>
|
||
|
||
<div className='mt-5 grid gap-4 sm:grid-cols-2'>
|
||
<div className='rounded-2xl bg-white/10 p-4 ring-1 ring-white/15'>
|
||
<label
|
||
className='mb-3 block text-sm font-semibold'
|
||
htmlFor='urgency'
|
||
>
|
||
Urgensi: {urgency}/5
|
||
</label>
|
||
<input
|
||
id='urgency'
|
||
type='range'
|
||
min='1'
|
||
max='5'
|
||
value={urgency}
|
||
onChange={(event) => setUrgency(Number(event.target.value))}
|
||
className='w-full accent-[#F97316]'
|
||
/>
|
||
</div>
|
||
<div className='rounded-2xl bg-white/10 p-4 ring-1 ring-white/15'>
|
||
<label
|
||
className='mb-3 block text-sm font-semibold'
|
||
htmlFor='impact'
|
||
>
|
||
Dampak finansial
|
||
</label>
|
||
<select
|
||
id='impact'
|
||
value={financialImpact}
|
||
onChange={(event) =>
|
||
setFinancialImpact(
|
||
event.target.value as Run['financialImpact'],
|
||
)
|
||
}
|
||
className='w-full rounded-xl border border-white/20 bg-[#102A43] px-3 py-2 text-sm text-white outline-none ring-cyan-200/40 focus:ring-4'
|
||
>
|
||
<option value='high'>Besar</option>
|
||
<option value='medium'>Sedang</option>
|
||
<option value='low'>Rendah</option>
|
||
<option value='unknown'>Belum diketahui</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
{notice && (
|
||
<div
|
||
className={`mt-5 rounded-2xl p-4 text-sm ${notice.type === 'success' ? 'bg-emerald-400/15 text-emerald-50' : 'bg-red-400/15 text-red-50'}`}
|
||
>
|
||
{notice.message}
|
||
</div>
|
||
)}
|
||
|
||
<BaseButton
|
||
className='mt-5 w-full border-0 bg-[#F97316] py-3 font-bold text-white hover:bg-[#EA580C]'
|
||
icon={mdiPlus}
|
||
label={
|
||
isSaving
|
||
? 'Menyimpan analisis...'
|
||
: 'Jalankan Analisis & Simpan Workflow'
|
||
}
|
||
onClick={handleAnalyze}
|
||
disabled={isSaving}
|
||
/>
|
||
</CardBox>
|
||
|
||
<CardBox className='shadow-sm' cardBoxClassName='p-5'>
|
||
<div className='mb-4 flex items-center justify-between'>
|
||
<h3 className='font-bold text-slate-900 dark:text-white'>
|
||
Riwayat Decision Runs
|
||
</h3>
|
||
<Pill tone='slate'>{runs.length} lokal</Pill>
|
||
</div>
|
||
{runs.length === 0 ? (
|
||
<div className='rounded-2xl border border-dashed border-slate-300 p-5 text-sm text-slate-500 dark:border-slate-700 dark:text-slate-400'>
|
||
Belum ada riwayat. Jalankan analisis pertama untuk melihat
|
||
list dan detail keputusan di sini.
|
||
</div>
|
||
) : (
|
||
<div className='space-y-3'>
|
||
{runs.map((run) => (
|
||
<button
|
||
key={run.id}
|
||
type='button'
|
||
onClick={() => setSelectedId(run.id)}
|
||
className={`w-full rounded-2xl border p-4 text-left transition hover:-translate-y-0.5 hover:shadow-md ${selectedRun?.id === run.id ? 'border-cyan-400 bg-cyan-50 dark:bg-cyan-950/40' : 'border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900'}`}
|
||
>
|
||
<div className='flex items-center justify-between gap-3'>
|
||
<p className='line-clamp-1 font-semibold text-slate-900 dark:text-white'>
|
||
{run.title}
|
||
</p>
|
||
<Pill
|
||
tone={
|
||
run.priority === 'critical' ||
|
||
run.priority === 'high'
|
||
? 'orange'
|
||
: 'blue'
|
||
}
|
||
>
|
||
{run.priorityScore}/100
|
||
</Pill>
|
||
</div>
|
||
<p className='mt-2 text-xs text-slate-500'>
|
||
{new Date(run.createdAt).toLocaleString('id-ID')}
|
||
</p>
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
</CardBox>
|
||
</div>
|
||
|
||
<div className='space-y-6'>
|
||
{!selectedRun ? (
|
||
<CardBox
|
||
className='min-h-[520px] items-center justify-center border-dashed text-center'
|
||
cardBoxClassName='p-10'
|
||
>
|
||
<div className='mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-3xl bg-cyan-50 text-cyan-700'>
|
||
<BaseIcon path={mdiBrain} size={34} />
|
||
</div>
|
||
<h2 className='text-2xl font-bold text-slate-900 dark:text-white'>
|
||
Belum ada analisis
|
||
</h2>
|
||
<p className='mx-auto mt-2 max-w-md text-slate-500'>
|
||
Isi masalah bisnis di panel kiri untuk menghasilkan detection
|
||
score, root cause, solusi terurut, dan action plan 5 hari.
|
||
</p>
|
||
</CardBox>
|
||
) : (
|
||
<>
|
||
<CardBox className='shadow-sm' cardBoxClassName='p-6'>
|
||
<div className='mb-5 flex flex-wrap items-start justify-between gap-4'>
|
||
<div>
|
||
<p className='text-sm font-semibold text-cyan-600'>
|
||
Detection Engine
|
||
</p>
|
||
<h2 className='mt-1 text-2xl font-bold text-slate-950 dark:text-white'>
|
||
{selectedRun.title}
|
||
</h2>
|
||
<p className='mt-2 max-w-3xl text-sm text-slate-500 dark:text-slate-400'>
|
||
{selectedRun.problem}
|
||
</p>
|
||
</div>
|
||
<Pill
|
||
tone={
|
||
selectedRun.priority === 'critical' ||
|
||
selectedRun.priority === 'high'
|
||
? 'orange'
|
||
: 'green'
|
||
}
|
||
>
|
||
Prioritas {selectedRun.priority}
|
||
</Pill>
|
||
</div>
|
||
|
||
<div className='grid gap-4 md:grid-cols-3'>
|
||
<div className='rounded-3xl bg-[#08111F] p-5 text-white'>
|
||
<p className='text-sm text-cyan-100'>Priority Score</p>
|
||
<p className='mt-2 text-4xl font-black'>
|
||
{selectedRun.priorityScore}
|
||
</p>
|
||
<p className='text-xs text-slate-300'>Skala 1–100</p>
|
||
</div>
|
||
<div className='rounded-3xl bg-emerald-50 p-5 text-emerald-900 dark:bg-emerald-950 dark:text-emerald-100'>
|
||
<p className='text-sm'>Solusi terbaik</p>
|
||
<p className='mt-2 text-4xl font-black'>
|
||
{selectedRun.selectedSolution.score}
|
||
</p>
|
||
<p className='text-xs'>Decision Score</p>
|
||
</div>
|
||
<div className='rounded-3xl bg-orange-50 p-5 text-orange-900 dark:bg-orange-950 dark:text-orange-100'>
|
||
<p className='text-sm'>Success Rate</p>
|
||
<p className='mt-2 text-4xl font-black'>
|
||
{selectedRun.selectedSolution.success}%
|
||
</p>
|
||
<p className='text-xs'>Simulasi awal</p>
|
||
</div>
|
||
</div>
|
||
</CardBox>
|
||
|
||
<CardBox className='shadow-sm' cardBoxClassName='p-6'>
|
||
<div className='mb-5 flex flex-wrap items-center justify-between gap-3'>
|
||
<div className='flex items-center gap-3'>
|
||
<BaseIcon
|
||
path={mdiChartTimelineVariant}
|
||
className='text-cyan-600'
|
||
size={24}
|
||
/>
|
||
<h3 className='text-xl font-bold text-slate-900 dark:text-white'>
|
||
Sumber Data Analisis
|
||
</h3>
|
||
</div>
|
||
<Pill tone='slate'>Maks. {MAX_DATA_SOURCES} data</Pill>
|
||
</div>
|
||
<div className='grid gap-4 md:grid-cols-3'>
|
||
{selectedDataSources.map((source, index) => (
|
||
<div
|
||
key={source.id}
|
||
className='rounded-3xl border border-slate-200 bg-slate-50 p-4 dark:border-slate-700 dark:bg-slate-900'
|
||
>
|
||
<div className='mb-3 flex items-center justify-between gap-3'>
|
||
<Pill
|
||
tone={
|
||
source.category === 'metric'
|
||
? 'green'
|
||
: source.category === 'pattern'
|
||
? 'orange'
|
||
: 'blue'
|
||
}
|
||
>
|
||
Data {index + 1}
|
||
</Pill>
|
||
<span className='text-xs font-semibold text-slate-500 dark:text-slate-400'>
|
||
Confidence {source.confidence}%
|
||
</span>
|
||
</div>
|
||
<h4 className='font-bold text-slate-900 dark:text-white'>
|
||
{source.title}
|
||
</h4>
|
||
<p className='mt-2 text-xs leading-5 text-slate-500 dark:text-slate-400'>
|
||
{source.summary}
|
||
</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</CardBox>
|
||
|
||
<CardBox className='shadow-sm' cardBoxClassName='p-6'>
|
||
<div className='mb-5 flex items-center gap-3'>
|
||
<BaseIcon
|
||
path={mdiBrain}
|
||
className='text-cyan-600'
|
||
size={24}
|
||
/>
|
||
<h3 className='text-xl font-bold text-slate-900 dark:text-white'>
|
||
Root Cause Map
|
||
</h3>
|
||
</div>
|
||
<div className='space-y-4'>
|
||
{selectedRun.causes.map((cause) => (
|
||
<div key={cause.id}>
|
||
<div className='mb-2 flex items-center justify-between gap-4 text-sm'>
|
||
<span className='font-semibold text-slate-700 dark:text-slate-200'>
|
||
{cause.label}
|
||
</span>
|
||
<span className='font-bold text-cyan-700'>
|
||
{cause.contribution}%
|
||
</span>
|
||
</div>
|
||
<div className='h-3 overflow-hidden rounded-full bg-slate-100 dark:bg-slate-800'>
|
||
<div
|
||
className='h-full rounded-full bg-gradient-to-r from-cyan-500 to-emerald-400'
|
||
style={{ width: `${cause.contribution}%` }}
|
||
/>
|
||
</div>
|
||
<p className='mt-2 text-xs text-slate-500 dark:text-slate-400'>
|
||
{cause.description}
|
||
</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</CardBox>
|
||
|
||
<CardBox className='shadow-sm' cardBoxClassName='p-6'>
|
||
<div className='mb-5 flex items-center gap-3'>
|
||
<BaseIcon
|
||
path={mdiLightbulbOnOutline}
|
||
className='text-orange-500'
|
||
size={24}
|
||
/>
|
||
<h3 className='text-xl font-bold text-slate-900 dark:text-white'>
|
||
Solution & Decision Scoring
|
||
</h3>
|
||
</div>
|
||
<div className='overflow-hidden rounded-3xl border border-slate-200 dark:border-slate-700'>
|
||
<table className='w-full min-w-[720px] text-left text-sm'>
|
||
<thead className='bg-slate-50 text-xs uppercase text-slate-500 dark:bg-slate-800 dark:text-slate-300'>
|
||
<tr>
|
||
<th className='px-4 py-3'>Solusi</th>
|
||
<th className='px-4 py-3'>I</th>
|
||
<th className='px-4 py-3'>E</th>
|
||
<th className='px-4 py-3'>S</th>
|
||
<th className='px-4 py-3'>Low Risk</th>
|
||
<th className='px-4 py-3'>Score</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className='divide-y divide-slate-100 dark:divide-slate-800'>
|
||
{selectedRun.solutions.map((solution) => (
|
||
<tr
|
||
key={solution.id}
|
||
className={
|
||
solution.id === selectedRun.selectedSolution.id
|
||
? 'bg-cyan-50/70 dark:bg-cyan-950/30'
|
||
: ''
|
||
}
|
||
>
|
||
<td className='px-4 py-4'>
|
||
<p className='font-semibold text-slate-900 dark:text-white'>
|
||
{solution.title}
|
||
</p>
|
||
<p className='mt-1 text-xs text-slate-500'>
|
||
{solution.description}
|
||
</p>
|
||
</td>
|
||
<td className='px-4 py-4 font-semibold'>
|
||
{solution.impact}
|
||
</td>
|
||
<td className='px-4 py-4 font-semibold'>
|
||
{solution.efficiency}
|
||
</td>
|
||
<td className='px-4 py-4 font-semibold'>
|
||
{solution.speed}
|
||
</td>
|
||
<td className='px-4 py-4 font-semibold'>
|
||
{solution.lowRisk}
|
||
</td>
|
||
<td className='px-4 py-4'>
|
||
<Pill tone='green'>{solution.score}</Pill>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div className='mt-6 space-y-4'>
|
||
{selectedRun.solutions.map((solution, index) => (
|
||
<div
|
||
key={`${solution.id}-details`}
|
||
className='rounded-3xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-700 dark:bg-slate-900'
|
||
>
|
||
<div className='flex flex-wrap items-start justify-between gap-3'>
|
||
<div>
|
||
<p className='text-xs font-semibold uppercase tracking-wide text-cyan-600'>
|
||
Detail Solusi {index + 1}
|
||
</p>
|
||
<h4 className='mt-1 text-lg font-bold text-slate-900 dark:text-white'>
|
||
{solution.title}
|
||
</h4>
|
||
</div>
|
||
<Pill
|
||
tone={
|
||
solution.id === selectedRun.selectedSolution.id
|
||
? 'green'
|
||
: 'slate'
|
||
}
|
||
>
|
||
{solution.id === selectedRun.selectedSolution.id
|
||
? 'Rekomendasi utama'
|
||
: `Score ${solution.score}`}
|
||
</Pill>
|
||
</div>
|
||
<p className='mt-3 text-sm leading-6 text-slate-600 dark:text-slate-300'>
|
||
{solution.rationale || solution.description}
|
||
</p>
|
||
<div className='mt-5 grid gap-4 lg:grid-cols-3'>
|
||
<div className='rounded-2xl bg-cyan-50 p-4 dark:bg-cyan-950/30'>
|
||
<h5 className='font-semibold text-slate-900 dark:text-white'>
|
||
Langkah implementasi
|
||
</h5>
|
||
<ol className='mt-3 list-decimal space-y-2 pl-4 text-xs leading-5 text-slate-600 dark:text-slate-300'>
|
||
{(
|
||
solution.executionSteps || [
|
||
solution.description,
|
||
]
|
||
).map((step) => (
|
||
<li key={step}>{step}</li>
|
||
))}
|
||
</ol>
|
||
</div>
|
||
<div className='rounded-2xl bg-emerald-50 p-4 dark:bg-emerald-950/30'>
|
||
<h5 className='font-semibold text-slate-900 dark:text-white'>
|
||
KPI kontrol
|
||
</h5>
|
||
<ul className='mt-3 list-disc space-y-2 pl-4 text-xs leading-5 text-slate-600 dark:text-slate-300'>
|
||
{(
|
||
solution.kpis || [
|
||
'Decision Score',
|
||
'Progress action plan',
|
||
]
|
||
).map((kpi) => (
|
||
<li key={kpi}>{kpi}</li>
|
||
))}
|
||
</ul>
|
||
</div>
|
||
<div className='rounded-2xl bg-orange-50 p-4 dark:bg-orange-950/30'>
|
||
<h5 className='font-semibold text-slate-900 dark:text-white'>
|
||
Risiko, mitigasi & resource
|
||
</h5>
|
||
<ul className='mt-3 list-disc space-y-2 pl-4 text-xs leading-5 text-slate-600 dark:text-slate-300'>
|
||
{(
|
||
solution.risks || [
|
||
'Validasi hasil harian agar risiko eksekusi cepat terlihat.',
|
||
]
|
||
).map((risk) => (
|
||
<li key={risk}>{risk}</li>
|
||
))}
|
||
</ul>
|
||
<p className='mt-3 text-xs font-semibold text-slate-500 dark:text-slate-400'>
|
||
Resource:{' '}
|
||
{(
|
||
solution.resources || [
|
||
'Owner/PIC',
|
||
'Data performa',
|
||
'Budget eksperimen',
|
||
]
|
||
).join(', ')}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</CardBox>
|
||
|
||
<CardBox className='shadow-sm' cardBoxClassName='p-6'>
|
||
<div className='mb-5 flex items-center gap-3'>
|
||
<BaseIcon
|
||
path={mdiClipboardTextClockOutline}
|
||
className='text-emerald-600'
|
||
size={24}
|
||
/>
|
||
<h3 className='text-xl font-bold text-slate-900 dark:text-white'>
|
||
Automated Action Plan
|
||
</h3>
|
||
</div>
|
||
<div className='grid gap-3 md:grid-cols-5'>
|
||
{selectedRun.tasks.map((task) => (
|
||
<div
|
||
key={task.id}
|
||
className='rounded-3xl border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-700 dark:bg-slate-900'
|
||
>
|
||
<Pill tone={task.day === 1 ? 'orange' : 'blue'}>
|
||
Hari {task.day}
|
||
</Pill>
|
||
<h4 className='mt-3 font-bold text-slate-900 dark:text-white'>
|
||
{task.title}
|
||
</h4>
|
||
<p className='mt-2 text-xs leading-5 text-slate-500 dark:text-slate-400'>
|
||
{task.description}
|
||
</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</CardBox>
|
||
|
||
<CardBox
|
||
className='border-0 bg-gradient-to-br from-[#08111F] via-[#102A43] to-[#13B8A6] text-white shadow-xl'
|
||
cardBoxClassName='p-6'
|
||
>
|
||
<div className='mb-4 flex flex-wrap items-center justify-between gap-3'>
|
||
<div className='flex items-center gap-3'>
|
||
<div className='rounded-2xl bg-white/15 p-3 ring-1 ring-white/20'>
|
||
<BaseIcon path={mdiLightbulbOnOutline} size={24} />
|
||
</div>
|
||
<div>
|
||
<p className='text-sm font-semibold text-cyan-100'>
|
||
Executive Summary
|
||
</p>
|
||
<h3 className='text-xl font-bold'>
|
||
Kesimpulan OPTEMA AI
|
||
</h3>
|
||
</div>
|
||
</div>
|
||
<Pill tone='orange'>Next: validasi 5 hari</Pill>
|
||
</div>
|
||
<p className='text-sm leading-7 text-cyan-50'>
|
||
{selectedConclusion}
|
||
</p>
|
||
</CardBox>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</SectionMain>
|
||
</>
|
||
);
|
||
};
|
||
|
||
OptemaAiPage.getLayout = function getLayout(page: ReactElement) {
|
||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
||
};
|
||
|
||
export default OptemaAiPage;
|