import React, { useState, useEffect, createContext, useContext } from 'react'; import { initializeApp } from 'firebase/app'; import { getAuth, createUserWithEmailAndPassword, signInWithEmailAndPassword, signOut, onAuthStateChanged, signInWithCustomToken, signInAnonymously } from 'firebase/auth'; import { getFirestore, collection, addDoc, getDocs, doc, updateDoc, deleteDoc, query, where, onSnapshot } from 'firebase/firestore'; // --- Konfigurasi dan Inisialisasi Firebase --- const firebaseConfig = { apiKey: "AIzaSyBea0EeQo94ovT0MPFo_U-aEETr79lOJY", authDomain: "ssdg-eda66.firebaseapp.com", projectId: "ssdg-eda66", storageBucket: "ssdg-eda66.firebasestorage.app", messagingSenderId: "1097467667119", appId: "1:1097467667119:web:d1c628941cdb6c7841aa5a" }; const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id'; const canvasFirebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : firebaseConfig; const initialAuthToken = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : null; const app = initializeApp(canvasFirebaseConfig); const auth = getAuth(app); const db = getFirestore(app); // --- Konteks Autentikasi untuk Manajemen Pengguna --- const AuthContext = createContext(null); function AuthProvider({ children }) { const [currentUser, setCurrentUser] = useState(null); const [loadingAuth, setLoadingAuth] = useState(true); useEffect(() => { const signInUser = async () => { try { if (initialAuthToken) { await signInWithCustomToken(auth, initialAuthToken); console.log("Berhasil masuk dengan token kustom."); } else { await signInAnonymously(auth); console.log("Berhasil masuk secara anonim."); } } catch (error) { console.error("Kesalahan autentikasi:", error); } finally { setLoadingAuth(false); } }; const unsubscribe = onAuthStateChanged(auth, user => { setCurrentUser(user); setLoadingAuth(false); }); signInUser(); return () => unsubscribe(); }, []); const value = { currentUser, loadingAuth, auth }; return ( {!loadingAuth && children} ); } function useAuth() { return useContext(AuthContext); } // --- Komponen UI --- function Login({ onLoginSuccess }) { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(''); const { auth } = useAuth(); const handleLogin = async (e) => { e.preventDefault(); setError(''); try { await signInWithEmailAndPassword(auth, email, password); onLoginSuccess(); } catch (err) { console.error("Kesalahan Login:", err); setError(err.message.includes('auth/invalid-credential') ? 'Email atau kata sandi tidak valid.' : err.message); } }; return (

Masuk ke Aplikasi Drivvo

setEmail(e.target.value)} required />
setPassword(e.target.value)} required />
{error &&

{error}

}

Belum punya akun?

); } function SignUp({ onSignUpSuccess }) { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [error, setError] = useState(''); const { auth } = useAuth(); const handleSignUp = async (e) => { e.preventDefault(); setError(''); if (password !== confirmPassword) { return setError('Kata sandi tidak cocok.'); } try { await createUserWithEmailAndPassword(auth, email, password); onSignUpSuccess(); } catch (err) { console.error("Kesalahan Pendaftaran:", err); setError(err.message.includes('auth/email-already-in-use') ? 'Email sudah digunakan.' : err.message); } }; return (

Buat Akun Anda

setEmail(e.target.value)} required />
setPassword(e.target.value)} required />
setConfirmPassword(e.target.value)} required />
{error &&

{error}

}

Sudah punya akun?

); } function Modal({ show, title, message, onConfirm, onCancel, type = 'info' }) { if (!show) return null; return (

{title}

{message}

{type === 'confirm' && ( )}
); } // Form Kendaraan untuk Tambah/Edit dengan detail STNK function VehicleForm({ initialData = {}, onSubmit, onCancel }) { const [jenis, setJenis] = useState(initialData.jenis || ''); const [merk, setMerk] = useState(initialData.merk || ''); const [type, setType] = useState(initialData.type || ''); const [tahunPembuatan, setTahunPembuatan] = useState(initialData.tahunPembuatan || ''); const [isiSilinder, setIsiSilinder] = useState(initialData.isiSilinder || ''); const [warna, setWarna] = useState(initialData.warna || ''); const [nomorRangka, setNomorRangka] = useState(initialData.nomorRangka || ''); const [nomorMesin, setNomorMesin] = useState(initialData.nomorMesin || ''); const [bahanBakar, setBahanBakar] = useState(initialData.bahanBakar || ''); const [jumlahSumbu, setJumlahSumbu] = useState(initialData.jumlahSumbu || ''); const [jumlahRoda, setJumlahRoda] = useState(initialData.jumlahRoda || ''); const [beratKosong, setBeratKosong] = useState(initialData.beratKosong || ''); const [beratMaksimum, setBeratMaksimum] = useState(initialData.beratMaksimum || ''); const [nomorBPKB, setNomorBPKB] = useState(initialData.nomorBPKB || ''); const [nomorPolisi, setNomorPolisi] = useState(initialData.nomorPolisi || ''); const handleSubmit = (e) => { e.preventDefault(); onSubmit({ jenis, merk, type, tahunPembuatan: Number(tahunPembuatan), isiSilinder: Number(isiSilinder), warna, nomorRangka, nomorMesin, bahanBakar, jumlahSumbu: Number(jumlahSumbu), jumlahRoda: Number(jumlahRoda), beratKosong: Number(beratKosong), beratMaksimum: Number(beratMaksimum), nomorBPKB, nomorPolisi }); }; return (

{initialData.id ? 'Edit Kendaraan' : 'Tambah Kendaraan Baru'}

