Работа с ведомостью, начало статистики

This commit is contained in:
2025-05-30 03:18:15 +04:00
parent 2db4bc0013
commit 5512335414
6 changed files with 601 additions and 2 deletions

View File

@@ -2,9 +2,108 @@
namespace App\Http\Controllers;
use App\Services\ApiService;
use Illuminate\Http\Request;
class StatementController extends Controller
{
//
protected ApiService $api;
public function __construct(ApiService $api)
{
$this->api = $api;
}
public function index()
{
$response = $this->api->get('/employee/statements');
if ($response->successful()) {
$statements = $response->json();
return view('statements.index', compact('statements'));
}
abort($response->status());
}
public function create()
{
$groups = $this->api->get('/employee/groups')->json();
$disciplines = $this->api->get('/employee/disciplines')->json();
$types = $this->api->get('/employee/types')->json();
$teachers = $this->api->get('/employee/teachers')->json();
return view('statements.form', [
'groups' => $groups,
'disciplines' => $disciplines,
'types' => $types,
'teachers' => $teachers
]);
}
public function store(Request $request)
{
$response = $this->api->post('/employee/statements', $request->all());
if ($response->successful()) {
return redirect()->route('statements.index')
->with('success', 'Ведомость успешно создана');
}
return back()->withErrors($response->json()['errors'] ?? []);
}
public function edit($id)
{
$statementResponse = $this->api->get("/employee/statements/{$id}");
$groupsResponse = $this->api->get('/employee/groups');
$disciplinesResponse = $this->api->get('/employee/disciplines');
$typesResponse = $this->api->get('/employee/types');
$teachersResponse = $this->api->get('/employee/teachers');
if ($statementResponse->successful() &&
$groupsResponse->successful() &&
$disciplinesResponse->successful() &&
$typesResponse->successful() &&
$teachersResponse->successful()) {
return view('statements.form', [
'statement' => $statementResponse->json(),
'groups' => $groupsResponse->json(),
'disciplines' => $disciplinesResponse->json(),
'types' => $typesResponse->json(),
'teachers' => $teachersResponse->json(),
'isEdit' => true
]);
}
abort($statementResponse->status());
}
public function update(Request $request, $id)
{
$data = $request->all();
$data['is_finalized'] = $data['is_finalized'] ?? false;
$response = $this->api->patch("/employee/statements/{$id}", $data);
if ($response->successful()) {
return redirect()->route('statements.index')
->with('success', 'Ведомость успешно обновлена');
}
return back()->withErrors($response->json()['errors'] ?? []);
}
public function destroy($id)
{
$response = $this->api->delete("/employee/statements/{$id}");
if ($response->successful()) {
return redirect()->route('statements.index')
->with('success', 'Ведомость удалена');
}
return back()->withErrors($response->json()['error'] ?? 'Ошибка при удалении');
}
}

View File

@@ -2,9 +2,45 @@
namespace App\Http\Controllers;
use App\Services\ApiService;
use Illuminate\Http\Request;
class StatisticController extends Controller
{
//
protected ApiService $api;
public function __construct(ApiService $api)
{
$this->api = $api;
}
public function index()
{
$response = $this->api->get('/employee/statistics');
if ($response->successful()) {
$statistics = $response->json();
return view('statistics.index', compact('statistics'));
}
abort($response->status());
}
public function generate(Request $request)
{
$validated = $request->validate([
'type' => 'required|string|in:group,direction,teacher',
'id' => 'required|integer',
'start_date' => 'required|date',
'end_date' => 'required|date|after_or_equal:start_date',
]);
$response = $this->api->get('/employee/statistics/generate', $validated);
if ($response->successful()) {
return $response;
}
return back()->withErrors($response->json()['error'] ?? 'Ошибка при генерации статистики');
}
}

View File

