import React, { useState, useEffect, useMemo, useRef } from 'react';
import { initializeApp } from 'firebase/app';
import { getFirestore, doc, setDoc, getDoc, collection, onSnapshot, updateDoc, arrayUnion } from 'firebase/firestore';
import { getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken } from 'firebase/auth';
import { Calendar as CalendarIcon, Users, Building, Plane, Car, Bus, PlusCircle, Trash2, Bot, CalendarDays, Clock, Wand2, PartyPopper, UploadCloud, FileText, Table, GanttChartSquare, History, CheckCircle } from 'lucide-react';
// --- SHADCN UI DUMMY COMPONENTS (for self-contained example) ---
const Card = ({ children, className }) =>
{children}
;
const CardHeader = ({ children, className }) => {children}
;
const CardTitle = ({ children, className }) => {children}
;
const CardDescription = ({ children, className }) => {children}
;
const CardContent = ({ children, className }) => {children}
;
const Button = ({ children, onClick, className, variant, size, disabled }) => {
const baseStyle = "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none";
const variantStyles = {
default: "bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600",
destructive: "bg-red-500 text-white hover:bg-red-600 dark:bg-red-600 dark:hover:bg-red-700",
outline: "border border-gray-300 dark:border-gray-700 bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800",
};
const sizeStyles = {
default: "h-10 py-2 px-4",
sm: "h-9 px-3 rounded-md",
lg: "h-11 px-8 rounded-md",
};
return ;
};
const Input = (props) => ;
const Label = (props) => ;
const Textarea = (props) => ;
const Badge = ({ children, className, variant }) => {
const variantStyles = {
default: "bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200",
success: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
warning: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200",
info: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200",
travel: "bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200",
};
return {children};
}
// --- Gantt Chart Component ---
const Gantt = ({ tasks }) => {
const ganttContainer = useRef(null);
useEffect(() => {
if (!ganttContainer.current || typeof window.gantt === 'undefined') return;
window.gantt.config.date_format = "%Y-%m-%d";
window.gantt.config.readonly = true;
window.gantt.config.scales = [
{unit: "month", step: 1, format: "%F, %Y"},
{unit: "day", step: 1, format: "%d, %D"}
];
window.gantt.config.columns = [
{name: "text", label: "Trainer / Task", tree: true, width: 200},
{name: "start_date", label: "Start", align: "center", width: 100},
{name: "duration", label: "Duration", align: "center", width: 80}
];
window.gantt.templates.task_class = (start, end, task) => {
if (task.type === 'project') return 'gantt_project_task';
if (task.color) return `gantt_task_${task.color}`;
return '';
};
window.gantt.init(ganttContainer.current);
window.gantt.clearAll();
window.gantt.parse(tasks);
}, [tasks]);
return (
<>
>
);
};
// --- Firebase Configuration ---
const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : {};
const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-trainer-scheduler';
// --- Main App Component ---
export default function App() {
// --- State Management ---
const [db, setDb] = useState(null);
const [auth, setAuth] = useState(null);
const [userId, setUserId] = useState(null);
// Data states
const [merchantName, setMerchantName] = useState("Mega Retail Corp");
const [outlets, setOutlets] = useState("Manila\nCebu\nDavao\nBaguio\nIloilo City\nCagayan de Oro\nBacolod\nGeneral Santos");
const [trainers, setTrainers] = useState([{ id: 1, name: 'Alex', location: 'Manila' }, { id: 2, name: 'Bea', location: 'Manila' }]);
const [blackoutDates, setBlackoutDates] = useState("2025-12-24\n2025-12-31");
const [projectStartDate, setProjectStartDate] = useState("2025-10-01");
const [allowWeekends, setAllowWeekends] = useState(false);
const [uploadedSchedule, setUploadedSchedule] = useState(null);
const [scheduleFileName, setScheduleFileName] = useState('');
const [locationFileName, setLocationFileName] = useState('');
// App logic states
const [holidays, setHolidays] = useState([]);
const [generatedSchedule, setGeneratedSchedule] = useState(null); // Will hold { timestamp, schedule: [] }
const [scheduleHistory, setScheduleHistory] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [isAuthReady, setIsAuthReady] = useState(false);
const [activeView, setActiveView] = useState('table'); // 'table' or 'gantt'
// --- Dynamically Load External Libraries ---
useEffect(() => {
const libs = [
{ id: 'sheetjs-script', src: "https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js" },
{ id: 'gantt-css', rel: 'stylesheet', href: 'https://cdn.dhtmlx.com/gantt/edge/dhtmlxgantt.css' },
{ id: 'gantt-script', src: 'https://cdn.dhtmlx.com/gantt/edge/dhtmlxgantt.js' }
];
libs.forEach(lib => {
if (document.getElementById(lib.id)) return;
let element;
if (lib.rel === 'stylesheet') {
element = document.createElement('link');
element.rel = lib.rel;
element.href = lib.href;
} else {
element = document.createElement('script');
element.src = lib.src;
element.async = true;
}
element.id = lib.id;
document.head.appendChild(element);
});
}, []);
// --- Firebase Initialization and Auth ---
useEffect(() => {
try {
if (Object.keys(firebaseConfig).length > 0) {
const app = initializeApp(firebaseConfig);
const firestore = getFirestore(app);
const authInstance = getAuth(app);
setDb(firestore);
setAuth(authInstance);
onAuthStateChanged(authInstance, async (user) => {
if (user) {
setUserId(user.uid);
} else {
try {
if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) {
await signInWithCustomToken(authInstance, __initial_auth_token);
} else {
await signInAnonymously(authInstance);
}
} catch (authError) {
console.error("Authentication failed:", authError);
setError("Could not authenticate with the server. Data will not be saved.");
}
}
setIsAuthReady(true);
});
} else {
console.warn("Firebase config is missing.");
setIsAuthReady(true);
}
} catch(e) {
console.error("Firebase initialization error:", e);
setError("Could not initialize the database connection.");
setIsAuthReady(true);
}
}, []);
// --- Data Loading from Firestore ---
useEffect(() => {
if (isAuthReady && db && userId) {
const docRef = doc(db, 'artifacts', appId, 'users', userId);
const unsubscribe = onSnapshot(docRef, (docSnap) => {
if (docSnap.exists()) {
const data = docSnap.data();
if(data.merchantName) setMerchantName(data.merchantName);
if(data.outlets) setOutlets(data.outlets);
if(data.trainers) setTrainers(data.trainers);
if(data.blackoutDates) setBlackoutDates(data.blackoutDates);
if(data.projectStartDate) setProjectStartDate(data.projectStartDate);
if(data.allowWeekends) setAllowWeekends(data.allowWeekends);
if(data.activeSchedule) setGeneratedSchedule(data.activeSchedule);
if(data.scheduleHistory) {
const sortedHistory = data.scheduleHistory.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
setScheduleHistory(sortedHistory);
}
}
}, (err) => {
console.error("Firestore snapshot error:", err);
});
return () => unsubscribe();
}
}, [isAuthReady, db, userId]);
// --- Holiday Fetching for 2025 and 2026 ---
useEffect(() => {
const fetchHolidays = async () => {
const startYear = projectStartDate ? new Date(projectStartDate).getFullYear() : new Date().getFullYear();
const endYear = 2026;
const yearsToFetch = [];
for (let year = startYear; year <= endYear; year++) {
yearsToFetch.push(year);
}
try {
const holidayPromises = yearsToFetch.map(year =>
fetch(`https://date.nager.at/api/v3/PublicHolidays/${year}/PH`).then(res => {
if (!res.ok) throw new Error(`Failed to fetch holidays for ${year}`);
return res.json();
})
);
const allHolidaysNested = await Promise.all(holidayPromises);
const allHolidaysFlat = allHolidaysNested.flat();
const uniqueHolidays = Array.from(new Set(allHolidaysFlat.map(h => h.date)))
.map(date => {
const holiday = allHolidaysFlat.find(h => h.date === date);
return { date: holiday.date, name: holiday.name };
});
setHolidays(uniqueHolidays);
} catch (error) {
console.error("Failed to fetch holidays:", error);
setError("Could not fetch Philippine holidays. Scheduling will proceed with a fallback list.");
setHolidays([
{ date: "2025-01-01", name: "New Year's Day" },
{ date: "2025-12-25", name: "Christmas Day" },
{ date: "2026-01-01", name: "New Year's Day" },
{ date: "2026-12-25", name: "Christmas Day" },
]);
}
};
fetchHolidays();
}, [projectStartDate]);
// --- Trainer Management ---
const handleAddTrainer = () => setTrainers([...trainers, { id: Date.now(), name: '', location: 'Manila' }]);
const handleTrainerChange = (id, field, value) => setTrainers(trainers.map(t => t.id === id ? { ...t, [field]: value } : t));
const handleRemoveTrainer = (id) => setTrainers(trainers.filter(t => t.id !== id));
// --- File Upload Handlers ---
const handleScheduleFileChange = (event) => {
const file = event.target.files[0];
if (!file) return;
setScheduleFileName(file.name);
const reader = new FileReader();
const fileExtension = file.name.split('.').pop().toLowerCase();
if (fileExtension === 'csv') {
reader.onload = (e) => {
const text = e.target.result;
const lines = text.split('\n').filter(line => line.trim() !== '');
if (lines.length > 1) {
const headers = lines[0].split(',').map(h => h.trim());
const data = lines.slice(1).map(line => {
const values = line.split(',').map(v => v.trim());
return headers.reduce((obj, header, index) => {
obj[header] = values[index];
return obj;
}, {});
});
setUploadedSchedule(data);
setError(null);
} else { setError("CSV file is empty or invalid."); }
};
reader.readAsText(file);
} else if (fileExtension === 'xlsx' || fileExtension === 'xls') {
if (typeof XLSX === 'undefined') { setError("Excel parsing library is still loading. Please try again in a moment."); return; }
reader.onload = (e) => {
const data = e.target.result;
const workbook = XLSX.read(data, { type: 'array' });
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
const json = XLSX.utils.sheet_to_json(worksheet);
setUploadedSchedule(json);
setError(null);
};
reader.readAsArrayBuffer(file);
} else {
setError("Unsupported file type for schedules. Please upload CSV or Excel.");
setUploadedSchedule(null);
setScheduleFileName('');
}
};
const handleLocationFileChange = (event) => {
const file = event.target.files[0];
if (!file) return;
setLocationFileName(file.name);
const reader = new FileReader();
const fileExtension = file.name.split('.').pop().toLowerCase();
const processLocations = (locationsArray) => {
if (locationsArray && locationsArray.length > 0) {
setOutlets(locationsArray.join('\n'));
setError(null);
} else {
setError("Could not find any locations in the uploaded file.");
}
};
if (fileExtension === 'csv') {
reader.onload = (e) => {
const text = e.target.result;
const locations = text.split('\n').map(line => line.trim()).filter(Boolean);
processLocations(locations);
};
reader.readAsText(file);
} else if (fileExtension === 'xlsx' || fileExtension === 'xls') {
if (typeof XLSX === 'undefined') { setError("Excel parsing library is still loading. Please try again in a moment."); return; }
reader.onload = (e) => {
const data = e.target.result;
const workbook = XLSX.read(data, { type: 'array' });
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
const json = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
const locations = json.map(row => row[0]).filter(Boolean);
processLocations(locations);
};
reader.readAsArrayBuffer(file);
} else {
setError("Unsupported file type for locations. Please upload CSV or Excel.");
setLocationFileName('');
}
};
// --- Schedule Generation & History ---
const generateSchedule = async () => {
setIsLoading(true);
setError(null);
const scheduleToBuildOn = uploadedSchedule || (generatedSchedule ? generatedSchedule.schedule : null);
const existingSchedulePrompt = scheduleToBuildOn
? `7. **Existing Schedule (must be respected):**\n${JSON.stringify(scheduleToBuildOn, null, 2)}`
: '';
const prompt = `
You are an expert logistics and training scheduler for a large-scale deployment in the Philippines.
Your task is to create an efficient training schedule.
**Output Format:**
Your response MUST be a valid JSON object with a single key "schedule" which is an array of daily events.
Each event must have: "date", "dayOfWeek", "trainer", "activity", "details", "location", "isWorkDay".
**Input Data & Constraints:**
1. **Project Start Date:** ${projectStartDate}
2. **Merchant:** ${merchantName}
3. **Outlets to be Trained:**\n${outlets}
4. **Available Trainers:**\n${JSON.stringify(trainers, null, 2)}
5. **Philippine Public Holidays (Blocked):**\n${holidays.map(h => h.date).join('\n')}
6. **Merchant Blackout Dates (Blocked):**\n${blackoutDates}
${existingSchedulePrompt}
**Scheduling Rules:**
- **Priority:** The "Existing Schedule" is the highest priority. Plan new trainings for outlets NOT mentioned in the existing schedule.
- **Work Week:** Monday to Friday, 9 AM - 6 PM. Weekends only if "Allow Weekends: ${allowWeekends}" is true.
- **Capacity:** Max 2 face-to-face trainings/day in the same city. Only 1 if inter-city travel is needed. Max 3 online trainings/day.
- **Travel:** A full day of "Travel" is needed to move between major islands (e.g., Manila to Cebu).
- **Efficiency:** Group trainings by region to minimize travel.
- **Completeness:** Ensure every outlet in the list is scheduled exactly once, considering both the existing and newly generated schedule.
- **Holidays/Blackouts:** These are non-working "Day Off" days.
Generate a complete and logical schedule starting from the project start date.
`;
try {
const chatHistory = [{ role: "user", parts: [{ text: prompt }] }];
const payload = { contents: chatHistory };
const apiKey = "";
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`;
const response = await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`API Error: ${response.status} - ${errorBody}`);
}
const result = await response.json();
if (result.candidates?.[0]?.content?.parts?.[0]?.text) {
let rawText = result.candidates[0].content.parts[0].text;
rawText = rawText.replace(/^```json\s*/, '').replace(/```$/, '').trim();
const scheduleData = JSON.parse(rawText);
const historyEntry = {
timestamp: new Date().toISOString(),
schedule: scheduleData.schedule
};
setGeneratedSchedule(historyEntry);
if (db && userId) {
const docRef = doc(db, 'artifacts', appId, 'users', userId);
await updateDoc(docRef, {
activeSchedule: historyEntry,
scheduleHistory: arrayUnion(historyEntry)
}).catch(async (err) => {
if (err.code === 'not-found') {
await setDoc(docRef, {
activeSchedule: historyEntry,
scheduleHistory: [historyEntry]
});
}
});
}
} else {
throw new Error("The AI returned an empty or malformed response.");
}
} catch (e) {
console.error("Error generating or parsing schedule:", e);
setError(`Failed to generate schedule. ${e.message}`);
} finally {
setIsLoading(false);
}
};
const loadScheduleFromHistory = async (historyEntry) => {
setGeneratedSchedule(historyEntry);
if (db && userId) {
const docRef = doc(db, 'artifacts', appId, 'users', userId);
await updateDoc(docRef, { activeSchedule: historyEntry });
}
};
// --- Transform schedule for Gantt Chart ---
const ganttTasks = useMemo(() => {
if (!generatedSchedule?.schedule) return { data: [] };
const tasks = [];
const trainersMap = {};
let taskId = 1;
trainers.forEach((trainer) => {
const trainerId = `trainer_${trainer.id}`;
trainersMap[trainer.name] = trainerId;
tasks.push({ id: trainerId, text: trainer.name, type: 'project', open: true });
taskId++;
});
generatedSchedule.schedule.forEach(event => {
const parentId = trainersMap[event.trainer];
if (!parentId) return;
let color = 'day_off';
if (event.activity.toLowerCase().includes('travel')) color = 'travel';
if (event.activity.toLowerCase().includes('training')) color = 'training';
tasks.push({
id: taskId++,
text: event.details,
start_date: event.date,
duration: 1,
parent: parentId,
color: color
});
});
return { data: tasks };
}, [generatedSchedule, trainers]);
return (
{/* --- LEFT COLUMN: INPUTS --- */}
{/* --- RIGHT COLUMN: OUTPUT --- */}
Generated Training Schedule
{isLoading && AI is planning the logistics. Please wait...
}
{!isLoading && !generatedSchedule && (
Schedule will appear here
Click "Generate Schedule with AI" to begin.
)}
{generatedSchedule?.schedule?.length > 0 && (
<>
{activeView === 'table' && (
Date |
Day |
Trainer |
Activity |
Details |
Location |
{generatedSchedule.schedule.map((event, index) => {
const isHoliday = holidays.some(h => h.date === event.date);
return (
{event.date} |
{event.dayOfWeek} |
{event.trainer} |
{isHoliday ? `Holiday: ${holidays.find(h=>h.date===event.date).name}` : event.activity}
|
{event.details} |
{event.location} |
);
})}
)}
{activeView === 'gantt' && }
>
)}
);
}