setMerk(e.target.value)} className="w-full p-2.5 sm:p-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 transition duration-200 text-sm sm:text-base" required />
setType(e.target.value)} className="w-full p-2.5 sm:p-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 transition duration-200 text-sm sm:text-base" placeholder="Contoh: Avanza G, Vario 160" required />
setNomorPolisi(e.target.value)} className="w-full p-2.5 sm:p-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 transition duration-200 text-sm sm:text-base" />
setTahunPembuatan(e.target.value)} className="w-full p-2.5 sm:p-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 transition duration-200 text-sm sm:text-base" />
setIsiSilinder(e.target.value)} className="w-full p-2.5 sm:p-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 transition duration-200 text-sm sm:text-base" />
setWarna(e.target.value)} className="w-full p-2.5 sm:p-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 transition duration-200 text-sm sm:text-base" />
setNomorRangka(e.target.value)} className="w-full p-2.5 sm:p-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 transition duration-200 text-sm sm:text-base" />
setNomorMesin(e.target.value)} className="w-full p-2.5 sm:p-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 transition duration-200 text-sm sm:text-base" />
setBahanBakar(e.target.value)} className="w-full p-2.5 sm:p-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 transition duration-200 text-sm sm:text-base" placeholder="Contoh: Bensin, Diesel" />
setJumlahSumbu(e.target.value)} className="w-full p-2.5 sm:p-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 transition duration-200 text-sm sm:text-base" />
setJumlahRoda(e.target.value)} className="w-full p-2.5 sm:p-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 transition duration-200 text-sm sm:text-base" />
setBeratKosong(e.target.value)} className="w-full p-2.5 sm:p-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 transition duration-200 text-sm sm:text-base" />
setBeratMaksimum(e.target.value)} className="w-full p-2.5 sm:p-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 transition duration-200 text-sm sm:text-base" />
setNomorBPKB(e.target.value)} className="w-full p-2.5 sm:p-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 transition duration-200 text-sm sm:text-base" />
); } function MaintenanceRecordForm({ initialData = {}, vehicleId, onSubmit, onCancel }) { const [type, setType] = useState(initialData.type || ''); const [date, setDate] = useState(initialData.date || new Date().toISOString().slice(0, 10)); const [mileage, setMileage] = useState(initialData.mileage || ''); const [cost, setCost] = useState(initialData.cost || ''); const [notes, setNotes] = useState(initialData.notes || ''); const handleSubmit = (e) => { e.preventDefault(); onSubmit({ type, date, mileage: Number(mileage), cost: Number(cost), notes }, initialData.id); }; return (

{initialData.id ? 'Edit Catatan' : 'Tambah Catatan Baru'}

setType(e.target.value)} className="w-full p-2.5 sm:p-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 transition duration-200 text-sm sm:text-base" placeholder="Contoh: Ganti Oli, Bahan Bakar, Rotasi Ban" required />
setDate(e.target.value)} className="w-full p-2.5 sm:p-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 transition duration-200 text-sm sm:text-base" required />
setMileage(e.target.value)} className="w-full p-2.5 sm:p-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 transition duration-200 text-sm sm:text-base" required />
setCost(e.target.value)} className="w-full p-2.5 sm:p-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 transition duration-200 text-sm sm:text-base" />
); } // Komponen Log Bahan Bakar function LogBBM({ fuelLogs, vehicles, onAddFuelLog, onDeleteFuelLog, setModalConfig, setShowModal, initialVehicleId }) { const [selectedVehicleId, setSelectedVehicleId] = useState(initialVehicleId || ''); const [date, setDate] = useState(new Date().toISOString().slice(0, 10)); const [volume, setVolume] = useState(''); const [pricePerLiter, setPricePerLiter] = useState(''); const [totalCost, setTotalCost] = useState(''); const [mileage, setMileage] = useState(''); const [notes, setNotes] = useState(''); useEffect(() => { setSelectedVehicleId(initialVehicleId || ''); }, [initialVehicleId]); useEffect(() => { if (volume && pricePerLiter) { setTotalCost(Math.round(Number(volume) * Number(pricePerLiter)).toString()); // Round to 0 decimal places } else { setTotalCost(''); } }, [volume, pricePerLiter]); const handleSubmit = (e) => { e.preventDefault(); if (!selectedVehicleId) { setModalConfig({ title: "Peringatan", message: "Harap pilih kendaraan untuk mencatat log bahan bakar.", onConfirm: () => setShowModal(false) }); setShowModal(true); return; } onAddFuelLog({ vehicleId: selectedVehicleId, date, volume: Number(volume), pricePerLiter: Number(pricePerLiter), totalCost: Number(totalCost), // totalCost is already rounded in state, convert back to number mileage: Number(mileage), notes }); // Clear form setDate(new Date().toISOString().slice(0, 10)); setVolume(''); setPricePerLiter(''); setTotalCost(''); setMileage(''); setNotes(''); }; const getVehicleName = (vehicle) => { return vehicle ? `${vehicle.merk} ${vehicle.type} (${vehicle.nomorPolisi || 'N/A'})` : 'Kendaraan Tidak Dikenal'; }; return (

Log Bahan Bakar

Tambah Log Bahan Bakar Baru

setDate(e.target.value)} className="w-full p-2.5 sm:p-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 transition duration-200 text-sm sm:text-base" required />
setVolume(e.target.value)} className="w-full p-2.5 sm:p-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 transition duration-200 text-sm sm:text-base" required />
setPricePerLiter(e.target.value)} className="w-full p-2.5 sm:p-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 transition duration-200 text-sm sm:text-base" required />
setMileage(e.target.value)} className="w-full p-2.5 sm:p-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 transition duration-200 text-sm sm:text-base" required />