@@ -0,0 +1,277 @@
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">
{{ isset($statement) ? 'Редактирование ведомости' : 'Создание новой ведомости' }}
</div>
<div class="card-body">
<form method="POST" action="{{ isset($statement) ? route('statements.update', $statement['id']) : route('statements.store') }}">
@csrf
@if(isset($statement))
@method('PATCH')
@endif
<div class="row mb-3">
<label for="discipline_id" class="col-md-4 col-form-label text-md-end">Дисциплина*</label>
<div class="col-md-6">
<select id="discipline_id" class="form-control @error('discipline_id') is-invalid @enderror" name="discipline_id" required>
<option value="" disabled {{ old('discipline_id', $statement['discipline']['id'] ?? '') == '' ? 'selected' : '' }}>Выберите дисциплину</option>
@isset($disciplines)
@foreach($disciplines as $discipline)
<option value="{{ $discipline['id'] }}" {{ (old('discipline_id', $statement['discipline']['id'] ?? '') == $discipline['id'] ? 'selected' : '') }}>
{{ $discipline['name'] }}
</option>
@endforeach
@else
<option value="" disabled>Нет доступных дисциплин</option>
@endisset
</select>
@error('discipline_id')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="row mb-3">
<label for="group_id" class="col-md-4 col-form-label text-md-end">Группа*</label>
<div class="col-md-6">
<select id="group_id" class="form-control @error('group_id') is-invalid @enderror" name="group_id" required>
<option value="" disabled {{ old('group_id', $statement['group']['id'] ?? '') == '' ? 'selected' : '' }}>Выберите группу</option>
@isset($groups)
@foreach($groups as $group)
<option value="{{ $group['id'] }}" {{ (old('group_id', $statement['group']['id'] ?? '') == $group['id'] ? 'selected' : '') }}>
{{ $group['name'] }}
</option>
@endforeach
@else
<option value="" disabled>Нет доступных групп</option>
@endisset
</select>
@error('group_id')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="row mb-3">
<label for="teacher_id" class="col-md-4 col-form-label text-md-end">Преподаватель*</label>
<div class="col-md-6">
<select id="teacher_id" name="teacher_id" required class="form-control @error('teacher_id') is-invalid @enderror">
<option value="" disabled {{ empty($statement['teacher']['teacher_id'] ?? null) ? 'selected' : '' }}>
Выберите преподавателя
</option>
@isset($teachers['data'])
@foreach($teachers['data'] as $teacher)
<option value="{{ $teacher['teacher_id'] }}"
{{ ($statement['teacher']['teacher_id'] ?? null) == $teacher['teacher_id'] ? 'selected' : '' }}>
{{ $teacher['surname'] }} {{ $teacher['name'] }} {{ $teacher['patronymic'] }}
</option>
@endforeach
@else
<option value="" disabled>Нет доступных преподавателей</option>
@endisset
</select>
@error('teacher_id')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="row mb-3">
<label for="type_certification_id" class="col-md-4 col-form-label text-md-end">Тип аттестации*</label>
<div class="col-md-6">
<select id="type_certification_id" class="form-control @error('type_certification_id') is-invalid @enderror" name="type_certification_id" required>
<option value="" disabled {{ old('type_certification_id', $statement['type_certification']['id'] ?? '') == '' ? 'selected' : '' }}>Выберите тип</option>
@isset($types)
@foreach($types as $type)
<option value="{{ $type['id'] }}" {{ (old('type_certification_id', $statement['type_certification']['id'] ?? '') == $type['id'] ? 'selected' : '') }}>
{{ $type['name'] }}
</option>
@endforeach
@else
<option value="" disabled>Нет доступных типов</option>
@endisset
</select>
@error('type_certification_id')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="row mb-3">
<label for="semester" class="col-md-4 col-form-label text-md-end">Семестр*</label>
<div class="col-md-6">
<select id="semester" class="form-control @error('semester') is-invalid @enderror" name="semester" required>
<option value="" disabled {{ empty(old('semester', $statement['semester'] ?? '')) ? 'selected' : '' }}>Выберите семестр</option>
@php
$semesters = [
1 => 'Первый семестр',
2 => 'Второй семестр',
3 => 'Третий семестр',
4 => 'Четвертый семестр',
5 => 'Пятый семестр',
6 => 'Шестой семестр',
7 => 'Седьмой семестр',
8 => 'Восьмой семестр',
9 => 'Девятый семестр',
10 => 'Десятый семестр',
11 => 'Одиннадцатый семестр',
12 => 'Двенадцатый семестр'
];
$selected = old('semester', $statement['semester'] ?? '');
@endphp
@foreach($semesters as $number => $name)
<option value="{{ $name }}" {{ $selected == $name ? 'selected' : '' }}>
{{ $name }}
</option>
@endforeach
</select>
@error('semester')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="row mb-3">
<label for="academic_year" class="col-md-4 col-form-label text-md-end">Учебный год*</label>
<div class="col-md-6">
<select id="academic_year" class="form-control @error('academic_year') is-invalid @enderror" name="academic_year" required>
<option value="" disabled {{ empty(old('academic_year', $statement['academic_year'] ?? '')) ? 'selected' : '' }}>Выберите учебный год</option>
@php
$currentYear = date('Y');
$nextYear = $currentYear + 1;
$selectedYear = old('academic_year', $statement['academic_year'] ?? '');
@endphp
@for($i = 0; $i < 10; $i++)
@php
$year = $currentYear - $i;
$next = $year + 1;
$yearRange = "{$year}-{$next}";
@endphp
<option value="{{ $yearRange }}" {{ $selectedYear == $yearRange ? 'selected' : '' }}>
{{ $yearRange }}
@if($i == 0) @endif
</option>
@endfor
</select>
@error('academic_year')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="row mb-3">
<label for="hours" class="col-md-4 col-form-label text-md-end">Часы*</label>
<div class="col-md-6">
<input id="hours" type="text" class="form-control @error('hours') is-invalid @enderror" name="hours" value="{{ old('hours', $statement['hours'] ?? '') }}" required>
@error('hours')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="row mb-3">
<label for="exam_date" class="col-md-4 col-form-label text-md-end">Дата занятия*</label>
<div class="col-md-6">
<input id="exam_date" type="date" class="form-control @error('exam_date') is-invalid @enderror" name="exam_date"
value="{{ old('exam_date', $statement['exam_date'] ?? '') }}"
min="" max="" required>
@error('exam_date')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
<small class="text-muted" id="date-range-info"></small>
</div>
</div>
<div class="row mb-0">
<div class="col-md-6 offset-md-4">
<button type="submit" class="btn btn-primary">
{{ isset($statement) ? 'Обновить' : 'Создать' }}
</button>
<a href="{{ route('statements.index') }}" class="btn btn-secondary">Отмена</a>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const academicYearSelect = document.getElementById('academic_year');
const examDateInput = document.getElementById('exam_date');
const dateRangeInfo = document.getElementById('date-range-info');
const form = document.getElementById('statementForm');
academicYearSelect.addEventListener('change', updateDateRange);
if (academicYearSelect.value) {
updateDateRange();
}
// Валидация перед отправкой формы
form.addEventListener('submit', function(event) {
if (!validateExamDate()) {
event.preventDefault();
alert('Дата занятия должна быть в пределах выбранного учебного года (с 1 сентября по 30 июня)');
}
});
function updateDateRange() {
const yearRange = academicYearSelect.value;
if (!yearRange) return;
const [startYear, endYear] = yearRange.split('-').map(Number);
const minDate = `${startYear}-09-01`;
const maxDate = `${endYear}-06-30`;
examDateInput.min = minDate;
examDateInput.max = maxDate;
dateRangeInfo.textContent = `Допустимый диапазон: 01.09.${startYear} - 30.06.${endYear}`;
if (examDateInput.value) {
validateExamDate();
}
}
function validateExamDate() {
const examDate = new Date(examDateInput.value);
const minDate = new Date(examDateInput.min);
const maxDate = new Date(examDateInput.max);
if (examDate < minDate || examDate > maxDate) {
examDateInput.classList.add('is-invalid');
return false;
} else {
examDateInput.classList.remove('is-invalid');
return true;
}
}
});
</script>
@endsection

