Files
magistr/frontend/react/admin/tabs/AuditoriumWorkloadTab.jsx
dipatrik10 97bf3cc50a feat: завершить миграцию фронтенда на React (этапы 1-5)
Админка, настройки, логин, кабинеты преподавателя и студента
полностью переведены на React 19 (модель strangler, docs/REACT_MIGRATION.md).

- 15 вкладок админки: lazy-загрузка, общий error boundary, сервисный слой
- Удалены legacy-views и переходный слой LegacyTabView (45 файлов)
- Сборка esbuild (frontend/scripts/build-react.mjs) с бюджетами gzip на чанки
- CSP-хэши, кэширование: chunk'и immutable, CSS/HTML no-cache (react-cache.conf)
- Фиксы: lazy-импорт именованных экспортов (краш после авторизации),
  растяжение #admin-root/#settings-root на всю ширину,
  возврат subjects/lessonTypes из справочников (краш просмотра расписаний),
  отказ от заведомо широкого поиска расписания без выбранной цели,
  центрирование карточки логина
- Тесты: 237/237 (npm run check), регрессионные на lazy-вкладки и ширину
- Документация: REACT_MIGRATION.md, REACT_MIGRATION_RESULT.md, FRONTEND.md
2026-09-28 23:15:04 +03:00