Riwayat Log Bahan Bakar

{fuelLogs.length === 0 ? (

Belum ada log bahan bakar yang dicatat.

) : (
{fuelLogs.map(log => (

{getVehicleName(vehicles.find(v_item => v_item.id === log.vehicleId))}

Tanggal: {log.date}

Volume: {log.volume} L

Biaya: IDR {Math.round(log.totalCost || 0)}

Jarak Tempuh: {log.mileage} km

{log.notes &&

Catatan: {log.notes}

}
))}
)}
); } // Komponen Laporan function Laporan({ vehicles, allMaintenanceRecords, fuelLogs, otherExpenses }) { const [selectedVehicleId, setSelectedVehicleId] = useState(''); const filteredMaintenanceRecords = selectedVehicleId ? allMaintenanceRecords.filter(record => record.vehicleId === selectedVehicleId) : allMaintenanceRecords; const filteredFuelLogs = selectedVehicleId ? fuelLogs.filter(log => log.vehicleId === selectedVehicleId) : fuelLogs; const filteredOtherExpenses = selectedVehicleId ? otherExpenses.filter(expense => expense.vehicleId === selectedVehicleId) : otherExpenses; const totalMaintenanceCost = filteredMaintenanceRecords.reduce((sum, record) => { const costValue = Number(record.cost) || 0; return sum + costValue; }, 0); const totalFuelCost = filteredFuelLogs.reduce((sum, log) => sum + (log.totalCost || 0), 0); const totalOtherExpensesCost = filteredOtherExpenses.reduce((sum, expense) => sum + (expense.cost || 0), 0); const totalMileageLogged = filteredFuelLogs.reduce((sum, log) => sum + (log.mileage || 0), 0); const getVehicleName = (vehicle) => { return vehicle ? `${vehicle.merk} ${vehicle.type} (${vehicle.nomorPolisi || 'N/A'})` : 'Semua Kendaraan'; }; return (

Laporan Kendaraan

Filter Laporan

Ringkasan untuk {getVehicleName(vehicles.find(v_item => v_item.id === selectedVehicleId))}

Total Biaya Perawatan

IDR {Math.round(totalMaintenanceCost)}

Total Biaya Bahan Bakar

IDR {Math.round(totalFuelCost)}

{/* New: Display Total Other Expenses */}

Total Pengeluaran Lainnya

IDR {Math.round(totalOtherExpensesCost)}

Total Semua Pengeluaran

IDR {Math.round(totalMaintenanceCost + totalFuelCost + totalOtherExpensesCost)}

Total Jarak Tempuh Tercatat

{Math.round(totalMileageLogged)} km

); } // Komponen Servis Overview function ServisOverview({ allMaintenanceRecords, vehicles }) { const getVehicleName = (vehicle) => { return vehicle ? `${vehicle.merk} ${vehicle.type} (${vehicle.nomorPolisi || 'N/A'})` : 'Kendaraan Tidak Dikenal'; }; return (

Ringkasan Servis

{allMaintenanceRecords.length === 0 ? (

Belum ada catatan servis di semua kendaraan.

Tambahkan kendaraan di "Garasi" dan catat servisnya di detail kendaraan.

) : (
{allMaintenanceRecords.map(record => (

{getVehicleName(vehicles.find(v_item => v_item.id === record.vehicleId))}

Jenis Servis: {record.type}

Tanggal: {record.date}

Jarak Tempuh: {record.mileage} km

Biaya: IDR {Math.round(record.cost || 0)}

{record.notes &&

Catatan: {record.notes}

}
))}
)}
); } // Komponen Pengeluaran Lainnya function PengeluaranLainnya({ otherExpenses, vehicles, onAddOtherExpense, onDeleteOtherExpense, setModalConfig, setShowModal, initialVehicleId }) { const [selectedVehicleId, setSelectedVehicleId] = useState(initialVehicleId || ''); const [jenisPengeluaran, setJenisPengeluaran] = useState(''); const [date, setDate] = useState(new Date().toISOString().slice(0, 10)); const [cost, setCost] = useState(''); const [notes, setNotes] = useState(''); useEffect(() => { setSelectedVehicleId(initialVehicleId || ''); }, [initialVehicleId]); const handleSubmit = (e) => { e.preventDefault(); if (!selectedVehicleId) { setModalConfig({ title: "Peringatan", message: "Harap pilih kendaraan untuk mencatat pengeluaran lainnya.", onConfirm: () => setShowModal(false) }); setShowModal(true); return; } onAddOtherExpense({ vehicleId: selectedVehicleId, jenisPengeluaran, date, cost: Math.round(Number(cost) || 0), // Round cost before submitting notes }); // Clear form setJenisPengeluaran(''); setDate(new Date().toISOString().slice(0, 10)); setCost(''); setNotes(''); }; const getVehicleName = (vehicle) => { return vehicle ? `${vehicle.merk} ${vehicle.type} (${vehicle.nomorPolisi || 'N/A'})` : 'Kendaraan Tidak Dikenal'; }; return (

Pengeluaran Lainnya

Tambah Pengeluaran Baru

setJenisPengeluaran(e.target.value)} className="w-full p-2.5 sm:p-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 transition duration-200 text-sm sm:text-base" placeholder="Contoh: Parkir, Tol, Cuci Mobil" required />
setDate(e.target.value)} className="w-full p-2.5 sm:p-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 transition duration-200 text-sm sm:text-base" required />
setCost(e.target.value)} className="w-full p-2.5 sm:p-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 transition duration-200 text-sm sm:text-base" />

