Работа со студентами готова

This commit is contained in:
2025-05-30 00:03:33 +04:00
parent f79e2c26fa
commit 10fcaae3ac
12 changed files with 786 additions and 70 deletions

View File

@@ -2,8 +2,10 @@
namespace App\Http\Controllers;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Session;
class AuthController extends Controller
{
@@ -63,9 +65,13 @@ class AuthController extends Controller
]);
if ($response->successful()) {
$token = $response->json()['token'];
session(['api_token' => $token]);
session()->forget('user_id');
$data = $response->json();
Session::put([
'api_token' => $data['token'],
'user_id' => $data['user']['id'] ?? null,
'user_name' => $data['user']['name'] ?? null,
'user_role' => $data['user']['roles_id'] ?? null,
]);
return redirect()->intended('/dashboard');
}
@@ -74,12 +80,15 @@ class AuthController extends Controller
]);
}
/**
* @throws ConnectionException
*/
public function logout(Request $request)
{
Http::withToken(session('api_token'))
$response = Http::withToken(Session::get('api_token'))
->post("{$this->apiBaseUrl}/employee/logout");
$request->session()->invalidate();
return redirect('/');
return redirect('/login');
}
}

View File

@@ -2,12 +2,37 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Services\ApiService;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use Illuminate\Foundation\Application;
use Illuminate\Http\Client\ConnectionException;
class DashboardController extends Controller
{
public function index()
protected ApiService $api;
public function __construct(ApiService $api)
{
return view('dashboard');
$this->api = $api;
}
/**
* @param ApiService $api
* @return Factory|View|Application|\Illuminate\View\View|object
* @throws ConnectionException
*/
public function index(ApiService $api)
{
$response = $api->withAuth()->get('/employee/me');
$user = $response->json();
$statsResponse = $api->withAuth()->get('/employee/statistics');
$stats = $statsResponse->json();
return view('dashboard', [
'user' => $user,
'stats' => $stats
]);
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class StatisticController extends Controller
{
//
}

View File

@@ -2,9 +2,93 @@
namespace App\Http\Controllers;
use App\Services\ApiService;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Request;
class StudentController extends Controller
{
//
protected ApiService $api;
public function __construct(ApiService $api)
{
$this->api = $api;
}
public function index()
{
$responseStudents = $this->api->get('/employee/students');
$responseGroups = $this->api->get('/employee/groups');
if ($responseStudents->successful() && $responseGroups->successful()) {
$students = $responseStudents->json();
$groups = $responseGroups->json();
$groupsList = collect($groups)->pluck('name', 'id')->toArray();
return view('students.index', compact('students', 'groupsList'));
}
abort($responseStudents->status());
}
public function create()
{
$groups = $this->api->get('/employee/groups')->json();
return view('students.form', ['groups' => $groups]);
}
public function store(Request $request)
{
$response = $this->api->post('/employee/students', $request->all());
if ($response->successful()) {
return redirect()->route('students.index')
->with('success', 'Студент успешно создан');
}
return back()->withErrors($response->json()['errors'] ?? []);
}
public function edit($id)
{
$response = $this->api->get("/employee/students/{$id}");
if ($response->successful()) {
$student = $response->json();
$groups = $this->api->get('/employee/groups')->json();
return view('students.form', [
'student' => $student,
'groups' => $groups,
'isEdit' => true
]);
}
abort($response->status());
}
/**
* @throws ConnectionException
*/
public function update(Request $request, $id)
{
$response = $this->api->patch("/employee/students/{$id}", $request->all());
if ($response->successful()) {
return redirect()->route('students.index')
->with('success', 'Данные студента обновлены');
}
return back()->withErrors($response->json()['errors'] ?? []);
}
public function destroy($id)
{
$response = $this->api->delete("/employee/students/{$id}");
if ($response->successful()) {
return redirect()->route('students.index')
->with('success', 'Студент удален');
}
return back()->withErrors($response->json()['error'] ?? 'Ошибка при удалении');
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Session;
class CheckJWTToken
{
/**
* @throws ConnectionException
*/
public function handle($request, Closure $next)
{
if (!Session::has('api_token')) {
return redirect()->route('login');
}
$response = Http::withToken(Session::get('api_token'))
->get(env('API_BASE_URL') . '/employee/me');
if ($response->failed()) {
Session::forget('api_token');
return redirect()->route('login');
}
return $next($request);
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Services;
use GuzzleHttp\Promise\PromiseInterface;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Session;
class ApiService
{
protected string $baseUrl;
public function __construct()
{
$this->baseUrl = env('API_BASE_URL', 'http://127.0.0.1:8000/api');
}
public function withAuth(): PendingRequest
{
return Http::withToken(Session::get('api_token'))
->baseUrl($this->baseUrl);
}
/**
* @throws ConnectionException
*/
public function get(string $url, array $params = []): PromiseInterface|Response
{
return $this->withAuth()->get($url, $params);
}
/**
* @throws ConnectionException
*/
public function post(string $url, array $data = []): PromiseInterface|Response
{
return $this->withAuth()->post($url, $data);
}
/**
* @throws ConnectionException
*/
public function patch(string $url, array $data = []): PromiseInterface|Response
{
return $this->withAuth()->patch($url, $data);
}
/**
* @throws ConnectionException
*/
public function put(string $url, array $data = []): PromiseInterface|Response
{
return $this->withAuth()->put($url, $data);
}
/**
* @throws ConnectionException
*/
public function delete(string $url, array $params = []): PromiseInterface|Response
{
return $this->withAuth()->delete($url, $params);
}
}

View File

@@ -1,5 +1,6 @@
<?php
use App\Http\Middleware\CheckJWTToken;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
@@ -11,7 +12,9 @@ return Application::configure(basePath: dirname(__DIR__))
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
//
$middleware->alias([
'jwt.auth' => CheckJWTToken::class,
]);
})
->withExceptions(function (Exceptions $exceptions) {
//

View File

@@ -6,112 +6,283 @@
<title>Панель управления</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
<style>
:root {
--primary-color: #3b82f6;
--secondary-color: #64748b;
--success-color: #10b981;
--warning-color: #f59e0b;
--danger-color: #ef4444;
}
body {
font-family: 'Inter', sans-serif;
background-color: #f8fafc;
}
.dashboard-container {
max-width: 1200px;
max-width: 1400px;
margin: 2rem auto;
padding: 1rem;
}
.dashboard-header {
margin-bottom: 2rem;
margin-bottom: 2.5rem;
text-align: center;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 1.5rem;
margin-bottom: 3rem;
}
.stat-card {
background: white;
border-radius: 0.75rem;
padding: 1.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
transition: transform 0.2s;
position: relative;
overflow: hidden;
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
}
.stat-card.students::before {
background-color: var(--primary-color);
}
.stat-card.groups::before {
background-color: var(--success-color);
}
.stat-card.directions::before {
background-color: var(--warning-color);
}
.stat-card.discipline::before {
background-color: var(--danger-color);
}
.stat-card.statements::before {
background-color: var(--color-amber-700);
}
.stat-value {
font-size: 2rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.stat-title {
color: var(--secondary-color);
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
}
.card {
background: white;
border-radius: 0.5rem;
padding: 1.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: transform 0.2s;
border-radius: 0.75rem;
padding: 1.75rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
text-align: center;
cursor: pointer;
border: 1px solid #e2e8f0;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
border-color: var(--primary-color);
}
.card-icon {
width: 64px;
height: 64px;
margin: 0 auto 1rem;
color: #3b82f6;
margin: 0 auto 1.25rem;
color: var(--primary-color);
transition: transform 0.3s;
}
.card:hover .card-icon {
transform: scale(1.1);
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: #1e293b;
}
.card-description {
color: #64748b;
color: var(--secondary-color);
font-size: 0.9375rem;
line-height: 1.5;
}
.logout-btn {
display: block;
margin: 3rem auto 0;
padding: 0.75rem 1.5rem;
background-color: var(--danger-color);
color: white;
border: none;
border-radius: 0.5rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
}
.logout-btn:hover {
background-color: #dc2626;
}
.section-title {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 1.5rem;
color: #1e293b;
position: relative;
padding-left: 1rem;
}
.section-title::before {
content: '';
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 4px;
background-color: var(--primary-color);
border-radius: 2px;
}
</style>
</head>
<body class="bg-gray-50">
<div class="dashboard-container">
<div class="dashboard-header">
<h1 class="text-3xl font-bold text-gray-800">Панель управления сотрудника</h1>
<p class="text-gray-600">Выберите раздел для работы</p>
<h1 class="text-4xl font-bold text-gray-800 mb-2">Панель управления</h1>
<p class="text-lg text-gray-600">Добро пожаловать, {{ $user['name'] }}!</p>
</div>
<div class="cards-grid">
<!-- Студенты -->
<a href="{{ route('students.index') }}" class="card">
<div class="card-icon">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
<div>
<h2 class="section-title">Статистика</h2>
<div class="stats-grid">
<div class="stat-card students">
<div class="stat-value">{{ number_format($stats['students_count'] ?? 0, 0, ',', ' ') }}</div>
<div class="stat-title">Студентов</div>
</div>
<h3 class="card-title">Студенты</h3>
<p class="card-description">Управление списком студентов</p>
</a>
<!-- Группы -->
<a href="{{ route('groups.index') }}" class="card">
<div class="card-icon">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
<div class="stat-card groups">
<div class="stat-value">{{ number_format($stats['groups_count'] ?? 0, 0, ',', ' ') }}</div>
<div class="stat-title">Учебных группы</div>
</div>
<h3 class="card-title">Группы</h3>
<p class="card-description">Управление учебными группами</p>
</a>
<!-- Направления -->
<a href="{{ route('directions.index') }}" class="card">
<div class="card-icon">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
</svg>
<div class="stat-card directions">
<div class="stat-value">{{ number_format($stats['directions_count'] ?? 0, 0, ',', ' ') }}</div>
<div class="stat-title">Направлений</div>
</div>
<h3 class="card-title">Направления</h3>
<p class="card-description">Управление направлениями подготовки</p>
</a>
<!-- Дисциплины -->
<a href="{{ route('disciplines.index') }}" class="card">
<div class="card-icon">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<div class="stat-card discipline">
<div class="stat-value">{{ number_format($stats['disciplines_count'] ?? 0, 0, ',', ' ') }}</div>
<div class="stat-title">Дисциплин</div>
</div>
<h3 class="card-title">Дисциплины</h3>
<p class="card-description">Управление учебными дисциплинами</p>
</a>
<!-- Ведомости -->
<a href="{{ route('statements.index') }}" class="card">
<div class="card-icon">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<div class="stat-card statements">
<div class="stat-value">{{ number_format($stats['statements_count'] ?? 0, 0, ',', ' ') }}</div>
<div class="stat-title">Ведомостей</div>
</div>
<h3 class="card-title">Ведомости</h3>
<p class="card-description">Работа с ведомостями успеваемости</p>
</a>
</div>
</div>
<div>
<h2 class="section-title">Быстрый доступ</h2>
<div class="cards-grid">
<a href="{{ route('students.index') }}" class="card">
<div class="card-icon">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
</div>
<h3 class="card-title">Студенты</h3>
<p class="card-description">Управление списком студентов</p>
</a>
<a href="{{ route('groups.index') }}" class="card">
<div class="card-icon">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
</div>
<h3 class="card-title">Группы</h3>
<p class="card-description">Управление учебными группами</p>
</a>
<a href="{{ route('directions.index') }}" class="card">
<div class="card-icon">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
</svg>
</div>
<h3 class="card-title">Направления</h3>
<p class="card-description">Управление направлениями подготовки</p>
</a>
<a href="{{ route('disciplines.index') }}" class="card">
<div class="card-icon">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<h3 class="card-title">Дисциплины</h3>
<p class="card-description">Управление учебными дисциплинами</p>
</a>
<a href="{{ route('statements.index') }}" class="card">
<div class="card-icon">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
</div>
<h3 class="card-title">Ведомости</h3>
<p class="card-description">Работа с ведомостями</p>
</a>
<a href="{{ route('statistics.index') }}" class="card">
<div class="card-icon">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
<h3 class="card-title">Аналитика</h3>
<p class="card-description">Подробная статистика и аналитические отчеты</p>
</a>
</div>
</div>
<form action="{{ route('logout') }}" method="POST">
@csrf
<button type="submit" class="logout-btn">
Выйти из системы
</button>
</form>
</div>
</body>
</html>

View File

@@ -0,0 +1,39 @@
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>@yield('title')</title>
<!-- Fonts -->
<link rel="dns-prefetch" href="//fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=Nunito" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
@yield('links')
<!-- Scripts -->
@vite([ 'resources/js/app.js'])
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</head>
<body>
<div id="app">
<nav class="navbar navbar-expand-md navbar-light bg-white shadow-sm">
<div class="container">
<a class="navbar-brand" href="{{ url('/dashboard') }}">
Университет
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="{{ __('Toggle navigation') }}">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</nav>
<main class="py-4">
@yield('content')
</main>
</div>
</body>
</html>

View File

@@ -0,0 +1,187 @@
@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($student) ? 'Редактирование студента' : 'Добавление нового студента' }}
</div>
<div class="card-body">
<form method="POST" action="{{ isset($student) ? route('students.update', $student['id']) : route('students.store') }}">
@csrf
@if(isset($student))
@method('PATCH')
@endif
<div class="row mb-3">
<label for="surname" class="col-md-4 col-form-label text-md-end">Фамилия*</label>
<div class="col-md-6">
<input id="surname" type="text" class="form-control @error('surname') is-invalid @enderror" name="surname" value="{{ old('surname', $student['surname'] ?? '') }}" required autocomplete="surname" autofocus>
@error('surname')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="row mb-3">
<label for="name" class="col-md-4 col-form-label text-md-end">Имя*</label>
<div class="col-md-6">
<input id="name" type="text" class="form-control @error('name') is-invalid @enderror" name="name" value="{{ old('name', $student['name'] ?? '') }}" required autocomplete="name">
@error('name')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="row mb-3">
<label for="patronymic" class="col-md-4 col-form-label text-md-end">Отчество</label>
<div class="col-md-6">
<input id="patronymic" type="text" class="form-control @error('patronymic') is-invalid @enderror" name="patronymic" value="{{ old('patronymic', $student['patronymic'] ?? '') }}" autocomplete="patronymic">
@error('patronymic')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="row mb-3">
<label for="email" class="col-md-4 col-form-label text-md-end">Email*</label>
<div class="col-md-6">
<input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ old('email', $student['email'] ?? '') }}" required autocomplete="email">
@error('email')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="row mb-3">
<label for="telephone" class="col-md-4 col-form-label text-md-end">Телефон*</label>
<div class="col-md-6">
<input id="telephone"
type="text"
class="form-control @error('telephone') is-invalid @enderror"
name="telephone"
value="{{ old('telephone', $student['telephone'] ?? '') }}"
required
pattern="^[0-9\+\-\(\)\s]+$"
inputmode="tel"
title="Введите только цифры, пробелы, +, -, ( )">
@error('telephone')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="row mb-3">
<label for="password" class="col-md-4 col-form-label text-md-end">{{ isset($student) ? 'Новый пароль' : 'Пароль*' }}</label>
<div class="col-md-6">
<input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" {{ isset($student) ? '' : 'required' }} autocomplete="new-password">
@error('password')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="row mb-3">
<label for="gender" class="col-md-4 col-form-label text-md-end">Пол*</label>
<div class="col-md-6">
<select id="gender" class="form-control @error('gender') is-invalid @enderror" name="gender" required>
<option value="" disabled {{ old('gender', $student['gender'] ?? '') == '' ? 'selected' : '' }} hidden>Выберите пол</option>
<option value="М" {{ (old('gender', $student['gender'] ?? '') == 'М' ? 'selected' : '') }}>Мужской</option>
<option value="Ж" {{ (old('gender', $student['gender'] ?? '') == 'Ж' ? 'selected' : '') }}>Женский</option>
</select>
@error('gender')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="row mb-3">
<label for="birth_date" class="col-md-4 col-form-label text-md-end">Дата рождения*</label>
<div class="col-md-6">
<input id="birth_date" type="date" class="form-control @error('birth_date') is-invalid @enderror" name="birth_date" value="{{ old('birth_date', $student['birth_date'] ?? '') }}" required>
@error('birth_date')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="row mb-3">
<label for="citizenship" class="col-md-4 col-form-label text-md-end">Гражданство*</label>
<div class="col-md-6">
<input id="citizenship" type="text" class="form-control @error('citizenship') is-invalid @enderror" name="citizenship" value="{{ old('citizenship', $student['citizenship'] ?? '') }}" required>
@error('citizenship')
<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">
<option value="" disabled {{ empty($student['student_profile']['group_id']) ? 'selected' : '' }} hidden>Не выбрана</option>
@foreach($groups as $group)
<option value="{{ $group['id'] }}"
{{ (isset($student['student_profile']['group_id']) && $student['student_profile']['group_id'] == $group['id']) ? 'selected' : '' }}>
{{ $group['name'] }}
</option>
@endforeach
</select>
@error('group_id')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="row mb-3">
<label for="matriculation_number" class="col-md-4 col-form-label text-md-end">Номер зачетной книжки</label>
<div class="col-md-6">
<input id="matriculation_number" type="text" class="form-control @error('matriculation_number') is-invalid @enderror" name="matriculation_number" value="{{ $student['student_profile']['matriculation_number'] ?? old('matriculation_number') }}">
@error('matriculation_number')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="row mb-0">
<div class="col-md-6 offset-md-4">
<button type="submit" class="btn btn-primary">
{{ isset($student) ? 'Обновить' : 'Создать' }}
</button>
<a href="{{ route('students.index') }}" class="btn btn-secondary">Отмена</a>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,87 @@
@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('students.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">Email</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($students as $student)
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap cursor-pointer" onclick="window.location='{{ route('students.edit', $student['id']) }}'">
<div class="text-sm font-medium text-gray-900">
{{ $student['surname'] }} {{ $student['name'] }} {{ $student['patronymic'] ?? '' }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap cursor-pointer" onclick="window.location='{{ route('students.edit', $student['id']) }}'">
<div class="text-sm text-gray-500">
{{ $groupsList[$student['student_profile']['group_id']] ?? 'Не указана' }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap cursor-pointer" onclick="window.location='{{ route('students.edit', $student['id']) }}'">
<div class="text-sm text-gray-500">{{ $student['email'] }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap cursor-pointer" onclick="window.location='{{ route('students.edit', $student['id']) }}'">
<div class="text-sm text-gray-500">{{ $student['telephone'] }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap cursor-pointer" onclick="window.location='{{ route('students.edit', $student['id']) }}'">
<div class="text-sm text-gray-500">{{ $student['gender'] }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap cursor-pointer" onclick="window.location='{{ route('students.edit', $student['id']) }}'">
<div class="text-sm text-gray-500">{{ \Carbon\Carbon::parse($student['birth_date'])->format('d.m.Y') }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap cursor-pointer" onclick="window.location='{{ route('students.edit', $student['id']) }}'">
<div class="text-sm text-gray-500">{{ $student['citizenship'] }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap cursor-pointer" onclick="window.location='{{ route('students.edit', $student['id']) }}'">
<div class="text-sm text-gray-500">
{{ $student['student_profile']['matriculation_number'] ?? 'Не указан' }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium flex justify-center items-center">
<form action="{{ route('students.destroy', $student['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

@@ -6,6 +6,7 @@ use App\Http\Controllers\DirectionController;
use App\Http\Controllers\DisciplineController;
use App\Http\Controllers\GroupController;
use App\Http\Controllers\StatementController;
use App\Http\Controllers\StatisticController;
use App\Http\Controllers\StudentController;
use Illuminate\Support\Facades\Route;
@@ -21,12 +22,12 @@ Route::controller(AuthController::class)->group(function () {
Route::post('/logout', 'logout')->name('logout');
});
Route::middleware(['auth'])->group(function () {
Route::middleware(['jwt.auth'])->group(function () {
// Dashboard
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
// Students
Route::get('/students', [StudentController::class, 'index'])->name('students.index');
Route::resource('students', StudentController::class)->except(['show']);
// Groups
Route::get('/groups', [GroupController::class, 'index'])->name('groups.index');
@@ -39,4 +40,7 @@ Route::middleware(['auth'])->group(function () {
// Statements
Route::get('/statements', [StatementController::class, 'index'])->name('statements.index');
// Statistics
Route::get('/statistics', [StatisticController::class, 'index'])->name('statistics.index');
});