Files
scripts/HTML/SCHEDULE/cong2025.html
Dobromir Popov 91285b4a4e cong
2025-06-16 10:47:17 +03:00

554 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="bg">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Event Scheduler</title>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
// Поставете тук вашия JSX код (без import/export редовете)
const EventScheduleGenerator = () => {
const supervisors = ['Белер Гаел', 'Георги Русков', 'Добромир Попов'];
const participants = [
'Белер Гаел', 'Георги Русков', 'Добромир Попов', 'Джуанна Рускова',
'Краси Трендафилова', 'Анджелика Трендафилова', 'Роксана Мирзоева',
'Абигейл Мустафова', 'Цветелина Нихтянова', 'Клеманс Белер',
'Златка Рускова', 'Сара Ръслер', 'Таня Иванова', 'Естера Биела', 'Радина Ейер'
];
const timeSlots = [
'08:00-10:40', '10:40-12:15', '12:15-14:50', '14:50-16:30', '16:30-19:00'
];
const days = ['Петък', 'Събота', 'Неделя'];
// availabilities: 0 = not set, 1 = available (green), 2 = unavailable (red)
const [availabilities, setAvailabilities] = useState(() => {
const initial = {};
participants.forEach(participant => {
initial[participant] = {};
days.forEach(day => {
initial[participant][day] = {};
timeSlots.forEach(slot => {
initial[participant][day][slot] = 0;
});
});
});
return initial;
});
const [peoplePerShift, setPeoplePerShift] = useState(1);
const [scheduleOptions, setScheduleOptions] = useState([]);
const cycleAvailability = (participant, day, slot) => {
setAvailabilities(prev => ({
...prev,
[participant]: {
...prev[participant],
[day]: {
...prev[participant][day],
[slot]: (prev[participant][day][slot] + 1) % 3
}
}
}));
};
const setAllAvailable = () => {
const newAvailabilities = {};
participants.forEach(participant => {
newAvailabilities[participant] = {};
days.forEach(day => {
newAvailabilities[participant][day] = {};
timeSlots.forEach(slot => {
newAvailabilities[participant][day][slot] = 1;
});
});
});
setAvailabilities(newAvailabilities);
};
const setAllUnavailable = () => {
const newAvailabilities = {};
participants.forEach(participant => {
newAvailabilities[participant] = {};
days.forEach(day => {
newAvailabilities[participant][day] = {};
timeSlots.forEach(slot => {
newAvailabilities[participant][day][slot] = 2;
});
});
});
setAvailabilities(newAvailabilities);
};
const clearAll = () => {
const newAvailabilities = {};
participants.forEach(participant => {
newAvailabilities[participant] = {};
days.forEach(day => {
newAvailabilities[participant][day] = {};
timeSlots.forEach(slot => {
newAvailabilities[participant][day][slot] = 0;
});
});
});
setAvailabilities(newAvailabilities);
};
const setPersonAllAvailable = (participant) => {
setAvailabilities(prev => ({
...prev,
[participant]: {
...prev[participant],
...days.reduce((dayAcc, day) => ({
...dayAcc,
[day]: {
...prev[participant][day],
...timeSlots.reduce((slotAcc, slot) => ({
...slotAcc,
[slot]: 1
}), {})
}
}), {})
}
}));
};
const setPersonAllUnavailable = (participant) => {
setAvailabilities(prev => ({
...prev,
[participant]: {
...prev[participant],
...days.reduce((dayAcc, day) => ({
...dayAcc,
[day]: {
...prev[participant][day],
...timeSlots.reduce((slotAcc, slot) => ({
...slotAcc,
[slot]: 2
}), {})
}
}), {})
}
}));
};
const setDayAllAvailable = (day) => {
setAvailabilities(prev => {
const newAvailabilities = { ...prev };
participants.forEach(participant => {
timeSlots.forEach(slot => {
newAvailabilities[participant][day][slot] = 1;
});
});
return newAvailabilities;
});
};
const getCellColor = (availability) => {
switch(availability) {
case 1: return 'bg-green-500 hover:bg-green-600';
case 2: return 'bg-red-500 hover:bg-red-600';
default: return 'bg-gray-200 hover:bg-gray-300';
}
};
const getCellText = (availability) => {
switch(availability) {
case 1: return '✓';
case 2: return '✗';
default: return '';
}
};
const generateSchedules = () => {
const options = [];
for (let optionNum = 0; optionNum < 3; optionNum++) {
const schedule = {};
days.forEach(day => {
schedule[day] = {};
timeSlots.forEach(slot => {
schedule[day][slot] = [];
});
});
const strategies = [
'balanced',
'supervisor_priority',
'random'
];
const strategy = strategies[optionNum];
const assignmentCount = {};
participants.forEach(p => assignmentCount[p] = 0);
// Create all possible assignments
const allSlots = [];
days.forEach(day => {
timeSlots.forEach(slot => {
allSlots.push({ day, slot });
});
});
if (strategy === 'supervisor_priority') {
// First ensure each slot has at least one supervisor if possible
allSlots.forEach(({ day, slot }) => {
const availableSupervisors = supervisors.filter(sup =>
availabilities[sup]?.[day]?.[slot] === 1
);
if (availableSupervisors.length > 0 && schedule[day][slot].length < peoplePerShift) {
// Choose supervisor with least assignments
const chosenSupervisor = availableSupervisors.reduce((min, sup) =>
assignmentCount[sup] < assignmentCount[min] ? sup : min
);
schedule[day][slot].push(chosenSupervisor);
assignmentCount[chosenSupervisor]++;
}
});
}
// Fill remaining slots
if (strategy === 'balanced') {
// Sort slots by current assignment count, fill least filled first
allSlots.sort((a, b) => schedule[a.day][a.slot].length - schedule[b.day][b.slot].length);
allSlots.forEach(({ day, slot }) => {
while (schedule[day][slot].length < peoplePerShift) {
const availableParticipants = participants.filter(p =>
availabilities[p]?.[day]?.[slot] === 1 &&
!schedule[day][slot].includes(p)
);
if (availableParticipants.length === 0) break;
// Sort by assignment count (least assigned first)
availableParticipants.sort((a, b) => assignmentCount[a] - assignmentCount[b]);
const chosen = availableParticipants[0];
schedule[day][slot].push(chosen);
assignmentCount[chosen]++;
}
});
} else if (strategy === 'supervisor_priority') {
// Continue filling with other participants
allSlots.forEach(({ day, slot }) => {
while (schedule[day][slot].length < peoplePerShift) {
const availableParticipants = participants.filter(p =>
availabilities[p]?.[day]?.[slot] === 1 &&
!schedule[day][slot].includes(p)
);
if (availableParticipants.length === 0) break;
// Prioritize supervisors, then by assignment count
availableParticipants.sort((a, b) => {
const aIsSupervisor = supervisors.includes(a) ? 0 : 1;
const bIsSupervisor = supervisors.includes(b) ? 0 : 1;
if (aIsSupervisor !== bIsSupervisor) return aIsSupervisor - bIsSupervisor;
return assignmentCount[a] - assignmentCount[b];
});
const chosen = availableParticipants[0];
schedule[day][slot].push(chosen);
assignmentCount[chosen]++;
}
});
} else {
// Random strategy
allSlots.sort(() => Math.random() - 0.5);
allSlots.forEach(({ day, slot }) => {
while (schedule[day][slot].length < peoplePerShift) {
const availableParticipants = participants.filter(p =>
availabilities[p]?.[day]?.[slot] === 1 &&
!schedule[day][slot].includes(p)
);
if (availableParticipants.length === 0) break;
const chosen = availableParticipants[Math.floor(Math.random() * availableParticipants.length)];
schedule[day][slot].push(chosen);
assignmentCount[chosen]++;
}
});
}
options.push({
id: optionNum + 1,
strategy: strategy,
schedule: schedule,
stats: assignmentCount
});
}
setScheduleOptions(options);
};
const getStrategyName = (strategy) => {
switch(strategy) {
case 'balanced': return 'Балансиран';
case 'supervisor_priority': return 'Приоритет супервайзори';
case 'random': return 'Случаен';
default: return strategy;
}
};
const ScheduleDisplay = ({ option }) => (
<div className="border rounded-lg p-4 mb-4 bg-gray-50">
<h3 className="font-bold text-lg mb-2 text-center">
Вариант {option.id} ({getStrategyName(option.strategy)})
</h3>
<div className="overflow-x-auto mb-4">
<table className="w-full border-collapse border border-gray-300 text-sm">
<thead>
<tr className="bg-gray-200">
<th className="border border-gray-300 p-2">Време</th>
{days.map(day => (
<th key={day} className="border border-gray-300 p-2">{day}</th>
))}
</tr>
</thead>
<tbody>
{timeSlots.map(slot => (
<tr key={slot}>
<td className="border border-gray-300 p-2 font-medium bg-gray-100">{slot}</td>
{days.map(day => (
<td key={`${day}-${slot}`} className="border border-gray-300 p-2 min-w-32">
{option.schedule[day][slot].map(person => (
<div
key={person}
className={`mb-1 p-1 rounded text-xs ${
supervisors.includes(person)
? 'bg-blue-200 font-bold border-2 border-blue-400'
: 'bg-green-200 border border-green-400'
}`}
>
{person}
</div>
))}
{option.schedule[day][slot].length === 0 && (
<div className="text-gray-400 text-xs italic">Няма назначени</div>
)}
<div className="text-xs text-gray-500 mt-1">
{option.schedule[day][slot].length}/{peoplePerShift}
</div>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
<div className="text-xs text-gray-600">
<strong>Статистика за назначения:</strong>
<div className="grid grid-cols-3 gap-2 mt-2">
{Object.entries(option.stats).map(([person, count]) => (
<div key={person} className={`p-1 rounded ${supervisors.includes(person) ? 'bg-blue-100' : 'bg-gray-100'}`}>
{person}: {count}
</div>
))}
</div>
</div>
</div>
);
return (
<div className="max-w-7xl mx-auto p-6">
<h1 className="text-2xl font-bold mb-6 text-center">Генератор на График за Събитие</h1>
{/* People per shift control */}
<div className="mb-6 p-4 bg-purple-50 rounded-lg">
<h3 className="font-semibold mb-3">Настройки на графика:</h3>
<div className="flex items-center gap-4">
<label className="font-medium">Брой хора на смяна:</label>
<select
value={peoplePerShift}
onChange={(e) => setPeoplePerShift(parseInt(e.target.value))}
className="border border-gray-300 rounded px-3 py-2 bg-white"
>
<option value={1}>1 човек</option>
<option value={2}>2 души</option>
<option value={3}>3 души</option>
</select>
<span className="text-sm text-gray-600">
(По подразбиране: 1 човек на смяна)
</span>
</div>
</div>
<div className="mb-6">
<h2 className="text-xl font-semibold mb-4">Наличности на участниците</h2>
{/* Global Controls */}
<div className="mb-4 p-4 bg-blue-50 rounded-lg">
<h3 className="font-semibold mb-3">Бързи команди:</h3>
<div className="flex flex-wrap gap-2 mb-3">
<button
onClick={setAllAvailable}
className="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded font-medium"
>
Всички наличи
</button>
<button
onClick={setAllUnavailable}
className="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded font-medium"
>
Всички неналични
</button>
<button
onClick={clearAll}
className="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded font-medium"
>
Изчисти всички
</button>
</div>
<div className="mb-3">
<h4 className="font-medium mb-2">Маркирай цял ден като наличен:</h4>
<div className="flex gap-2">
{days.map(day => (
<button
key={day}
onClick={() => setDayAllAvailable(day)}
className="bg-blue-500 hover:bg-blue-600 text-white px-3 py-1 rounded text-sm"
>
{day}
</button>
))}
</div>
</div>
</div>
<div className="mb-4 p-3 bg-yellow-50 rounded">
<p className="text-sm text-gray-700 mb-2">
<strong>Инструкции:</strong> Кликнете върху клетките, за да зададете наличност:
</p>
<div className="flex gap-4 text-sm">
<span className="flex items-center gap-2">
<div className="w-4 h-4 bg-gray-200 border"></div>
Не е зададено
</span>
<span className="flex items-center gap-2">
<div className="w-4 h-4 bg-green-500 border"></div>
Наличен (зелено)
</span>
<span className="flex items-center gap-2">
<div className="w-4 h-4 bg-red-500 border"></div>
Неналичен (червено)
</span>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full border-collapse border border-gray-300 text-xs">
<thead>
<tr className="bg-gray-200">
<th className="border border-gray-300 p-2 min-w-32">Участник</th>
{days.map(day => (
<th key={day} className="border border-gray-300 p-1" colSpan={timeSlots.length}>
{day}
</th>
))}
<th className="border border-gray-300 p-2">Действия</th>
</tr>
<tr className="bg-gray-100">
<th className="border border-gray-300 p-2"></th>
{days.map(day =>
timeSlots.map(slot => (
<th key={`${day}-${slot}`} className="border border-gray-300 p-1 text-xs min-w-16">
{slot.split('-')[0]}
</th>
))
)}
<th className="border border-gray-300 p-2"></th>
</tr>
</thead>
<tbody>
{participants.map(participant => (
<tr key={participant}>
<td className={`border border-gray-300 p-2 ${supervisors.includes(participant) ? 'font-bold bg-blue-50' : ''}`}>
{participant}
{supervisors.includes(participant) && <div className="text-xs text-blue-600">(Супервайзор)</div>}
</td>
{days.map(day =>
timeSlots.map(slot => (
<td key={`${participant}-${day}-${slot}`} className="border border-gray-300 p-0">
<button
onClick={() => cycleAvailability(participant, day, slot)}
className={`w-full h-8 text-white font-bold ${getCellColor(availabilities[participant]?.[day]?.[slot])}`}
title={`${participant} - ${day} ${slot}`}
>
{getCellText(availabilities[participant]?.[day]?.[slot])}
</button>
</td>
))
)}
<td className="border border-gray-300 p-1">
<div className="flex flex-col gap-1">
<button
onClick={() => setPersonAllAvailable(participant)}
className="bg-green-400 hover:bg-green-500 text-white text-xs px-2 py-1 rounded"
title={`Маркирай ${participant} като наличен за всички смени`}
>
Всички
</button>
<button
onClick={() => setPersonAllUnavailable(participant)}
className="bg-red-400 hover:bg-red-500 text-white text-xs px-2 py-1 rounded"
title={`Маркирай ${participant} като неналичен за всички смени`}
>
Всички
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<div className="text-center mb-6">
<button
onClick={generateSchedules}
className="bg-blue-500 hover:bg-blue-600 text-white font-bold py-3 px-8 rounded-lg text-lg"
>
Генерирай 3 Варианта на График
</button>
</div>
{scheduleOptions.length > 0 && (
<div>
<h2 className="text-xl font-semibold mb-4">Предложени Варианти</h2>
<div className="mb-4 p-3 bg-yellow-50 rounded">
<p className="text-sm text-gray-700">
<strong>Легенда:</strong> Супервайзорите са маркирани в синьо с удебелен шрифт и рамка.
Останалите участници са в зелено. Показва се {peoplePerShift} {peoplePerShift === 1 ? 'човек' : 'души'} на смяна.
Числото под всяка клетка показва колко души са назначени от максимума.
</p>
</div>
{scheduleOptions.map(option => (
<ScheduleDisplay key={option.id} option={option} />
))}
</div>
)}
</div>
);
};
ReactDOM.render(<EventScheduleGenerator />, document.getElementById('root'));
</script>
</body>
</html>