Riwayat Pengeluaran Lainnya

{otherExpenses.length === 0 ? (

Belum ada pengeluaran lain yang dicatat.

) : (
{otherExpenses.map(expense => (

{getVehicleName(vehicles.find(v_item => v_item.id === expense.vehicleId))}

Jenis: {expense.jenisPengeluaran}

Tanggal: {expense.date}

Biaya: IDR {Math.round(expense.cost || 0)}

{expense.notes &&

Catatan: {expense.notes}

}
))}
)}
); } // Komponen Saran AI function SaranAI({ vehicles, selectedVehicle, allMaintenanceRecords, fuelLogs, otherExpenses, setModalConfig, setShowModal }) { const [selectedAIAnalysisVehicleId, setSelectedAIAnalysisVehicleId] = useState(selectedVehicle ? selectedVehicle.id : ''); const [prompt, setPrompt] = useState(''); const [aiResponse, setAiResponse] = useState(''); const [isLoadingAI, setIsLoadingAI] = useState(false); const API_KEY = "AIzaSyCTDVKW4K68Bc4TjHGsLVgV0olwtgjyRsY"; // API Key from user // Helper function to get vehicle name for display const getVehicleName = (vehicle) => { return vehicle ? `${vehicle.merk} ${vehicle.type} (${vehicle.nomorPolisi || 'N/A'})` : 'Kendaraan Tidak Dikenal'; }; useEffect(() => { // Set initial AI greeting when a vehicle is selected if (selectedAIAnalysisVehicleId) { const vehicle = vehicles.find(v => v.id === selectedAIAnalysisVehicleId); if (vehicle) { setAiResponse(`Halo, saya Asisten Anda yang ahli dalam pengetahuan kendaraan. Apa yang bisa saya bantu untuk ${vehicle.merk} ${vehicle.type}?`); } } else { setAiResponse("Halo! Silakan pilih kendaraan terlebih dahulu untuk mendapatkan saran yang lebih spesifik."); } }, [selectedAIAnalysisVehicleId, vehicles]); const handleGenerateAdvice = async () => { if (!selectedAIAnalysisVehicleId) { setModalConfig({ title: "Peringatan", message: "Harap pilih kendaraan terlebih dahulu untuk mendapatkan saran.", onConfirm: () => setShowModal(false) }); setShowModal(true); return; } if (!prompt.trim()) { setModalConfig({ title: "Peringatan", message: "Harap masukkan pertanyaan Anda.", onConfirm: () => setShowModal(false) }); setShowModal(true); return; } setIsLoadingAI(true); setAiResponse(''); // Clear previous response try { const vehicle = vehicles.find(v => v.id === selectedAIAnalysisVehicleId); let vehicleDataString = ""; if (vehicle) { vehicleDataString += `Data Kendaraan: \n`; for (const key in vehicle) { if (key !== 'id') { // Exclude Firebase ID vehicleDataString += `- ${key}: ${vehicle[key]}\n`; } } const maintenanceRecordsForVehicle = allMaintenanceRecords.filter(rec => rec.vehicleId === selectedAIAnalysisVehicleId); if (maintenanceRecordsForVehicle.length > 0) { vehicleDataString += `\nRiwayat Servis:\n`; maintenanceRecordsForVehicle.forEach(rec => { vehicleDataString += `- Jenis: ${rec.type}, Tanggal: ${rec.date}, Jarak: ${rec.mileage} km, Biaya: IDR ${Math.round(rec.cost || 0)}\n`; }); } const fuelLogsForVehicle = fuelLogs.filter(log => log.vehicleId === selectedAIAnalysisVehicleId); if (fuelLogsForVehicle.length > 0) { vehicleDataString += `\nRiwayat Log BBM:\n`; fuelLogsForVehicle.forEach(log => { vehicleDataString += `- Tanggal: ${log.date}, Volume: ${log.volume} L, Biaya: IDR ${Math.round(log.totalCost || 0)}, Jarak: ${log.mileage} km\n`; }); } const otherExpensesForVehicle = otherExpenses.filter(exp => exp.vehicleId === selectedAIAnalysisVehicleId); if (otherExpensesForVehicle.length > 0) { vehicleDataString += `\nRiwayat Pengeluaran Lainnya:\n`; otherExpensesForVehicle.forEach(exp => { vehicleDataString += `- Jenis: ${exp.jenisPengeluaran}, Tanggal: ${exp.date}, Biaya: IDR ${Math.round(exp.cost || 0)}\n`; }); } } const systemInstruction = `Anda adalah seorang ahli kendaraan yang sangat berpengetahuan luas, mencakup motor dan mobil. Anda ahli dalam perawatan, mekanik mesin, dan servis. Berikan saran yang akurat, informatif, dan mudah dipahami. Jika ada data kendaraan yang relevan, gunakan data tersebut untuk konteks.`; let chatHistory = []; chatHistory.push({ role: "user", parts: [{ text: `${systemInstruction}\n\n${vehicleDataString}\n\nPertanyaan saya: ${prompt}` }] }); const payload = { contents: chatHistory }; const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${API_KEY}`; const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const result = await response.json(); if (result.candidates && result.candidates.length > 0 && result.candidates[0].content && result.candidates[0].content.parts && result.candidates[0].content.parts.length > 0) { const text = result.candidates[0].content.parts[0].text; setAiResponse(text); } else { setAiResponse("Maaf, saya tidak dapat menghasilkan saran saat ini. Coba pertanyaan lain."); console.error("Unexpected API response structure:", result); } } catch (error) { console.error("Error calling Gemini API:", error); setModalConfig({ title: "Kesalahan AI", message: "Terjadi kesalahan saat berkomunikasi dengan AI. Silakan coba lagi nanti.", onConfirm: () => setShowModal(false) }); setShowModal(true); } finally { setIsLoadingAI(false); } }; // Corrected: getVehicleNameFull now takes a vehicle object, not an ID const getVehicleNameFull = (vehicle) => { return vehicle ? `${vehicle.merk} ${vehicle.type} (${vehicle.nomorPolisi || 'N/A'})` : '-- Pilih Kendaraan --'; }; return (