776 lines
33 KiB
JavaScript
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.
import { useEffect, useMemo, useRef, useState } from 'react';
import { toIsoDate } from '../../shared/dates.js';
import { FormAlert } from '../../shared/ui/FormAlert.jsx';
import {
CELL_PARITY_LAYOUT,
DAY_FULL_LABELS,
DAY_ORDER,
DISPLAY_ALL,
TARGET_CLASSROOMS,
TARGET_DEPARTMENTS,
TARGET_TEACHERS,
WORKLOAD_TARGETS,
buildingOptions,
countOccupiedCells,
datesBetween,
entityAxisParts,
entityOptionLabel,
entitySlotKey,
equipmentOptions,
filteredClassrooms,
formatDateShort,
loadEntityLessons,
loadOverviewLessons,
loadSlotsForDates,
overviewSummaryText,
parseMinCapacity,
roomSlotState,
roomSlotStatesEqual,
roomSummaryParts,
roomWeekCaption,
sameId,
scopeShort,
semestersFromYears,
sortClassrooms,
sortDepartments,
sortTeachers,
timeSlotLabel,
twoWeekRange,
buildDateGridByDay,
buildSlotRows,
countAvailableRoomCells,
countOccupiedRoomCells,
dayShort,
fetchAcademicYears,
fetchEffectiveTimeSlots,
fetchWorkloadClassrooms,
fetchWorkloadDepartments,
fetchWorkloadEquipments,
fetchWorkloadGroups,
fetchWorkloadTeachers
} from '../auditorium-workload-service.js';
export function AuditoriumWorkloadTab() {
const [date, setDate] = useState(() => toIsoDate(new Date()));
const [currentTarget, setCurrentTarget] = useState(TARGET_CLASSROOMS);
const [displayValue, setDisplayValue] = useState(DISPLAY_ALL);
const [buildings, setBuildings] = useState([]);
const [equipment, setEquipment] = useState([]);
const [minCapacityText, setMinCapacityText] = useState('');
const [classrooms, setClassrooms] = useState([]);
const [teachers, setTeachers] = useState([]);
const [departments, setDepartments] = useState([]);
const [equipments, setEquipments] = useState([]);
const [groups, setGroups] = useState([]);
const [semesters, setSemesters] = useState([]);
const [overview, setOverview] = useState({
loading: true,
message: 'Загрузка справочников и расписания...',
error: '',
timeSlots: [],
lessonsByEntitySlot: new Map(),
failedGroupLoads: 0,
summaryReady: false
});
const [detail, setDetail] = useState({
loading: false,
message: '',
error: '',
summary: null,
weekTable: null
});
const [alertState, setAlertState] = useState(null);
const dateRef = useRef(date);
const targetRef = useRef(currentTarget);
const displayRef = useRef(displayValue);
const targetConfig = WORKLOAD_TARGETS[currentTarget] || WORKLOAD_TARGETS[TARGET_CLASSROOMS];
const roomMode = displayValue !== DISPLAY_ALL;
const showOverviewFilters = !roomMode && currentTarget === TARGET_CLASSROOMS;
function showWorkloadAlert(text) {
setAlertState({ message: text, type: 'error' });
}
function hideWorkloadAlert() {
setAlertState(null);
}
useEffect(() => {
let active = true;
(async () => {
try {
const [loadedClassrooms, loadedEquipments, loadedGroups, academicYears, loadedTeachers, loadedDepartments] = await Promise.all([
fetchWorkloadClassrooms(),
fetchWorkloadEquipments(),
fetchWorkloadGroups(),
fetchAcademicYears(),
fetchWorkloadTeachers(),
fetchWorkloadDepartments()
]);
if (!active) return;
const dictionaries = {
classrooms: sortClassrooms(loadedClassrooms),
teachers: sortTeachers(loadedTeachers),
departments: sortDepartments(loadedDepartments),
equipments: loadedEquipments || [],
groups: loadedGroups || [],
semesters: semestersFromYears(academicYears)
};
setClassrooms(dictionaries.classrooms);
setTeachers(dictionaries.teachers);
setDepartments(dictionaries.departments);
setEquipments(dictionaries.equipments);
setGroups(dictionaries.groups);
setSemesters(dictionaries.semesters);
await loadCurrentView({ dictionaries });
} catch (error) {
if (!active) return;
const message = error.message || 'Ошибка загрузки данных загруженности';
setOverview(current => ({ ...current, loading: false, error: message }));
setDetail(current => ({ ...current, loading: false, error: message }));
showWorkloadAlert(message);
}
})();
return () => {
active = false;
};
}, []);
async function loadCurrentView(context = {}) {
const targetValue = context.target ?? targetRef.current;
const displayVal = context.display ?? displayRef.current;
const dateValue = context.date ?? dateRef.current;
const source = context.dictionaries ?? { classrooms, teachers, departments, groups, semesters };
const config = WORKLOAD_TARGETS[targetValue] || WORKLOAD_TARGETS[TARGET_CLASSROOMS];
const entities = targetValue === TARGET_TEACHERS
? source.teachers
: targetValue === TARGET_DEPARTMENTS ? source.departments : source.classrooms;
if (displayVal !== DISPLAY_ALL) {
await loadDetail({
dateValue,
targetValue,
displayVal,
entities,
groups: source.groups,
teachers: source.teachers,
departments: source.departments,
semesters: source.semesters
});
} else {
await loadOverview({
dateValue,
targetValue,
groups: source.groups,
teachers: source.teachers,
departments: source.departments
});
}
}
async function loadOverview({ dateValue, targetValue, groups: groupList, teachers: teacherList, departments: departmentList }) {
if (!dateValue) {
showWorkloadAlert('Выберите дату');
return;
}
hideWorkloadAlert();
setOverview(current => ({
...current,
loading: true,
message: 'Загрузка расписания на выбранную дату...',
error: ''
}));
try {
const slots = await fetchEffectiveTimeSlots(dateValue);
if (!(groupList || []).length) {
setOverview({
loading: false,
error: '',
timeSlots: slots || [],
lessonsByEntitySlot: new Map(),
failedGroupLoads: 0,
summaryReady: true
});
showWorkloadAlert('Группы не созданы, загруженность пуста');
return;
}
const { lessonsByEntitySlot, failedLoads } = await loadOverviewLessons({
groups: groupList,
date: dateValue,
target: targetValue,
teachers: teacherList,
departments: departmentList
});
setOverview({
loading: false,
error: '',
timeSlots: slots || [],
lessonsByEntitySlot,
failedGroupLoads: failedLoads,
summaryReady: true
});
if (failedLoads > 0) {
showWorkloadAlert(`Часть групп не загрузилась: ${failedLoads}. Показаны доступные данные.`);
}
} catch (error) {
const message = error.message || 'Ошибка загрузки загруженности';
setOverview(current => ({ ...current, loading: false, error: message }));
showWorkloadAlert(message);
}
}
async function loadDetail({ dateValue, targetValue, displayVal, entities, groups: groupList, teachers: teacherList, departments: departmentList, semesters: semesterList }) {
const config = WORKLOAD_TARGETS[targetValue] || WORKLOAD_TARGETS[TARGET_CLASSROOMS];
const entityId = displayVal !== DISPLAY_ALL ? displayVal.slice(config.prefix.length) : null;
const entity = entityId
? (entities || []).find(item => sameId(item.id, entityId)) || null
: null;
if (!dateValue) {
showWorkloadAlert('Выберите дату');
setDetail({
loading: false,
message: '',
error: 'Выберите дату для расчёта двухнедельного периода',
summary: null,
weekTable: null
});
return;
}
if (!entity) {
showWorkloadAlert(config.selectError);
setDetail({
loading: false,
message: '',
error: `${config.selectError} в поле «Отображение»`,
summary: null,
weekTable: null
});
return;
}
hideWorkloadAlert();
setDetail({
loading: true,
message: 'Загрузка расписания на две недели...',
error: '',
summary: null,
weekTable: null
});
try {
const range = twoWeekRange(dateValue);
const dates = datesBetween(range.startDate, range.endDate);
const [slotLoad, lessonLoad] = await Promise.all([
loadSlotsForDates(dates),
loadEntityLessons({
groups: groupList,
startDate: range.startDate,
endDate: range.endDate,
entityId: entity.id,
target: targetValue,
teachers: teacherList,
departments: departmentList
})
]);
const slotRows = buildSlotRows(dates, slotLoad.slotsByDate, lessonLoad.lessons);
const dateGrid = buildDateGridByDay(dates, lessonLoad.parityLessons, semesterList);
setDetail({
loading: false,
message: '',
error: '',
summary: roomSummaryParts({
target: targetValue,
entity,
range,
totalCells: countAvailableRoomCells(dates, slotLoad.slotsByDate, lessonLoad.lessonsByDateSlot),
occupiedCells: countOccupiedRoomCells(lessonLoad.lessonsByDateSlot),
failedGroupLoads: lessonLoad.failedLoads,
failedSlotLoads: slotLoad.failedLoads,
unknownParityDates: dateGrid.unknownParityDates
}),
weekTable: slotRows.length ? {
datesByDay: dateGrid.datesByDay,
slotRows,
slotsByDate: slotLoad.slotsByDate,
lessonsByDateSlot: lessonLoad.lessonsByDateSlot
} : null
});
const warnings = [];
if (lessonLoad.failedLoads > 0) warnings.push(`групп не загрузилось: ${lessonLoad.failedLoads}`);
if (slotLoad.failedLoads > 0) warnings.push(`сеток времени не загрузилось: ${slotLoad.failedLoads}`);
if (warnings.length) {
showWorkloadAlert(`Часть данных не загрузилась (${warnings.join(', ')}). Показаны доступные данные.`);
}
} catch (error) {
const message = error.message || 'Ошибка загрузки загруженности';
setDetail({
loading: false,
message: '',
error: message,
summary: null,
weekTable: null
});
showWorkloadAlert(message);
}
}
function handleDateChange(value) {
dateRef.current = value;
setDate(value);
void loadCurrentView();
}
function handleTargetChange(value) {
const nextTarget = value || TARGET_CLASSROOMS;
targetRef.current = nextTarget;
displayRef.current = DISPLAY_ALL;
setCurrentTarget(nextTarget);
setDisplayValue(DISPLAY_ALL);
void loadCurrentView({ target: nextTarget, display: DISPLAY_ALL });
}
function handleDisplayChange(value) {
displayRef.current = value;
setDisplayValue(value);
void loadCurrentView({ display: value });
}
function toggleBuilding(value) {
setBuildings(current => (
current.includes(value) ? current.filter(item => item !== value) : [...current, value]
));
}
function toggleEquipment(value) {
setEquipment(current => (
current.includes(value) ? current.filter(item => item !== value) : [...current, value]
));
}
const buildingOptionItems = useMemo(() => buildingOptions(classrooms), [classrooms]);
const equipmentOptionItems = useMemo(
() => equipmentOptions(equipments, classrooms),
[equipments, classrooms]
);
const minCapacity = parseMinCapacity(minCapacityText);
const entities = currentTarget === TARGET_TEACHERS
? teachers
: currentTarget === TARGET_DEPARTMENTS ? departments : classrooms;
const gridRows = useMemo(() => {
if (currentTarget === TARGET_CLASSROOMS) {
return filteredClassrooms(classrooms, { buildings, minCapacity, equipment });
}
return currentTarget === TARGET_TEACHERS ? teachers : departments;
}, [currentTarget, classrooms, teachers, departments, buildings, minCapacity, equipment]);
const displayOptions = useMemo(() => ([
{ value: DISPLAY_ALL, label: targetConfig.allLabel },
...entities.map(entity => ({
value: `${targetConfig.prefix}${entity.id}`,
label: entityOptionLabel(currentTarget, entity)
}))
]), [currentTarget, entities, targetConfig]);
const summaryText = !overview.loading && overview.summaryReady
? overviewSummaryText({
target: currentTarget,
dateValue: date,
entityCount: gridRows.length,
occupiedCells: countOccupiedCells(gridRows, overview.timeSlots, overview.lessonsByEntitySlot),
totalCells: gridRows.length * overview.timeSlots.length,
failedGroupLoads: overview.failedGroupLoads,
selectedEntityId: null
})
: '';
const stateColSpan = Math.max(overview.timeSlots.length, 1) + 1;
return (
<div className="card">
<div className="card-header-row workload-card-header">
<h2>Загруженность</h2>
<button
type="button"
className="btn btn-md btn-secondary"
id="workload-refresh"
onClick={() => void loadCurrentView()}
>
Обновить
</button>
</div>
<div className="filter-row workload-filter-row">
<div className="form-group workload-target-control">
<label htmlFor="workload-target-select">Что смотреть</label>
<select
id="workload-target-select"
className="workload-display-select"
value={currentTarget}
onChange={event => handleTargetChange(event.target.value)}
>
<option value={TARGET_CLASSROOMS}>Аудитории</option>
<option value={TARGET_TEACHERS}>Преподаватели</option>
<option value={TARGET_DEPARTMENTS}>Кафедры</option>
</select>
</div>
<div className="form-group workload-display-control">
<label htmlFor="workload-display-select">Отображение</label>
<select
id="workload-display-select"
className="workload-display-select"
value={displayValue}
onChange={event => handleDisplayChange(event.target.value)}
>
{displayOptions.map(option => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
</select>
</div>
{showOverviewFilters && (
<div className="form-group workload-overview-filter">
<label>Корпус</label>
<WorkloadMultiSelect
options={buildingOptionItems}
selectedValues={buildings}
emptyText="Все корпуса"
onToggle={toggleBuilding}
/>
</div>
)}
{showOverviewFilters && (
<div className="form-group workload-overview-filter">
<label htmlFor="workload-min-capacity">Мин. вместимость</label>
<input
type="number"
id="workload-min-capacity"
className="workload-min-capacity"
placeholder="0"
min="0"
value={minCapacityText}
onChange={event => setMinCapacityText(event.target.value)}
/>
</div>
)}
{showOverviewFilters && (
<div className="form-group workload-overview-filter">
<label>Оборудование</label>
<WorkloadMultiSelect
options={equipmentOptionItems}
selectedValues={equipment}
emptyText="Любое оборудование"
onToggle={toggleEquipment}
/>
</div>
)}
<div className="form-group workload-date-field">
<label htmlFor="workload-date">{roomMode ? 'Дата в периоде' : 'Дата'}</label>
<input
type="date"
id="workload-date"
value={date}
onChange={event => handleDateChange(event.target.value)}
/>
</div>
</div>
<FormAlert alert={alertState} id="workload-alert" />
{!roomMode && (
<div id="workload-overview-block">
<div className="workload-summary" id="workload-summary">{summaryText}</div>
<div className="workload-grid-container">
<table className="workload-table" id="workload-table">
<thead>
<tr id="workload-header-row">
<th className="top-left-cell">
<span className="top-label">Время</span>
<span className="bottom-label">{targetConfig.rowLabel}</span>
</th>
{overview.timeSlots.map(slot => (
<th key={slot.id ?? slot.orderNumber}>
<strong>{slot.orderNumber || ''} пара</strong>
<span>{timeSlotLabel(slot)}</span>
{slot.scopeApplyMode !== 'DEFAULT' && <small>{scopeShort(slot)}</small>}
</th>
))}
</tr>
</thead>
<tbody id="workload-tbody">
{overview.loading ? (
<tr>
<td colSpan={stateColSpan} className="loading-row">{overview.message}</td>
</tr>
) : overview.error ? (
<tr>
<td colSpan={stateColSpan} className="loading-row">{overview.error}</td>
</tr>
) : gridRows.length === 0 ? (
<tr>
<td colSpan={overview.timeSlots.length + 1} className="loading-row">
{targetConfig.emptyLabel}
</td>
</tr>
) : gridRows.map(entity => (
<WorkloadRow
key={entity.id}
target={currentTarget}
entity={entity}
timeSlots={overview.timeSlots}
lessonsByEntitySlot={overview.lessonsByEntitySlot}
/>
))}
</tbody>
</table>
</div>
</div>
)}
{roomMode && (
<div className="room-workload-block" id="room-workload-block">
<div className="room-workload-summary" id="room-workload-summary">
{detail.loading || detail.error || !detail.summary ? '' : (
<>
<div className="room-summary-main">
<strong>{detail.summary.name}</strong>
{detail.summary.details && <span>{detail.summary.details}</span>}
</div>
<span className="room-summary-chip">{detail.summary.rangeLabel}</span>
<span className="room-summary-chip">{detail.summary.occupancyLabel}</span>
{detail.summary.warnings && (
<span className="room-summary-warning">{detail.summary.warnings}</span>
)}
</>
)}
</div>
<div className="room-workload-tables" id="room-workload-tables">
{detail.loading ? (
<div className="loading-row room-workload-state">{detail.message}</div>
) : detail.error ? (
<div className="loading-row room-workload-state">{detail.error}</div>
) : !detail.weekTable ? (
<div className="loading-row room-workload-state">
За выбранный период нет временных слотов
</div>
) : (
<RoomWeekTable
target={currentTarget}
semesters={semesters}
datesByDay={detail.weekTable.datesByDay}
slotRows={detail.weekTable.slotRows}
slotsByDate={detail.weekTable.slotsByDate}
lessonsByDateSlot={detail.weekTable.lessonsByDateSlot}
/>
)}
</div>
</div>
)}
</div>
);
}
function WorkloadRow({ target, entity, timeSlots, lessonsByEntitySlot }) {
const axis = entityAxisParts(target, entity);
const axisClass = `axis-cell${target === TARGET_CLASSROOMS && entity.isAvailable === false ? ' room-unavailable' : ''}`;
return (
<tr>
<td className={axisClass}>
<strong>{axis.title}</strong>
<span>{axis.subtitle}</span>
{axis.note && <small>{axis.note}</small>}
</td>
{timeSlots.map(slot => {
const lessons = lessonsByEntitySlot.get(entitySlotKey(entity.id, slot.id)) || [];
return (
<td
key={slot.id ?? slot.orderNumber}
className={lessons.length ? 'workload-busy-cell' : 'workload-free-cell'}
>
{lessons.length
? lessons.map((lesson, index) => <LessonCard key={index} lesson={lesson} />)
: <span className="free-slot">Свободно</span>}
</td>
);
})}
</tr>
);
}
function LessonCard({ lesson }) {
const subgroupText = (lesson.subgroupNames || []).join(', ');
return (
<div className="lesson-card">
<div className="lesson-subject">{lesson.subjectName || 'Без дисциплины'}</div>
<div className="lesson-group">{(lesson.groupNames || []).join(', ') || 'Группа не указана'}</div>
{subgroupText && <div className="lesson-subgroup">{subgroupText}</div>}
<div className="lesson-teacher">{lesson.teacherName || 'Преподаватель не указан'}</div>
<div className="lesson-meta">
{[lesson.lessonTypeName, lesson.lessonFormat].filter(Boolean).join(' · ')}
</div>
</div>
);
}
function RoomWeekTable({ target, semesters, datesByDay, slotRows, slotsByDate, lessonsByDateSlot }) {
const config = WORKLOAD_TARGETS[target] || WORKLOAD_TARGETS[TARGET_CLASSROOMS];
return (
<section className="room-week-panel">
<div className="room-week-heading">
<h3>{config.periodTitle}</h3>
<span>{roomWeekCaption(datesByDay, semesters)}</span>
</div>
<div className="workload-grid-container room-grid-container">
<table className="workload-table room-week-table">
<thead>
<tr>
<th className="room-time-heading">Время</th>
{DAY_ORDER.map(day => (
<th className="room-day-cell" key={day}>
<strong>{dayShort(day)}</strong>
<span>{DAY_FULL_LABELS[day] || ''}</span>
<div className="room-day-parity-dates">
{CELL_PARITY_LAYOUT.map(({ parity, label }) => (
<span key={parity}>
{label}: {datesByDay[day]?.[parity]
? formatDateShort(datesByDay[day][parity])
: '-'}
</span>
))}
</div>
</th>
))}
</tr>
</thead>
<tbody>
{slotRows.map(row => (
<tr key={row.key}>
<td className="room-time-cell">
<strong>{row.orderLabel}</strong>
<span>{row.timeLabel}</span>
</td>
{DAY_ORDER.map(day => (
<RoomCombinedCell
key={day}
datesByParity={datesByDay[day] || {}}
row={row}
slotsByDate={slotsByDate}
lessonsByDateSlot={lessonsByDateSlot}
/>
))}
</tr>
))}
</tbody>
</table>
</div>
</section>
);
}
function RoomCombinedCell({ datesByParity, row, slotsByDate, lessonsByDateSlot }) {
const states = CELL_PARITY_LAYOUT.map(({ parity, label }) => ({
parity,
label,
state: roomSlotState(datesByParity[parity], row, slotsByDate, lessonsByDateSlot)
}));
if (roomSlotStatesEqual(states[0].state, states[1].state)) {
const state = states[0].state;
return (
<td className="room-combined-cell room-single-cell">
<div className={`room-week-unified room-week-unified-${state.kind === 'busy' ? 'busy' : state.kind}`}>
{state.kind === 'busy'
? state.lessons.map((lesson, index) => <LessonCard key={index} lesson={lesson} />)
: state.kind === 'missing' ? <span className="slot-unavailable">Нет слота</span>
: state.kind === 'empty' ? <span className="slot-unavailable">Нет даты</span>
: <span className="free-slot">Свободно</span>}
</div>
</td>
);
}
return (
<td className="room-combined-cell">
{states.map(({ parity, label, state }) => (
<div
key={parity}
className={`room-week-half room-week-half-${parity.toLowerCase()} room-week-half-${state.kind === 'busy' ? 'busy' : state.kind}`}
>
<span className="room-half-marker">{label}</span>
{state.kind === 'busy'
? state.lessons.map((lesson, index) => <LessonCard key={index} lesson={lesson} />)
: state.kind === 'missing' ? <span className="slot-unavailable">Нет слота</span>
: state.kind === 'empty' ? <span className="slot-unavailable">Нет даты</span>
: <span className="free-slot">Свободно</span>}
</div>
))}
</td>
);
}
function WorkloadMultiSelect({ options, selectedValues, emptyText, onToggle }) {
const rootRef = useRef(null);
const [open, setOpen] = useState(false);
useEffect(() => {
if (!open) return undefined;
function handlePointerDown(event) {
if (rootRef.current && !rootRef.current.contains(event.target)) {
setOpen(false);
}
}
document.addEventListener('mousedown', handlePointerDown);
return () => document.removeEventListener('mousedown', handlePointerDown);
}, [open]);
const selectedLabels = options
.filter(option => selectedValues.includes(option.value))
.map(option => option.label);
const label = selectedLabels.length === 0
? emptyText
: selectedLabels.length === 1 ? selectedLabels[0] : `Выбрано: ${selectedLabels.length}`;
return (
<div className="custom-multi-select" ref={rootRef}>
<button
type="button"
className={`select-box${open ? ' active' : ''}`}
aria-expanded={open}
onClick={() => setOpen(current => !current)}
>
<span className="select-text">{label}</span>
<svg className="dropdown-icon" width="12" height="8" viewBox="0 0 12 8" fill="none" aria-hidden="true">
<path d="M1 1.5L6 6.5L11 1.5" stroke="#9ca3af" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
<div className={`dropdown-menu${open ? ' open' : ''}`}>
<div className="checkbox-group-vertical">
{options.length === 0 ? (
<div className="workload-filter-empty">Нет данных</div>
) : options.map(option => (
<label className="checkbox-item" key={option.value}>
<input
type="checkbox"
value={option.value}
checked={selectedValues.includes(option.value)}
onChange={() => onToggle(option.value)}
/>
<span className="checkmark"></span>
<span className="checkbox-label">{option.label}</span>
</label>
))}
</div>
</div>
</div>
);
}