View File

@@ -0,0 +1,98 @@
@extends('layouts.app')
@section('links')
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<link href="/resources/css/app.css" rel="stylesheet">
@endsection
@section('content')
<div class="container mx-auto px-4 py-8">
<div class="flex justify-between items-center mb-8">
<h1 class="text-2xl font-bold">Список ведомостей</h1>
<div class="flex flex-wrap gap-2 justify-end">
<a href="{{ url('/dashboard') }}" class="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded">
Перейти в панель
</a>
<a href="{{ route('statements.create') }}" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">
Создать ведомость
</a>
</div>
</div>
<div class="bg-white rounded-lg shadow overflow-hidden">
<div class="table-responsive">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Номер ведомости</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Дата занятия</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Дисциплина</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Семестр</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Группа</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Преподаватель</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Тип</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Статус</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@foreach($statements as $statement)
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap cursor-pointer" onclick="window.location='{{ route('statements.edit', $statement['id']) }}'">
<div class="text-sm font-medium text-gray-900">
{{ $statement['id'] }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap cursor-pointer" onclick="window.location='{{ route('statements.edit', $statement['id']) }}'">
<div class="text-sm text-gray-500">
{{ \Carbon\Carbon::parse($statement['exam_date'])->format('d.m.Y') }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap cursor-pointer" onclick="window.location='{{ route('statements.edit', $statement['id']) }}'">
<div class="text-sm font-medium text-gray-900">
{{ $statement['discipline']['name'] }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap cursor-pointer" onclick="window.location='{{ route('statements.edit', $statement['id']) }}'">
<div class="text-sm text-gray-500">
{{ $statement['semester'] }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap cursor-pointer" onclick="window.location='{{ route('statements.edit', $statement['id']) }}'">
<div class="text-sm text-gray-500">
{{ $statement['group']['name'] }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap cursor-pointer" onclick="window.location='{{ route('statements.edit', $statement['id']) }}'">
<div class="text-sm text-gray-500">
{{ $statement['teacher']['surname'] }}
{{ $statement['teacher']['name'] }}
{{ $statement['teacher']['patronymic'] }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap cursor-pointer" onclick="window.location='{{ route('statements.edit', $statement['id']) }}'">
<div class="text-sm text-gray-500">
{{ $statement['type_certification']['name'] }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap cursor-pointer" onclick="window.location='{{ route('statements.edit', $statement['id']) }}'">
<div class="text-sm text-gray-500">
{{ $statement['status']['name'] }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium flex justify-center items-center space-x-2">
<form action="{{ route('statements.destroy', $statement['id']) }}" method="POST" class="inline">
@csrf
@method('DELETE')
<button type="submit" class="text-red-500 hover:text-red-700" onclick="return confirm('Вы уверены?')">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,88 @@
@extends('layouts.app')
@section('links')
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<link href="/resources/css/app.css" rel="stylesheet">
@endsection
@section('content')
<div class="container mx-auto px-4 py-8">
<div class="flex justify-between items-center mb-8">
<h1 class="text-2xl font-bold">Статистика</h1>
<div class="flex flex-wrap gap-2 justify-end">
<a href="{{ url('/dashboard') }}" class="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded">
Перейти в панель
</a>
</div>
</div>
<div class="bg-white rounded-lg shadow p-6 mb-8">
<h2 class="text-xl font-semibold mb-4">Сформировать отчет</h2>
<form method="GET" action="{{ route('statistics.generate') }}" class="space-y-4">
@csrf
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="type" class="block text-sm font-medium text-gray-700">Тип отчета*</label>
<select id="type" name="type" class="form-select mt-1 block w-full" required>
<option value="" disabled selected>Выберите тип</option>
<option value="group">По группе</option>
<option value="direction">По направлению</option>
<option value="teacher">По преподавателю</option>
</select>
</div>
<div>
<label for="id" class="block text-sm font-medium text-gray-700">ID*</label>
<input type="number" id="id" name="id" class="form-input mt-1 block w-full" required>
</div>
<div>
<label for="start_date" class="block text-sm font-medium text-gray-700">Начальная дата*</label>
<input type="date" id="start_date" name="start_date" class="form-input mt-1 block w-full" required>
</div>
<div>
<label for="end_date" class="block text-sm font-medium text-gray-700">Конечная дата*</label>
<input type="date" id="end_date" name="end_date" class="form-input mt-1 block w-full" required>
</div>
</div>
<div class="flex justify-end">
<button type="submit" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">
Сформировать PDF
</button>
</div>
</form>
</div>
@isset($statistics)
<div class="bg-white rounded-lg shadow overflow-hidden">
<div class="table-responsive">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Показатель</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Значение</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@foreach($statistics as $key => $value)
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium text-gray-900">
{{ ucfirst(str_replace('_', ' ', $key)) }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-500">
{{ is_array($value) ? json_encode($value) : $value }}
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@endisset
</div>
@endsection

View File

@@ -43,4 +43,5 @@ Route::middleware(['jwt.auth'])->group(function () {
// Statistics
Route::get('/statistics', [StatisticController::class, 'index'])->name('statistics.index');
Route::get('/statistics/generate', [StatisticController::class, 'generate'])->name('statistics.generate');
});