Saran AI (Ahli Kendaraan)

{selectedAIAnalysisVehicleId ? (
) : (
Silakan pilih kendaraan di atas untuk memulai percakapan dengan AI.
)} {aiResponse && (

Saran dari AI:

{aiResponse}

)}
); } // --- Komponen Aplikasi Utama --- export default function App() { // Mengubah menjadi default export const { currentUser, loadingAuth, auth } = useAuth(); const [vehicles, setVehicles] = useState([]); const [allMaintenanceRecords, setAllMaintenanceRecords] = useState([]); const [selectedVehicle, setSelectedVehicle] = useState(null); const [maintenanceRecordsForSelected, setMaintenanceRecordsForSelected] = useState([]); const [fuelLogs, setFuelLogs] = useState([]); const [otherExpenses, setOtherExpenses] = useState([]); // New: State for other expenses const [currentView, setCurrentView] = useState('dashboard'); const [showModal, setShowModal] = useState(false); const [modalConfig, setModalConfig] = useState({}); const [maintenanceRefreshTrigger, setMaintenanceRefreshTrigger] = useState(0); const [otherExpensesRefreshTrigger, setOtherExpensesRefreshTrigger] = useState(0); // New: Trigger for other expenses // Fungsi untuk mendapatkan kelas tombol navigasi aktif const navButtonClass = (viewName) => { const baseClass = "px-3 py-1.5 sm:px-4 sm:py-2 rounded-lg font-semibold transition duration-200 text-xs sm:text-sm"; return currentView === viewName ? `${baseClass} bg-blue-600 text-white shadow-md` : `${baseClass} bg-gray-200 text-gray-800 hover:bg-gray-300`; }; useEffect(() => { if (!loadingAuth) { if (!currentUser || currentUser.isAnonymous) { setCurrentView('login'); } else { setCurrentView('dashboard'); } } }, [currentUser, loadingAuth]); // Fetch vehicles and all maintenance records useEffect(() => { if (currentUser && !currentUser.isAnonymous && db) { const userId = currentUser.uid; const vehiclesColRef = collection(db, `artifacts/${appId}/users/${userId}/vehicles`); const unsubscribe = onSnapshot(vehiclesColRef, async (snapshot) => { const fetchedVehicles = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); setVehicles(fetchedVehicles); let allRecords = []; for (const vehicle of fetchedVehicles) { const recordsColRef = collection(db, `artifacts/${appId}/users/${userId}/vehicles/${vehicle.id}/maintenanceRecords`); const recordsSnapshot = await getDocs(recordsColRef); recordsSnapshot.docs.forEach(doc => { allRecords.push({ id: doc.id, vehicleId: vehicle.id, ...doc.data() }); }); } setAllMaintenanceRecords(allRecords.sort((a, b) => new Date(b.date) - new Date(a.date))); }, (error) => { console.error("Kesalahan mengambil kendaraan:", error); setModalConfig({ title: "Kesalahan", message: "Gagal memuat kendaraan. Silakan coba lagi.", onConfirm: () => setShowModal(false) }); setShowModal(true); }); return () => unsubscribe(); } else { setVehicles([]); setAllMaintenanceRecords([]); } }, [currentUser, db, maintenanceRefreshTrigger]); // Fetch maintenance records for the selected vehicle (for vehicleDetails view) useEffect(() => { if (selectedVehicle && currentUser && !currentUser.isAnonymous && db) { const userId = currentUser.uid; const recordsColRef = collection(db, `artifacts/${appId}/users/${userId}/vehicles/${selectedVehicle.id}/maintenanceRecords`); const unsubscribe = onSnapshot(recordsColRef, (snapshot) => { const fetchedRecords = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })).sort((a, b) => new Date(b.date) - new Date(a.date)); setMaintenanceRecordsForSelected(fetchedRecords); }, (error) => { console.error("Kesalahan mengambil catatan perawatan:", error); setModalConfig({ title: "Kesalahan", message: "Gagal memuat catatan perawatan. Silakan coba lagi.", onConfirm: () => setShowModal(false) }); setShowModal(true); }); return () => unsubscribe(); } else { setMaintenanceRecordsForSelected([]); } }, [selectedVehicle, currentUser, db]); // Fetch all fuel logs useEffect(() => { if (currentUser && !currentUser.isAnonymous && db) { const userId = currentUser.uid; const fuelLogsColRef = collection(db, `artifacts/${appId}/users/${userId}/fuelLogs`); const unsubscribe = onSnapshot(fuelLogsColRef, (snapshot) => { const fetchedLogs = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })).sort((a, b) => new Date(b.date) - new Date(a.date)); setFuelLogs(fetchedLogs); }, (error) => { console.error("Kesalahan mengambil log BBM:", error); setModalConfig({ title: "Kesalahan", message: "Gagal memuat log bahan bakar. Silakan coba lagi.", onConfirm: () => setShowModal(false) }); setShowModal(true); }); return () => unsubscribe(); } else { setFuelLogs([]); } }, [currentUser, db]); // New: Fetch all other expenses useEffect(() => { if (currentUser && !currentUser.isAnonymous && db) { const userId = currentUser.uid; const otherExpensesColRef = collection(db, `artifacts/${appId}/users/${userId}/otherExpenses`); const unsubscribe = onSnapshot(otherExpensesColRef, (snapshot) => { const fetchedExpenses = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })).sort((a, b) => new Date(b.date) - new Date(a.date)); setOtherExpenses(fetchedExpenses); }, (error) => { console.error("Kesalahan mengambil pengeluaran lainnya:", error); setModalConfig({ title: "Kesalahan", message: "Gagal memuat pengeluaran lainnya. Silakan coba lagi.", onConfirm: () => setShowModal(false) }); setShowModal(true); }); return () => unsubscribe(); } else { setOtherExpenses([]); } }, [currentUser, db, otherExpensesRefreshTrigger]); // New: Dependency for refresh const handleLogout = async () => { try { await signOut(auth); setCurrentView('login'); setSelectedVehicle(null); setVehicles([]); setAllMaintenanceRecords([]); setMaintenanceRecordsForSelected([]); setFuelLogs([]); setOtherExpenses([]); // Clear other expenses on logout setModalConfig({ title: "Berhasil Keluar", message: "Anda telah berhasil keluar.", onConfirm: () => setShowModal(false) }); setShowModal(true); } catch (error) { console.error("Kesalahan Logout:", error); setModalConfig({ title: "Kesalahan", message: "Gagal keluar. Silakan coba lagi.", onConfirm: () => setShowModal(false) }); setShowModal(true); } }; const handleAuthSuccess = (nextView = 'dashboard') => { setCurrentView(nextView); }; const handleAddVehicle = async (vehicleData) => { try { const userId = currentUser.uid; const vehiclesColRef = collection(db, `artifacts/${appId}/users/${userId}/vehicles`); await addDoc(vehiclesColRef, vehicleData); setCurrentView('garasi'); setModalConfig({ title: "Berhasil", message: "Kendaraan berhasil ditambahkan!", onConfirm: () => setShowModal(false) }); setShowModal(true); } catch (error) { console.error("Kesalahan menambahkan kendaraan:", error); setModalConfig({ title: "Kesalahan", message: "Gagal menambahkan kendaraan. Silakan coba lagi.", onConfirm: () => setShowModal(false) }); setShowModal(true); } }; const handleEditVehicle = async (vehicleData) => { try { const userId = currentUser.uid; const vehicleDocRef = doc(db, `artifacts/${appId}/users/${userId}/vehicles`, selectedVehicle.id); await updateDoc(vehicleDocRef, vehicleData); setSelectedVehicle({ ...selectedVehicle, ...vehicleData }); setCurrentView('vehicleDetails'); setModalConfig({ title: "Berhasil", message: "Kendaraan berhasil diperbarui!", onConfirm: () => setShowModal(false) }); setShowModal(true); } catch (error) { console.error("Kesalahan memperbarui kendaraan:", error); setModalConfig({ title: "Kesalahan", message: "Gagal memperbarui kendaraan. Silakan coba lagi.", onConfirm: () => setShowModal(false) }); setShowModal(true); } }; const handleDeleteVehicle = (vehicleId) => { setModalConfig({ title: "Konfirmasi Penghapusan", message: "Apakah Anda yakin ingin menghapus kendaraan ini dan semua catatan perawatannya? Tindakan ini tidak dapat dibatalkan.", type: 'confirm', onConfirm: async () => { try { const userId = currentUser.uid; const vehicleDocRef = doc(db, `artifacts/${appId}/users/${userId}/vehicles`, vehicleId); const recordsColRef = collection(db, `artifacts/${appId}/users/${userId}/vehicles/${vehicleId}/maintenanceRecords`); const recordsSnapshot = await getDocs(recordsColRef); const deleteRecordPromises = recordsSnapshot.docs.map(d => deleteDoc(d.ref)); await Promise.all(deleteRecordPromises); const fuelLogsQuery = query(collection(db, `artifacts/${appId}/users/${userId}/fuelLogs`), where("vehicleId", "==", vehicleId)); const fuelLogsSnapshot = await getDocs(fuelLogsQuery); const deleteFuelLogPromises = fuelLogsSnapshot.docs.map(d => deleteDoc(d.ref)); await Promise.all(deleteFuelLogPromises); // New: Delete associated other expenses for this vehicle const otherExpensesQuery = query(collection(db, `artifacts/${appId}/users/${userId}/otherExpenses`), where("vehicleId", "==", vehicleId)); const otherExpensesSnapshot = await getDocs(otherExpensesQuery); const deleteOtherExpensePromises = otherExpensesSnapshot.docs.map(d => deleteDoc(d.ref)); await Promise.all(deleteOtherExpensePromises); await deleteDoc(vehicleDocRef); setCurrentView('garasi'); setSelectedVehicle(null); setMaintenanceRecordsForSelected([]); setMaintenanceRefreshTrigger(prev => prev + 1); setOtherExpensesRefreshTrigger(prev => prev + 1); setShowModal(false); setModalConfig({ title: "Berhasil", message: "Kendaraan dan semua catatannya berhasil dihapus!", onConfirm: () => setShowModal(false) }); setShowModal(true); } catch (error) { console.error("Kesalahan menghapus kendaraan:", error); setShowModal(false); setModalConfig({ title: "Kesalahan", message: "Gagal menghapus kendaraan. Silakan coba lagi.", onConfirm: () => setShowModal(false) }); setShowModal(true); } }, onCancel: () => setShowModal(false) }); setShowModal(true); }; const handleAddMaintenanceRecord = async (recordData) => { try { const userId = currentUser.uid; const recordsColRef = collection(db, `artifacts/${appId}/users/${userId}/vehicles/${selectedVehicle.id}/maintenanceRecords`); await addDoc(recordsColRef, recordData); setMaintenanceRefreshTrigger(prev => prev + 1); setCurrentView('vehicleDetails'); setModalConfig({ title: "Berhasil", message: "Catatan perawatan berhasil ditambahkan!", onConfirm: () => setShowModal(false) }); setShowModal(true); } catch (error) { console.error("Kesalahan menambahkan catatan perawatan:", error); setModalConfig({ title: "Kesalahan", message: "Gagal menambahkan catatan. Silakan coba lagi.", onConfirm: () => setShowModal(false) }); setShowModal(true); } }; const handleEditMaintenanceRecord = async (recordData, recordId) => { try { const userId = currentUser.uid; const recordDocRef = doc(db, `artifacts/${appId}/users/${userId}/vehicles/${selectedVehicle.id}/maintenanceRecords`, recordId); await updateDoc(recordDocRef, recordData); setMaintenanceRefreshTrigger(prev => prev + 1); setCurrentView('vehicleDetails'); setModalConfig({ title: "Berhasil", message: "Catatan perawatan berhasil diperbarui!", onConfirm: () => setShowModal(false) }); setShowModal(true); } catch (error) { console.error("Kesalahan memperbarui catatan perawatan:", error); setModalConfig({ title: "Kesalahan", message: "Gagal memperbarui catatan. Silakan coba lagi.", onConfirm: () => setShowModal(false) }); setShowModal(true); } }; const handleDeleteMaintenanceRecord = (recordId) => { setModalConfig({ title: "Konfirmasi Penghapusan", message: "Apakah Anda yakin ingin menghapus catatan perawatan ini?", type: 'confirm', onConfirm: async () => { try { const userId = currentUser.uid; const recordDocRef = doc(db, `artifacts/${appId}/users/${userId}/vehicles/${selectedVehicle.id}/maintenanceRecords`, recordId); await deleteDoc(recordDocRef); setMaintenanceRefreshTrigger(prev => prev + 1); setShowModal(false); setModalConfig({ title: "Berhasil", message: "Catatan perawatan berhasil dihapus!", onConfirm: () => setShowModal(false) }); setShowModal(true); } catch (error) { console.error("Kesalahan menghapus catatan perawatan:", error); setShowModal(false); setModalConfig({ title: "Kesalahan", message: "Gagal menghapus catatan. Silakan coba lagi.", onConfirm: () => setShowModal(false) }); setShowModal(true); } }, onCancel: () => setShowModal(false) }); setShowModal(true); }; const handleAddFuelLog = async (logData) => { try { const userId = currentUser.uid; const fuelLogsColRef = collection(db, `artifacts/${appId}/users/${userId}/fuelLogs`); await addDoc(fuelLogsColRef, logData); setModalConfig({ title: "Berhasil", message: "Log bahan bakar berhasil ditambahkan!", onConfirm: () => setShowModal(false) }); setShowModal(true); } catch (error) { console.error("Kesalahan menambahkan log BBM:", error); setModalConfig({ title: "Kesalahan", message: "Gagal menambahkan log bahan bakar. Silakan coba lagi.", onConfirm: () => setShowModal(false) }); setShowModal(true); } }; const handleDeleteFuelLog = (logId) => { setModalConfig({ title: "Konfirmasi Penghapusan", message: "Apakah Anda yakin ingin menghapus log bahan bakar ini?", type: 'confirm', onConfirm: async () => { try { const userId = currentUser.uid; const logDocRef = doc(db, `artifacts/${appId}/users/${userId}/fuelLogs`, logId); await deleteDoc(logDocRef); setShowModal(false); setModalConfig({ title: "Berhasil", message: "Log bahan bakar berhasil dihapus!", onConfirm: () => setShowModal(false) }); setShowModal(true); } catch (error) { console.error("Kesalahan menghapus log BBM:", error); setShowModal(false); setModalConfig({ title: "Kesalahan", message: "Gagal menghapus log bahan bakar. Silakan coba lagi.", onConfirm: () => setShowModal(false) }); setShowModal(true); } }, onCancel: () => setShowModal(false) }); setShowModal(true); }; // New: Handlers for other expenses const handleAddOtherExpense = async (expenseData) => { try { const userId = currentUser.uid; const otherExpensesColRef = collection(db, `artifacts/${appId}/users/${userId}/otherExpenses`); await addDoc(otherExpensesColRef, expenseData); setOtherExpensesRefreshTrigger(prev => prev + 1); setModalConfig({ title: "Berhasil", message: "Pengeluaran lainnya berhasil ditambahkan!", onConfirm: () => setShowModal(false) }); setShowModal(true); } catch (error) { console.error("Kesalahan menambahkan pengeluaran lainnya:", error); setModalConfig({ title: "Kesalahan", message: "Gagal menambahkan pengeluaran lainnya. Silakan coba lagi.", onConfirm: () => setShowModal(false) }); setShowModal(true); } }; const handleDeleteOtherExpense = (expenseId) => { setModalConfig({ title: "Konfirmasi Penghapusan", message: "Apakah Anda yakin ingin menghapus pengeluaran ini?", type: 'confirm', onConfirm: async () => { try { const userId = currentUser.uid; const expenseDocRef = doc(db, `artifacts/${appId}/users/${userId}/otherExpenses`, expenseId); await deleteDoc(expenseDocRef); setOtherExpensesRefreshTrigger(prev => prev + 1); setShowModal(false); setModalConfig({ title: "Berhasil", message: "Pengeluaran berhasil dihapus!", onConfirm: () => setShowModal(false) }); setShowModal(true); } catch (error) { console.error("Kesalahan menghapus pengeluaran lainnya:", error); setShowModal(false); setModalConfig({ title: "Kesalahan", message: "Gagal menghapus pengeluaran. Silakan coba lagi.", onConfirm: () => setShowModal(false) }); setShowModal(true); } }, onCancel: () => setShowModal(false) }); setShowModal(true); }; const renderContent = () => { if (!currentUser || currentUser.isAnonymous) { switch (currentView) { case 'login': return ; case 'signup': return ; default: return (

