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
Belum punya akun? onLoginSuccess('signup')} className="text-blue-600 hover:text-blue-800 font-semibold focus:outline-none text-sm sm:text-base">Daftar
);
}
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
Sudah punya akun? onSignUpSuccess('login')} className="text-blue-600 hover:text-blue-800 font-semibold focus:outline-none text-sm sm:text-base">Masuk
);
}
function Modal({ show, title, message, onConfirm, onCancel, type = 'info' }) {
if (!show) return null;
return (
{title}
{message}
{type === 'confirm' && (
Batal
)}
{type === 'confirm' ? 'Konfirmasi' : 'OK'}
);
}
// 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'}
);
}
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'}
);
}
// 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
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}
}
onDeleteFuelLog(log.id)}
className="px-3 py-1.5 bg-red-500 text-white rounded-lg hover:bg-red-600 transition duration-300 text-xs font-semibold mt-2 sm:mt-0"
>
Hapus
))}
)}
);
}
// 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
Pilih Kendaraan
setSelectedVehicleId(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">
Semua Kendaraan
{vehicles.map(v_item => (
{getVehicleName(v_item)}
))}
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
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}
}
onDeleteOtherExpense(expense.id)}
className="px-3 py-1.5 bg-red-500 text-white rounded-lg hover:bg-red-600 transition duration-300 text-xs font-semibold mt-2 sm:mt-0"
>
Hapus
))}
)}
);
}
// 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)
Pilih Kendaraan untuk Analisis AI
setSelectedAIAnalysisVehicleId(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">
-- Pilih Kendaraan --
{vehicles.map(v => (
{getVehicleNameFull(v)}
))}
{selectedAIAnalysisVehicleId ? (
Pertanyaan Anda
setPrompt(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"
rows="4"
placeholder={`Tanyakan tentang ${getVehicleName(vehicles.find(v_item => v_item.id === selectedAIAnalysisVehicleId))} atau hal lain tentang perawatan kendaraan...`}
>
{isLoadingAI ? 'Memuat Saran...' : 'Dapatkan Saran AI'}
) : (
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.
setCurrentView('login')} className="bg-white text-blue-600 font-bold py-2.5 px-5 sm:py-3 sm:px-6 rounded-lg shadow-lg hover:bg-gray-100 transition duration-300 transform hover:scale-105 text-base sm:text-lg">
Masuk
setCurrentView('signup')} className="bg-blue-700 text-white font-bold py-2.5 px-5 sm:py-3 sm:px-6 rounded-lg shadow-lg hover:bg-blue-800 transition duration-300 transform hover:scale-105 text-base sm:text-lg">
Daftar
);
}
}
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
setCurrentView('addVehicle')}
className="px-5 py-2.5 sm:px-6 sm:py-3 bg-green-500 text-white rounded-lg shadow-md hover:bg-green-600 transition duration-300 font-semibold transform hover:scale-105 text-sm sm:text-base"
>
+ Tambah Kendaraan Baru
{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)}
{ setSelectedVehicle(vehicle); setCurrentView('addMaintenance'); }}
className="px-2 py-1 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition duration-300 text-xs font-semibold flex-grow sm:flex-grow-0"
>
Catat Servis
{ setSelectedVehicle(vehicle); setCurrentView('log-bbm'); }}
className="px-2 py-1 bg-purple-500 text-white rounded-lg hover:bg-purple-600 transition duration-300 text-xs font-semibold flex-grow sm:flex-grow-0"
>
Catat BBM
{/* New: Button for Other Expenses */}
{ setSelectedVehicle(vehicle); setCurrentView('other-expenses'); }}
className="px-2 py-1 bg-orange-500 text-white rounded-lg hover:bg-orange-600 transition duration-300 text-xs font-semibold flex-grow sm:flex-grow-0"
>
Pengeluaran
{ setSelectedVehicle(vehicle); setCurrentView('editVehicle'); }}
className="px-2 py-1 bg-yellow-500 text-white rounded-lg hover:bg-yellow-600 transition duration-300 text-xs font-semibold flex-grow sm:flex-grow-0"
>
Edit Data
handleDeleteVehicle(vehicle.id)}
className="px-2 py-1 bg-red-500 text-white rounded-lg hover:bg-red-600 transition duration-300 text-xs font-semibold flex-grow sm:flex-grow-0"
>
Hapus
))}
)}
);
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')} className="px-4 py-2 mb-4 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 transition duration-200 font-semibold text-sm sm:text-base">
← Kembali ke Garasi
setCurrentView('garasi')} />
);
case 'editVehicle':
return (
setCurrentView('vehicleDetails')} className="px-4 py-2 mb-4 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 transition duration-200 font-semibold text-sm sm:text-base">
← Kembali ke Detail Kendaraan
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 (
{ setCurrentView('garasi'); setSelectedVehicle(null); }} className="px-4 py-2 mb-4 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 transition duration-200 font-semibold text-sm sm:text-base">
← Kembali ke Garasi
{selectedVehicle.merk} {selectedVehicle.type}
setCurrentView('editVehicle')}
className="px-3.5 py-1.5 sm:px-4 sm:py-2 bg-yellow-500 text-white rounded-lg hover:bg-yellow-600 transition duration-300 font-semibold text-xs sm:text-sm">
Edit Kendaraan
handleDeleteVehicle(selectedVehicle.id)}
className="px-3.5 py-1.5 sm:px-4 sm:py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition duration-300 font-semibold text-xs sm:text-sm"
>
Hapus Kendaraan
Catatan Perawatan
setCurrentView('addMaintenance')}
className="px-5 py-2.5 sm:px-6 sm:py-3 bg-blue-600 text-white rounded-lg shadow-md hover:bg-blue-700 transition duration-300 font-semibold transform hover:scale-105 text-sm sm:text-base"
>
+ Tambah Catatan Baru
{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}
}
{ setSelectedRecord(record); setCurrentView('editMaintenance'); }}
className="px-3.5 py-1.5 sm:px-4 sm:py-2 bg-yellow-500 text-white rounded-lg hover:bg-yellow-600 transition duration-300 font-semibold text-xs sm:text-sm"
>
Edit
handleDeleteMaintenanceRecord(record.id)}
className="px-3.5 py-1.5 sm:px-4 sm:py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition duration-300 font-semibold text-xs sm:text-sm"
>
Hapus
))}
)}
);
case 'addMaintenance':
return (
setCurrentView('vehicleDetails')} className="px-4 py-2 mb-4 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 transition duration-200 font-semibold text-sm sm:text-base">
← Kembali ke Detail Kendaraan
setCurrentView('vehicleDetails')} />
);
case 'editMaintenance':
const selectedRecord = allMaintenanceRecords.find(rec => rec.id === modalConfig.recordIdToEdit);
return (
setCurrentView('vehicleDetails')} className="px-4 py-2 mb-4 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 transition duration-200 font-semibold text-sm sm:text-base">
← Kembali ke Detail Kendaraan
{
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 */}
{/* 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 (
);
}