187 lines
6.4 KiB
React
187 lines
6.4 KiB
React
|
|
import { useState, useEffect } from 'react';
|
||
|
|
import { Layers, Plus, Trash2, Save, RefreshCw } from 'lucide-react';
|
||
|
|
import { matchingAPI, divisionsAPI } from '../../services/api';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Schedule configuration section for division collision groups
|
||
|
|
* Allows organizing divisions into time slots for matching algorithm
|
||
|
|
*/
|
||
|
|
const ScheduleConfigSection = ({ slug, event, onRefresh }) => {
|
||
|
|
const [divisions, setDivisions] = useState([]);
|
||
|
|
const [scheduleSlots, setScheduleSlots] = useState([]);
|
||
|
|
const [savingSchedule, setSavingSchedule] = useState(false);
|
||
|
|
|
||
|
|
// Load divisions on mount
|
||
|
|
useEffect(() => {
|
||
|
|
loadDivisions();
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
// Initialize schedule slots when event changes
|
||
|
|
useEffect(() => {
|
||
|
|
if (event?.scheduleConfig?.slots) {
|
||
|
|
setScheduleSlots(event.scheduleConfig.slots);
|
||
|
|
} else {
|
||
|
|
setScheduleSlots([]);
|
||
|
|
}
|
||
|
|
}, [event]);
|
||
|
|
|
||
|
|
const loadDivisions = async () => {
|
||
|
|
try {
|
||
|
|
const data = await divisionsAPI.getAll();
|
||
|
|
setDivisions(data);
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Failed to load divisions:', err);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleAddSlot = () => {
|
||
|
|
const maxOrder = scheduleSlots.length > 0
|
||
|
|
? Math.max(...scheduleSlots.map(s => s.order))
|
||
|
|
: 0;
|
||
|
|
setScheduleSlots([...scheduleSlots, { order: maxOrder + 1, divisionIds: [] }]);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleRemoveSlot = (order) => {
|
||
|
|
setScheduleSlots(scheduleSlots.filter(s => s.order !== order));
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleToggleDivision = (slotOrder, divisionId) => {
|
||
|
|
setScheduleSlots(scheduleSlots.map(slot => {
|
||
|
|
if (slot.order !== slotOrder) {
|
||
|
|
// Remove division from other slots if it's being added to this one
|
||
|
|
return {
|
||
|
|
...slot,
|
||
|
|
divisionIds: slot.divisionIds.filter(id => id !== divisionId)
|
||
|
|
};
|
||
|
|
}
|
||
|
|
// Toggle division in this slot
|
||
|
|
const hasDiv = slot.divisionIds.includes(divisionId);
|
||
|
|
return {
|
||
|
|
...slot,
|
||
|
|
divisionIds: hasDiv
|
||
|
|
? slot.divisionIds.filter(id => id !== divisionId)
|
||
|
|
: [...slot.divisionIds, divisionId]
|
||
|
|
};
|
||
|
|
}));
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleSaveSchedule = async () => {
|
||
|
|
try {
|
||
|
|
setSavingSchedule(true);
|
||
|
|
const scheduleConfig = scheduleSlots.length > 0
|
||
|
|
? { slots: scheduleSlots.filter(s => s.divisionIds.length > 0) }
|
||
|
|
: null;
|
||
|
|
await matchingAPI.setScheduleConfig(slug, scheduleConfig);
|
||
|
|
onRefresh?.();
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Failed to save schedule:', err);
|
||
|
|
alert('Nie udalo sie zapisac harmonogramu');
|
||
|
|
} finally {
|
||
|
|
setSavingSchedule(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Get all assigned division IDs
|
||
|
|
const getAssignedDivisionIds = () => {
|
||
|
|
return new Set(scheduleSlots.flatMap(s => s.divisionIds));
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||
|
|
<h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
|
||
|
|
<Layers className="text-primary-600" />
|
||
|
|
Konfiguracja harmonogramu
|
||
|
|
</h2>
|
||
|
|
<p className="text-sm text-gray-600 mb-4">
|
||
|
|
Dywizje w tym samym slocie czasowym koliduja ze soba (nie mozna nagrywac podczas gdy sie tancczy).
|
||
|
|
Dywizje bez przypisanego slotu sa traktowane jako osobne.
|
||
|
|
</p>
|
||
|
|
|
||
|
|
{/* Slots */}
|
||
|
|
<div className="space-y-4 mb-4">
|
||
|
|
{scheduleSlots
|
||
|
|
.sort((a, b) => a.order - b.order)
|
||
|
|
.map((slot) => (
|
||
|
|
<div key={slot.order} className="border border-gray-200 rounded-lg p-4">
|
||
|
|
<div className="flex items-center justify-between mb-3">
|
||
|
|
<h3 className="font-medium text-gray-900">
|
||
|
|
Slot {slot.order}
|
||
|
|
</h3>
|
||
|
|
<button
|
||
|
|
onClick={() => handleRemoveSlot(slot.order)}
|
||
|
|
className="p-1 text-red-500 hover:bg-red-50 rounded"
|
||
|
|
title="Usun slot"
|
||
|
|
>
|
||
|
|
<Trash2 size={16} />
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
<div className="flex flex-wrap gap-2">
|
||
|
|
{divisions.map((division) => {
|
||
|
|
const isInSlot = slot.divisionIds.includes(division.id);
|
||
|
|
const assignedDivIds = getAssignedDivisionIds();
|
||
|
|
const isInOtherSlot = assignedDivIds.has(division.id) && !isInSlot;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<button
|
||
|
|
key={division.id}
|
||
|
|
onClick={() => handleToggleDivision(slot.order, division.id)}
|
||
|
|
className={`px-3 py-1.5 text-sm rounded-full border transition-colors ${
|
||
|
|
isInSlot
|
||
|
|
? 'bg-primary-600 text-white border-primary-600'
|
||
|
|
: isInOtherSlot
|
||
|
|
? 'bg-gray-100 text-gray-400 border-gray-200 cursor-pointer'
|
||
|
|
: 'bg-white text-gray-700 border-gray-300 hover:border-primary-400'
|
||
|
|
}`}
|
||
|
|
title={isInOtherSlot ? `Przypisane do innego slotu` : division.name}
|
||
|
|
>
|
||
|
|
{division.abbreviation}
|
||
|
|
</button>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
{slot.divisionIds.length === 0 && (
|
||
|
|
<p className="text-sm text-gray-400 mt-2 italic">
|
||
|
|
Kliknij dywizje aby dodac do slotu
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Add slot button */}
|
||
|
|
<div className="flex gap-3">
|
||
|
|
<button
|
||
|
|
onClick={handleAddSlot}
|
||
|
|
className="flex items-center gap-2 px-4 py-2 border border-dashed border-gray-300 rounded-lg text-gray-600 hover:border-primary-400 hover:text-primary-600"
|
||
|
|
>
|
||
|
|
<Plus size={16} />
|
||
|
|
Dodaj slot
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
onClick={handleSaveSchedule}
|
||
|
|
disabled={savingSchedule}
|
||
|
|
className="flex items-center gap-2 px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50"
|
||
|
|
>
|
||
|
|
{savingSchedule ? (
|
||
|
|
<RefreshCw size={16} className="animate-spin" />
|
||
|
|
) : (
|
||
|
|
<Save size={16} />
|
||
|
|
)}
|
||
|
|
Zapisz harmonogram
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Current config info */}
|
||
|
|
{event?.scheduleConfig?.slots?.length > 0 && (
|
||
|
|
<div className="mt-4 bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||
|
|
<p className="text-sm text-blue-800">
|
||
|
|
Zapisany harmonogram: {event.scheduleConfig.slots.length} slot(ow)
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export default ScheduleConfigSection;
|