{{-- "Добавить результаты" matrix form + "Прогресс по ученикам" history table, rendered both by admin/ent_scores/index.blade.php (initial load) and by EntScoreController::store()'s AJAX branch, which re-renders this partial after a successful submit and returns it as data.html for the js-ajax-form handler (layouts/app.blade.php) to swap in via data-replace-target="#entScoresCard" — needed to clear the matrix back to blank inputs and refresh the progress table, both of which only happened via full reload before. Params: $search, $classId, $students, $sections, $lastProfile, $detail, $attempts (see EntScoreController::cardData(), which assembles all of them for every call site). The progress table's rows expand into every sitting that student has (from $attempts), each with an edit and a delete button — update()/destroy() answer both with a re-rendered copy of this same partial, so the two write paths added later need no swap logic of their own. Two entry modes over the same posted shape (scores[student_id][section_id]): - compact (default) — the mandatory sections as fixed columns, then two "profile subject" slots per student, each a subject dropdown paired with a score box. That mirrors an actual ЕНТ sitting (3 mandatory + 2 subjects of the student's own choosing) instead of asking the user to find two of eleven profile columns in a table that scrolls sideways. - full — the original every-student × every-section matrix, kept for what the two slots can't express (a student with three subjects on record, edits across many subjects at once) and reached via the "Показать все предметы" switch. Both tables are always rendered, and every input in the inactive one is disabled so the two can share input names without the browser posting each score twice. Which one is active is JS's business (see index.blade.php); with JS off the compact table is the one that works, including whatever its dropdowns were preselected with server-side. --}} @php $mandatorySections = array_values(array_filter($sections, fn ($s) => (int) $s['is_mandatory'] === 1)); $profileSections = array_values(array_filter($sections, fn ($s) => (int) $s['is_mandatory'] !== 1)); $formatMax = fn ($max) => rtrim(rtrim(number_format((float) $max, 1, '.', ''), '0'), '.'); // Subject names for the expanded sitting rows, which carry section ids and nothing else. $sectionNames = array_column($sections, 'name', 'id'); // Which subject each dropdown starts on. old() wins (a rejected submit has to come back // exactly as typed), otherwise it's whatever the student sat last time — see // EntScoreController::lastProfileSections(). Only ids still in the dictionary survive, so a // section deleted since that sitting doesn't select a phantom option. $oldScores = old('scores', []); $profileIds = array_map(fn ($s) => (int) $s['id'], $profileSections); $preselect = []; foreach ($students as $s) { $sid = (int) $s['id']; $typed = array_map('intval', array_keys(array_filter( $oldScores[$sid] ?? [], fn ($v) => $v !== null && $v !== '' ))); $chosenIds = array_values(array_intersect($typed, $profileIds)) ?: array_values(array_intersect($lastProfile[$sid] ?? [], $profileIds)); $preselect[$sid] = array_slice($chosenIds, 0, 2); } @endphp
Добавить результаты @if (! empty($students))
@endif
@csrf
@error('test_date')
{{ $message }}
@enderror
@error('scores')
{{ $message }}
@enderror @if (empty($students))
Нет учеников по заданному фильтру.
@else {{-- Sets both slots for every student at once: the common case is a whole class sitting the same pair, and clicking through 2 × N dropdowns for that is exactly what made this page tedious. Fills empty slots only, so students already carrying their own subjects keep them. --}}
Профильные предметы для всего списка
@foreach ([0, 1] as $slot)
@endforeach
@foreach ($mandatorySections as $section) @endforeach @foreach ([0, 1] as $slot) @endforeach @foreach ($students as $s) @foreach ($mandatorySections as $section) @endforeach @foreach ([0, 1] as $slot) @php $chosenId = $preselect[(int) $s['id']][$slot] ?? null; $chosen = null; foreach ($profileSections as $candidate) { if ((int) $candidate['id'] === $chosenId) { $chosen = $candidate; break; } } @endphp @endforeach @endforeach
Ученик {{ $section['name'] }} Обяз.
макс. {{ $formatMax($section['max_score']) }}
Профильный {{ $slot + 1 }}
предмет и балл
{{ $s['last_name'] . ' ' . $s['first_name'] }}
{{-- The score box's name carries the section id, so it gets rewritten whenever this dropdown changes, and the box stays disabled (and therefore unposted) while no subject is chosen. --}} @if ($chosen) @else @endif
@foreach ($sections as $section) @endforeach @foreach ($students as $s) @foreach ($sections as $section) @endforeach @endforeach
Ученик {{ $section['name'] }} @if ($section['is_mandatory']) Обяз. @endif
макс. {{ $formatMax($section['max_score']) }}
{{ $s['last_name'] . ' ' . $s['first_name'] }}
@endif
@if (!empty($detail))
Прогресс по ученикам
{{-- Table sits directly in the section (no ax-info-section-body) so it stays flush to the edges, as it did inside the old card. --}}
@foreach ($detail as $row) @php $studentName = $row['last_name'] . ' ' . $row['first_name']; @endphp {{-- --ax-gray-50, not Bootstrap's .bg-body-tertiary: this app's dark mode is its own token set (css/style.css), and Bootstrap's theme variables never flip with it — .bg-body-tertiary would be a white panel in the middle of a dark page. --}} @endforeach
УченикПредыдущийПоследнийΔСр. прогрессВсе результаты
{{ $studentName }} {{ $row['previous_score'] ?? '—' }} {{ $row['latest_score'] }} @if ($row['delta'] !== null) {{ ($row['delta'] >= 0 ? '+' : '') . $row['delta'] }} @else — @endif @if ($row['avg_delta'] !== null) {{ ($row['avg_delta'] >= 0 ? '+' : '') . $row['avg_delta'] }} @else — @endif {{-- Collapsed by default: most visits to this page are entry, not correction, and a student with six sittings would otherwise push the next student off the screen. --}}
@foreach ($attempts[$row['student_id']] ?? [] as $attempt) @endforeach
Дата Сумма Предметы Заметка Действия
{{ $attempt['date'] }} {{ $attempt['total'] }} @foreach ($attempt['sections'] as $sectionId => $score) {{ $sectionNames[$sectionId] ?? ('#' . $sectionId) }}: {{ $score }} @endforeach @if ($attempt['unassigned'] !== null) {{-- A pre-021 row: no subject to name, but it still counts toward the sum above, so say where the difference is. --}} без предмета: {{ $attempt['unassigned'] }} @endif {{ $attempt['note'] ?? '—' }} {{-- The dialog itself is shared and filled in from these attributes on click (see index.blade.php): one modal per sitting would be every student × every sitting × every subject worth of inputs in the page. --}}
@csrf
{{-- Edit dialog, one for the whole card. Every subject in the dictionary gets a field, not just the ones the sitting already has: "this score is filed under the wrong subject" and "I missed one" are the two edits this exists for, and neither is expressible otherwise. An emptied field removes that subject from the sitting — update() reconciles the set. Sits outside the entry form on purpose (a nested
is invalid HTML and the browser would drop this one), and inside #entScoresCard so a successful save replaces it along with everything else — the js-ajax-form handler hides the modal and waits for 'hidden.bs.modal' before swapping, so the backdrop doesn't get stranded. --}}
@endif