Selamat Datang di Aplikasi Drivvo!

Silakan masuk atau daftar untuk mengelola kendaraan Anda.

); } } switch (currentView) { case 'dashboard': return (

Dasbor Anda

Selamat datang di Dasbor Anda!

Gunakan menu navigasi di atas untuk mengelola kendaraan Anda.

Jumlah Kendaraan: {vehicles.length}

Total Catatan Servis: {allMaintenanceRecords.length}

Total Log Bahan Bakar: {fuelLogs.length}

Total Pengeluaran Lainnya: {otherExpenses.length}

); case 'garasi': // Helper function to calculate vehicle age const calculateVehicleAge = (yearMade) => { if (!yearMade) return 'N/A'; const currentYear = new Date().getFullYear(); const ageInYears = currentYear - yearMade; if (ageInYears <= 0) return 'Baru'; return `${ageInYears} tahun`; }; return (

Armada Anda

{vehicles.length === 0 ? (

Anda belum menambahkan kendaraan apapun.

Klik "Tambah Kendaraan Baru" untuk memulai!

) : (
{vehicles.map(vehicle => (

{vehicle.merk} {vehicle.type}

Nomor Polisi: {vehicle.nomorPolisi || 'N/A'}

Usia Kendaraan: {calculateVehicleAge(vehicle.tahunPembuatan)}

{/* New: Button for Other Expenses */}
))}
)}
); case 'servis-overview': return ; case 'log-bbm': return ( ); case 'other-expenses': // Render the new Other Expenses page return ( ); case 'laporan': return ; case 'saran-ai': // New: AI Advice page return ( ); case 'addVehicle': return (
setCurrentView('garasi')} />
); case 'editVehicle': return (
setCurrentView('vehicleDetails')} />
); case 'vehicleDetails': if (!selectedVehicle) { setCurrentView('garasi'); return null; } const vehicleDetailDisplay = `

Jenis: ${selectedVehicle.jenis || 'N/A'}

Tahun Pembuatan: ${selectedVehicle.tahunPembuatan || 'N/A'}

Isi Silinder/CC: ${selectedVehicle.isiSilinder ? `${selectedVehicle.isiSilinder} cc` : 'N/A'}

Warna: ${selectedVehicle.warna || 'N/A'}

Nomor Rangka (VIN): ${selectedVehicle.nomorRangka || 'N/A'}

Nomor Mesin: ${selectedVehicle.nomorMesin || 'N/A'}

Bahan Bakar: ${selectedVehicle.bahanBakar || 'N/A'}

Jumlah Sumbu: ${selectedVehicle.jumlahSumbu || 'N/A'}

Jumlah Roda: ${selectedVehicle.jumlahRoda || 'N/A'}

Berat Kosong: ${selectedVehicle.beratKosong ? `${selectedVehicle.beratKosong} kg` : 'N/A'}

Berat Maksimum: ${selectedVehicle.beratMaksimum ? `${selectedVehicle.beratMaksimum} kg` : 'N/A'}

Nomor BPKB: ${selectedVehicle.nomorBPKB || 'N/A'}

Nomor Polisi: ${selectedVehicle.nomorPolisi || 'N/A'}

`; return (

{selectedVehicle.merk} {selectedVehicle.type}

Catatan Perawatan

{maintenanceRecordsForSelected.length === 0 ? (

Belum ada catatan perawatan untuk kendaraan ini.

Klik "Tambah Catatan Baru" untuk mencatat layanan pertama Anda!

) : (
{maintenanceRecordsForSelected.map(record => (

{record.type}

Tanggal: {record.date}

Jarak Tempuh: {record.mileage} km

Biaya: IDR {Math.round(record.cost)}

{record.notes &&

Catatan: {record.notes}

}
))}
)}
); case 'addMaintenance': return (
setCurrentView('vehicleDetails')} />
); case 'editMaintenance': const selectedRecord = allMaintenanceRecords.find(rec => rec.id === modalConfig.recordIdToEdit); return (
{ setCurrentView('vehicleDetails'); setModalConfig({}); }} />
); default: return (

Ada yang salah. Tampilan tidak diketahui.

); } }; const setSelectedRecord = (record) => { setModalConfig({ recordIdToEdit: record.id, initialRecordData: record }); }; return (
{/* Main application container for authenticated users */} {currentUser && !currentUser.isAnonymous ? (
{/* Header Section */}

Aplikasi Drivvo

ID Pengguna: {currentUser?.uid || 'N/A'}
{/* Navigation Menu */}
{/* Content Area */}
{renderContent()}
{/* Footer Section */}
© {new Date().getFullYear()} Klon Aplikasi Drivvo. Semua hak dilindungi.
) : ( // Render login/signup views if not authenticated renderContent() )}
); } export function DrivvoAppWithAuth() { return ( ); }