yandri918 commited on
Commit
4e7d8c3
·
1 Parent(s): 575b421

Enhance Module 19: Perencana Hasil Panen (AI) with comprehensive yield planning

Browse files

- Add commodity-specific yield benchmarks database (padi, jagung, kedelai, cabai, tomat)
- Enhance generate_yield_plan() with detailed recommendations
- Add NPK to fertilizer product conversion
- Implement cost calculation and ROI estimation
- Generate cultivation timeline with phases
- Create modern UI with tabbed results display
- Add feasibility indicators and variety recommendations

add_pesticide_card.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+
3
+ # Read the file
4
+ with open('templates/home.html', 'r', encoding='utf-8') as f:
5
+ lines = f.readlines()
6
+
7
+ # Find line with "pustaka-dokumen" and add pesticide card after its closing </a>
8
+ pesticide_card_lines = [
9
+ '\n',
10
+ ' <a href="/modules/pesticide-knowledge" class="module-card">\n',
11
+ ' <div class="module-icon">🧪</div>\n',
12
+ ' <h3 class="module-title">Info Pestisida</h3>\n',
13
+ ' <p class="module-desc">Direktori bahan aktif, cara kerja, dan keamanan.</p>\n',
14
+ ' <span class="module-link">Cari Bahan Aktif</span>\n',
15
+ ' </a>\n'
16
+ ]
17
+
18
+ # Find the index where we need to insert (after Pustaka Dokumen)
19
+ insert_index = None
20
+ for i, line in enumerate(lines):
21
+ if 'pustaka-dokumen' in line:
22
+ # Find the closing </a> tag for this card
23
+ for j in range(i, min(i+10, len(lines))):
24
+ if '</a>' in lines[j] and 'href' not in lines[j]:
25
+ insert_index = j + 1
26
+ break
27
+ break
28
+
29
+ if insert_index:
30
+ # Insert the pesticide card
31
+ lines[insert_index:insert_index] = pesticide_card_lines
32
+
33
+ # Write back
34
+ with open('templates/home.html', 'w', encoding='utf-8') as f:
35
+ f.writelines(lines)
36
+ print(f'✅ Pesticide card added successfully after line {insert_index}!')
37
+ else:
38
+ print('❌ Could not find insertion point')
add_secondary_nutrients.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+
3
+ # Read the file
4
+ with open('templates/modules/kalkulator_pupuk_holistik.html', 'r', encoding='utf-8') as f:
5
+ content = f.read()
6
+
7
+ # Find the location to insert (after "html += '</div>';")
8
+ # We'll insert after the summary grid closing div
9
+ secondary_nutrients_section = '''
10
+ // Secondary Macro Nutrients Section
11
+ html += `
12
+ <h3 style="margin-top: 30px; margin-bottom: 15px; color: var(--primary-dark);">🧪 Kebutuhan Hara Makro Sekunder</h3>
13
+ <div class="info-box" style="background: #f0f9ff; border-left-color: var(--secondary);">
14
+ <div class="info-box-title" style="color: var(--secondary);">📊 Kalsium (Ca), Magnesium (Mg), Sulfur (S)</div>
15
+ <div class="info-box-content" style="color: #1e40af;">
16
+ <div style="margin-top: 12px;">
17
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-top: 10px;">
18
+ <div style="background: white; padding: 15px; border-radius: 8px; border: 1px solid #dbeafe;">
19
+ <div style="font-weight: 700; color: var(--secondary); margin-bottom: 8px;">🔹 Kalsium (Ca)</div>
20
+ <div style="font-size: 0.9rem;">
21
+ <strong>Fungsi:</strong> Pembentukan dinding sel, pertumbuhan akar<br>
22
+ <strong>Sumber:</strong> Dolomit, Kapur Pertanian, Gypsum<br>
23
+ <strong>Dosis:</strong> 200-500 kg/ha
24
+ </div>
25
+ </div>
26
+ <div style="background: white; padding: 15px; border-radius: 8px; border: 1px solid #dbeafe;">
27
+ <div style="font-weight: 700; color: var(--secondary); margin-bottom: 8px;">🔹 Magnesium (Mg)</div>
28
+ <div style="font-size: 0.9rem;">
29
+ <strong>Fungsi:</strong> Komponen klorofil, aktivasi enzim<br>
30
+ <strong>Sumber:</strong> Dolomit, Kieserit, MgSO₄<br>
31
+ <strong>Dosis:</strong> 50-150 kg/ha
32
+ </div>
33
+ </div>
34
+ <div style="background: white; padding: 15px; border-radius: 8px; border: 1px solid #dbeafe;">
35
+ <div style="font-weight: 700; color: var(--secondary); margin-bottom: 8px;">🔹 Sulfur (S)</div>
36
+ <div style="font-size: 0.9rem;">
37
+ <strong>Fungsi:</strong> Sintesis protein, pembentukan klorofil<br>
38
+ <strong>Sumber:</strong> ZA, Gypsum, Elemental S<br>
39
+ <strong>Dosis:</strong> 20-40 kg/ha
40
+ </div>
41
+ </div>
42
+ </div>
43
+ <div style="margin-top: 15px; padding: 12px; background: #fef3c7; border-radius: 6px; border-left: 3px solid var(--accent);">
44
+ <strong style="color: #92400e;">💡 Catatan:</strong>
45
+ <span style="color: #78350f; font-size: 0.9rem;">
46
+ Hara sekunder sering diabaikan namun sangat penting untuk hasil optimal.
47
+ Aplikasikan bersamaan dengan pengapuran atau sebagai pupuk dasar.
48
+ </span>
49
+ </div>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ `;
54
+
55
+ '''
56
+
57
+ # Find the pattern to insert after
58
+ pattern = r"(html \+= '</div>';[\r\n]+[\r\n]+ // Notes)"
59
+
60
+ # Replace with the pattern + our new section
61
+ replacement = r"html += '</div>';\n\n" + secondary_nutrients_section + "\n // Notes"
62
+
63
+ new_content = re.sub(pattern, replacement, content)
64
+
65
+ # Write back
66
+ with open('templates/modules/kalkulator_pupuk_holistik.html', 'w', encoding='utf-8') as f:
67
+ f.write(new_content)
68
+
69
+ print("✅ Secondary nutrients section added successfully!")
app/data/yield_benchmarks.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Yield benchmarks and cultivation data for major commodities."""
2
+
3
+
4
+ class YieldBenchmarks:
5
+ """Database of yield benchmarks and cultivation requirements for major crops."""
6
+
7
+ @staticmethod
8
+ def get_commodity_data():
9
+ """Get comprehensive yield and cultivation data for all commodities."""
10
+ return {
11
+ "padi": {
12
+ "name": "Padi",
13
+ "icon": "🌾",
14
+ "unit": "ton/ha",
15
+ "benchmarks": {
16
+ "low": {"min": 3.0, "max": 4.0, "label": "Rendah"},
17
+ "average": {"min": 4.5, "max": 6.0, "label": "Rata-rata"},
18
+ "high": {"min": 7.0, "max": 8.5, "label": "Tinggi"},
19
+ "record": {"min": 9.0, "max": 12.0, "label": "Rekor"}
20
+ },
21
+ "optimal_conditions": {
22
+ "temperature": {"min": 22, "max": 32, "unit": "°C"},
23
+ "rainfall": {"min": 1500, "max": 2000, "unit": "mm/tahun"},
24
+ "ph": {"min": 5.5, "max": 7.0},
25
+ "altitude": {"min": 0, "max": 1500, "unit": "mdpl"}
26
+ },
27
+ "npk_ratios": {
28
+ "low": {"N": 90, "P": 60, "K": 60},
29
+ "average": {"N": 120, "P": 75, "K": 75},
30
+ "high": {"N": 150, "P": 90, "K": 90},
31
+ "record": {"N": 180, "P": 110, "K": 110}
32
+ },
33
+ "varieties": {
34
+ "low": ["IR64", "Ciherang"],
35
+ "average": ["Inpari 32", "Mekongga"],
36
+ "high": ["Inpari 42", "Inpari 43"],
37
+ "record": ["Inpari 48", "Hybrid (Hipa 18)"]
38
+ },
39
+ "growth_duration": 110, # days
40
+ "critical_factors": [
41
+ "Pengairan teratur sangat penting",
42
+ "Pengendalian hama penggerek batang",
43
+ "Pemupukan berimbang sesuai fase",
44
+ "Varietas unggul bersertifikat"
45
+ ]
46
+ },
47
+ "jagung": {
48
+ "name": "Jagung",
49
+ "icon": "🌽",
50
+ "unit": "ton/ha",
51
+ "benchmarks": {
52
+ "low": {"min": 3.0, "max": 4.5, "label": "Rendah"},
53
+ "average": {"min": 5.0, "max": 7.0, "label": "Rata-rata"},
54
+ "high": {"min": 8.0, "max": 10.0, "label": "Tinggi"},
55
+ "record": {"min": 11.0, "max": 14.0, "label": "Rekor"}
56
+ },
57
+ "optimal_conditions": {
58
+ "temperature": {"min": 21, "max": 34, "unit": "°C"},
59
+ "rainfall": {"min": 400, "max": 800, "unit": "mm/musim"},
60
+ "ph": {"min": 5.5, "max": 7.5},
61
+ "altitude": {"min": 0, "max": 1800, "unit": "mdpl"}
62
+ },
63
+ "npk_ratios": {
64
+ "low": {"N": 100, "P": 75, "K": 50},
65
+ "average": {"N": 150, "P": 100, "K": 75},
66
+ "high": {"N": 200, "P": 125, "K": 100},
67
+ "record": {"N": 250, "P": 150, "K": 125}
68
+ },
69
+ "varieties": {
70
+ "low": ["Lokal", "Bisma"],
71
+ "average": ["Bisi 18", "Pioneer 21"],
72
+ "high": ["NK 212", "Bisi 222"],
73
+ "record": ["Hybrid Premium", "DK 979"]
74
+ },
75
+ "growth_duration": 100, # days
76
+ "critical_factors": [
77
+ "Drainase baik untuk mencegah genangan",
78
+ "Pemupukan N tinggi saat vegetatif",
79
+ "Pengendalian ulat grayak",
80
+ "Benih hybrid bersertifikat"
81
+ ]
82
+ },
83
+ "kedelai": {
84
+ "name": "Kedelai",
85
+ "icon": "🫘",
86
+ "unit": "ton/ha",
87
+ "benchmarks": {
88
+ "low": {"min": 1.0, "max": 1.3, "label": "Rendah"},
89
+ "average": {"min": 1.5, "max": 2.0, "label": "Rata-rata"},
90
+ "high": {"min": 2.3, "max": 2.8, "label": "Tinggi"},
91
+ "record": {"min": 3.0, "max": 3.8, "label": "Rekor"}
92
+ },
93
+ "optimal_conditions": {
94
+ "temperature": {"min": 23, "max": 30, "unit": "°C"},
95
+ "rainfall": {"min": 300, "max": 400, "unit": "mm/musim"},
96
+ "ph": {"min": 6.0, "max": 7.0},
97
+ "altitude": {"min": 0, "max": 900, "unit": "mdpl"}
98
+ },
99
+ "npk_ratios": {
100
+ "low": {"N": 25, "P": 75, "K": 50},
101
+ "average": {"N": 30, "P": 100, "K": 75},
102
+ "high": {"N": 40, "P": 125, "K": 90},
103
+ "record": {"N": 50, "P": 150, "K": 110}
104
+ },
105
+ "varieties": {
106
+ "low": ["Lokal", "Wilis"],
107
+ "average": ["Grobogan", "Anjasmoro"],
108
+ "high": ["Dena 1", "Dega 1"],
109
+ "record": ["Demas 1", "Hybrid"]
110
+ },
111
+ "growth_duration": 80, # days
112
+ "critical_factors": [
113
+ "Inokulasi rhizobium untuk fiksasi N",
114
+ "Drainase sempurna",
115
+ "Pengendalian lalat kacang",
116
+ "Pemupukan P dan K lebih tinggi dari N"
117
+ ]
118
+ },
119
+ "cabai": {
120
+ "name": "Cabai",
121
+ "icon": "🌶️",
122
+ "unit": "ton/ha",
123
+ "benchmarks": {
124
+ "low": {"min": 10, "max": 15, "label": "Rendah"},
125
+ "average": {"min": 18, "max": 22, "label": "Rata-rata"},
126
+ "high": {"min": 25, "max": 30, "label": "Tinggi"},
127
+ "record": {"min": 32, "max": 40, "label": "Rekor"}
128
+ },
129
+ "optimal_conditions": {
130
+ "temperature": {"min": 24, "max": 28, "unit": "°C"},
131
+ "rainfall": {"min": 600, "max": 1200, "unit": "mm/tahun"},
132
+ "ph": {"min": 6.0, "max": 7.0},
133
+ "altitude": {"min": 200, "max": 1200, "unit": "mdpl"}
134
+ },
135
+ "npk_ratios": {
136
+ "low": {"N": 120, "P": 150, "K": 150},
137
+ "average": {"N": 180, "P": 200, "K": 200},
138
+ "high": {"N": 250, "P": 250, "K": 250},
139
+ "record": {"N": 300, "P": 300, "K": 300}
140
+ },
141
+ "varieties": {
142
+ "low": ["Lokal", "Keriting"],
143
+ "average": ["Lado", "PM 999"],
144
+ "high": {"Laris", "Gada"},
145
+ "record": ["Tanjung 2", "Hot Beauty"]
146
+ },
147
+ "growth_duration": 90, # days to first harvest, continues for months
148
+ "critical_factors": [
149
+ "Mulsa plastik untuk kontrol gulma dan kelembaban",
150
+ "Pemupukan intensif setiap minggu",
151
+ "Pengendalian thrips dan antraknosa",
152
+ "Sistem drip irrigation ideal"
153
+ ]
154
+ },
155
+ "tomat": {
156
+ "name": "Tomat",
157
+ "icon": "🍅",
158
+ "unit": "ton/ha",
159
+ "benchmarks": {
160
+ "low": {"min": 15, "max": 20, "label": "Rendah"},
161
+ "average": {"min": 25, "max": 35, "label": "Rata-rata"},
162
+ "high": {"min": 40, "max": 50, "label": "Tinggi"},
163
+ "record": {"min": 55, "max": 70, "label": "Rekor"}
164
+ },
165
+ "optimal_conditions": {
166
+ "temperature": {"min": 20, "max": 27, "unit": "°C"},
167
+ "rainfall": {"min": 750, "max": 1250, "unit": "mm/tahun"},
168
+ "ph": {"min": 6.0, "max": 6.8},
169
+ "altitude": {"min": 300, "max": 1500, "unit": "mdpl"}
170
+ },
171
+ "npk_ratios": {
172
+ "low": {"N": 100, "P": 150, "K": 150},
173
+ "average": {"N": 150, "P": 200, "K": 200},
174
+ "high": {"N": 200, "P": 250, "K": 250},
175
+ "record": {"N": 250, "P": 300, "K": 300}
176
+ },
177
+ "varieties": {
178
+ "low": ["Lokal", "Permata"],
179
+ "average": ["Servo", "Tymoti"],
180
+ "high": ["Fortuna", "Intan"],
181
+ "record": ["Hybrid F1", "Betavila"]
182
+ },
183
+ "growth_duration": 70, # days to first harvest
184
+ "critical_factors": [
185
+ "Bedengan tinggi dengan mulsa",
186
+ "Pemangkasan dan pewiwilan rutin",
187
+ "Pengendalian layu fusarium",
188
+ "Kalsium cukup untuk mencegah blossom end rot"
189
+ ]
190
+ }
191
+ }
192
+
193
+ @staticmethod
194
+ def get_yield_category(commodity, target_yield):
195
+ """Determine yield category for given target."""
196
+ data = YieldBenchmarks.get_commodity_data().get(commodity)
197
+ if not data:
198
+ return "unknown"
199
+
200
+ benchmarks = data["benchmarks"]
201
+
202
+ if target_yield < benchmarks["low"]["min"]:
203
+ return "very_low"
204
+ elif benchmarks["low"]["min"] <= target_yield <= benchmarks["low"]["max"]:
205
+ return "low"
206
+ elif benchmarks["average"]["min"] <= target_yield <= benchmarks["average"]["max"]:
207
+ return "average"
208
+ elif benchmarks["high"]["min"] <= target_yield <= benchmarks["high"]["max"]:
209
+ return "high"
210
+ elif target_yield >= benchmarks["record"]["min"]:
211
+ return "record"
212
+ else:
213
+ # Between categories
214
+ if target_yield < benchmarks["average"]["min"]:
215
+ return "low"
216
+ elif target_yield < benchmarks["high"]["min"]:
217
+ return "average"
218
+ else:
219
+ return "high"
220
+
221
+ @staticmethod
222
+ def get_npk_for_yield(commodity, target_yield):
223
+ """Get recommended NPK based on target yield category."""
224
+ data = YieldBenchmarks.get_commodity_data().get(commodity)
225
+ if not data:
226
+ return None
227
+
228
+ category = YieldBenchmarks.get_yield_category(commodity, target_yield)
229
+
230
+ # Map category to NPK ratio key
231
+ if category in ["very_low", "low"]:
232
+ npk_key = "low"
233
+ elif category == "average":
234
+ npk_key = "average"
235
+ elif category == "high":
236
+ npk_key = "high"
237
+ else: # record
238
+ npk_key = "record"
239
+
240
+ return data["npk_ratios"].get(npk_key, data["npk_ratios"]["average"])
241
+
242
+ @staticmethod
243
+ def get_variety_recommendations(commodity, target_yield):
244
+ """Get recommended varieties for target yield."""
245
+ data = YieldBenchmarks.get_commodity_data().get(commodity)
246
+ if not data:
247
+ return []
248
+
249
+ category = YieldBenchmarks.get_yield_category(commodity, target_yield)
250
+
251
+ # Map category to variety key
252
+ if category in ["very_low", "low"]:
253
+ variety_key = "low"
254
+ elif category == "average":
255
+ variety_key = "average"
256
+ elif category == "high":
257
+ variety_key = "high"
258
+ else: # record
259
+ variety_key = "record"
260
+
261
+ return data["varieties"].get(variety_key, data["varieties"]["average"])
app/services/ml_service.py CHANGED
@@ -258,18 +258,28 @@ class MLService:
258
  'nutrient_amount_kg': nutrient_amount_kg
259
  }
260
 
 
261
  @staticmethod
262
  def generate_yield_plan(commodity=None, target_yield_ton_ha=None):
263
- """Generate plan to achieve target yield. Accepts both old and new signatures."""
 
 
264
  # Handle both old (single param) and new (two params) signatures
265
  if target_yield_ton_ha is None and commodity is not None:
266
  # Old signature: only target_yield_ton_ha provided as first param
267
  target_yield_ton_ha = float(commodity) if isinstance(commodity, (int, float, str)) else None
268
- commodity = None
269
 
270
  if target_yield_ton_ha is None:
271
  raise ValueError("target_yield_ton_ha is required")
272
 
 
 
 
 
 
 
 
273
  dataset_path = get_dataset_path('EDA_500.csv')
274
  if not os.path.exists(dataset_path):
275
  raise RuntimeError("Dataset EDA_500.csv tidak ditemukan.")
@@ -286,15 +296,271 @@ class MLService:
286
 
287
  result = best_match_row.iloc[0]
288
  plan = {
289
- "Nitrogen (kg/ha)": round(float(result['Nitrogen']), 2),
290
- "Phosphorus (kg/ha)": round(float(result['Phosphorus']), 2),
291
- "Potassium (kg/ha)": round(float(result['Potassium']), 2),
292
- "Temperature (°C)": round(float(result['Temperature']), 2),
293
- "Rainfall (mm)": round(float(result['Rainfall']), 2),
294
- "pH Tanah": round(float(result['pH']), 2),
295
- "Hasil Panen Aktual dari Data": f"{round(float(result['Yield'])/1000, 2)} ton/ha"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
  }
 
297
  return plan
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
 
299
  @staticmethod
300
  def predict_success(data):
 
258
  'nutrient_amount_kg': nutrient_amount_kg
259
  }
260
 
261
+
262
  @staticmethod
263
  def generate_yield_plan(commodity=None, target_yield_ton_ha=None):
264
+ """Generate comprehensive plan to achieve target yield with commodity-specific recommendations."""
265
+ from app.data.yield_benchmarks import YieldBenchmarks
266
+
267
  # Handle both old (single param) and new (two params) signatures
268
  if target_yield_ton_ha is None and commodity is not None:
269
  # Old signature: only target_yield_ton_ha provided as first param
270
  target_yield_ton_ha = float(commodity) if isinstance(commodity, (int, float, str)) else None
271
+ commodity = "umum" # Default to general
272
 
273
  if target_yield_ton_ha is None:
274
  raise ValueError("target_yield_ton_ha is required")
275
 
276
+ # If commodity is specified and supported, use benchmark data
277
+ if commodity and commodity != "umum":
278
+ commodity_data = YieldBenchmarks.get_commodity_data().get(commodity)
279
+ if commodity_data:
280
+ return MLService._generate_commodity_specific_plan(commodity, target_yield_ton_ha, commodity_data)
281
+
282
+ # Fallback to EDA dataset for general/unsupported commodities
283
  dataset_path = get_dataset_path('EDA_500.csv')
284
  if not os.path.exists(dataset_path):
285
  raise RuntimeError("Dataset EDA_500.csv tidak ditemukan.")
 
296
 
297
  result = best_match_row.iloc[0]
298
  plan = {
299
+ "commodity_name": "Umum",
300
+ "target_yield": target_yield_ton_ha,
301
+ "feasibility": "unknown",
302
+ "npk_requirements": {
303
+ "Nitrogen (kg/ha)": round(float(result['Nitrogen']), 2),
304
+ "Phosphorus (kg/ha)": round(float(result['Phosphorus']), 2),
305
+ "Potassium (kg/ha)": round(float(result['Potassium']), 2)
306
+ },
307
+ "environmental_conditions": {
308
+ "Temperature (°C)": round(float(result['Temperature']), 2),
309
+ "Rainfall (mm)": round(float(result['Rainfall']), 2),
310
+ "pH Tanah": round(float(result['pH']), 2)
311
+ },
312
+ "actual_yield_from_data": f"{round(float(result['Yield'])/1000, 2)} ton/ha"
313
+ }
314
+ return plan
315
+
316
+ @staticmethod
317
+ def _generate_commodity_specific_plan(commodity, target_yield, commodity_data):
318
+ """Generate detailed commodity-specific yield plan."""
319
+ from app.data.yield_benchmarks import YieldBenchmarks
320
+
321
+ # Assess feasibility
322
+ yield_category = YieldBenchmarks.get_yield_category(commodity, target_yield)
323
+ benchmarks = commodity_data["benchmarks"]
324
+
325
+ # Determine feasibility status
326
+ if yield_category == "very_low":
327
+ feasibility = "Sangat Rendah - Target di bawah standar"
328
+ feasibility_color = "red"
329
+ elif yield_category == "low":
330
+ feasibility = "Rendah - Dapat dicapai dengan input minimal"
331
+ feasibility_color = "orange"
332
+ elif yield_category == "average":
333
+ feasibility = "Realistis - Target standar petani"
334
+ feasibility_color = "green"
335
+ elif yield_category == "high":
336
+ feasibility = "Tinggi - Memerlukan manajemen intensif"
337
+ feasibility_color = "blue"
338
+ else: # record
339
+ feasibility = "Sangat Tinggi - Target ambisius, perlu teknologi canggih"
340
+ feasibility_color = "purple"
341
+
342
+ # Get NPK recommendations
343
+ npk = YieldBenchmarks.get_npk_for_yield(commodity, target_yield)
344
+
345
+ # Get variety recommendations
346
+ varieties = YieldBenchmarks.get_variety_recommendations(commodity, target_yield)
347
+
348
+ # Convert NPK to fertilizer products
349
+ fertilizers = MLService._convert_npk_to_fertilizers(npk['N'], npk['P'], npk['K'])
350
+
351
+ # Calculate costs (estimated)
352
+ costs = MLService._calculate_input_costs(fertilizers, commodity, target_yield)
353
+
354
+ # Generate timeline
355
+ timeline = MLService._generate_cultivation_timeline(commodity, commodity_data['growth_duration'])
356
+
357
+ # Build comprehensive plan
358
+ plan = {
359
+ "commodity_name": commodity_data["name"],
360
+ "commodity_icon": commodity_data["icon"],
361
+ "target_yield": target_yield,
362
+ "yield_unit": commodity_data["unit"],
363
+ "feasibility": {
364
+ "status": feasibility,
365
+ "color": feasibility_color,
366
+ "category": yield_category,
367
+ "benchmark_range": {
368
+ "low": f"{benchmarks['low']['min']}-{benchmarks['low']['max']} {commodity_data['unit']}",
369
+ "average": f"{benchmarks['average']['min']}-{benchmarks['average']['max']} {commodity_data['unit']}",
370
+ "high": f"{benchmarks['high']['min']}-{benchmarks['high']['max']} {commodity_data['unit']}",
371
+ "record": f"{benchmarks['record']['min']}-{benchmarks['record']['max']} {commodity_data['unit']}"
372
+ }
373
+ },
374
+ "npk_requirements": {
375
+ "Nitrogen (N)": f"{npk['N']} kg/ha",
376
+ "Phosphorus (P)": f"{npk['P']} kg/ha",
377
+ "Potassium (K)": f"{npk['K']} kg/ha"
378
+ },
379
+ "fertilizer_products": fertilizers,
380
+ "environmental_conditions": {
381
+ "Suhu Optimal": f"{commodity_data['optimal_conditions']['temperature']['min']}-{commodity_data['optimal_conditions']['temperature']['max']} °C",
382
+ "Curah Hujan": f"{commodity_data['optimal_conditions']['rainfall']['min']}-{commodity_data['optimal_conditions']['rainfall']['max']} {commodity_data['optimal_conditions']['rainfall']['unit']}",
383
+ "pH Tanah": f"{commodity_data['optimal_conditions']['ph']['min']}-{commodity_data['optimal_conditions']['ph']['max']}",
384
+ "Ketinggian": f"{commodity_data['optimal_conditions']['altitude']['min']}-{commodity_data['optimal_conditions']['altitude']['max']} {commodity_data['optimal_conditions']['altitude']['unit']}"
385
+ },
386
+ "recommended_varieties": varieties,
387
+ "critical_factors": commodity_data["critical_factors"],
388
+ "cost_estimate": costs,
389
+ "cultivation_timeline": timeline,
390
+ "growth_duration": f"{commodity_data['growth_duration']} hari"
391
  }
392
+
393
  return plan
394
+
395
+ @staticmethod
396
+ def _convert_npk_to_fertilizers(n_kg, p_kg, k_kg):
397
+ """Convert NPK kg/ha to actual fertilizer products."""
398
+ fertilizers = {}
399
+
400
+ # Urea for Nitrogen (46% N)
401
+ if n_kg > 0:
402
+ urea_kg = round(n_kg / 0.46, 2)
403
+ fertilizers["Urea (46% N)"] = f"{urea_kg} kg/ha"
404
+
405
+ # SP-36 for Phosphorus (36% P2O5 = ~15.8% P)
406
+ if p_kg > 0:
407
+ sp36_kg = round(p_kg / 0.158, 2)
408
+ fertilizers["SP-36 (36% P2O5)"] = f"{sp36_kg} kg/ha"
409
+
410
+ # KCl for Potassium (60% K2O = ~50% K)
411
+ if k_kg > 0:
412
+ kcl_kg = round(k_kg / 0.50, 2)
413
+ fertilizers["KCl (60% K2O)"] = f"{kcl_kg} kg/ha"
414
+
415
+ # Add organic fertilizer recommendation
416
+ fertilizers["Pupuk Kandang/Kompos"] = "2-5 ton/ha (aplikasi dasar)"
417
+
418
+ return fertilizers
419
+
420
+ @staticmethod
421
+ def _calculate_input_costs(fertilizers, commodity, target_yield):
422
+ """Calculate estimated input costs."""
423
+ # Estimated prices (Rp/kg) - can be updated with real market data
424
+ prices = {
425
+ "Urea": 2500,
426
+ "SP-36": 3000,
427
+ "KCl": 4500,
428
+ "Pupuk Kandang": 500,
429
+ "Benih": 50000 # per kg or unit
430
+ }
431
+
432
+ total_fertilizer_cost = 0
433
+ breakdown = {}
434
+
435
+ for fert_name, amount_str in fertilizers.items():
436
+ if "Pupuk Kandang" in fert_name or "Kompos" in fert_name:
437
+ # Extract ton amount
438
+ avg_ton = 3.5 # average of 2-5 ton
439
+ cost = avg_ton * 1000 * prices["Pupuk Kandang"]
440
+ breakdown[fert_name] = f"Rp {cost:,.0f}"
441
+ total_fertilizer_cost += cost
442
+ else:
443
+ # Extract kg amount
444
+ try:
445
+ kg = float(amount_str.split()[0])
446
+ fert_type = fert_name.split()[0]
447
+ if fert_type in prices:
448
+ cost = kg * prices[fert_type]
449
+ breakdown[fert_name] = f"Rp {cost:,.0f}"
450
+ total_fertilizer_cost += cost
451
+ except:
452
+ pass
453
+
454
+ # Estimate seed cost (varies by commodity)
455
+ seed_costs = {
456
+ "padi": 100000,
457
+ "jagung": 500000,
458
+ "kedelai": 200000,
459
+ "cabai": 2000000,
460
+ "tomat": 1500000
461
+ }
462
+ seed_cost = seed_costs.get(commodity, 300000)
463
+
464
+ # Estimate labor and other costs
465
+ labor_cost = 3000000 # Rp 3 juta for land prep, planting, maintenance
466
+ pesticide_cost = 1500000 # Rp 1.5 juta
467
+
468
+ total_cost = total_fertilizer_cost + seed_cost + labor_cost + pesticide_cost
469
+
470
+ # Estimate revenue (very rough, needs market price data)
471
+ price_per_ton = {
472
+ "padi": 5000000,
473
+ "jagung": 4000000,
474
+ "kedelai": 8000000,
475
+ "cabai": 15000000,
476
+ "tomat": 8000000
477
+ }
478
+ commodity_price = price_per_ton.get(commodity, 5000000)
479
+ estimated_revenue = target_yield * commodity_price
480
+ estimated_profit = estimated_revenue - total_cost
481
+
482
+ return {
483
+ "fertilizer_breakdown": breakdown,
484
+ "total_fertilizer": f"Rp {total_fertilizer_cost:,.0f}",
485
+ "seed_cost": f"Rp {seed_cost:,.0f}",
486
+ "labor_cost": f"Rp {labor_cost:,.0f}",
487
+ "pesticide_cost": f"Rp {pesticide_cost:,.0f}",
488
+ "total_input_cost": f"Rp {total_cost:,.0f}",
489
+ "estimated_revenue": f"Rp {estimated_revenue:,.0f}",
490
+ "estimated_profit": f"Rp {estimated_profit:,.0f}",
491
+ "note": "Estimasi kasar, sesuaikan dengan harga lokal"
492
+ }
493
+
494
+ @staticmethod
495
+ def _generate_cultivation_timeline(commodity, growth_duration):
496
+ """Generate week-by-week cultivation timeline."""
497
+ timeline = []
498
+
499
+ # Generic timeline structure
500
+ phases = [
501
+ {
502
+ "phase": "Persiapan Lahan",
503
+ "weeks": "2 minggu sebelum tanam",
504
+ "activities": [
505
+ "Pembajakan dan penggemburan tanah",
506
+ "Aplikasi pupuk kandang/kompos",
507
+ "Pengapuran jika pH rendah",
508
+ "Pembuatan bedengan (jika perlu)"
509
+ ]
510
+ },
511
+ {
512
+ "phase": "Penanaman",
513
+ "weeks": "Minggu 0",
514
+ "activities": [
515
+ "Penanaman benih/bibit berkualitas",
516
+ "Aplikasi pupuk dasar (P dan K)",
517
+ "Penyiraman awal"
518
+ ]
519
+ },
520
+ {
521
+ "phase": "Vegetatif Awal",
522
+ "weeks": "Minggu 1-3",
523
+ "activities": [
524
+ "Penyulaman tanaman mati",
525
+ "Penyiangan gulma",
526
+ "Aplikasi pupuk N pertama",
527
+ "Monitoring hama/penyakit"
528
+ ]
529
+ },
530
+ {
531
+ "phase": "Vegetatif Lanjut",
532
+ "weeks": f"Minggu 4-{growth_duration//14}",
533
+ "activities": [
534
+ "Aplikasi pupuk N susulan",
535
+ "Pengendalian hama/penyakit",
536
+ "Pengairan teratur",
537
+ "Pewiwilan (untuk tanaman tertentu)"
538
+ ]
539
+ },
540
+ {
541
+ "phase": "Generatif",
542
+ "weeks": f"Minggu {growth_duration//14 + 1}-{growth_duration//7}",
543
+ "activities": [
544
+ "Aplikasi pupuk K tinggi",
545
+ "Pengurangan N",
546
+ "Monitoring pembungaan/pembuahan",
547
+ "Pengendalian hama buah/bulir"
548
+ ]
549
+ },
550
+ {
551
+ "phase": "Pematangan & Panen",
552
+ "weeks": f"Minggu {growth_duration//7 + 1}-{growth_duration//7 + 2}",
553
+ "activities": [
554
+ "Pengurangan pengairan",
555
+ "Monitoring kematangan",
556
+ "Persiapan alat panen",
557
+ "Panen tepat waktu"
558
+ ]
559
+ }
560
+ ]
561
+
562
+ return phases
563
+
564
 
565
  @staticmethod
566
  def predict_success(data):
templates/modules/perencana_hasil_panen_ai.html CHANGED
@@ -1,238 +1,271 @@
1
-
2
  <!DOCTYPE html>
3
-
4
  <html lang="id">
 
5
  <head>
6
- <meta charset="utf-8"/>
7
- <meta content="width=device-width, initial-scale=1.0" name="viewport"/>
8
- <title>🎯 Modul 19: Perencana Hasil Panen (AI) - Agrisensa</title>
9
- <link href="https://fonts.googleapis.com" rel="preconnect"/>
10
- <link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect"/>
11
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;display=swap" rel="stylesheet"/>
12
- <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
13
- <style>
14
- :root {
15
- --primary-color: #2e7d32;
16
  --primary-light: #4caf50;
17
- --background-color: #f8f9fa;
18
- --card-background: #ffffff;
19
- --text-color: #212529;
 
 
 
20
  --text-muted: #6c757d;
21
- --border-color: #dee2e6;
22
- --shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
 
 
 
 
 
 
 
23
  }
24
 
25
  body {
26
  font-family: 'Inter', sans-serif;
27
- max-width: 900px;
28
- margin: 20px auto;
29
- padding: 15px;
30
- background-color: var(--background-color);
31
- color: var(--text-color);
32
  }
33
 
34
- .header {
35
- text-align: center;
36
- margin-bottom: 40px;
37
  }
38
 
39
- h1 {
40
- color: var(--primary-color);
41
- font-weight: 700;
42
- font-size: 2.5em;
 
 
 
 
 
43
  }
44
 
45
- .price-ticker-container {
46
- background-color: var(--text-color);
47
- color: white;
48
- padding: 10px 0;
49
- margin-bottom: 30px;
50
- border-radius: 8px;
51
- overflow: hidden;
52
- white-space: nowrap;
53
  }
54
 
55
- .price-ticker-content {
56
- display: inline-block;
57
- padding-left: 100%;
58
- animation: scroll-left 40s linear infinite;
 
 
 
59
  }
60
 
61
- .ticker-item {
62
- display: inline-block;
63
- margin: 0 25px;
 
 
 
 
64
  }
65
 
66
- .ticker-item .name {
67
- font-weight: 500;
 
68
  }
69
 
70
- .ticker-item .price {
71
- font-weight: 700;
72
- color: var(--primary-light);
73
- margin-left: 8px;
 
 
 
74
  }
75
 
76
- @keyframes scroll-left {
77
- 0% {
78
- transform: translateX(0%);
79
- }
 
 
 
 
80
 
81
- 100% {
82
- transform: translateX(-100%);
83
- }
 
 
84
  }
85
 
86
- .module {
87
- background-color: var(--card-background);
88
- border: 1px solid var(--border-color);
89
- border-radius: 12px;
90
- padding: 30px;
91
- margin-bottom: 30px;
92
- box-shadow: var(--shadow);
93
  }
94
 
95
- h2 {
 
 
 
96
  display: flex;
97
  align-items: center;
98
- gap: 12px;
99
- color: var(--primary-color);
100
- border-bottom: 1px solid var(--border-color);
101
- padding-bottom: 15px;
102
- margin-top: 0;
103
- font-size: 1.5em;
104
- font-weight: 600;
105
  }
106
 
107
- h3 {
108
- color: var(--primary-color);
109
- border-bottom: 1px solid #eee;
110
- padding-bottom: 5px;
111
- margin-top: 25px;
112
- font-weight: 600;
 
 
 
113
  }
114
 
115
- h4 {
116
- color: #333;
117
- margin-top: 15px;
118
- margin-bottom: 5px;
 
119
  }
120
 
121
- button,
122
- .option-button {
123
- background-color: var(--primary-light);
124
  color: white;
125
- padding: 12px 20px;
126
  border: none;
127
  border-radius: 8px;
 
 
128
  cursor: pointer;
129
- font-size: 1em;
130
- font-weight: 500;
131
  width: 100%;
132
- transition: all 0.2s;
133
- margin-bottom: 10px;
134
  }
135
 
136
- button:hover,
137
- .option-button:hover {
138
- background-color: var(--primary-color);
139
  transform: translateY(-2px);
 
140
  }
141
 
142
- .result-box {
143
- margin-top: 20px;
144
- padding: 20px;
145
- border-radius: 8px;
146
- background-color: #f8f9fa;
147
- line-height: 1.6;
148
- border: 1px solid var(--border-color);
149
  }
150
 
151
- input,
152
- select {
153
- width: 100%;
154
- padding: 12px;
155
- margin-bottom: 15px;
156
- border-radius: 8px;
157
- border: 1px solid var(--border-color);
158
- font-size: 1em;
159
- box-sizing: border-box;
160
  }
161
 
162
- label {
163
- font-weight: 500;
164
- margin-bottom: 8px;
165
- display: block;
 
 
 
 
166
  }
167
 
168
- ul {
169
- padding-left: 20px;
 
 
170
  }
171
 
172
- li {
173
- margin-bottom: 10px;
 
174
  }
175
 
176
- .item-list {
177
- list-style-type: none;
178
- padding-left: 0;
179
  }
180
 
181
- .price-item,
182
- .fertilizer-item,
183
- .pdf-item,
184
- .shap-item,
185
- .plan-item {
186
- display: flex;
187
- justify-content: space-between;
188
- align-items: center;
189
- border-bottom: 1px solid #eee;
190
- padding: 12px 0;
191
- }
192
 
193
- .item-name,
194
- .plan-item-label {
195
- font-weight: 500;
 
196
  }
197
 
198
- .item-value,
199
- .plan-item-value {
200
- font-size: 1.1em;
201
- color: var(--primary-color);
202
  font-weight: 600;
 
 
203
  }
204
 
205
- .pdf-item a {
206
- text-decoration: none;
207
- color: var(--primary-color);
208
- font-weight: 500;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  }
210
 
211
- .pdf-item a:hover {
212
- text-decoration: underline;
 
213
  }
214
 
215
  .tabs {
216
  display: flex;
217
- border-bottom: 1px solid #ccc;
218
- margin-bottom: 20px;
 
219
  flex-wrap: wrap;
220
  }
221
 
222
- .tab-button {
 
223
  background: none;
224
  border: none;
225
- padding: 12px 18px;
226
  cursor: pointer;
227
- font-size: 1em;
228
  font-weight: 500;
229
- border-bottom: 3px solid transparent;
230
  color: var(--text-muted);
 
 
231
  }
232
 
233
- .tab-button.active {
234
- border-bottom: 3px solid var(--primary-light);
235
- color: var(--primary-color);
 
 
 
 
236
  font-weight: 600;
237
  }
238
 
@@ -242,1187 +275,498 @@
242
 
243
  .tab-content.active {
244
  display: block;
245
- animation: fadeIn 0.5s;
246
  }
247
 
248
- @keyframes fadeIn {
249
- from {
250
- opacity: 0;
251
- }
 
 
252
 
253
- to {
254
- opacity: 1;
255
- }
 
 
256
  }
257
 
258
- .strategy-cycle {
259
- border-left: 3px solid var(--primary-light);
260
- padding-left: 20px;
261
- margin-bottom: 25px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  }
263
 
264
- table {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  width: 100%;
266
  border-collapse: collapse;
267
- margin-top: 15px;
268
  }
269
 
270
- th,
271
- td {
272
- text-align: left;
273
  padding: 12px;
274
- border-bottom: 1px solid var(--border-color);
 
275
  }
276
 
277
- th {
278
- background-color: #f8f9fa;
279
  font-weight: 600;
 
280
  }
281
 
282
- td.cost {
283
- text-align: right;
284
- font-weight: 500;
285
  }
286
 
287
- .total-cost {
288
- font-weight: bold;
289
- text-align: right;
290
  }
291
 
292
- .input-grid {
293
- display: grid;
294
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
295
- gap: 15px;
296
  }
297
 
298
- .shap-positive {
299
- color: #2e7d32;
300
- font-weight: bold;
301
  }
302
 
303
- .shap-negative {
304
- color: #d32f2f;
305
- font-weight: bold;
 
306
  }
307
- </style>
308
- </head>
309
- <body>
310
- <div class="container mx-auto p-4 md:p-8">
311
- <a href="/dashboard" style="display: inline-block; margin-bottom: 20px; color: var(--primary-color); text-decoration: none; font-weight: 500;">← Kembali ke Dasbor</a>
312
- <div class="module">
313
- <h2>🎯 Modul 19: Perencana Hasil Panen (AI)</h2>
314
- <p>Tentukan target hasil panen Anda, dan biarkan AI menyusun resep kondisi lahan yang dibutuhkan untuk
315
- mencapainya.</p>
316
- <form id="yield-plan-form">
317
- <div class="input-grid">
318
- <div>
319
- <label for="plan-commodity-select">Pilih Komoditas:</label>
320
- <select id="plan-commodity-select" required="">
321
- <option selected="" value="umum">Umum (Semua Tanaman)</option>
322
- </select>
323
- </div>
324
- <div>
325
- <label for="plan-target-yield-input">Target Hasil Panen (ton/ha):</label>
326
- <input id="plan-target-yield-input" placeholder="Contoh: 7.5" required="" step="any" type="number"/>
327
- </div>
328
- </div>
329
- <button class="mt-4" type="submit">Buat Rencana Lahan</button>
330
- </form>
331
- <div class="result-box" id="result-yield-plan">
332
- <p>Rencana kondisi lahan akan muncul di sini.</p>
333
- </div>
334
- </div></div>
335
- <script>
336
- document.addEventListener('DOMContentLoaded', () => {
337
- console.log('🚀 Modul loaded:', window.location.pathname);
338
- const baseUrl = ''; // Kosongkan jika file HTML berada di domain yang sama
339
- const apiPrefix = ''; // Menggunakan legacy endpoints yang sudah tersedia
340
-
341
- function formatRupiah(angka) {
342
- if (angka === null || angka === undefined) return "N/A";
343
- return new Intl.NumberFormat('id-ID', {
344
- style: 'currency',
345
- currency: 'IDR',
346
- minimumFractionDigits: 0
347
- }).format(angka);
348
- }
349
 
350
- // Fungsi openTab harus tersedia secara global atau dilampirkan ke window
351
- window.openTab = function(evt, tabName, element) {
352
- let i, tabcontent, tablinks;
353
- const parent = element.closest('.module');
354
- tabcontent = parent.getElementsByClassName("tab-content");
355
- for (i = 0; i < tabcontent.length; i++) {
356
- tabcontent[i].style.display = "none";
357
- }
358
- tablinks = parent.getElementsByClassName("tab-button");
359
- for (i = 0; i < tablinks.length; i++) {
360
- tablinks[i].className = tablinks[i].className.replace(" active", "");
361
- }
362
- const targetTab = document.getElementById(tabName);
363
- if (targetTab) {
364
- targetTab.style.display = "block";
365
- }
366
- if (evt.currentTarget) {
367
- evt.currentTarget.className += " active";
368
- }
369
- }
370
 
371
- const tickerContent = document.getElementById('price-ticker-content');
372
- async function fetchTickerData() {
373
- try {
374
- const response = await fetch(`${baseUrl}${apiPrefix}/get-ticker-prices`);
375
- const responseData = await response.json();
376
- if (responseData.success) {
377
- let tickerHtml = '';
378
- const tickerItems = [...responseData.data, ...responseData.data]; // Duplikasi untuk efek loop
379
- tickerItems.forEach(item => {
380
- tickerHtml +=
381
- `<div class="ticker-item"><span class="name">${item.name}:</span><span class="price">${formatRupiah(item.price)}/${item.unit}</span></div>`;
382
- });
383
- tickerContent.innerHTML = tickerHtml;
384
- }
385
- } catch (error) {
386
- console.error("Gagal memuat data ticker:", error);
387
- }
388
- }
389
- if (tickerContent) {
390
- fetchTickerData();
391
- setInterval(fetchTickerData, 60000); // Refresh setiap 60 detik
392
- }
393
 
394
- // --- Logika untuk Modul 1 (Analisis BWD) ---
395
- const formBwd = document.getElementById('upload-form'),
396
- resultBwdDiv = document.getElementById('result-bwd'),
397
- bwdInput = document.getElementById('bwd-input');
398
- if (formBwd) {
399
- formBwd.addEventListener('submit', async (event) => {
400
- event.preventDefault();
401
- console.log('📝 Modul 1: BWD Analysis: Form submitted!');
402
- resultBwdDiv.innerHTML = '<p>Menganalisis gambar...</p>';
403
- const formData = new FormData(formBwd);
404
- try {
405
- const response = await fetch(`${baseUrl}${apiPrefix}/analyze`, {
406
- method: 'POST',
407
- body: formData
408
- });
409
- if (!response.ok && response.status !== 201) {
410
- throw new Error(`HTTP error! status: ${response.status}`);
411
- }
412
- const data = await response.json();
413
- if (data.success) {
414
- resultBwdDiv.innerHTML =
415
- `<p><strong>Skor BWD:</strong> ${data.bwd_score} | <strong>Kepercayaan:</strong> ${data.confidence_percent}%</p>`;
416
- if (bwdInput) bwdInput.value = data.bwd_score;
417
- } else {
418
- resultBwdDiv.innerHTML =
419
- `<p style="color: red;"><strong>Error:</strong> ${data.message || data.error}</p>`;
420
- }
421
- } catch (error) {
422
- console.error("Error:", error);
423
- resultBwdDiv.innerHTML = `<p style="color: red;"><strong>Error:</strong> Gagal terhubung.</p>`;
424
- }
425
- });
426
- }
427
 
428
- // --- Logika untuk Modul 2 (Rekomendasi Pupuk) ---
429
- const formRec = document.getElementById('recommendation-form'),
430
- resultRecDiv = document.getElementById('result-recommendation');
431
- if (formRec) {
432
- formRec.addEventListener('submit', async (event) => {
433
- event.preventDefault();
434
- console.log('📝 Modul 2: Recommendation: Form submitted!');
435
- resultRecDiv.innerHTML = '<p>Menghitung rekomendasi...</p>';
436
- const requestData = {
437
- ph_tanah: formRec.querySelector('#ph-input').value,
438
- skor_bwd: bwdInput.value,
439
- kelembaban_tanah: formRec.querySelector('#moisture-input').value,
440
- umur_tanaman_hari: formRec.querySelector('#age-input').value,
441
- };
442
- try {
443
- const response = await fetch(`${baseUrl}${apiPrefix}/recommendation`, {
444
- method: 'POST',
445
- headers: {
446
- 'Content-Type': 'application/json'
447
- },
448
- body: JSON.stringify(requestData)
449
- });
450
- const data = await response.json();
451
- if (data.success) {
452
- let html = `<h3>${data.recommendation.rekomendasi_utama}</h3><ul class="item-list">`;
453
- for (const [key, value] of Object.entries(data.recommendation.rekomendasi_pupuk_ml)) {
454
- html +=
455
- `<li class="fertilizer-item"><span class="item-name">${key}</span><span class="item-value">${value}</li>`;
456
- }
457
- html += '</ul>';
458
- if (data.recommendation.peringatan_penting.length > 0) {
459
- html += '<h4 style="color: orange;">Peringatan:</h4><ul>';
460
- data.recommendation.peringatan_penting.forEach(w => {
461
- html += `<li>${w}</li>`;
462
- });
463
- html += '</ul>';
464
- }
465
- resultRecDiv.innerHTML = html;
466
- } else {
467
- resultRecDiv.innerHTML = `<p style="color: red;"><strong>Error:</strong> ${data.error}</p>`;
468
- }
469
- } catch (error) {
470
- console.error("Error:", error);
471
- resultRecDiv.innerHTML = `<p style="color: red;"><strong>Error:</strong> Gagal terhubung.</p>`;
472
- }
473
- });
474
- }
475
 
476
- // --- Logika untuk Modul 3 (Analisis NPK Manual) ---
477
- const formNpk = document.getElementById('npk-form'),
478
- resultNpkDiv = document.getElementById('result-npk');
479
- if (formNpk) {
480
- formNpk.addEventListener('submit', async (event) => {
481
- event.preventDefault();
482
- console.log('📝 Modul 3: NPK Analysis: Form submitted!');
483
- resultNpkDiv.innerHTML = '<p>Menganalisis data NPK...</p>';
484
- const requestData = {
485
- n_value: formNpk.querySelector('#n-input').value,
486
- p_value: formNpk.querySelector('#p-input').value,
487
- k_value: formNpk.querySelector('#k-input').value,
488
- };
489
- try {
490
- const response = await fetch(`${baseUrl}${apiPrefix}/analyze-npk`, {
491
- method: 'POST',
492
- headers: {
493
- 'Content-Type': 'application/json'
494
- },
495
- body: JSON.stringify(requestData)
496
- });
497
- const data = await response.json();
498
- if (data.success) {
499
- let html = '<h3>Hasil Analisis Tanah:</h3><ul class="item-list">';
500
- const labels = {
501
- 'Rendah': 'label-rendah',
502
- 'Optimal': 'label-optimal',
503
- 'Berlebih': 'label-berlebih'
504
- };
505
- for (const [n, r] of Object.entries(data.analysis)) {
506
- html +=
507
- `<li><strong>${n}:</strong> <span class="label ${labels[r.label] || ''}">${r.label}</span><br><small>${r.rekomendasi}</small></li>`;
508
- }
509
- html += '</ul><p><small>Data ini telah disimpan.</small></p>';
510
- resultNpkDiv.innerHTML = html;
511
- } else {
512
- resultNpkDiv.innerHTML = `<p style="color: red;"><strong>Error:</strong> ${data.error}</p>`;
513
- }
514
- } catch (error) {
515
- console.error("Error:", error);
516
- resultNpkDiv.innerHTML = `<p style="color: red;"><strong>Error:</strong> Gagal terhubung.</p>`;
517
- }
518
- });
519
- }
520
 
521
- // --- Logika untuk Modul 4 (Intelijen Harga Pasar) ---
522
- const priceForm = document.getElementById('price-form'),
523
- resultPriceDiv = document.getElementById('result-price');
524
- if (priceForm) {
525
- priceForm.addEventListener('submit', async (event) => {
526
- event.preventDefault();
527
- console.log('📝 Modul 4: Price Check: Form submitted!');
528
- resultPriceDiv.innerHTML = '<p>Mengambil data harga...</p>';
529
- try {
530
- const response = await fetch(`${baseUrl}${apiPrefix}/get-prices`, {
531
- method: 'POST',
532
- headers: {
533
- 'Content-Type': 'application/json'
534
- },
535
- body: JSON.stringify({
536
- commodity: priceForm.querySelector('#price-commodity-select').value
537
- })
538
- });
539
- const data = await response.json();
540
- if (data.success) {
541
- let html = `<h3>Data Harga: ${data.data.name}</h3><ul class="item-list">`;
542
- for (const [m, p] of Object.entries(data.data.prices)) {
543
- html +=
544
- `<li class="price-item"><span class="item-name">${m}</span><span class="item-value">${formatRupiah(p)} / ${data.data.unit}</span></li>`;
545
- }
546
- html += '</ul>';
547
- resultPriceDiv.innerHTML = html;
548
- } else {
549
- resultPriceDiv.innerHTML = `<p style="color: red;"><strong>Error:</strong> ${data.error}</p>`;
550
- }
551
- } catch (error) {
552
- console.error("Error:", error);
553
- resultPriceDiv.innerHTML = `<p style="color: red;"><strong>Error:</strong> Gagal terhubung.</p>`;
554
- }
555
- });
556
- }
557
 
558
- // --- Logika untuk Modul 5 (Basis Pengetahuan Budidaya) ---
559
- const knowledgeSelect = document.getElementById('knowledge-commodity-select'),
560
- resultKnowledgeDiv = document.getElementById('result-knowledge');
561
- if (knowledgeSelect) {
562
- async function fetchKnowledge() {
563
- if (!knowledgeSelect.value) return;
564
- resultKnowledgeDiv.innerHTML = '<p>Mengambil data...</p>';
565
- try {
566
- const response = await fetch(`${baseUrl}${apiPrefix}/get-knowledge`, {
567
- method: 'POST',
568
- headers: {
569
- 'Content-Type': 'application/json'
570
- },
571
- body: JSON.stringify({
572
- commodity: knowledgeSelect.value
573
- })
574
- });
575
- const data = await response.json();
576
- if (data.success) {
577
- const kd = data.data;
578
- let html = `<h3>${kd.icon} ${kd.name}</h3><div class="tabs">`;
579
- Object.keys(kd.data).forEach((t, i) => {
580
- html +=
581
- `<button class="tab-button ${i === 0 ? 'active' : ''}" onclick="openTab(event, 'k-${t.replace(/\s+/g, '-')}', this)">${t}</button>`;
582
- });
583
- html += '</div>';
584
- Object.entries(kd.data).forEach(([t, c], i) => {
585
- html +=
586
- `<div id="k-${t.replace(/\s+/g, '-')}" class="tab-content ${i === 0 ? 'active' : ''}"><ul>`;
587
- c.forEach(item => {
588
- html += `<li>${item}</li>`;
589
- });
590
- html += '</ul></div>';
591
- });
592
- resultKnowledgeDiv.innerHTML = html;
593
- } else {
594
- resultKnowledgeDiv.innerHTML =
595
- `<p style="color: red;"><strong>Error:</strong> ${data.error}</p>`;
596
- }
597
- } catch (error) {
598
- console.error("Error:", error);
599
- resultKnowledgeDiv.innerHTML =
600
- `<p style="color: red;"><strong>Error:</strong> Gagal terhubung.</p>`;
601
- }
602
- }
603
- knowledgeSelect.addEventListener('change', fetchKnowledge);
604
  }
605
 
606
- // --- Logika untuk Modul 7 (Kalkulator Pupuk Holistik) ---
607
- const fertForm = document.getElementById('fertilizer-calculator-form'),
608
- resultFertDiv = document.getElementById('result-fertilizer');
609
- if (fertForm) {
610
- fertForm.addEventListener('submit', async (event) => {
611
- event.preventDefault();
612
- resultFertDiv.innerHTML = '<p>Menghitung kebutuhan...</p>';
613
- const requestData = {
614
- commodity: fertForm.querySelector('#fert-commodity-select').value,
615
- area_sqm: fertForm.querySelector('#area-input').value,
616
- ph_tanah: fertForm.querySelector('#fert-ph-input').value
617
- };
618
- try {
619
- const response = await fetch(`${baseUrl}${apiPrefix}/calculate-fertilizer`, {
620
- method: 'POST',
621
- headers: {
622
- 'Content-Type': 'application/json'
623
- },
624
- body: JSON.stringify(requestData)
625
- });
626
- const data = await response.json();
627
- if (data.success) {
628
- const {
629
- data: d,
630
- commodity_name: cn,
631
- area_sqm: a
632
- } = data;
633
- let html =
634
- `<h3>Rekomendasi Pupuk untuk ${cn}</h3><p>Lahan <strong>${a} m²</strong> dengan pH <strong>${requestData.ph_tanah}</strong>:</p>`;
635
- const sections = {
636
- 'Perbaikan Tanah': d.perbaikan_tanah,
637
- 'Pupuk Organik': d.organik,
638
- 'Pupuk Anorganik': d.anorganik
639
- };
640
- for (const [title, content] of Object.entries(sections)) {
641
- if (content && Object.keys(content).length > 0) {
642
- html += `<h4>${title}:</h4><ul class="item-list">`;
643
- for (const [fert, amount] of Object.entries(content)) {
644
- html +=
645
- `<li class="fertilizer-item"><span class="item-name">${fert}</span><span class="item-value">${amount} kg</span></li>`;
646
- }
647
- html += '</ul>';
648
- }
649
- }
650
- html +=
651
- '<br><small>Catatan: Rekomendasi pupuk dasar. Sesuaikan dengan kondisi tanah dan fase pertumbuhan.</small>';
652
- resultFertDiv.innerHTML = html;
653
- } else {
654
- resultFertDiv.innerHTML = `<p style="color: red;"><strong>Error:</strong> ${data.error}</p>`;
655
- }
656
- } catch (error) {
657
- console.error("Error:", error);
658
- resultFertDiv.innerHTML = `<p style="color: red;"><strong>Error:</strong> Gagal terhubung.</p>`;
659
- }
660
- });
661
  }
662
 
663
- // --- Logika untuk Modul 8 (Pustaka Dokumen) ---
664
- const pdfForm = document.getElementById('pdf-upload-form'),
665
- pdfStatus = document.getElementById('pdf-upload-status'),
666
- pdfList = document.getElementById('pdf-list');
667
- if (pdfForm) {
668
- async function fetchPdfs() {
669
- pdfList.innerHTML = '<p>Memuat daftar...</p>';
670
- try {
671
- const response = await fetch(`${baseUrl}${apiPrefix}/get-pdfs`);
672
- if (!response.ok && response.status !== 201) {
673
- throw new Error(`HTTP error! status: ${response.status}`);
674
- }
675
- const data = await response.json();
676
- if (data.success && data.files) {
677
- if (data.files.length > 0) {
678
- let html = '<ul class="item-list">';
679
- data.files.forEach(f => {
680
- html +=
681
- `<li class="pdf-item"><a href="${baseUrl}${apiPrefix}/view-pdf/${f}" target="_blank">${f}</a></li>`;
682
- });
683
- html += '</ul>';
684
- pdfList.innerHTML = html;
685
- } else {
686
- pdfList.innerHTML = '<p>Belum ada dokumen.</p>';
687
- }
688
- } else {
689
- pdfList.innerHTML = `<p style="color: red;">Error: ${data.error}</p>`;
690
- }
691
- } catch (error) {
692
- console.error("Error:", error);
693
- pdfList.innerHTML = `<p style="color: red;">Gagal terhubung.</p>`;
694
- }
695
- }
696
- fetchPdfs();
697
- pdfForm.addEventListener('submit', async (event) => {
698
- event.preventDefault();
699
- pdfStatus.style.display = 'block';
700
- pdfStatus.innerHTML = '<p>Mengunggah...</p>';
701
- const formData = new FormData(pdfForm);
702
- try {
703
- const response = await fetch(`${baseUrl}${apiPrefix}/upload-pdf`, {
704
- method: 'POST',
705
- body: formData
706
- });
707
- if (!response.ok && response.status !== 201) {
708
- throw new Error(`HTTP error! status: ${response.status}`);
709
- }
710
- const data = await response.json();
711
- if (data.success) {
712
- pdfStatus.innerHTML = `<p style="color: green;">Sukses: ${data.message}</p>`;
713
- pdfForm.reset();
714
- fetchPdfs();
715
- } else {
716
- pdfStatus.innerHTML = `<p style="color: red;">Error: ${data.error}</p>`;
717
- }
718
- } catch (error) {
719
- console.error("Error:", error);
720
- pdfStatus.innerHTML = `<p style="color: red;">Gagal terhubung.</p>`;
721
- }
722
- });
723
  }
 
 
 
724
 
725
- // --- Logika untuk Modul 9 (Rekomendasi Terpadu) ---
726
- const intForm = document.getElementById('integrated-recommendation-form'),
727
- resultInt = document.getElementById('result-integrated');
728
- if (intForm) {
729
- intForm.addEventListener('submit', async (event) => {
730
- event.preventDefault();
731
- resultInt.innerHTML = '<p>Menyusun rekomendasi...</p>';
732
- const requestData = {
733
- ketinggian: intForm.querySelector('#ketinggian-select').value,
734
- iklim: intForm.querySelector('#iklim-select').value,
735
- fase: intForm.querySelector('#fase-select').value,
736
- masalah: intForm.querySelector('#masalah-select').value
737
- };
738
- try {
739
- const response = await fetch(`${baseUrl}${apiPrefix}/get-integrated-recommendation`, {
740
- method: 'POST',
741
- headers: {
742
- 'Content-Type': 'application/json'
743
- },
744
- body: JSON.stringify(requestData)
745
- });
746
- const data = await response.json();
747
- if (data.success) {
748
- let html = '<h3>Rekomendasi Strategis:</h3>';
749
- html += `<h4>🌱 Pemilihan Bibit</h4><p>${data.data.bibit}</p>`;
750
- html += `<h4>🌿 Pemupukan</h4><p>${data.data.pemupukan}</p>`;
751
- html += `<h4>🛡️ Penyemprotan</h4><p>${data.data.penyemprotan}</p>`;
752
- resultInt.innerHTML = html;
753
- } else {
754
- resultInt.innerHTML = `<p style="color: red;">Error: ${data.error}</p>`;
755
- }
756
- } catch (error) {
757
- console.error("Error:", error);
758
- resultInt.innerHTML = `<p style="color: red;">Gagal terhubung.</p>`;
759
- }
760
- });
761
- }
762
 
763
- // --- Logika untuk Modul 10 (Strategi Penyemprotan) ---
764
- const sprayForm = document.getElementById('spraying-strategy-form'),
765
- resultSpray = document.getElementById('result-spraying');
766
- if (sprayForm) {
767
- sprayForm.addEventListener('submit', async (event) => {
768
- event.preventDefault();
769
- resultSpray.innerHTML = '<p>Menyusun rencana...</p>';
770
- try {
771
- const response = await fetch(`${baseUrl}${apiPrefix}/get-spraying-recommendation`, {
772
- method: 'POST',
773
- headers: {
774
- 'Content-Type': 'application/json'
775
- },
776
- body: JSON.stringify({
777
- pest: sprayForm.querySelector('#pest-select').value
778
- })
779
- });
780
- const data = await response.json();
781
- if (data.success) {
782
- const d = data.data;
783
- let html = `<h3>${d.strategy.name}</h3><p>${d.strategy.description}</p>`;
784
- d.strategy.cycles.forEach(c => {
785
- html +=
786
- `<div class="strategy-cycle"><h4>${c.weeks}: ${c.level}</h4><p><strong>Bahan Aktif:</strong> ${c.active_ingredient}<br><strong>Grup IRAC:</strong> ${c.irac_code}<br><strong>SOP:</strong> ${c.sop}</p></div>`;
787
- });
788
- html += `<h3>${d.protocol.title}</h3><ul>`;
789
- d.protocol.steps.forEach(s => {
790
- html += `<li>${s}</li>`;
791
- });
792
- html += '</ul>';
793
- resultSpray.innerHTML = html;
794
- } else {
795
- resultSpray.innerHTML = `<p style="color: red;">Error: ${data.error}</p>`;
796
- }
797
- } catch (error) {
798
- console.error("Error:", error);
799
- resultSpray.innerHTML = `<p style="color: red;">Gagal terhubung.</p>`;
800
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
801
  });
802
- }
803
 
804
- // --- Logika untuk Modul 11 (Grafik Harga) ---
805
- const histPriceForm = document.getElementById('historical-price-form'),
806
- resultHistPrice = document.getElementById('result-historical-price');
807
- let priceChart;
808
- if (histPriceForm) {
809
- const chartCanvas = document.getElementById('price-chart').getContext('2d');
810
- histPriceForm.addEventListener('submit', async (event) => {
811
- event.preventDefault();
812
- resultHistPrice.querySelector('p').textContent = 'Memvisualisasikan data...';
813
- const requestData = {
814
- commodity: histPriceForm.querySelector('#historical-commodity-select').value,
815
- range: histPriceForm.querySelector('#time-range-select').value
816
- };
817
- try {
818
- const response = await fetch(`${baseUrl}${apiPrefix}/get-historical-prices`, {
819
- method: 'POST',
820
- headers: {
821
- 'Content-Type': 'application/json'
822
- },
823
- body: JSON.stringify(requestData)
824
- });
825
- const data = await response.json();
826
- if (data.success) {
827
- resultHistPrice.querySelector('p').textContent = `Grafik tren harga:`;
828
- if (priceChart) priceChart.destroy();
829
- priceChart = new Chart(chartCanvas, {
830
- type: 'line',
831
- data: {
832
- labels: data.labels,
833
- datasets: [{
834
- label: 'Harga (Rp)',
835
- data: data.prices,
836
- borderColor: 'rgba(46, 125, 50, 1)',
837
- backgroundColor: 'rgba(76, 175, 80, 0.2)',
838
- borderWidth: 2,
839
- fill: true,
840
- tension: 0.3
841
- }]
842
- },
843
- options: {
844
- responsive: true,
845
- scales: {
846
- y: {
847
- ticks: {
848
- callback: (v) => formatRupiah(v)
849
- }
850
- }
851
- }
852
- }
853
- });
854
- } else {
855
- resultHistPrice.querySelector('p').textContent = `Error: ${data.error}`;
856
- }
857
- } catch (error) {
858
- console.error("Error:", error);
859
- resultHistPrice.querySelector('p').textContent = 'Error: Gagal terhubung.';
860
- }
861
- });
862
  }
863
-
864
- // --- Logika untuk Modul 12 (Ensiklopedia Komoditas) ---
865
- const guideForm = document.getElementById('guide-form'),
866
- resultGuideDiv = document.getElementById('result-guide');
867
- if (guideForm) {
868
- guideForm.addEventListener('submit', async (event) => {
869
- event.preventDefault();
870
- resultGuideDiv.style.display = 'block';
871
- resultGuideDiv.innerHTML = '<p>Memuat panduan...</p>';
872
- try {
873
- const response = await fetch(`${baseUrl}${apiPrefix}/get-commodity-guide`, {
874
- method: 'POST',
875
- headers: {
876
- 'Content-Type': 'application/json'
877
- },
878
- body: JSON.stringify({
879
- commodity: guideForm.querySelector('#guide-commodity-select').value
880
- })
881
- });
882
- const data = await response.json();
883
- if (data.success) {
884
- const gd = data.data;
885
- let html = `<h2>${gd.icon} Panduan Lengkap: ${gd.name}</h2><p>${gd.description}</p>`;
886
- html += '<div class="tabs">';
887
- html +=
888
- `<button class="tab-button active" onclick="openTab(event, 'sop-budidaya', this)">SOP Budidaya</button>`;
889
- html +=
890
- `<button class="tab-button" onclick="openTab(event, 'analisis-bisnis', this)">Analisis Bisnis</button>`;
891
- html += '</div>';
892
- html += `<div id="sop-budidaya" class="tab-content active">`;
893
- for (const [phase, steps] of Object.entries(gd.sop)) {
894
- html += `<h3>${phase}</h3><ul>`;
895
- steps.forEach(step => {
896
- html += `<li>${step}</li>`;
897
- });
898
- html += `</ul>`;
899
- }
900
- html += `</div>`;
901
- const analysis = gd.business_analysis;
902
- html +=
903
- `<div id="analisis-bisnis" class="tab-content"><h3>${analysis.title}</h3><p><strong>Asumsi:</strong> Lahan ${analysis.assumptions.Luas_Lahan}, populasi ${analysis.assumptions.Populasi_Tanaman}.</p>`;
904
- html +=
905
- `<h4>Estimasi Biaya Produksi</h4><table><tr><th>Komponen</th><th>Kebutuhan</th><th style="text-align:right;">Biaya</th></tr>`;
906
- let totalCost = 0;
907
- analysis.costs.forEach(i => {
908
- html +=
909
- `<tr><td>${i.item}</td><td>${i.amount}</td><td class="cost">${formatRupiah(i.cost)}</td></tr>`;
910
- totalCost += i.cost;
911
- });
912
- html +=
913
- `<tr><td colspan="2" class="total-cost"><strong>Total Biaya</strong></td><td class="total-cost"><strong>${formatRupiah(totalCost)}</strong></td></tr></table>`;
914
- html +=
915
- `<h4>Simulasi Pendapatan</h4><table><tr><th>Skenario Harga</th><th>Panen Konservatif</th><th>Panen Optimal</th></tr>`;
916
- analysis.revenue_scenarios.forEach(s => {
917
- const revCons = analysis.yield_potential[0].total_yield_kg * s.price_per_kg;
918
- const revOpt = analysis.yield_potential[1].total_yield_kg * s.price_per_kg;
919
- html +=
920
- `<tr><td>${s.price_level} (${formatRupiah(s.price_per_kg)}/kg)</td><td>${formatRupiah(revCons)}</td><td>${formatRupiah(revOpt)}</td></tr>`;
921
- });
922
- html += `</table><br><small><strong>Disclaimer:</strong> Angka ini adalah estimasi.</small></div>`;
923
- resultGuideDiv.innerHTML = html;
924
- } else {
925
- resultGuideDiv.innerHTML = `<p style="color: red;">Error: ${data.error}</p>`;
926
- }
927
- } catch (error) {
928
- console.error("Error:", error);
929
- resultGuideDiv.innerHTML = `<p style="color: red;">Gagal terhubung.</p>`;
930
- }
931
- });
932
  }
933
 
934
- // --- Logika untuk Modul 13 (Pusat Pengetahuan pH) ---
935
- const showPhBtn = document.getElementById('show-ph-info-btn'),
936
- resultPhDiv = document.getElementById('result-ph-info');
937
- if (showPhBtn) {
938
- showPhBtn.addEventListener('click', async (event) => {
939
- event.preventDefault();
940
- if (resultPhDiv.style.display === 'block') {
941
- resultPhDiv.style.display = 'none';
942
- showPhBtn.textContent = 'Tampilkan Informasi Lengkap';
943
- return;
944
- }
945
- resultPhDiv.style.display = 'block';
946
- showPhBtn.textContent = 'Sembunyikan Informasi';
947
- resultPhDiv.innerHTML = '<p>Memuat informasi...</p>';
948
- try {
949
- const response = await fetch(`${baseUrl}${apiPrefix}/get-ph-info`);
950
- if (!response.ok && response.status !== 201) {
951
- throw new Error(`HTTP error! status: ${response.status}`);
952
- }
953
- const data = await response.json();
954
- if (data.success) {
955
- const pd = data.data;
956
- let html = `<h2>${pd.icon} ${pd.title}</h2><div class="tabs">`;
957
- Object.keys(pd.sections).forEach((k, i) => {
958
- html +=
959
- `<button class="tab-button ${i === 0 ? 'active' : ''}" onclick="openTab(event, 'ph-${k.replace(/[^a-zA-Z0-9]/g, '-')}', this)">${pd.sections[k].title}</button>`;
960
- });
961
- html += '</div>';
962
- Object.entries(pd.sections).forEach(([k, s], i) => {
963
- html +=
964
- `<div id="ph-${k.replace(/[^a-zA-Z0-9]/g, '-')}" class="tab-content ${i === 0 ? 'active' : ''}"><ul>`;
965
- s.content.forEach(item => {
966
- html += `<li>${item}</li>`;
967
- });
968
- html += `</ul></div>`;
969
- });
970
- resultPhDiv.innerHTML = html;
971
- } else {
972
- resultPhDiv.innerHTML = `<p style="color: red;">Error: ${data.error}</p>`;
973
- }
974
- } catch (error) {
975
- console.error("Error:", error);
976
- resultPhDiv.innerHTML = `<p style="color: red;">Gagal terhubung.</p>`;
977
- }
978
- });
979
- }
980
 
981
- // --- Logika untuk Modul 14 (Rekomendasi Tanaman) ---
982
- const cropForm = document.getElementById('crop-recommendation-form'),
983
- resultCropDiv = document.getElementById('result-crop');
984
- if (cropForm) {
985
- cropForm.addEventListener('submit', async (event) => {
986
- event.preventDefault();
987
- resultCropDiv.innerHTML = '<p>Menganalisis data dan mencari rekomendasi terbaik...</p>';
988
- const requestData = {
989
- n_value: document.getElementById('crop-n-input').value,
990
- p_value: document.getElementById('crop-p-input').value,
991
- k_value: document.getElementById('crop-k-input').value,
992
- temperature: document.getElementById('crop-temp-input').value,
993
- humidity: document.getElementById('crop-humidity-input').value,
994
- ph: document.getElementById('crop-ph-input').value,
995
- rainfall: document.getElementById('crop-rainfall-input').value
996
- };
997
- try {
998
- const response = await fetch(`${baseUrl}${apiPrefix}/recommend-crop`, {
999
- method: 'POST',
1000
- headers: {
1001
- 'Content-Type': 'application/json'
1002
- },
1003
- body: JSON.stringify(requestData)
1004
- });
1005
- const responseData = await response.json();
1006
- if (responseData.success) {
1007
- resultCropDiv.innerHTML =
1008
- `<h3>Rekomendasi Tanaman Optimal</h3><p style="font-size: 1.5em; text-align: center; font-weight: bold; color: var(--primary-color);">${responseData.recommended_crop}</p><p><small>Rekomendasi ini dihasilkan oleh model Machine Learning berdasarkan data yang Anda masukkan.</small></p>`;
1009
- } else {
1010
- resultCropDiv.innerHTML =
1011
- `<p style="color: red;"><strong>Error:</strong> ${responseData.error}</p>`;
1012
- }
1013
- } catch (error) {
1014
- console.error("Error:", error);
1015
- resultCropDiv.innerHTML =
1016
- `<p style="color: red;"><strong>Error:</strong> Tidak dapat terhubung ke server.</p>`;
1017
- }
1018
- });
1019
- }
1020
 
1021
- // --- Logika untuk Modul 15 (Prediksi Panen) ---
1022
- const yieldForm = document.getElementById('yield-prediction-form'),
1023
- resultYieldDiv = document.getElementById('result-yield');
1024
- if (yieldForm) {
1025
- yieldForm.addEventListener('submit', async (event) => {
1026
- event.preventDefault();
1027
- resultYieldDiv.innerHTML = '<p>Menganalisis data dan membuat prediksi...</p>';
1028
- const requestData = {
1029
- nitrogen: document.getElementById('yield-n-input').value,
1030
- phosphorus: document.getElementById('yield-p-input').value,
1031
- potassium: document.getElementById('yield-k-input').value,
1032
- temperature: document.getElementById('yield-temp-input').value,
1033
- rainfall: document.getElementById('yield-rainfall-input').value,
1034
- ph: document.getElementById('yield-ph-input').value,
1035
- };
1036
- try {
1037
- const response = await fetch(`${baseUrl}${apiPrefix}/predict-yield`, {
1038
- method: 'POST',
1039
- headers: {
1040
- 'Content-Type': 'application/json'
1041
- },
1042
- body: JSON.stringify(requestData)
1043
- });
1044
- const responseData = await response.json();
1045
- if (responseData.success) {
1046
- resultYieldDiv.innerHTML =
1047
- `<h3>Estimasi Potensi Hasil Panen</h3><p style="font-size: 1.8em; text-align: center; font-weight: bold; color: var(--primary-color);">${responseData.predicted_yield_ton_ha} ton/ha</p><p><small>Estimasi ini dihasilkan oleh model Machine Learning berdasarkan data yang Anda masukkan.</small></p>`;
1048
- } else {
1049
- resultYieldDiv.innerHTML =
1050
- `<p style="color: red;"><strong>Error:</strong> ${responseData.error}</p>`;
1051
- }
1052
- } catch (error) {
1053
- console.error("Error:", error);
1054
- resultYieldDiv.innerHTML =
1055
- `<p style="color: red;"><strong>Error:</strong> Tidak dapat terhubung ke server.</p>`;
1056
- }
1057
- });
1058
- }
1059
 
1060
- // --- Logika untuk Modul 16 (Prediksi Panen XAI) ---
1061
- const xaiYieldForm = document.getElementById('xai-yield-prediction-form'),
1062
- resultXaiYieldDiv = document.getElementById('result-xai-yield');
1063
- let importanceChart;
1064
- if (xaiYieldForm) {
1065
- xaiYieldForm.addEventListener('submit', async (event) => {
1066
- event.preventDefault();
1067
- resultXaiYieldDiv.style.display = 'block';
1068
- resultXaiYieldDiv.innerHTML = '<p>Menganalisis data dengan XAI...</p>';
1069
-
1070
- const requestData = {
1071
- nitrogen: document.getElementById('xai-nitrogen').value,
1072
- phosphorus: document.getElementById('xai-phosphorus').value,
1073
- potassium: document.getElementById('xai-potassium').value,
1074
- temperature: document.getElementById('xai-temperature').value,
1075
- rainfall: document.getElementById('xai-rainfall').value,
1076
- ph: document.getElementById('xai-ph').value,
1077
- };
1078
-
1079
- try {
1080
- const response = await fetch(`${baseUrl}${apiPrefix}/predict-yield-advanced`, {
1081
- method: 'POST',
1082
- headers: {
1083
- 'Content-Type': 'application/json'
1084
- },
1085
- body: JSON.stringify(requestData)
1086
- });
1087
- const data = await response.json();
1088
-
1089
- if (data.success) {
1090
- if (importanceChart) {
1091
- importanceChart.destroy();
1092
- }
1093
-
1094
- let htmlResult = `
1095
- <h3>Hasil Prediksi & Analisis</h3>
1096
- <p style="font-size: 1.8em; text-align: center; font-weight: bold; color: var(--primary-color);">
1097
- Estimasi Panen: ${data.predicted_yield_ton_ha} ton/ha
1098
- </p>
1099
-
1100
- <h4>Faktor Pendorong & Penghambat (Analisis SHAP)</h4>
1101
- <p>Analisis ini menjelaskan mengapa prediksi Anda berbeda dari rata-rata (${data.base_value} ton/ha).</p>
1102
- <ul class="item-list">`;
1103
-
1104
- for (const [feature, value] of Object.entries(data.shap_values)) {
1105
- const change = value / 1000;
1106
- const direction = change >= 0 ? 'Meningkatkan' : 'Menurunkan';
1107
- const cssClass = change >= 0 ? 'shap-positive' : 'shap-negative';
1108
- htmlResult += `<li class="shap-item"><span>${feature}</span> <span class="${cssClass}">${direction} estimasi sebesar ${Math.abs(change).toFixed(2)} ton/ha</span></li>`;
1109
- }
1110
-
1111
- htmlResult += `</ul>
1112
-
1113
- <h4>Faktor Paling Berpengaruh (Secara Umum)</h4>
1114
- <canvas id="importance-chart"></canvas>
1115
- `;
1116
-
1117
- resultXaiYieldDiv.innerHTML = htmlResult;
1118
-
1119
- const chartCtx = document.getElementById('importance-chart').getContext('2d');
1120
- const labels = data.feature_importances.map(item => item[0]);
1121
- const values = data.feature_importances.map(item => item[1]);
1122
-
1123
- importanceChart = new Chart(chartCtx, {
1124
- type: 'bar',
1125
- data: {
1126
- labels: labels,
1127
- datasets: [{
1128
- label: 'Tingkat Kepentingan',
1129
- data: values,
1130
- backgroundColor: 'rgba(76, 175, 80, 0.6)',
1131
- borderColor: 'rgba(46, 125, 50, 1)',
1132
- borderWidth: 1
1133
- }]
1134
- },
1135
- options: {
1136
- indexAxis: 'y',
1137
- responsive: true,
1138
- plugins: {
1139
- legend: {
1140
- display: false
1141
- },
1142
- title: {
1143
- display: true,
1144
- text: 'Faktor yang Paling Memengaruhi Hasil Panen'
1145
- }
1146
- }
1147
- }
1148
- });
1149
-
1150
- } else {
1151
- resultXaiYieldDiv.innerHTML = `<p style="color: red;"><strong>Error:</strong> ${data.error}</p>`;
1152
- }
1153
- } catch (error) {
1154
- console.error('Error fetching XAI prediction:', error);
1155
- resultXaiYieldDiv.innerHTML =
1156
- `<p style="color: red;"><strong>Error:</strong> Tidak dapat terhubung ke server.</p>`;
1157
- }
1158
- });
1159
- }
1160
 
1161
- // --- Logika untuk Modul 17 (Kalkulator Konversi Pupuk) ---
1162
- const fertConversionForm = document.getElementById('fertilizer-conversion-form'),
1163
- resultFertConversionDiv = document.getElementById('result-fertilizer-conversion');
1164
- if (fertConversionForm) {
1165
- fertConversionForm.addEventListener('submit', async (event) => {
1166
- event.preventDefault();
1167
- resultFertConversionDiv.innerHTML = '<p>Menghitung konversi...</p>';
1168
- const requestData = {
1169
- nutrient_needed: document.getElementById('nutrient-needed-select').value,
1170
- nutrient_amount_kg: document.getElementById('nutrient-amount-input').value,
1171
- fertilizer_type: document.getElementById('fertilizer-type-select').value
1172
- };
1173
- try {
1174
- const response = await fetch(`${baseUrl}${apiPrefix}/calculate-fertilizer-bags`, {
1175
- method: 'POST',
1176
- headers: {
1177
- 'Content-Type': 'application/json'
1178
- },
1179
- body: JSON.stringify(requestData)
1180
- });
1181
- const data = await response.json();
1182
- if (data.success) {
1183
- resultFertConversionDiv.innerHTML = `<h3>Hasil Perhitungan</h3>
1184
- <p>Untuk memenuhi kebutuhan <strong>${data.nutrient_amount_kg} kg</strong> unsur hara <strong>${data.nutrient_needed}</strong>, Anda memerlukan:</p>
1185
- <p style="font-size: 1.8em; text-align: center; font-weight: bold; color: var(--primary-color);">${data.required_fertilizer_kg} kg pupuk ${data.fertilizer_name}</p>`;
1186
- } else {
1187
- resultFertConversionDiv.innerHTML =
1188
- `<p style="color: red;"><strong>Error:</strong> ${data.error}</p>`;
1189
- }
1190
- } catch (error) {
1191
- console.error("Error:", error);
1192
- resultFertConversionDiv.innerHTML =
1193
- `<p style="color: red;"><strong>Error:</strong> Tidak dapat terhubung ke server.</p>`;
1194
- }
1195
- });
1196
- }
1197
 
1198
- // --- Logika untuk Modul 18 (Diagnostik Gejala Cerdas) ---
1199
- const startBtn = document.getElementById('start-diagnostic-btn'),
1200
- questionsDiv = document.getElementById('diagnostic-questions'),
1201
- resultDiv = document.getElementById('diagnostic-result');
1202
- let diagnosticTree = null;
1203
- if (startBtn) {
1204
- async function startDiagnostic() {
1205
- startBtn.style.display = 'none';
1206
- questionsDiv.style.display = 'block';
1207
- resultDiv.style.display = 'none';
1208
- resultDiv.innerHTML = '';
1209
- questionsDiv.innerHTML = '<p>Memuat data diagnostik...</p>';
1210
- try {
1211
- if (!diagnosticTree) {
1212
- const response = await fetch(`${baseUrl}${apiPrefix}/get-diagnostic-tree`);
1213
- const responseData = await response.json();
1214
- if (responseData.success) {
1215
- diagnosticTree = responseData.data;
1216
- } else {
1217
- throw new Error(responseData.error);
1218
- }
1219
- }
1220
- renderQuestion(diagnosticTree.start, []);
1221
- } catch (error) {
1222
- console.error("Error:", error);
1223
- questionsDiv.innerHTML = `<p style="color: red;"><strong>Error:</strong> ${error.message}</p>`;
1224
  }
 
1225
  }
1226
 
1227
- function renderQuestion(node, path) {
1228
- questionsDiv.innerHTML = `<h4>${node.question}</h4>`;
1229
- for (const [key, nextNode] of Object.entries(node.options)) {
1230
- const button = document.createElement('button');
1231
- button.textContent = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
1232
- button.classList.add('option-button');
1233
- button.onclick = () => {
1234
- if (typeof nextNode === 'string') {
1235
- renderResult(nextNode);
1236
- } else {
1237
- renderQuestion(nextNode, [...path, key]);
1238
- }
1239
- };
1240
- questionsDiv.appendChild(button);
1241
  }
 
1242
  }
1243
 
1244
- function renderResult(diagnosis) {
1245
- questionsDiv.style.display = 'none';
1246
- resultDiv.style.display = 'block';
1247
- resultDiv.innerHTML =
1248
- `<h3>Hasil Diagnosis</h3><p>${diagnosis}</p><button class="option-button" id="restart-diagnostic-btn">Mulai Lagi</button>`;
1249
- document.getElementById('restart-diagnostic-btn').onclick = () => {
1250
- startBtn.style.display = 'block';
1251
- questionsDiv.style.display = 'none';
1252
- resultDiv.style.display = 'none';
1253
- };
1254
  }
1255
- startBtn.addEventListener('click', startDiagnostic);
1256
- }
1257
 
1258
- // --- Logika untuk Modul 19 (Perencana Hasil Panen) ---
1259
- const yieldPlanForm = document.getElementById('yield-plan-form'),
1260
- resultYieldPlanDiv = document.getElementById('result-yield-plan');
1261
- if (yieldPlanForm) {
1262
- yieldPlanForm.addEventListener('submit', async (event) => {
1263
- event.preventDefault();
1264
- resultYieldPlanDiv.innerHTML = '<p>Mencari resep lahan optimal...</p>';
1265
- const requestData = {
1266
- commodity: document.getElementById('plan-commodity-select').value,
1267
- target_yield: document.getElementById('plan-target-yield-input').value
1268
- };
1269
- try {
1270
- const response = await fetch(`${baseUrl}${apiPrefix}/generate-yield-plan`, {
1271
- method: 'POST',
1272
- headers: {
1273
- 'Content-Type': 'application/json'
1274
- },
1275
- body: JSON.stringify(requestData)
1276
- });
1277
- const data = await response.json();
1278
- if (data.success) {
1279
- let htmlResult = `<h3>Rekomendasi Kondisi Lahan</h3>
1280
- <p>Untuk mencapai target panen sekitar <strong>${requestData.target_yield} ton/ha</strong>, berikut adalah resep kondisi lahan berdasarkan data kami yang paling mendekati:</p>
1281
- <ul class="item-list mt-4">`;
1282
- for (const [key, value] of Object.entries(data.plan)) {
1283
- htmlResult +=
1284
- `<li class="plan-item"><span class="plan-item-label">${key}</span><span class="plan-item-value">${value}</span></li>`;
1285
- }
1286
- htmlResult +=
1287
- `</ul><br><small><strong>Disclaimer:</strong> Ini adalah target berdasarkan data historis, bukan jaminan.</small>`;
1288
- resultYieldPlanDiv.innerHTML = htmlResult;
1289
- } else {
1290
- resultYieldPlanDiv.innerHTML = `<p style="color: red;"><strong>Error:</strong> ${data.error}</p>`;
1291
- }
1292
- } catch (error) {
1293
- console.error("Error:", error);
1294
- resultYieldPlanDiv.innerHTML =
1295
- `<p style="color: red;"><strong>Error:</strong> Gagal terhubung ke server.</p>`;
1296
  }
1297
- });
1298
- }
 
 
 
 
 
 
 
1299
 
1300
- // --- Logika untuk Modul 21 (Analis Risiko Keberhasilan) ---
1301
- const successPredictionForm = document.getElementById('success-prediction-form'),
1302
- resultSuccessPredictionDiv = document.getElementById('result-success-prediction');
1303
- if (successPredictionForm) {
1304
- successPredictionForm.addEventListener('submit', async (event) => {
1305
- event.preventDefault();
1306
- console.log('📝 Modul 21: Success Prediction form submitted!');
1307
- resultSuccessPredictionDiv.innerHTML = '<p>Menganalisis probabilitas keberhasilan...</p>';
1308
-
1309
- const requestData = {
1310
- nitrogen: document.getElementById('sp-nitrogen').value,
1311
- phosphorus: document.getElementById('sp-phosphorus').value,
1312
- potassium: document.getElementById('sp-potassium').value,
1313
- temperature: document.getElementById('sp-temperature').value,
1314
- rainfall: document.getElementById('sp-rainfall').value,
1315
- ph: document.getElementById('sp-ph').value,
1316
- penggunaan_benih: document.getElementById('sp-benih').value,
1317
- aplikasi_organik: document.getElementById('sp-organik').value,
1318
- manajemen_gulma: document.getElementById('sp-gulma').value
1319
- };
1320
-
1321
- try {
1322
- const response = await fetch(`${baseUrl}${apiPrefix}/predict-success`, {
1323
- method: 'POST',
1324
- headers: {
1325
- 'Content-Type': 'application/json'
1326
- },
1327
- body: JSON.stringify(requestData)
1328
- });
1329
-
1330
- const data = await response.json();
1331
-
1332
- if (data.success) {
1333
- const statusColor = data.status === "Berhasil" ? "text-green-600" : "text-red-600";
1334
- resultSuccessPredictionDiv.innerHTML = `
1335
- <h3>Hasil Analisis Risiko</h3>
1336
- <p>Berdasarkan parameter yang Anda masukkan, status rencana tanam Anda adalah:</p>
1337
- <p style="font-size: 1.8em; text-align: center; font-weight: bold;" class="${statusColor}">
1338
- ${data.status}</p>
1339
- <p class="text-center">Dengan probabilitas keberhasilan sebesar <strong>${data.probability_of_success}%</strong></p>
1340
- `;
1341
- } else {
1342
- resultSuccessPredictionDiv.innerHTML =
1343
- `<p style="color: red;"><strong>Error:</strong> ${data.error}</p>`;
1344
  }
1345
- } catch (error) {
1346
- console.error('Error fetching success probability:', error);
1347
- resultSuccessPredictionDiv.innerHTML =
1348
- `<p style="color: red;"><strong>Error:</strong> Tidak dapat terhubung ke server.</p>`;
1349
  }
1350
- });
1351
- }
1352
 
1353
- // --- Logika untuk Modul 20 (Dokter Tanaman Canggih - Roboflow AI) ---
1354
- const advancedDiseaseForm = document.getElementById('advanced-disease-form');
1355
- const resultAdvancedDiseaseDiv = document.getElementById('result-advanced-disease');
1356
- if (advancedDiseaseForm) {
1357
- advancedDiseaseForm.addEventListener('submit', async (event) => {
1358
- event.preventDefault();
1359
- console.log('📝 Modul 20: Roboflow form submitted!');
1360
- resultAdvancedDiseaseDiv.innerHTML = '<p>Menganalisis dengan Roboflow AI...</p>';
1361
- const fileInput = document.getElementById('disease-image-input');
1362
- const file = fileInput.files[0];
1363
- if (!file) {
1364
- resultAdvancedDiseaseDiv.innerHTML =
1365
- '<p style="color: red;"><strong>Error:</strong> Silakan pilih file gambar terlebih dahulu.</p>';
1366
- return;
1367
- }
1368
 
1369
- const formData = new FormData();
1370
- formData.append('file', file);
 
 
 
 
1371
 
1372
- try {
1373
- const response = await fetch(`${baseUrl}${apiPrefix}/analyze-disease-advanced`, {
1374
- method: 'POST',
1375
- body: formData
1376
- });
 
 
 
 
 
 
 
 
 
 
1377
 
1378
- const backendResponse = await response.json();
 
 
1379
 
1380
- if (backendResponse.success) {
1381
- displayRoboflowResults(backendResponse.data);
1382
- } else {
1383
- resultAdvancedDiseaseDiv.innerHTML =
1384
- `<p style="color: red;"><strong>Error dari server:</strong> ${backendResponse.error || 'Gagal menganalisis.'}</p>`;
1385
  }
1386
- } catch (error) {
1387
- console.error('Error calling backend for advanced analysis:', error);
1388
- resultAdvancedDiseaseDiv.innerHTML =
1389
- `<p style="color: red;"><strong>Error:</strong> Tidak dapat terhubung ke server backend Anda.</p>`;
1390
  }
1391
- });
1392
 
1393
- function displayRoboflowResults(results) {
1394
- let html = '<h3>Hasil Diagnosis dari Roboflow AI</h3>';
1395
- if (results && results.outputs && results.outputs.length > 0) {
1396
- html += '<ul style="list-style-type: none; padding-left: 0;">';
1397
- results.outputs.forEach((output, index) => {
1398
- const detection = output.detection;
1399
- const classification = output.classification;
1400
- html += `<li class="mb-4 p-4 border rounded-lg bg-white">`;
1401
- html += `<strong class="text-lg text-emerald-700">Deteksi #${index + 1}</strong><br>`;
1402
- if (detection && detection.predictions && detection.predictions.length > 0) {
1403
- const pred = detection.predictions[0];
1404
- html += `<strong>Objek Terdeteksi:</strong> ${pred.class} (Kepercayaan: ${Math.round(pred.confidence * 100)}%)<br>`;
1405
- } else {
1406
- html += `<strong>Objek Terdeteksi:</strong> Tidak ada.<br>`;
1407
- }
1408
- if (classification && classification.predictions && classification.predictions.length > 0) {
1409
- const topClass = classification.predictions[0];
1410
- html +=
1411
- `<strong>Klasifikasi Penyakit:</strong> ${topClass.class} (Kepercayaan: ${Math.round(topClass.confidence * 100)}%)`;
1412
- } else {
1413
- html += `<strong>Klasifikasi Penyakit:</strong> Tidak ada.`;
1414
- }
1415
- html += `</li>`;
1416
- });
1417
  html += '</ul>';
1418
- } else {
1419
- html += '<p>Tidak ada hasil deteksi atau klasifikasi yang ditemukan dalam gambar.</p>';
1420
  }
1421
- resultAdvancedDiseaseDiv.innerHTML = html;
 
1422
  }
 
 
1423
  }
1424
 
1425
- }); // --- TUTUP DOMCONTENTLOADED UTAMA ---
1426
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1427
  </body>
1428
- </html>
 
 
 
1
  <!DOCTYPE html>
 
2
  <html lang="id">
3
+
4
  <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>🎯 Modul 19: Perencana Hasil Panen (AI) - AgriSensa</title>
8
+ <link href="https://fonts.googleapis.com" rel="preconnect">
9
+ <link href="https://fonts.gstatic.com" crossorigin rel="preconnect">
10
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
11
+ <style>
12
+ :root {
13
+ --primary: #2e7d32;
 
14
  --primary-light: #4caf50;
15
+ --primary-dark: #1b5e20;
16
+ --secondary: #1976d2;
17
+ --accent: #ff6f00;
18
+ --background: #f5f7fa;
19
+ --card-bg: #ffffff;
20
+ --text: #212529;
21
  --text-muted: #6c757d;
22
+ --border: #dee2e6;
23
+ --shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
24
+ --shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.15);
25
+ }
26
+
27
+ * {
28
+ margin: 0;
29
+ padding: 0;
30
+ box-sizing: border-box;
31
  }
32
 
33
  body {
34
  font-family: 'Inter', sans-serif;
35
+ background: var(--background);
36
+ color: var(--text);
37
+ line-height: 1.6;
38
+ padding: 20px;
 
39
  }
40
 
41
+ .container {
42
+ max-width: 1200px;
43
+ margin: 0 auto;
44
  }
45
 
46
+ .back-link {
47
+ display: inline-flex;
48
+ align-items: center;
49
+ gap: 8px;
50
+ color: var(--primary);
51
+ text-decoration: none;
52
+ font-weight: 500;
53
+ margin-bottom: 24px;
54
+ transition: all 0.2s;
55
  }
56
 
57
+ .back-link:hover {
58
+ gap: 12px;
59
+ color: var(--primary-dark);
 
 
 
 
 
60
  }
61
 
62
+ .header {
63
+ background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
64
+ color: white;
65
+ padding: 32px;
66
+ border-radius: 16px;
67
+ margin-bottom: 32px;
68
+ box-shadow: var(--shadow-lg);
69
  }
70
 
71
+ .header h1 {
72
+ font-size: 2rem;
73
+ font-weight: 700;
74
+ margin-bottom: 12px;
75
+ display: flex;
76
+ align-items: center;
77
+ gap: 12px;
78
  }
79
 
80
+ .header p {
81
+ font-size: 1.1rem;
82
+ opacity: 0.95;
83
  }
84
 
85
+ .card {
86
+ background: var(--card-bg);
87
+ border-radius: 12px;
88
+ padding: 28px;
89
+ margin-bottom: 24px;
90
+ box-shadow: var(--shadow);
91
+ border: 1px solid var(--border);
92
  }
93
 
94
+ .card-title {
95
+ font-size: 1.3rem;
96
+ font-weight: 600;
97
+ color: var(--primary-dark);
98
+ margin-bottom: 20px;
99
+ padding-bottom: 12px;
100
+ border-bottom: 2px solid var(--primary-light);
101
+ }
102
 
103
+ .form-grid {
104
+ display: grid;
105
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
106
+ gap: 20px;
107
+ margin-bottom: 24px;
108
  }
109
 
110
+ .form-group {
111
+ display: flex;
112
+ flex-direction: column;
 
 
 
 
113
  }
114
 
115
+ label {
116
+ font-weight: 500;
117
+ margin-bottom: 8px;
118
+ color: var(--text);
119
  display: flex;
120
  align-items: center;
121
+ gap: 6px;
 
 
 
 
 
 
122
  }
123
 
124
+ select,
125
+ input {
126
+ padding: 12px 16px;
127
+ border: 2px solid var(--border);
128
+ border-radius: 8px;
129
+ font-size: 1rem;
130
+ font-family: inherit;
131
+ transition: all 0.2s;
132
+ background: white;
133
  }
134
 
135
+ select:focus,
136
+ input:focus {
137
+ outline: none;
138
+ border-color: var(--primary-light);
139
+ box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.1);
140
  }
141
 
142
+ .btn-primary {
143
+ background: linear-gradient(135deg, var(--primary-light) 0%, var(--primary) 100%);
 
144
  color: white;
145
+ padding: 14px 32px;
146
  border: none;
147
  border-radius: 8px;
148
+ font-size: 1.05rem;
149
+ font-weight: 600;
150
  cursor: pointer;
151
+ transition: all 0.3s;
152
+ box-shadow: 0 4px 12px rgba(46, 125, 50, 0.3);
153
  width: 100%;
 
 
154
  }
155
 
156
+ .btn-primary:hover {
 
 
157
  transform: translateY(-2px);
158
+ box-shadow: 0 6px 16px rgba(46, 125, 50, 0.4);
159
  }
160
 
161
+ .btn-primary:active {
162
+ transform: translateY(0);
 
 
 
 
 
163
  }
164
 
165
+ .btn-primary:disabled {
166
+ opacity: 0.6;
167
+ cursor: not-allowed;
168
+ transform: none;
 
 
 
 
 
169
  }
170
 
171
+ .spinner {
172
+ display: inline-block;
173
+ width: 16px;
174
+ height: 16px;
175
+ border: 3px solid rgba(255, 255, 255, 0.3);
176
+ border-radius: 50%;
177
+ border-top-color: white;
178
+ animation: spin 0.8s linear infinite;
179
  }
180
 
181
+ @keyframes spin {
182
+ to {
183
+ transform: rotate(360deg);
184
+ }
185
  }
186
 
187
+ .result-section {
188
+ display: none;
189
+ animation: fadeIn 0.5s;
190
  }
191
 
192
+ .result-section.active {
193
+ display: block;
 
194
  }
195
 
196
+ @keyframes fadeIn {
197
+ from {
198
+ opacity: 0;
199
+ transform: translateY(20px);
200
+ }
 
 
 
 
 
 
201
 
202
+ to {
203
+ opacity: 1;
204
+ transform: translateY(0);
205
+ }
206
  }
207
 
208
+ .feasibility-badge {
209
+ display: inline-block;
210
+ padding: 8px 16px;
211
+ border-radius: 20px;
212
  font-weight: 600;
213
+ font-size: 0.95rem;
214
+ margin: 12px 0;
215
  }
216
 
217
+ .feasibility-badge.green {
218
+ background: #d4edda;
219
+ color: #155724;
220
+ }
221
+
222
+ .feasibility-badge.blue {
223
+ background: #d1ecf1;
224
+ color: #0c5460;
225
+ }
226
+
227
+ .feasibility-badge.orange {
228
+ background: #fff3cd;
229
+ color: #856404;
230
+ }
231
+
232
+ .feasibility-badge.red {
233
+ background: #f8d7da;
234
+ color: #721c24;
235
  }
236
 
237
+ .feasibility-badge.purple {
238
+ background: #e7d6f8;
239
+ color: #5a1a7a;
240
  }
241
 
242
  .tabs {
243
  display: flex;
244
+ gap: 8px;
245
+ margin-bottom: 24px;
246
+ border-bottom: 2px solid var(--border);
247
  flex-wrap: wrap;
248
  }
249
 
250
+ .tab-btn {
251
+ padding: 12px 20px;
252
  background: none;
253
  border: none;
254
+ border-bottom: 3px solid transparent;
255
  cursor: pointer;
 
256
  font-weight: 500;
 
257
  color: var(--text-muted);
258
+ transition: all 0.2s;
259
+ font-size: 0.95rem;
260
  }
261
 
262
+ .tab-btn:hover {
263
+ color: var(--primary);
264
+ }
265
+
266
+ .tab-btn.active {
267
+ color: var(--primary);
268
+ border-bottom-color: var(--primary);
269
  font-weight: 600;
270
  }
271
 
 
275
 
276
  .tab-content.active {
277
  display: block;
278
+ animation: fadeIn 0.3s;
279
  }
280
 
281
+ .info-grid {
282
+ display: grid;
283
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
284
+ gap: 16px;
285
+ margin: 20px 0;
286
+ }
287
 
288
+ .info-item {
289
+ background: var(--background);
290
+ padding: 16px;
291
+ border-radius: 8px;
292
+ border-left: 4px solid var(--primary);
293
  }
294
 
295
+ .info-label {
296
+ font-size: 0.85rem;
297
+ color: var(--text-muted);
298
+ margin-bottom: 4px;
299
+ }
300
+
301
+ .info-value {
302
+ font-size: 1.1rem;
303
+ font-weight: 600;
304
+ color: var(--primary-dark);
305
+ }
306
+
307
+ .timeline-item {
308
+ background: white;
309
+ border-left: 4px solid var(--primary-light);
310
+ padding: 20px;
311
+ margin-bottom: 16px;
312
+ border-radius: 8px;
313
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
314
+ }
315
+
316
+ .timeline-phase {
317
+ font-weight: 600;
318
+ color: var(--primary-dark);
319
+ font-size: 1.1rem;
320
+ margin-bottom: 8px;
321
  }
322
 
323
+ .timeline-weeks {
324
+ color: var(--secondary);
325
+ font-size: 0.9rem;
326
+ margin-bottom: 12px;
327
+ }
328
+
329
+ .timeline-activities {
330
+ list-style: none;
331
+ padding-left: 0;
332
+ }
333
+
334
+ .timeline-activities li {
335
+ padding: 6px 0 6px 24px;
336
+ position: relative;
337
+ }
338
+
339
+ .timeline-activities li:before {
340
+ content: "✓";
341
+ position: absolute;
342
+ left: 0;
343
+ color: var(--primary);
344
+ font-weight: bold;
345
+ }
346
+
347
+ .cost-table {
348
  width: 100%;
349
  border-collapse: collapse;
350
+ margin: 16px 0;
351
  }
352
 
353
+ .cost-table th,
354
+ .cost-table td {
 
355
  padding: 12px;
356
+ text-align: left;
357
+ border-bottom: 1px solid var(--border);
358
  }
359
 
360
+ .cost-table th {
361
+ background: var(--background);
362
  font-weight: 600;
363
+ color: var(--primary-dark);
364
  }
365
 
366
+ .cost-table .total-row {
367
+ font-weight: 700;
368
+ background: #f8f9fa;
369
  }
370
 
371
+ .cost-table .profit-row {
372
+ font-weight: 700;
373
+ font-size: 1.1rem;
374
  }
375
 
376
+ .cost-table .profit-positive {
377
+ color: #28a745;
 
 
378
  }
379
 
380
+ .cost-table .profit-negative {
381
+ color: #dc3545;
 
382
  }
383
 
384
+ .alert {
385
+ padding: 16px;
386
+ border-radius: 8px;
387
+ margin: 16px 0;
388
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
 
390
+ .alert-info {
391
+ background: #d1ecf1;
392
+ color: #0c5460;
393
+ border-left: 4px solid #17a2b8;
394
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
395
 
396
+ .alert-warning {
397
+ background: #fff3cd;
398
+ color: #856404;
399
+ border-left: 4px solid #ffc107;
400
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
 
402
+ .variety-list {
403
+ display: flex;
404
+ flex-wrap: wrap;
405
+ gap: 12px;
406
+ margin: 16px 0;
407
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
408
 
409
+ .variety-tag {
410
+ background: var(--primary-light);
411
+ color: white;
412
+ padding: 8px 16px;
413
+ border-radius: 20px;
414
+ font-size: 0.9rem;
415
+ font-weight: 500;
416
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
417
 
418
+ .critical-factors {
419
+ list-style: none;
420
+ padding: 0;
421
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
422
 
423
+ .critical-factors li {
424
+ padding: 12px;
425
+ margin-bottom: 8px;
426
+ background: #fff3cd;
427
+ border-left: 4px solid var(--accent);
428
+ border-radius: 4px;
429
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
430
 
431
+ @media (max-width: 768px) {
432
+ .header h1 {
433
+ font-size: 1.5rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
434
  }
435
 
436
+ .form-grid {
437
+ grid-template-columns: 1fr;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
438
  }
439
 
440
+ .tabs {
441
+ overflow-x: auto;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
  }
443
+ }
444
+ </style>
445
+ </head>
446
 
447
+ <body>
448
+ <div class="container">
449
+ <a href="/dashboard" class="back-link">← Kembali ke Dashboard</a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
450
 
451
+ <div class="header">
452
+ <h1>🎯 Perencana Hasil Panen (AI)</h1>
453
+ <p>Tentukan target hasil panen Anda, dan biarkan AI menyusun resep kondisi lahan yang dibutuhkan untuk
454
+ mencapainya.</p>
455
+ </div>
456
+
457
+ <div class="card">
458
+ <div class="card-title">📋 Input Target Panen</div>
459
+ <form id="yield-plan-form">
460
+ <div class="form-grid">
461
+ <div class="form-group">
462
+ <label for="commodity-select">
463
+ 🌾 Pilih Komoditas
464
+ </label>
465
+ <select id="commodity-select" required>
466
+ <option value="">-- Pilih Komoditas --</option>
467
+ <option value="padi">🌾 Padi</option>
468
+ <option value="jagung">🌽 Jagung</option>
469
+ <option value="kedelai">🫘 Kedelai</option>
470
+ <option value="cabai">🌶️ Cabai</option>
471
+ <option value="tomat">🍅 Tomat</option>
472
+ <option value="umum">📊 Umum (Semua Tanaman)</option>
473
+ </select>
474
+ </div>
475
+ <div class="form-group">
476
+ <label for="target-yield">
477
+ 🎯 Target Hasil Panen (ton/ha)
478
+ </label>
479
+ <input type="number" id="target-yield" step="0.1" min="0.1" placeholder="Contoh: 7.5" required>
480
+ </div>
481
+ </div>
482
+ <button type="submit" class="btn-primary" id="submit-btn">
483
+ Buat Rencana Lahan
484
+ </button>
485
+ </form>
486
+ </div>
487
+
488
+ <div class="result-section" id="result-section">
489
+ <div id="result-content"></div>
490
+ </div>
491
+ </div>
492
+
493
+ <script>
494
+ const form = document.getElementById('yield-plan-form');
495
+ const submitBtn = document.getElementById('submit-btn');
496
+ const resultSection = document.getElementById('result-section');
497
+ const resultContent = document.getElementById('result-content');
498
+
499
+ form.addEventListener('submit', async (e) => {
500
+ e.preventDefault();
501
+
502
+ const commodity = document.getElementById('commodity-select').value;
503
+ const targetYield = parseFloat(document.getElementById('target-yield').value);
504
+
505
+ // Show loading
506
+ submitBtn.disabled = true;
507
+ submitBtn.innerHTML = '<span class="spinner"></span> Menyusun Rencana...';
508
+ resultSection.classList.remove('active');
509
+
510
+ try {
511
+ const response = await fetch('/generate-yield-plan', {
512
+ method: 'POST',
513
+ headers: { 'Content-Type': 'application/json' },
514
+ body: JSON.stringify({
515
+ commodity: commodity,
516
+ target_yield: targetYield
517
+ })
518
  });
 
519
 
520
+ const data = await response.json();
521
+
522
+ if (data.success) {
523
+ displayResults(data.plan);
524
+ resultSection.classList.add('active');
525
+ resultSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
526
+ } else {
527
+ resultContent.innerHTML = `
528
+ <div class="card">
529
+ <div class="alert alert-warning">
530
+ ⚠️ <strong>Error:</strong> ${data.error}
531
+ </div>
532
+ </div>
533
+ `;
534
+ resultSection.classList.add('active');
535
+ }
536
+ } catch (error) {
537
+ console.error('Error:', error);
538
+ resultContent.innerHTML = `
539
+ <div class="card">
540
+ <div class="alert alert-warning">
541
+ ⚠️ <strong>Error:</strong> Gagal terhubung ke server. Silakan coba lagi.
542
+ </div>
543
+ </div>
544
+ `;
545
+ resultSection.classList.add('active');
546
+ } finally {
547
+ submitBtn.disabled = false;
548
+ submitBtn.innerHTML = 'Buat Rencana Lahan';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
549
  }
550
+ });
551
+
552
+ function displayResults(plan) {
553
+ let html = '';
554
+
555
+ // Header with feasibility
556
+ html += `
557
+ <div class="card">
558
+ <h2 style="color: var(--primary-dark); margin-bottom: 16px;">
559
+ ${plan.commodity_icon || '🌱'} ${plan.commodity_name}
560
+ </h2>
561
+ <p style="font-size: 1.2rem; margin-bottom: 12px;">
562
+ Target: <strong>${plan.target_yield} ${plan.yield_unit || 'ton/ha'}</strong>
563
+ </p>
564
+ `;
565
+
566
+ if (plan.feasibility && typeof plan.feasibility === 'object') {
567
+ html += `
568
+ <div class="feasibility-badge ${plan.feasibility.color}">
569
+ ${plan.feasibility.status}
570
+ </div>
571
+ `;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
572
  }
573
 
574
+ html += '</div>';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
575
 
576
+ // Tabs for detailed information
577
+ if (plan.npk_requirements || plan.environmental_conditions) {
578
+ html += `
579
+ <div class="card">
580
+ <div class="tabs">
581
+ <button class="tab-btn active" onclick="openTab(event, 'tab-conditions')">📊 Kondisi Lahan</button>
582
+ `;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
583
 
584
+ if (plan.fertilizer_products) {
585
+ html += `<button class="tab-btn" onclick="openTab(event, 'tab-fertilizers')">🧪 Pemupukan</button>`;
586
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
587
 
588
+ if (plan.cultivation_timeline) {
589
+ html += `<button class="tab-btn" onclick="openTab(event, 'tab-timeline')">📅 Timeline</button>`;
590
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
591
 
592
+ if (plan.cost_estimate) {
593
+ html += `<button class="tab-btn" onclick="openTab(event, 'tab-costs')">💰 Estimasi Biaya</button>`;
594
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
595
 
596
+ if (plan.recommended_varieties || plan.critical_factors) {
597
+ html += `<button class="tab-btn" onclick="openTab(event, 'tab-tips')">💡 Tips & Rekomendasi</button>`;
598
+ }
599
+
600
+ html += '</div>';
601
+
602
+ // Tab 1: Conditions
603
+ html += `<div id="tab-conditions" class="tab-content active">`;
604
+
605
+ if (plan.npk_requirements) {
606
+ html += '<h3 style="color: var(--primary-dark); margin-bottom: 16px;">Kebutuhan NPK</h3>';
607
+ html += '<div class="info-grid">';
608
+ for (const [key, value] of Object.entries(plan.npk_requirements)) {
609
+ html += `
610
+ <div class="info-item">
611
+ <div class="info-label">${key}</div>
612
+ <div class="info-value">${value}</div>
613
+ </div>
614
+ `;
 
 
 
 
 
 
 
615
  }
616
+ html += '</div>';
617
  }
618
 
619
+ if (plan.environmental_conditions) {
620
+ html += '<h3 style="color: var(--primary-dark); margin: 24px 0 16px;">Kondisi Lingkungan Optimal</h3>';
621
+ html += '<div class="info-grid">';
622
+ for (const [key, value] of Object.entries(plan.environmental_conditions)) {
623
+ html += `
624
+ <div class="info-item">
625
+ <div class="info-label">${key}</div>
626
+ <div class="info-value">${value}</div>
627
+ </div>
628
+ `;
 
 
 
 
629
  }
630
+ html += '</div>';
631
  }
632
 
633
+ if (plan.feasibility && plan.feasibility.benchmark_range) {
634
+ html += `
635
+ <div class="alert alert-info" style="margin-top: 24px;">
636
+ <strong>📊 Benchmark Hasil Panen:</strong><br>
637
+ Rendah: ${plan.feasibility.benchmark_range.low}<br>
638
+ Rata-rata: ${plan.feasibility.benchmark_range.average}<br>
639
+ Tinggi: ${plan.feasibility.benchmark_range.high}<br>
640
+ Rekor: ${plan.feasibility.benchmark_range.record}
641
+ </div>
642
+ `;
643
  }
 
 
644
 
645
+ html += '</div>';
646
+
647
+ // Tab 2: Fertilizers
648
+ if (plan.fertilizer_products) {
649
+ html += `<div id="tab-fertilizers" class="tab-content">`;
650
+ html += '<h3 style="color: var(--primary-dark); margin-bottom: 16px;">Rekomendasi Pupuk</h3>';
651
+ html += '<div class="info-grid">';
652
+ for (const [fert, amount] of Object.entries(plan.fertilizer_products)) {
653
+ html += `
654
+ <div class="info-item">
655
+ <div class="info-label">${fert}</div>
656
+ <div class="info-value">${amount}</div>
657
+ </div>
658
+ `;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
659
  }
660
+ html += '</div>';
661
+ html += `
662
+ <div class="alert alert-info" style="margin-top: 20px;">
663
+ 💡 <strong>Catatan:</strong> Aplikasikan pupuk sesuai fase pertumbuhan tanaman.
664
+ Pupuk organik diberikan saat persiapan lahan, sedangkan pupuk anorganik dibagi dalam beberapa tahap.
665
+ </div>
666
+ `;
667
+ html += '</div>';
668
+ }
669
 
670
+ // Tab 3: Timeline
671
+ if (plan.cultivation_timeline) {
672
+ html += `<div id="tab-timeline" class="tab-content">`;
673
+ html += '<h3 style="color: var(--primary-dark); margin-bottom: 16px;">Jadwal Budidaya</h3>';
674
+ if (plan.growth_duration) {
675
+ html += `<p style="margin-bottom: 20px;"><strong>Durasi Total:</strong> ${plan.growth_duration}</p>`;
676
+ }
677
+ for (const phase of plan.cultivation_timeline) {
678
+ html += `
679
+ <div class="timeline-item">
680
+ <div class="timeline-phase">${phase.phase}</div>
681
+ <div class="timeline-weeks">${phase.weeks}</div>
682
+ <ul class="timeline-activities">
683
+ `;
684
+ for (const activity of phase.activities) {
685
+ html += `<li>${activity}</li>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
686
  }
687
+ html += '</ul></div>';
 
 
 
688
  }
689
+ html += '</div>';
690
+ }
691
 
692
+ // Tab 4: Costs
693
+ if (plan.cost_estimate) {
694
+ html += `<div id="tab-costs" class="tab-content">`;
695
+ html += '<h3 style="color: var(--primary-dark); margin-bottom: 16px;">Estimasi Biaya & Keuntungan</h3>';
696
+ html += '<table class="cost-table"><thead><tr><th>Item</th><th>Biaya</th></tr></thead><tbody>';
 
 
 
 
 
 
 
 
 
 
697
 
698
+ if (plan.cost_estimate.fertilizer_breakdown) {
699
+ html += '<tr><td colspan="2" style="font-weight: 600; background: #f8f9fa;">Pupuk</td></tr>';
700
+ for (const [fert, cost] of Object.entries(plan.cost_estimate.fertilizer_breakdown)) {
701
+ html += `<tr><td>${fert}</td><td>${cost}</td></tr>`;
702
+ }
703
+ }
704
 
705
+ html += `
706
+ <tr class="total-row"><td>Total Pupuk</td><td>${plan.cost_estimate.total_fertilizer}</td></tr>
707
+ <tr><td>Benih</td><td>${plan.cost_estimate.seed_cost}</td></tr>
708
+ <tr><td>Tenaga Kerja</td><td>${plan.cost_estimate.labor_cost}</td></tr>
709
+ <tr><td>Pestisida</td><td>${plan.cost_estimate.pesticide_cost}</td></tr>
710
+ <tr class="total-row"><td>TOTAL BIAYA INPUT</td><td>${plan.cost_estimate.total_input_cost}</td></tr>
711
+ <tr style="background: #e8f5e9;"><td>Estimasi Pendapatan</td><td>${plan.cost_estimate.estimated_revenue}</td></tr>
712
+ <tr class="profit-row ${plan.cost_estimate.estimated_profit.includes('-') ? 'profit-negative' : 'profit-positive'}">
713
+ <td>ESTIMASI KEUNTUNGAN</td><td>${plan.cost_estimate.estimated_profit}</td>
714
+ </tr>
715
+ `;
716
+ html += '</tbody></table>';
717
+ html += `<div class="alert alert-warning">${plan.cost_estimate.note}</div>`;
718
+ html += '</div>';
719
+ }
720
 
721
+ // Tab 5: Tips
722
+ if (plan.recommended_varieties || plan.critical_factors) {
723
+ html += `<div id="tab-tips" class="tab-content">`;
724
 
725
+ if (plan.recommended_varieties) {
726
+ html += '<h3 style="color: var(--primary-dark); margin-bottom: 16px;">Varietas Rekomendasi</h3>';
727
+ html += '<div class="variety-list">';
728
+ for (const variety of plan.recommended_varieties) {
729
+ html += `<span class="variety-tag">${variety}</span>`;
730
  }
731
+ html += '</div>';
 
 
 
732
  }
 
733
 
734
+ if (plan.critical_factors) {
735
+ html += '<h3 style="color: var(--primary-dark); margin: 24px 0 16px;">Faktor Kritis Keberhasilan</h3>';
736
+ html += '<ul class="critical-factors">';
737
+ for (const factor of plan.critical_factors) {
738
+ html += `<li>⚠️ ${factor}</li>`;
739
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
740
  html += '</ul>';
 
 
741
  }
742
+
743
+ html += '</div>';
744
  }
745
+
746
+ html += '</div>';
747
  }
748
 
749
+ resultContent.innerHTML = html;
750
+ }
751
+
752
+ function openTab(evt, tabId) {
753
+ // Hide all tab contents
754
+ const tabContents = document.getElementsByClassName('tab-content');
755
+ for (let content of tabContents) {
756
+ content.classList.remove('active');
757
+ }
758
+
759
+ // Remove active class from all tabs
760
+ const tabBtns = document.getElementsByClassName('tab-btn');
761
+ for (let btn of tabBtns) {
762
+ btn.classList.remove('active');
763
+ }
764
+
765
+ // Show selected tab and mark button as active
766
+ document.getElementById(tabId).classList.add('active');
767
+ evt.currentTarget.classList.add('active');
768
+ }
769
+ </script>
770
  </body>
771
+
772
+ </html>