-
-
Save yuuum4/219aa834dd2717383c58d95d44efaa7d to your computer and use it in GitHub Desktop.
g1_project koizumiブランチの主要ファイル(公開Gist)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# In all environments, the following files are loaded if they exist, | |
# the latter taking precedence over the former: | |
# | |
# * .env contains default values for the environment variables needed by the app | |
# * .env.local uncommitted file with local overrides | |
# * .env.$APP_ENV committed environment-specific defaults | |
# * .env.$APP_ENV.local uncommitted environment-specific overrides | |
# | |
# Real environment variables win over .env files. | |
# | |
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. | |
# https://symfony.com/doc/current/configuration/secrets.html | |
# | |
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). | |
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration | |
###> symfony/framework-bundle ### | |
APP_ENV=dev | |
APP_SECRET=8a4df466ae3148c5536b48b2514c89d9 | |
###< symfony/framework-bundle ### | |
###> doctrine/doctrine-bundle ### | |
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url | |
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml | |
# | |
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db" | |
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8&charset=utf8mb4" | |
DATABASE_URL="postgresql://symfony_user:symfony_password@db:5432/symfony_db?serverVersion=16&charset=utf8" | |
###< doctrine/doctrine-bundle ### | |
###> symfony/messenger ### | |
# Choose one of the transports below | |
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages | |
# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages | |
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0 | |
###< symfony/messenger ### | |
###> symfony/mailer ### | |
MAILER_DSN=smtp://g1.project.system@gmail.com:xznhfgpyzhrlsbqu@smtp.gmail.com:587?encryption=tls | |
###< symfony/mailer ### | |
###> symfony/google-mailer ### | |
# Gmail SHOULD NOT be used on production, use it in development only. | |
# MAILER_DSN=gmail://USERNAME:PASSWORD@default | |
###< symfony/google-mailer ### | |
APP_BASE_URL=https://7806-210-191-216-146.ngrok-free.app |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
MAILER_DSN=smtp://g1.project.system@gmail.com:tlszxsipybozsmpo@smtp.gmail.com:587?encryption=tls&auth_mode=login | |
APP_URL=https://7806-210-191-216-146.ngrok-free.app | |
APP_BASE_URL=https://7806-210-191-216-146.ngrok-free.app | |
APP_ENV=dev #prod | |
APP_DEBUG=1 | |
MESSENGER_TRANSPORT_DSN=sync:// | |
MAILER_ENCRYPTION=tls | |
DATABASE_URL="postgresql://symfony_user:symfony_password@database:5432/symfony_db" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>{% block title %}権限制御管理 - 開発者ツール{% endblock %}</title> | |
<!-- Bootstrap CSS --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"> | |
<!-- Font Awesome --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<style> | |
body { | |
background-color: #f4c2a1; | |
min-height: 100vh; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
margin: 0; | |
padding: 0; | |
} | |
.header { | |
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%); | |
color: white; | |
padding: 15px 40px; | |
font-size: 1.6rem; | |
font-weight: bold; | |
position: relative; | |
} | |
.back-btn { | |
position: absolute; | |
right: 20px; | |
top: 50%; | |
transform: translateY(-50%); | |
background-color: #6c757d; | |
color: white; | |
border: none; | |
padding: 8px 16px; | |
border-radius: 4px; | |
text-decoration: none; | |
font-size: 0.9rem; | |
transition: background-color 0.3s ease; | |
} | |
.back-btn:hover { | |
background-color: #5a6268; | |
color: white; | |
text-decoration: none; | |
} | |
.main-container { | |
padding: 20px; | |
max-width: 800px; | |
margin: 0 auto; | |
} | |
.control-card { | |
background: white; | |
border-radius: 8px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
padding: 30px; | |
margin-bottom: 20px; | |
} | |
.control-title { | |
color: #2c3e50; | |
font-size: 2rem; | |
font-weight: bold; | |
margin-bottom: 20px; | |
text-align: center; | |
} | |
.developer-badge { | |
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%); | |
color: white; | |
padding: 8px 16px; | |
border-radius: 20px; | |
font-size: 0.9rem; | |
font-weight: bold; | |
display: inline-block; | |
margin-bottom: 20px; | |
} | |
.crown-icon { | |
color: #f1c40f; | |
margin-right: 5px; | |
} | |
.current-status { | |
padding: 20px; | |
border-radius: 8px; | |
margin-bottom: 30px; | |
text-align: center; | |
} | |
.status-enabled { | |
background: #fee; | |
border: 2px solid #e74c3c; | |
color: #c0392b; | |
} | |
.status-disabled { | |
background: #efe; | |
border: 2px solid #27ae60; | |
color: #1e8449; | |
} | |
.status-icon { | |
font-size: 2rem; | |
margin-bottom: 10px; | |
} | |
.control-form { | |
background: #f8f9fa; | |
padding: 25px; | |
border-radius: 8px; | |
margin-bottom: 20px; | |
} | |
.radio-group { | |
display: flex; | |
gap: 30px; | |
justify-content: center; | |
margin: 20px 0; | |
} | |
.radio-option { | |
display: flex; | |
align-items: center; | |
gap: 10px; | |
padding: 15px 20px; | |
border: 2px solid #dee2e6; | |
border-radius: 8px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
min-width: 200px; | |
} | |
.radio-option:hover { | |
border-color: #2c3e50; | |
background: #f8f9fa; | |
} | |
.radio-option.selected { | |
border-color: #2c3e50; | |
background: #e8f4f8; | |
} | |
.radio-option input[type="radio"] { | |
margin: 0; | |
transform: scale(1.2); | |
} | |
.radio-label { | |
font-weight: bold; | |
font-size: 1.1rem; | |
} | |
.radio-description { | |
font-size: 0.9rem; | |
color: #6c757d; | |
margin-top: 5px; | |
} | |
.save-btn { | |
background-color: #2c3e50; | |
color: white; | |
border: none; | |
padding: 12px 30px; | |
font-size: 1.1rem; | |
font-weight: bold; | |
border-radius: 4px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
width: 100%; | |
} | |
.save-btn:hover { | |
background-color: #34495e; | |
transform: translateY(-1px); | |
} | |
.info-section { | |
background: #e3f2fd; | |
border: 1px solid #bbdefb; | |
padding: 20px; | |
border-radius: 8px; | |
margin-bottom: 20px; | |
} | |
.info-title { | |
color: #1565c0; | |
font-weight: bold; | |
margin-bottom: 10px; | |
} | |
.info-list { | |
color: #1976d2; | |
margin: 0; | |
padding-left: 20px; | |
} | |
.last-updated { | |
background: #f8f9fa; | |
padding: 15px; | |
border-radius: 6px; | |
margin-top: 20px; | |
font-size: 0.9rem; | |
color: #6c757d; | |
} | |
.alert { | |
padding: 15px; | |
border-radius: 6px; | |
margin-bottom: 20px; | |
} | |
.alert-success { | |
background: #d4edda; | |
color: #155724; | |
border: 1px solid #c3e6cb; | |
} | |
@media (max-width: 768px) { | |
.header { | |
padding: 10px 15px; | |
font-size: 1.3rem; | |
} | |
.back-btn { | |
position: static; | |
transform: none; | |
margin-top: 10px; | |
display: block; | |
width: 100px; | |
margin-left: auto; | |
} | |
.main-container { | |
padding: 10px; | |
} | |
.control-title { | |
font-size: 1.5rem; | |
} | |
.radio-group { | |
flex-direction: column; | |
gap: 15px; | |
} | |
.radio-option { | |
min-width: auto; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="header"> | |
<i class="fas fa-shield-alt crown-icon"></i>権限制御管理 | |
<a href="{{ path('app_developer_home') }}" class="back-btn"> | |
<i class="fas fa-arrow-left"></i> 戻る | |
</a> | |
</div> | |
<div class="main-container"> | |
{% for message in app.flashes('success') %} | |
<div class="alert alert-success"> | |
<i class="fas fa-check-circle"></i> {{ message }} | |
</div> | |
{% endfor %} | |
<div class="control-card"> | |
<div class="text-center"> | |
<div class="developer-badge"> | |
<i class="fas fa-crown crown-icon"></i>DEVELOPER ONLY | |
</div> | |
</div> | |
<h1 class="control-title">システム権限制御管理</h1> | |
<!-- 現在の状態表示 --> | |
<div class="current-status {% if access_control_enabled %}status-enabled{% else %}status-disabled{% endif %}"> | |
<div class="status-icon"> | |
{% if access_control_enabled %} | |
<i class="fas fa-lock"></i> | |
{% else %} | |
<i class="fas fa-unlock"></i> | |
{% endif %} | |
</div> | |
<h3> | |
{% if access_control_enabled %} | |
権限制御: 適用中 | |
{% else %} | |
権限制御: 解除中 | |
{% endif %} | |
</h3> | |
<p> | |
{% if access_control_enabled %} | |
各ユーザーは自分の権限に応じたページのみアクセス可能です | |
{% else %} | |
全ユーザーが開発者画面以外の全ページにアクセス可能です | |
{% endif %} | |
</p> | |
</div> | |
<!-- 制御フォーム --> | |
<form method="post" class="control-form"> | |
<h4 style="text-align: center; margin-bottom: 20px; color: #2c3e50;"> | |
<i class="fas fa-cog"></i> 権限制御設定 | |
</h4> | |
<div class="radio-group"> | |
<label class="radio-option {% if access_control_enabled %}selected{% endif %}"> | |
<input type="radio" name="access_control_enabled" value="true" {% if access_control_enabled %}checked{% endif %}> | |
<div> | |
<div class="radio-label"> | |
<i class="fas fa-lock" style="color: #e74c3c;"></i> 適用 | |
</div> | |
<div class="radio-description"> | |
厳格な権限制御を適用<br> | |
各ユーザーは自分の権限のページのみアクセス可能 | |
</div> | |
</div> | |
</label> | |
<label class="radio-option {% if not access_control_enabled %}selected{% endif %}"> | |
<input type="radio" name="access_control_enabled" value="false" {% if not access_control_enabled %}checked{% endif %}> | |
<div> | |
<div class="radio-label"> | |
<i class="fas fa-unlock" style="color: #27ae60;"></i> 解除 | |
</div> | |
<div class="radio-description"> | |
権限制御を一時解除<br> | |
全ユーザーが全ページにアクセス可能 | |
</div> | |
</div> | |
</label> | |
</div> | |
<button type="submit" class="save-btn"> | |
<i class="fas fa-save"></i> 設定を保存 | |
</button> | |
</form> | |
<!-- 説明セクション --> | |
<div class="info-section"> | |
<div class="info-title"> | |
<i class="fas fa-info-circle"></i> 権限制御について | |
</div> | |
<ul class="info-list"> | |
<li><strong>適用時:</strong> 管理者→管理者ページのみ、教員→教員ページのみ、学生→学生ページのみ</li> | |
<li><strong>解除時:</strong> 全ユーザーが開発者画面以外の全ページにアクセス可能</li> | |
<li><strong>開発者:</strong> 常に全ページにアクセス可能(設定に関係なく)</li> | |
<li><strong>即座反映:</strong> 設定変更は即座にシステム全体に反映されます</li> | |
</ul> | |
</div> | |
{% if last_updated %} | |
<div class="last-updated"> | |
<i class="fas fa-clock"></i> | |
最終更新: {{ last_updated.updatedAt|date('Y年m月d日 H:i:s') }} | |
{% if last_updated.updatedBy %} | |
by {{ last_updated.updatedBy }} | |
{% endif %} | |
</div> | |
{% endif %} | |
</div> | |
</div> | |
<!-- Bootstrap JS --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
const radioOptions = document.querySelectorAll('.radio-option'); | |
const radioInputs = document.querySelectorAll('input[type="radio"]'); | |
// ラジオボタンの見た目を更新 | |
function updateRadioStyles() { | |
radioOptions.forEach(option => { | |
const input = option.querySelector('input[type="radio"]'); | |
if (input.checked) { | |
option.classList.add('selected'); | |
} else { | |
option.classList.remove('selected'); | |
} | |
}); | |
} | |
// ラジオボタンクリック時の処理 | |
radioInputs.forEach(input => { | |
input.addEventListener('change', updateRadioStyles); | |
}); | |
// ラベル全体をクリック可能にする | |
radioOptions.forEach(option => { | |
option.addEventListener('click', function() { | |
const input = this.querySelector('input[type="radio"]'); | |
input.checked = true; | |
updateRadioStyles(); | |
}); | |
}); | |
// 初期状態の設定 | |
updateRadioStyles(); | |
// フォーム送信時の確認 | |
const form = document.querySelector('form'); | |
form.addEventListener('submit', function(e) { | |
const selectedValue = document.querySelector('input[name="access_control_enabled"]:checked').value; | |
const isEnabled = selectedValue === 'true'; | |
let message; | |
if (isEnabled) { | |
message = '権限制御を「適用」に設定します。\n\n各ユーザーは自分の権限に応じたページのみアクセス可能になります。\n\n続行しますか?'; | |
} else { | |
message = '権限制御を「解除」に設定します。\n\n全ユーザーが開発者画面以外の全ページにアクセス可能になります。\n\n続行しますか?'; | |
} | |
if (!confirm(message)) { | |
e.preventDefault(); | |
} | |
}); | |
}); | |
</script> | |
</body> | |
</html> | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use App\Repository\SystemSettingRepository; | |
use Doctrine\ORM\EntityManagerInterface; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\Request; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Annotation\Route; | |
class AccessControlController extends AbstractController | |
{ | |
public function __construct( | |
private SystemSettingRepository $systemSettingRepository, | |
private EntityManagerInterface $entityManager | |
) {} | |
#[Route('/developer/access-control', name: 'app_developer_access_control')] | |
public function accessControl(Request $request): Response | |
{ | |
// 開発者権限チェック | |
$this->denyAccessUnlessGranted('ROLE_DEVELOPER'); | |
if ($request->isMethod('POST')) { | |
$accessControlEnabled = $request->request->get('access_control_enabled'); | |
$userEmail = $this->getUser()->getEmail(); | |
// 設定を保存 | |
$this->systemSettingRepository->setSetting( | |
'access_control_enabled', | |
$accessControlEnabled === 'true' ? 'true' : 'false', | |
$userEmail | |
); | |
$this->addFlash('success', '権限制御設定が正常に更新されました。'); | |
return $this->redirectToRoute('app_developer_access_control'); | |
} | |
// 現在の設定を取得 | |
$currentSetting = $this->systemSettingRepository->getSettingValue('access_control_enabled', 'true'); | |
$lastUpdated = $this->systemSettingRepository->findByKey('access_control_enabled'); | |
return $this->render('developer/access_control.html.twig', [ | |
'access_control_enabled' => $currentSetting === 'true', | |
'last_updated' => $lastUpdated, | |
]); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\JsonResponse; | |
use Symfony\Component\Routing\Annotation\Route; | |
use Symfony\Component\HttpFoundation\Request; | |
class AccessControlStatusController extends AbstractController | |
{ | |
#[Route('/api/access-control/status', name: 'api_access_control_status', methods: ['GET'])] | |
public function getStatus(Request $request): JsonResponse | |
{ | |
try { | |
// ★★ 実際のシステム権限制御の状態を取得 ★★ | |
$session = $request->getSession(); | |
// システム権限制御管理画面で設定された状態を取得 | |
// デフォルトは false(解除中)に変更 | |
$accessControlEnabled = $session->get('system_access_control_enabled', false); | |
// 環境変数からも確認(開発環境では通常は無効) | |
$envAccessControl = $_ENV['APP_ACCESS_CONTROL_ENABLED'] ?? 'false'; | |
$envEnabled = ($envAccessControl === 'true' || $envAccessControl === '1'); | |
// セッションの値を優先、なければ環境変数の値を使用 | |
$finalEnabled = $session->has('system_access_control_enabled') ? $accessControlEnabled : $envEnabled; | |
return new JsonResponse([ | |
'enabled' => $finalEnabled, | |
'status' => $finalEnabled ? 'active' : 'disabled', | |
'message' => $finalEnabled ? | |
'システム権限制御が適用されています' : | |
'システム権限制御が解除されています', | |
'source' => $session->has('system_access_control_enabled') ? 'session' : 'environment', | |
'session_value' => $accessControlEnabled, | |
'env_value' => $envEnabled, | |
'timestamp' => date('Y-m-d H:i:s') | |
]); | |
} catch (\Exception $e) { | |
return new JsonResponse([ | |
'enabled' => false, // エラー時は解除状態をデフォルトとする | |
'status' => 'error', | |
'message' => 'アクセス制御状態の取得に失敗しました', | |
'error' => $e->getMessage(), | |
'timestamp' => date('Y-m-d H:i:s') | |
], 500); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Security; | |
use App\Repository\SystemSettingRepository; | |
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; | |
use Symfony\Component\Security\Core\Authorization\Voter\Voter; | |
use Symfony\Component\Security\Core\User\UserInterface; | |
use Symfony\Component\HttpFoundation\RequestStack; | |
class AccessControlVoter extends Voter | |
{ | |
public const ACCESS_ADMIN = 'ACCESS_ADMIN'; | |
public const ACCESS_TEACHER = 'ACCESS_TEACHER'; | |
public const ACCESS_STUDENT = 'ACCESS_STUDENT'; | |
public const ACCESS_DATABASE = 'ACCESS_DATABASE'; | |
public function __construct( | |
private SystemSettingRepository $systemSettingRepository, | |
private RequestStack $requestStack | |
) {} | |
protected function supports(string $attribute, mixed $subject): bool | |
{ | |
// ★★ マジックリンク関連とauth-checkパスでは権限チェックをスキップ ★★ | |
$request = $this->requestStack->getCurrentRequest(); | |
if ($request) { | |
$path = $request->getPathInfo(); | |
if (str_starts_with($path, '/magic-') || str_starts_with($path, '/auth-check')) { | |
return false; | |
} | |
} | |
return in_array($attribute, [ | |
self::ACCESS_ADMIN, | |
self::ACCESS_TEACHER, | |
self::ACCESS_STUDENT, | |
self::ACCESS_DATABASE, | |
]); | |
} | |
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool | |
{ | |
$user = $token->getUser(); | |
if (!$user instanceof UserInterface) { | |
return false; | |
} | |
// 開発者は常に全アクセス可能 | |
if (in_array('ROLE_DEVELOPER', $user->getRoles())) { | |
return true; | |
} | |
// 権限制限が無効化されている場合は全ユーザーがアクセス可能 | |
$accessControlEnabled = $this->systemSettingRepository->getSettingValue('access_control_enabled', 'true'); | |
if ($accessControlEnabled === 'false') { | |
return true; | |
} | |
// 権限制限が有効な場合の厳格なアクセス制御 | |
return match ($attribute) { | |
self::ACCESS_ADMIN => in_array('ROLE_ADMIN', $user->getRoles()), | |
self::ACCESS_TEACHER => in_array('ROLE_TEACHER', $user->getRoles()), | |
self::ACCESS_STUDENT => in_array('ROLE_STUDENT', $user->getRoles()), | |
self::ACCESS_DATABASE => in_array('ROLE_ADMIN', $user->getRoles()), | |
default => false, | |
}; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>{% block title %}ユーザー追加 - 時間割管理システム{% endblock %}</title> | |
<!-- Bootstrap CSS --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"> | |
<!-- Font Awesome --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<style> | |
body { | |
background-color: #f4c2a1; | |
min-height: 100vh; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
margin: 0; | |
padding: 0; | |
} | |
.header { | |
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); | |
color: white; | |
padding: 15px 40px; | |
font-size: 1.6rem; | |
font-weight: bold; | |
position: relative; | |
} | |
.main-container { | |
padding: 20px; | |
max-width: 600px; | |
margin: 0 auto; | |
} | |
.add-user-container { | |
background: white; | |
border-radius: 8px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
overflow: hidden; | |
} | |
.add-user-header { | |
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); | |
color: white; | |
padding: 20px; | |
text-align: center; | |
} | |
.add-user-header h1 { | |
margin: 0; | |
font-size: 2rem; | |
font-weight: bold; | |
} | |
.form-container { | |
padding: 30px; | |
} | |
.form-group { | |
margin-bottom: 20px; | |
} | |
.form-label { | |
color: #333; | |
font-size: 1rem; | |
font-weight: 500; | |
margin-bottom: 8px; | |
display: block; | |
} | |
.required { | |
color: #e74c3c; | |
} | |
.form-control { | |
width: 100%; | |
padding: 12px 16px; | |
font-size: 1rem; | |
border: 2px solid #dee2e6; | |
border-radius: 4px; | |
background-color: white; | |
color: #333; | |
transition: border-color 0.3s ease; | |
} | |
.form-control:focus { | |
outline: none; | |
border-color: #4a90e2; | |
box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1); | |
} | |
.form-select { | |
width: 100%; | |
padding: 12px 16px; | |
font-size: 1rem; | |
border: 2px solid #dee2e6; | |
border-radius: 4px; | |
background-color: white; | |
color: #333; | |
cursor: pointer; | |
} | |
.form-select:focus { | |
outline: none; | |
border-color: #4a90e2; | |
box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1); | |
} | |
.submit-btn { | |
background-color: #4a90e2; | |
color: white; | |
border: none; | |
padding: 12px 30px; | |
font-size: 1.1rem; | |
font-weight: bold; | |
border-radius: 4px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
width: 100%; | |
margin-top: 10px; | |
} | |
.submit-btn:hover { | |
background-color: #357abd; | |
transform: translateY(-1px); | |
} | |
.submit-btn:disabled { | |
background-color: #bdc3c7; | |
cursor: not-allowed; | |
transform: none; | |
} | |
.back-btn { | |
background-color: #6c757d; | |
color: white; | |
border: none; | |
padding: 12px 30px; | |
font-size: 1.1rem; | |
font-weight: bold; | |
border-radius: 4px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
width: 100%; | |
margin-top: 10px; | |
text-decoration: none; | |
display: inline-block; | |
text-align: center; | |
} | |
.back-btn:hover { | |
background-color: #5a6268; | |
transform: translateY(-1px); | |
color: white; | |
text-decoration: none; | |
} | |
.alert { | |
padding: 15px; | |
border-radius: 4px; | |
margin-bottom: 20px; | |
} | |
.alert-success { | |
background-color: #d4edda; | |
color: #155724; | |
border: 1px solid #c3e6cb; | |
} | |
.alert-danger { | |
background-color: #f8d7da; | |
color: #721c24; | |
border: 1px solid #f5c6cb; | |
} | |
.role-info { | |
background: #e3f2fd; | |
border: 1px solid #bbdefb; | |
padding: 15px; | |
border-radius: 4px; | |
margin-top: 10px; | |
font-size: 0.9rem; | |
color: #1565c0; | |
} | |
.developer-note { | |
background: #fff3cd; | |
border: 1px solid #ffeaa7; | |
padding: 15px; | |
border-radius: 4px; | |
margin-bottom: 20px; | |
font-size: 0.9rem; | |
color: #856404; | |
} | |
.developer-note strong { | |
color: #b8860b; | |
} | |
@media (max-width: 768px) { | |
.header { | |
padding: 10px 15px; | |
font-size: 1.3rem; | |
} | |
.main-container { | |
padding: 10px; | |
} | |
.add-user-header h1 { | |
font-size: 1.5rem; | |
} | |
.form-container { | |
padding: 20px; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="header"> | |
ユーザー追加 | |
</div> | |
<div class="main-container"> | |
<div class="add-user-container"> | |
<div class="add-user-header"> | |
<h1><i class="fas fa-user-plus"></i> 新規ユーザー追加</h1> | |
</div> | |
<div class="form-container"> | |
{% if success is defined %} | |
<div class="alert alert-success"> | |
<i class="fas fa-check-circle"></i> {{ success }} | |
</div> | |
{% endif %} | |
{% if error is defined %} | |
<div class="alert alert-danger"> | |
<i class="fas fa-exclamation-triangle"></i> {{ error }} | |
</div> | |
{% endif %} | |
<form method="post" id="addUserForm"> | |
<div class="form-group"> | |
<label class="form-label" for="lastName"> | |
姓 | |
</label> | |
<input type="text" | |
class="form-control" | |
id="lastName" | |
name="lastName" | |
placeholder="山田" | |
value="{{ formData.lastName ?? '' }}"> | |
</div> | |
<div class="form-group"> | |
<label class="form-label" for="firstName"> | |
名 | |
</label> | |
<input type="text" | |
class="form-control" | |
id="firstName" | |
name="firstName" | |
placeholder="太郎" | |
value="{{ formData.firstName ?? '' }}"> | |
</div> | |
<div class="form-group"> | |
<label class="form-label" for="email"> | |
メールアドレス <span class="required">*</span> | |
</label> | |
<input type="email" | |
class="form-control" | |
id="email" | |
name="email" | |
placeholder="user@example.com" | |
value="{{ formData.email ?? '' }}" | |
required> | |
</div> | |
<div class="form-group"> | |
<label class="form-label" for="password"> | |
パスワード <span class="required">*</span> | |
</label> | |
<input type="password" | |
class="form-control" | |
id="password" | |
name="password" | |
placeholder="4文字以上のパスワード" | |
required> | |
</div> | |
<div class="form-group"> | |
<label class="form-label" for="role"> | |
ユーザー権限 <span class="required">*</span> | |
</label> | |
<select class="form-select" id="role" name="role" required> | |
<option value="">権限を選択してください</option> | |
<option value="admin" {{ (formData.role ?? '') == 'admin' ? 'selected' : '' }}>管理者</option> | |
<option value="teacher" {{ (formData.role ?? '') == 'teacher' ? 'selected' : '' }}>教員</option> | |
<option value="student" {{ (formData.role ?? '') == 'student' ? 'selected' : '' }}>学生</option> | |
</select> | |
</div> | |
<button type="submit" class="submit-btn" id="submitBtn"> | |
<i class="fas fa-user-plus"></i> ユーザーを追加 | |
</button> | |
<a href="{{ path('app_user_database') }}" class="back-btn"> | |
<i class="fas fa-arrow-left"></i> ユーザー一覧に戻る | |
</a> | |
</form> | |
</div> | |
</div> | |
</div> | |
<!-- Bootstrap JS --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
const form = document.getElementById('addUserForm'); | |
const emailInput = document.getElementById('email'); | |
const passwordInput = document.getElementById('password'); | |
const roleSelect = document.getElementById('role'); | |
const submitBtn = document.getElementById('submitBtn'); | |
// フォーム送信時の処理 | |
form.addEventListener('submit', function(e) { | |
// バリデーション | |
if (!validateForm()) { | |
e.preventDefault(); | |
return; | |
} | |
// 送信中の状態に変更 | |
submitBtn.disabled = true; | |
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 追加中...'; | |
}); | |
// リアルタイムバリデーション | |
emailInput.addEventListener('blur', function() { | |
validateEmail(this.value); | |
}); | |
passwordInput.addEventListener('blur', function() { | |
validatePassword(this.value); | |
}); | |
roleSelect.addEventListener('change', function() { | |
validateRole(this.value); | |
}); | |
// バリデーション関数 | |
function validateForm() { | |
let isValid = true; | |
if (!validateEmail(emailInput.value)) { | |
isValid = false; | |
} | |
if (!validatePassword(passwordInput.value)) { | |
isValid = false; | |
} | |
if (!validateRole(roleSelect.value)) { | |
isValid = false; | |
} | |
return isValid; | |
} | |
function validateEmail(email) { | |
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; | |
if (!email) { | |
showFieldError(emailInput, 'メールアドレスが必要です'); | |
return false; | |
} else if (!emailRegex.test(email)) { | |
showFieldError(emailInput, '有効なメールアドレスを入力してください'); | |
return false; | |
} else { | |
clearFieldError(emailInput); | |
return true; | |
} | |
} | |
function validatePassword(password) { | |
if (!password) { | |
showFieldError(passwordInput, 'パスワードが必要です'); | |
return false; | |
} else if (password.length < 4) { | |
showFieldError(passwordInput, 'パスワードは4文字以上である必要があります'); | |
return false; | |
} else { | |
clearFieldError(passwordInput); | |
return true; | |
} | |
} | |
function validateRole(role) { | |
if (!role) { | |
showFieldError(roleSelect, '権限を選択してください'); | |
return false; | |
} else { | |
clearFieldError(roleSelect); | |
return true; | |
} | |
} | |
// エラー表示関数 | |
function showFieldError(field, message) { | |
field.style.borderColor = '#e74c3c'; | |
// 既存のエラーメッセージを削除 | |
const existingError = field.parentNode.querySelector('.field-error'); | |
if (existingError) { | |
existingError.remove(); | |
} | |
// 新しいエラーメッセージを追加 | |
const errorDiv = document.createElement('div'); | |
errorDiv.className = 'field-error'; | |
errorDiv.style.color = '#e74c3c'; | |
errorDiv.style.fontSize = '0.9rem'; | |
errorDiv.style.marginTop = '5px'; | |
errorDiv.innerHTML = '<i class="fas fa-exclamation-circle"></i> ' + message; | |
field.parentNode.appendChild(errorDiv); | |
} | |
function clearFieldError(field) { | |
field.style.borderColor = '#dee2e6'; | |
// エラーメッセージを削除 | |
const existingError = field.parentNode.querySelector('.field-error'); | |
if (existingError) { | |
existingError.remove(); | |
} | |
} | |
// 成功メッセージがある場合、3秒後にフェードアウト | |
const successAlert = document.querySelector('.alert-success'); | |
if (successAlert) { | |
setTimeout(() => { | |
successAlert.style.transition = 'opacity 0.5s ease'; | |
successAlert.style.opacity = '0'; | |
setTimeout(() => { | |
successAlert.remove(); | |
}, 500); | |
}, 3000); | |
} | |
}); | |
</script> | |
</body> | |
</html> | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use App\Entity\User; | |
use App\Security\AccessControlVoter; | |
use Doctrine\ORM\EntityManagerInterface; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\Request; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; | |
use Symfony\Component\Routing\Annotation\Route; | |
class AddUserController extends AbstractController | |
{ | |
public function __construct( | |
private EntityManagerInterface $entityManager, | |
private UserPasswordHasherInterface $passwordHasher | |
) {} | |
#[Route('/add-user', name: 'app_add_user')] | |
public function addUser(Request $request): Response | |
{ | |
$this->denyAccessUnlessGranted(AccessControlVoter::ACCESS_DATABASE); | |
if ($request->isMethod('POST')) { | |
$email = $request->request->get('email'); | |
$password = $request->request->get('password'); | |
$lastName = $request->request->get('lastName'); | |
$firstName = $request->request->get('firstName'); | |
$role = $request->request->get('role'); | |
// バリデーション | |
if (empty($email) || empty($password) || empty($role)) { | |
return $this->render('add_user/add_user.html.twig', [ | |
'error' => '必須項目を入力してください。', | |
'formData' => [ | |
'email' => $email, | |
'lastName' => $lastName, | |
'firstName' => $firstName, | |
'role' => $role, | |
] | |
]); | |
} | |
// メールアドレスの重複チェック | |
$existingUser = $this->entityManager | |
->getRepository(User::class) | |
->findOneBy(['email' => $email]); | |
if ($existingUser) { | |
return $this->render('add_user/add_user.html.twig', [ | |
'error' => 'このメールアドレスは既に登録されています。', | |
'formData' => [ | |
'email' => $email, | |
'lastName' => $lastName, | |
'firstName' => $firstName, | |
'role' => $role, | |
] | |
]); | |
} | |
// 新しいユーザーを作成 | |
$user = new User(); | |
$user->setEmail($email); | |
$user->setLastName($lastName); | |
$user->setFirstName($firstName); | |
// パスワードをハッシュ化 | |
$hashedPassword = $this->passwordHasher->hashPassword($user, $password); | |
$user->setPassword($hashedPassword); | |
// ★★ ロールを設定(4つのロールのみ対応)★★ | |
switch ($role) { | |
case 'admin': | |
$user->setRoles(['ROLE_ADMIN']); | |
break; | |
case 'teacher': | |
$user->setRoles(['ROLE_TEACHER']); | |
break; | |
case 'student': | |
$user->setRoles(['ROLE_STUDENT']); // ★★ ここを修正 ★★ | |
break; | |
case 'developer': | |
$user->setRoles(['ROLE_DEVELOPER']); | |
break; | |
default: | |
$user->setRoles(['ROLE_STUDENT']); // ★★ デフォルトも学生に ★★ | |
break; | |
} | |
// メール認証済みに設定 | |
$user->setIsVerified(true); | |
// データベースに保存 | |
$this->entityManager->persist($user); | |
$this->entityManager->flush(); | |
return $this->render('add_user/add_user.html.twig', [ | |
'success' => 'ユーザー「' . $email . '」を正常に追加しました。', | |
]); | |
} | |
return $this->render('add_user/add_user.html.twig'); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>{% block title %}アカウント管理 - 時間割管理システム{% endblock %}</title> | |
<!-- Bootstrap CSS --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"> | |
<!-- Font Awesome --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<style> | |
body { | |
margin: 0; | |
padding: 0; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
background-color: #f4c2a1; | |
min-height: 100vh; | |
} | |
.header { | |
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); | |
color: white; | |
padding: 15px 30px; | |
font-size: 1.6rem; | |
font-weight: bold; | |
} | |
.main-container { | |
padding: 20px 30px; | |
max-width: 1400px; | |
margin: 0 auto; | |
height: calc(100vh - 70px); | |
overflow: hidden; | |
display: flex; | |
gap: 20px; | |
} | |
.accounts-section { | |
flex: 1; | |
} | |
.accounts-table-container { | |
background: white; | |
border-radius: 8px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
overflow: hidden; | |
height: calc(100vh - 180px); | |
overflow-y: auto; | |
} | |
.accounts-table { | |
width: 100%; | |
border-collapse: collapse; | |
font-size: 0.9rem; | |
} | |
.accounts-table th { | |
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); | |
color: white; | |
padding: 12px 15px; | |
text-align: center; | |
font-weight: bold; | |
font-size: 1rem; | |
position: sticky; | |
top: 0; | |
z-index: 10; | |
} | |
.accounts-table td { | |
padding: 10px 15px; | |
text-align: center; | |
border-bottom: 1px solid #ddd; | |
background: #f8f9fa; | |
font-weight: 500; | |
vertical-align: middle; | |
} | |
.accounts-table tbody tr:nth-child(odd) { | |
background: #e9ecef; | |
} | |
.accounts-table tbody tr:nth-child(even) { | |
background: #f8f9fa; | |
} | |
.accounts-table tbody tr:hover { | |
background: #e3f2fd; | |
cursor: pointer; | |
} | |
.accounts-table td.username { | |
text-align: left; | |
font-family: monospace; | |
font-size: 0.85rem; | |
} | |
.accounts-table td.name { | |
text-align: left; | |
} | |
.permission-badge { | |
padding: 4px 10px; | |
border-radius: 4px; | |
font-weight: bold; | |
font-size: 0.85rem; | |
} | |
.permission-badge.admin { | |
background-color: #f8d7da; | |
color: #721c24; | |
border: 1px solid #f5c6cb; | |
} | |
.permission-badge.teacher { | |
background-color: #d1ecf1; | |
color: #0c5460; | |
border: 1px solid #bee5eb; | |
} | |
.permission-badge.student { | |
background-color: #d4edda; | |
color: #155724; | |
border: 1px solid #c3e6cb; | |
} | |
.controls-section { | |
width: 250px; | |
display: flex; | |
flex-direction: column; | |
gap: 10px; | |
} | |
.control-group { | |
background: white; | |
padding: 15px; | |
border-radius: 8px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
display: flex; | |
flex-direction: column; | |
gap: 10px; | |
} | |
.action-btn { | |
background: #f1c40f; | |
color: #333; | |
border: none; | |
padding: 12px 15px; | |
font-size: 0.9rem; | |
font-weight: bold; | |
border-radius: 4px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
text-align: center; | |
} | |
.action-btn:hover { | |
background: #d4ac0d; | |
transform: translateY(-1px); | |
} | |
.back-btn { | |
background: #6c757d; | |
color: white; | |
border: none; | |
padding: 10px 25px; | |
font-size: 0.9rem; | |
font-weight: bold; | |
border-radius: 4px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
text-decoration: none; | |
display: inline-block; | |
text-align: center; | |
margin-top: auto; | |
} | |
.back-btn:hover { | |
background: #5a6268; | |
transform: translateY(-1px); | |
color: white; | |
} | |
.empty-row { | |
background: #f1f3f4 !important; | |
} | |
.empty-row:hover { | |
background: #f1f3f4 !important; | |
cursor: default; | |
} | |
.empty-cell { | |
color: #999; | |
font-style: italic; | |
} | |
@media (max-width: 1200px) { | |
.main-container { | |
flex-direction: column; | |
padding: 15px 20px; | |
} | |
.controls-section { | |
width: 100%; | |
order: -1; | |
} | |
.control-group { | |
flex-direction: row; | |
justify-content: center; | |
gap: 15px; | |
} | |
.action-btn { | |
flex: 1; | |
min-width: 150px; | |
} | |
.accounts-table-container { | |
height: calc(100vh - 220px); | |
} | |
} | |
@media (max-width: 768px) { | |
.header { | |
padding: 10px 15px; | |
font-size: 1.3rem; | |
} | |
.main-container { | |
padding: 10px 15px; | |
height: calc(100vh - 60px); | |
} | |
.accounts-table-container { | |
height: calc(100vh - 180px); | |
} | |
.accounts-table { | |
font-size: 0.75rem; | |
} | |
.accounts-table th, | |
.accounts-table td { | |
padding: 8px 6px; | |
} | |
.accounts-table th { | |
font-size: 0.85rem; | |
} | |
.accounts-table td.username { | |
font-size: 0.7rem; | |
} | |
.permission-badge { | |
padding: 3px 6px; | |
font-size: 0.7rem; | |
} | |
.control-group { | |
flex-direction: column; | |
gap: 10px; | |
} | |
.action-btn { | |
font-size: 0.8rem; | |
padding: 10px 12px; | |
min-width: auto; | |
} | |
.back-btn { | |
padding: 8px 20px; | |
font-size: 0.8rem; | |
} | |
} | |
@media (max-width: 480px) { | |
.accounts-table { | |
font-size: 0.7rem; | |
} | |
.accounts-table th, | |
.accounts-table td { | |
padding: 6px 4px; | |
} | |
.accounts-table td.username { | |
font-size: 0.65rem; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="header"> | |
アカウント管理 | |
</div> | |
<div class="main-container"> | |
<div class="accounts-section"> | |
<div class="accounts-table-container"> | |
<table class="accounts-table"> | |
<thead> | |
<tr> | |
<th>username</th> | |
<th>氏名</th> | |
<th>権限</th> | |
</tr> | |
</thead> | |
<tbody id="accountsTableBody"> | |
<!-- Dummy data - replace with database content --> | |
<tr onclick="showAccountDetails('nk12345@stu.tomakomai-ct.ac.jp', '中村管理', '管理者')"> | |
<td class="username">nk12345@stu.tomakomai-ct.ac.jp</td> | |
<td class="name">中村管理</td> | |
<td><span class="permission-badge admin">管理者</span></td> | |
</tr> | |
<tr onclick="showAccountDetails('jr99999@stu.tomakomai-ct.ac.jp', '実験レポート', '管理者')"> | |
<td class="username">jr99999@stu.tomakomai-ct.ac.jp</td> | |
<td class="name">実験レポート</td> | |
<td><span class="permission-badge admin">管理者</span></td> | |
</tr> | |
<tr onclick="showAccountDetails('pp22222@stu.tomakomai-ct.ac.jp', 'ひかびかピカチュウ', '教員')"> | |
<td class="username">pp22222@stu.tomakomai-ct.ac.jp</td> | |
<td class="name">ひかびかピカチュウ</td> | |
<td><span class="permission-badge teacher">教員</span></td> | |
</tr> | |
<tr onclick="showAccountDetails('nk54321@stu.tomakomai-ct.ac.jp', '中村教員', '教員')"> | |
<td class="username">nk54321@stu.tomakomai-ct.ac.jp</td> | |
<td class="name">中村教員</td> | |
<td><span class="permission-badge teacher">教員</span></td> | |
</tr> | |
<tr onclick="showAccountDetails('ns13579@stu.tomakomai-ct.ac.jp', '中村生徒', '生徒')"> | |
<td class="username">ns13579@stu.tomakomai-ct.ac.jp</td> | |
<td class="name">中村生徒</td> | |
<td><span class="permission-badge student">生徒</span></td> | |
</tr> | |
<tr onclick="showAccountDetails('rr11111@stu.tomakomai-ct.ac.jp', '林檎リンゴ', '生徒')"> | |
<td class="username">rr11111@stu.tomakomai-ct.ac.jp</td> | |
<td class="name">林檎リンゴ</td> | |
<td><span class="permission-badge student">生徒</span></td> | |
</tr> | |
<!-- Empty rows for spacing --> | |
<tr class="empty-row"> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
</tr> | |
<tr class="empty-row"> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
</tr> | |
<tr class="empty-row"> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
</tr> | |
</tbody> | |
</table> | |
</div> | |
</div> | |
<div class="controls-section"> | |
<div class="control-group"> | |
<button class="action-btn" id="addAccountBtn">アカウント追加</button> | |
<button class="action-btn" id="editAccountBtn">アカウント情報変更</button> | |
</div> | |
<button class="back-btn" onclick="goBack()">戻る</button> | |
</div> | |
</div> | |
<!-- Bootstrap JS --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script> | |
<script> | |
// Function to show account details when row is clicked (placeholder) | |
function showAccountDetails(username, name, permission) { | |
// Placeholder for future navigation functionality | |
alert(`アカウント詳細:\nユーザー名: ${username}\n氏名: ${name}\n権限: ${permission}`); | |
// TODO: Implement navigation to account detail/edit page | |
// Example: window.location.href = `/admin/account-details/${encodeURIComponent(username)}`; | |
} | |
// Action button handlers | |
document.getElementById('addAccountBtn').addEventListener('click', function() { | |
alert('アカウント追加画面に移動します'); | |
// Navigate to add account page | |
window.location.href = '/admin/add-account'; | |
}); | |
document.getElementById('editAccountBtn').addEventListener('click', function() { | |
alert('アカウント情報変更機能です\n編集したいアカウントをリストから選択してください'); | |
// Could show instructions or open edit mode | |
}); | |
// Function to go back to previous page | |
function goBack() { | |
// Check if there's a previous page in history | |
if (document.referrer) { | |
window.history.back(); | |
} else { | |
// Default fallback to admin home | |
window.location.href = '/admin/home'; | |
} | |
} | |
// Function to load accounts from database (placeholder) | |
function loadAccountsFromDatabase() { | |
// This is where you'll implement the actual database loading | |
// Example AJAX call structure: | |
/* | |
fetch('/api/accounts') | |
.then(response => response.json()) | |
.then(data => { | |
updateAccountsTable(data); | |
}) | |
.catch(error => { | |
console.error('Error loading accounts:', error); | |
}); | |
*/ | |
} | |
// Function to update table with real data (placeholder) | |
function updateAccountsTable(accountsData) { | |
// This function will replace the dummy data with real database data | |
const tbody = document.getElementById('accountsTableBody'); | |
tbody.innerHTML = ''; // Clear existing content | |
accountsData.forEach(account => { | |
const row = document.createElement('tr'); | |
row.onclick = () => showAccountDetails( | |
account.username, | |
account.name, | |
account.permission | |
); | |
const permissionClass = getPermissionClass(account.permission); | |
row.innerHTML = ` | |
<td class="username">${account.username}</td> | |
<td class="name">${account.name}</td> | |
<td><span class="permission-badge ${permissionClass}">${account.permission}</span></td> | |
`; | |
tbody.appendChild(row); | |
}); | |
} | |
// Helper function to get CSS class for permission type | |
function getPermissionClass(permission) { | |
switch(permission) { | |
case '管理者': return 'admin'; | |
case '教員': return 'teacher'; | |
case '生徒': return 'student'; | |
default: return 'student'; | |
} | |
} | |
// Initialize page | |
document.addEventListener('DOMContentLoaded', function() { | |
// loadAccountsFromDatabase(); // Uncomment when ready to load from database | |
}); | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>{% block title %}承認済み - 時間割管理システム{% endblock %}</title> | |
<!-- Bootstrap CSS --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"> | |
<!-- Font Awesome --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<style> | |
body { | |
margin: 0; | |
padding: 0; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
background-color: #f4c2a1; | |
min-height: 100vh; | |
} | |
.header { | |
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); | |
color: white; | |
padding: 15px 30px; | |
font-size: 1.6rem; | |
font-weight: bold; | |
} | |
.main-container { | |
padding: 40px 30px; | |
max-width: 1200px; | |
margin: 0 auto; | |
height: calc(100vh - 70px); | |
overflow: hidden; | |
position: relative; | |
} | |
.message-content { | |
font-size: 1.2rem; | |
color: #333; | |
line-height: 1.6; | |
margin-top: 20px; | |
} | |
.back-button-container { | |
position: absolute; | |
bottom: 30px; | |
right: 30px; | |
} | |
.back-btn { | |
background: #6c757d; | |
color: white; | |
border: none; | |
padding: 12px 30px; | |
font-size: 1rem; | |
font-weight: bold; | |
border-radius: 4px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
text-decoration: none; | |
display: inline-block; | |
} | |
.back-btn:hover { | |
background: #5a6268; | |
transform: translateY(-1px); | |
color: white; | |
} | |
@media (max-width: 768px) { | |
.header { | |
padding: 10px 20px; | |
font-size: 1.4rem; | |
} | |
.main-container { | |
padding: 30px 20px; | |
height: calc(100vh - 60px); | |
} | |
.message-content { | |
font-size: 1.1rem; | |
} | |
.back-button-container { | |
bottom: 20px; | |
right: 20px; | |
} | |
.back-btn { | |
padding: 10px 25px; | |
font-size: 0.9rem; | |
} | |
} | |
@media (max-width: 480px) { | |
.header { | |
font-size: 1.2rem; | |
} | |
.main-container { | |
padding: 20px 15px; | |
} | |
.message-content { | |
font-size: 1rem; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="header"> | |
承認済み | |
</div> | |
<div class="main-container"> | |
<div class="message-content"> | |
(中村管理)さんは<br> | |
(中村教員)さんからの申請を承認しました | |
</div> | |
<div class="back-button-container"> | |
<button class="back-btn" onclick="goBack()"> | |
戻る | |
</button> | |
</div> | |
</div> | |
<!-- Bootstrap JS --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script> | |
<script> | |
// Function to go back to previous page | |
function goBack() { | |
// Check if there's a previous page in history | |
if (document.referrer) { | |
window.history.back(); | |
} else { | |
// Default fallback - adjust these URLs based on your routing | |
// Determine user type and redirect accordingly | |
const userType = getUserType(); // You'll need to implement this | |
switch(userType) { | |
case 'admin': | |
window.location.href = '/admin/home'; | |
break; | |
case 'teacher': | |
window.location.href = '/teacher/home'; | |
break; | |
case 'student': | |
window.location.href = '/student/home'; | |
break; | |
default: | |
window.location.href = '/'; | |
} | |
} | |
} | |
// Helper function to determine user type | |
// This is a placeholder - implement based on your authentication system | |
function getUserType() { | |
// You can check URL, session data, or other indicators | |
const path = window.location.pathname; | |
if (path.includes('/admin/')) return 'admin'; | |
if (path.includes('/teacher/')) return 'teacher'; | |
if (path.includes('/student/')) return 'student'; | |
return 'admin'; // default for approval messages | |
} | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>申請承認</title> | |
<style> | |
body { | |
background-color: #f4c99a; | |
font-family: "Arial", sans-serif; | |
margin: 0; | |
position: relative; | |
min-height: 100vh; | |
} | |
.header { | |
background-color: #428bca; | |
color: white; | |
padding: 20px; | |
font-size: 32px; | |
font-weight: bold; | |
text-align: left; | |
position: relative; | |
} | |
.admin-button { | |
position: absolute; | |
top: 20px; | |
right: 20px; | |
background-color: red; | |
color: white; | |
padding: 8px 12px; | |
border: none; | |
font-weight: bold; | |
cursor: pointer; | |
} | |
.content { | |
margin: 40px 80px; | |
font-size: 18px; | |
} | |
.left-buttons { | |
position: absolute; | |
bottom: 40px; | |
left: 80px; | |
} | |
.right-button { | |
position: absolute; | |
bottom: 40px; | |
right: 80px; | |
} | |
.btn { | |
background-color: yellow; | |
padding: 10px 30px; | |
margin-right: 20px; | |
font-size: 18px; | |
font-weight: bold; | |
box-shadow: 2px 2px #aaa; | |
cursor: pointer; | |
} | |
.back-btn { | |
background-color: white; | |
} | |
a { | |
color: black; | |
text-decoration: none; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="header"> | |
申請承認 | |
<button class="admin-button" onclick="location.href='/admin/home'">管理者画面</button> | |
</div> | |
<div class="content"> | |
<p>(中村管理)さん</p> | |
<p>(中村教員)さんから授業変更の申請が来ています</p> | |
<p>クラス:5-5</p> | |
<p>追加:2025/04/24 1時限 英語</p> | |
</div> | |
<div class="left-buttons"> | |
<form action="/admin/confirm" method="post" style="display:inline;"> | |
<input type="hidden" name="action" value="approve"> | |
<button class="btn">承認</button> | |
</form> | |
<form action="/admin/confirm" method="post" style="display:inline;"> | |
<input type="hidden" name="action" value="reject"> | |
<button class="btn">拒否</button> | |
</form> | |
</div> | |
<div class="right-button"> | |
<button class="btn back-btn" onclick="history.back()">戻る</button> | |
</div> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{% extends 'base.html.twig' %} | |
{% block title %}時間割表のダウンロード{% endblock %} | |
{% block body %} | |
<style> | |
body { | |
background-color: #f5f5f5; | |
} | |
.wrapper { | |
background-color: #facc9d; | |
padding: 40px; | |
font-family: sans-serif; | |
min-height: 90vh; | |
} | |
.header { | |
background-color: #5da3dc; | |
color: white; | |
font-size: 32px; | |
font-weight: bold; | |
padding: 20px; | |
margin: -40px -40px 30px -40px; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
} | |
.admin-button { | |
background-color: red; | |
color: white; | |
padding: 5px 15px; | |
font-weight: bold; | |
border: none; | |
border-radius: 5px; | |
cursor: pointer; | |
} | |
.form-group { | |
font-size: 20px; | |
margin-bottom: 30px; | |
} | |
select { | |
font-size: 18px; | |
padding: 4px; | |
margin: 0 10px; | |
} | |
.download-btn { | |
background-color: yellow; | |
font-weight: bold; | |
padding: 10px 30px; | |
border: 1px solid #999; | |
box-shadow: 2px 2px 2px #999; | |
margin-top: 20px; | |
} | |
.back-btn { | |
margin-top: 80px; | |
background-color: white; | |
border: 1px solid #999; | |
box-shadow: 2px 2px 2px #999; | |
font-size: 18px; | |
padding: 8px 25px; | |
} | |
.source-info { | |
font-size: 12px; | |
color: #444; | |
text-align: right; | |
margin-top: 60px; | |
} | |
</style> | |
<div class="wrapper"> | |
<div class="header"> | |
<div>時間割表のダウンロード</div> | |
<button class="admin-button">管理者画面</button> | |
</div> | |
<div class="form-group"> | |
ダウンロードしたいクラスの時間割<br><br> | |
<select name="grade"> | |
<option>1</option> | |
<option>2</option> | |
<option>3</option> | |
</select>年 | |
<select name="class"> | |
<option>A</option> | |
<option>B</option> | |
<option>C</option> | |
</select>組 | |
</div> | |
<button class="download-btn">ダウンロード</button> | |
<div class="source-info"> | |
AdminDownloadController.php | |
</div> | |
<button class="back-btn">戻る</button> | |
</div> | |
{% endblock %} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>{% block title %}管理者ホーム - 時間割管理システム{% endblock %}</title> | |
<!-- Bootstrap CSS --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"> | |
<!-- Font Awesome --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<style> | |
body { | |
background-color: #f4c2a1; | |
min-height: 100vh; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
margin: 0; | |
padding: 0; | |
} | |
.header { | |
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%); | |
color: white; | |
padding: 15px 40px; | |
font-size: 1.6rem; | |
font-weight: bold; | |
position: relative; | |
} | |
.user-info { | |
position: absolute; | |
right: 20px; | |
top: 50%; | |
transform: translateY(-50%); | |
font-size: 1rem; | |
background: rgba(255, 255, 255, 0.2); | |
padding: 8px 16px; | |
border-radius: 20px; | |
} | |
.main-container { | |
padding: 20px; | |
max-width: 1200px; | |
margin: 0 auto; | |
} | |
.welcome-section { | |
background: white; | |
border-radius: 8px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
padding: 30px; | |
margin-bottom: 30px; | |
text-align: center; | |
} | |
.welcome-title { | |
color: #dc3545; | |
font-size: 2.5rem; | |
font-weight: bold; | |
margin-bottom: 15px; | |
} | |
.welcome-subtitle { | |
color: #6c757d; | |
font-size: 1.2rem; | |
margin-bottom: 20px; | |
} | |
.admin-badge { | |
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%); | |
color: white; | |
padding: 10px 20px; | |
border-radius: 25px; | |
font-size: 1rem; | |
font-weight: bold; | |
display: inline-block; | |
} | |
.menu-grid { | |
display: grid; | |
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); | |
gap: 20px; | |
margin-top: 30px; | |
} | |
.menu-card { | |
background: white; | |
border-radius: 8px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
padding: 25px; | |
text-align: center; | |
transition: all 0.3s ease; | |
border: 2px solid transparent; | |
} | |
.menu-card:hover { | |
transform: translateY(-5px); | |
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); | |
border-color: #dc3545; | |
} | |
.menu-icon { | |
font-size: 3rem; | |
margin-bottom: 15px; | |
color: #dc3545; | |
} | |
.menu-title { | |
color: #2c3e50; | |
font-size: 1.5rem; | |
font-weight: bold; | |
margin-bottom: 10px; | |
} | |
.menu-description { | |
color: #6c757d; | |
font-size: 1rem; | |
margin-bottom: 20px; | |
line-height: 1.5; | |
} | |
.menu-link { | |
background-color: #dc3545; | |
color: white; | |
padding: 12px 25px; | |
border-radius: 4px; | |
text-decoration: none; | |
font-weight: bold; | |
display: inline-block; | |
transition: all 0.3s ease; | |
} | |
.menu-link:hover { | |
background-color: #c82333; | |
color: white; | |
text-decoration: none; | |
transform: translateY(-1px); | |
} | |
.stats-section { | |
background: white; | |
border-radius: 8px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
padding: 25px; | |
margin-top: 30px; | |
} | |
.stats-title { | |
color: #dc3545; | |
font-size: 1.5rem; | |
font-weight: bold; | |
margin-bottom: 20px; | |
text-align: center; | |
} | |
.stats-grid { | |
display: grid; | |
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
gap: 20px; | |
} | |
.stat-item { | |
text-align: center; | |
padding: 20px; | |
background: #f8f9fa; | |
border-radius: 8px; | |
border-left: 4px solid #dc3545; | |
} | |
.stat-number { | |
font-size: 2rem; | |
font-weight: bold; | |
color: #dc3545; | |
margin-bottom: 5px; | |
} | |
.stat-label { | |
color: #6c757d; | |
font-size: 1rem; | |
} | |
.logout-section { | |
text-align: center; | |
margin-top: 30px; | |
padding: 20px; | |
} | |
.logout-btn { | |
background-color: #6c757d; | |
color: white; | |
padding: 12px 30px; | |
border: none; | |
border-radius: 4px; | |
font-size: 1rem; | |
font-weight: bold; | |
text-decoration: none; | |
display: inline-block; | |
transition: all 0.3s ease; | |
} | |
.logout-btn:hover { | |
background-color: #5a6268; | |
color: white; | |
text-decoration: none; | |
transform: translateY(-1px); | |
} | |
@media (max-width: 768px) { | |
.header { | |
padding: 10px 15px; | |
font-size: 1.3rem; | |
} | |
.user-info { | |
position: static; | |
transform: none; | |
margin-top: 10px; | |
display: block; | |
text-align: center; | |
} | |
.main-container { | |
padding: 10px; | |
} | |
.welcome-title { | |
font-size: 2rem; | |
} | |
.welcome-subtitle { | |
font-size: 1rem; | |
} | |
.menu-grid { | |
grid-template-columns: 1fr; | |
gap: 15px; | |
} | |
.menu-card { | |
padding: 20px; | |
} | |
.menu-icon { | |
font-size: 2.5rem; | |
} | |
.menu-title { | |
font-size: 1.3rem; | |
} | |
.stats-grid { | |
grid-template-columns: repeat(2, 1fr); | |
gap: 15px; | |
} | |
.stat-item { | |
padding: 15px; | |
} | |
.stat-number { | |
font-size: 1.5rem; | |
} | |
} | |
@media (max-width: 480px) { | |
.welcome-section { | |
padding: 20px; | |
} | |
.welcome-title { | |
font-size: 1.8rem; | |
} | |
.menu-card { | |
padding: 15px; | |
} | |
.stats-grid { | |
grid-template-columns: 1fr; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="header"> | |
<i class="fas fa-user-shield"></i> 管理者ダッシュボード | |
<div class="user-info"> | |
<i class="fas fa-user"></i> {{ app.user.email }} | |
</div> | |
</div> | |
<div class="main-container"> | |
<!-- ウェルカムセクション --> | |
<div class="welcome-section"> | |
<h1 class="welcome-title"> | |
<i class="fas fa-home"></i> 管理者ホーム | |
</h1> | |
<p class="welcome-subtitle">時間割管理システムの管理者機能をご利用いただけます</p> | |
<div class="admin-badge"> | |
<i class="fas fa-crown"></i> ADMINISTRATOR | |
</div> | |
</div> | |
<!-- メニューグリッド --> | |
<div class="menu-grid"> | |
<!-- 時間割管理 --> | |
<div class="menu-card"> | |
<div class="menu-icon"> | |
<i class="fas fa-table"></i> | |
</div> | |
<h3 class="menu-title">時間割管理</h3> | |
<p class="menu-description"> | |
クラスの時間割を作成・編集・削除できます。<br> | |
各クラスの授業スケジュールを管理します。 | |
</p> | |
<a href="{{ path('app_admin_timetable') }}" class="menu-link"> | |
<i class="fas fa-arrow-right"></i> 時間割管理へ | |
</a> | |
</div> | |
<!-- ユーザー管理 --> | |
<div class="menu-card"> | |
<div class="menu-icon"> | |
<i class="fas fa-users"></i> | |
</div> | |
<h3 class="menu-title">ユーザー管理</h3> | |
<p class="menu-description"> | |
システムユーザーの一覧表示・追加・削除ができます。<br> | |
学生・教員・管理者の管理を行います。 | |
</p> | |
<a href="{{ path('app_user_database') }}" class="menu-link"> | |
<i class="fas fa-arrow-right"></i> ユーザー管理へ | |
</a> | |
</div> | |
<!-- システム設定 --> | |
<div class="menu-card"> | |
<div class="menu-icon"> | |
<i class="fas fa-cogs"></i> | |
</div> | |
<h3 class="menu-title">システム設定</h3> | |
<p class="menu-description"> | |
システムの各種設定を変更できます。<br> | |
権限制御やその他の設定を管理します。 | |
</p> | |
<a href="{{ path('app_developer_access_control') }}" class="menu-link"> | |
<i class="fas fa-arrow-right"></i> システム設定へ | |
</a> | |
</div> | |
<!-- レポート・統計 --> | |
<div class="menu-card"> | |
<div class="menu-icon"> | |
<i class="fas fa-chart-bar"></i> | |
</div> | |
<h3 class="menu-title">レポート・統計</h3> | |
<p class="menu-description"> | |
システムの利用状況やユーザー統計を確認できます。<br> | |
各種データの分析を行います。 | |
</p> | |
<a href="#" class="menu-link" onclick="alert('この機能は開発中です')"> | |
<i class="fas fa-arrow-right"></i> レポートへ | |
</a> | |
</div> | |
</div> | |
<!-- 統計セクション --> | |
<div class="stats-section"> | |
<h3 class="stats-title"> | |
<i class="fas fa-chart-line"></i> システム統計 | |
</h3> | |
<div class="stats-grid"> | |
<div class="stat-item"> | |
<div class="stat-number" id="userCount">-</div> | |
<div class="stat-label">登録ユーザー数</div> | |
</div> | |
<div class="stat-item"> | |
<div class="stat-number" id="timetableCount">-</div> | |
<div class="stat-label">作成済み時間割</div> | |
</div> | |
<div class="stat-item"> | |
<div class="stat-number" id="adminCount">-</div> | |
<div class="stat-label">管理者数</div> | |
</div> | |
<div class="stat-item"> | |
<div class="stat-number" id="teacherCount">-</div> | |
<div class="stat-label">教員数</div> | |
</div> | |
</div> | |
</div> | |
<!-- ログアウトセクション --> | |
<div class="logout-section"> | |
<a href="{{ path('app_logout') }}" class="logout-btn"> | |
<i class="fas fa-sign-out-alt"></i> ログアウト | |
</a> | |
</div> | |
</div> | |
<!-- Bootstrap JS --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
// 統計データを取得(実際のAPIエンドポイントに置き換える) | |
loadStatistics(); | |
// メニューカードのアニメーション | |
const menuCards = document.querySelectorAll('.menu-card'); | |
menuCards.forEach((card, index) => { | |
card.style.opacity = '0'; | |
card.style.transform = 'translateY(20px)'; | |
setTimeout(() => { | |
card.style.transition = 'all 0.5s ease'; | |
card.style.opacity = '1'; | |
card.style.transform = 'translateY(0)'; | |
}, index * 100); | |
}); | |
}); | |
// 統計データの読み込み(ダミーデータ) | |
function loadStatistics() { | |
// 実際の実装では、APIから統計データを取得 | |
setTimeout(() => { | |
document.getElementById('userCount').textContent = '25'; | |
document.getElementById('timetableCount').textContent = '8'; | |
document.getElementById('adminCount').textContent = '3'; | |
document.getElementById('teacherCount').textContent = '12'; | |
// 数値のカウントアップアニメーション | |
animateNumbers(); | |
}, 500); | |
} | |
// 数値のカウントアップアニメーション | |
function animateNumbers() { | |
const statNumbers = document.querySelectorAll('.stat-number'); | |
statNumbers.forEach(element => { | |
const finalValue = parseInt(element.textContent); | |
let currentValue = 0; | |
const increment = Math.ceil(finalValue / 20); | |
const timer = setInterval(() => { | |
currentValue += increment; | |
if (currentValue >= finalValue) { | |
currentValue = finalValue; | |
clearInterval(timer); | |
} | |
element.textContent = currentValue; | |
}, 50); | |
}); | |
} | |
// ページ読み込み時のウェルカムメッセージ | |
function showWelcomeMessage() { | |
const currentHour = new Date().getHours(); | |
let greeting; | |
if (currentHour < 12) { | |
greeting = 'おはようございます'; | |
} else if (currentHour < 18) { | |
greeting = 'こんにちは'; | |
} else { | |
greeting = 'こんばんは'; | |
} | |
// 実際の実装では、ユーザー名を取得して表示 | |
console.log(`${greeting}、管理者さん!`); | |
} | |
// 定期的にシステム状態をチェック | |
function checkSystemStatus() { | |
// 実際の実装では、システムの健全性をチェック | |
fetch('/api/system/status') | |
.then(response => response.json()) | |
.then(data => { | |
if (data.status === 'healthy') { | |
console.log('システムは正常に動作しています'); | |
} | |
}) | |
.catch(error => { | |
console.warn('システム状態の確認に失敗しました:', error); | |
}); | |
} | |
// 初期化処理 | |
showWelcomeMessage(); | |
// 5分ごとにシステム状態をチェック | |
setInterval(checkSystemStatus, 300000); | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>{% block title %}拒否済み - 時間割管理システム{% endblock %}</title> | |
<!-- Bootstrap CSS --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"> | |
<!-- Font Awesome --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<style> | |
body { | |
margin: 0; | |
padding: 0; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
background-color: #f4c2a1; | |
min-height: 100vh; | |
} | |
.header { | |
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); | |
color: white; | |
padding: 15px 30px; | |
font-size: 1.6rem; | |
font-weight: bold; | |
} | |
.main-container { | |
padding: 40px 30px; | |
max-width: 1200px; | |
margin: 0 auto; | |
height: calc(100vh - 70px); | |
overflow: hidden; | |
position: relative; | |
} | |
.message-content { | |
font-size: 1.2rem; | |
color: #333; | |
line-height: 1.6; | |
margin-top: 20px; | |
} | |
.reason-section { | |
margin-top: 30px; | |
font-size: 1.2rem; | |
color: #333; | |
line-height: 1.6; | |
} | |
.reason-text { | |
display: inline-block; | |
border-bottom: 2px dotted #333; | |
min-width: 300px; | |
padding-bottom: 2px; | |
} | |
.back-button-container { | |
position: absolute; | |
bottom: 30px; | |
right: 30px; | |
} | |
.back-btn { | |
background: #6c757d; | |
color: white; | |
border: none; | |
padding: 12px 30px; | |
font-size: 1rem; | |
font-weight: bold; | |
border-radius: 4px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
text-decoration: none; | |
display: inline-block; | |
} | |
.back-btn:hover { | |
background: #5a6268; | |
transform: translateY(-1px); | |
color: white; | |
} | |
@media (max-width: 768px) { | |
.header { | |
padding: 10px 20px; | |
font-size: 1.4rem; | |
} | |
.main-container { | |
padding: 30px 20px; | |
height: calc(100vh - 60px); | |
} | |
.message-content, | |
.reason-section { | |
font-size: 1.1rem; | |
} | |
.reason-text { | |
min-width: 200px; | |
} | |
.back-button-container { | |
bottom: 20px; | |
right: 20px; | |
} | |
.back-btn { | |
padding: 10px 25px; | |
font-size: 0.9rem; | |
} | |
} | |
@media (max-width: 480px) { | |
.header { | |
font-size: 1.2rem; | |
} | |
.main-container { | |
padding: 20px 15px; | |
} | |
.message-content, | |
.reason-section { | |
font-size: 1rem; | |
} | |
.reason-text { | |
min-width: 150px; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="header"> | |
拒否済み | |
</div> | |
<div class="main-container"> | |
<div class="message-content"> | |
(中村管理)さんは<br> | |
(中村教員)さんからの申請を拒否しました | |
</div> | |
<div class="reason-section"> | |
理由:<span class="reason-text">........................</span> | |
</div> | |
<div class="back-button-container"> | |
<button class="back-btn" onclick="goBack()"> | |
戻る | |
</button> | |
</div> | |
</div> | |
<!-- Bootstrap JS --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script> | |
<script> | |
// Function to go back to previous page | |
function goBack() { | |
// Check if there's a previous page in history | |
if (document.referrer) { | |
window.history.back(); | |
} else { | |
// Default fallback - adjust these URLs based on your routing | |
// Determine user type and redirect accordingly | |
const userType = getUserType(); // You'll need to implement this | |
switch(userType) { | |
case 'admin': | |
window.location.href = '/admin/home'; | |
break; | |
case 'teacher': | |
window.location.href = '/teacher/home'; | |
break; | |
case 'student': | |
window.location.href = '/student/home'; | |
break; | |
default: | |
window.location.href = '/'; | |
} | |
} | |
} | |
// Helper function to determine user type | |
// This is a placeholder - implement based on your authentication system | |
function getUserType() { | |
// You can check URL, session data, or other indicators | |
const path = window.location.pathname; | |
if (path.includes('/admin/')) return 'admin'; | |
if (path.includes('/teacher/')) return 'teacher'; | |
if (path.includes('/student/')) return 'student'; | |
return 'admin'; // default for rejection messages | |
} | |
// Function to populate rejection reason (for dynamic content) | |
function setRejectionReason(reason) { | |
const reasonElement = document.querySelector('.reason-text'); | |
if (reason && reason.trim()) { | |
reasonElement.textContent = reason; | |
} | |
} | |
// Initialize page | |
document.addEventListener('DOMContentLoaded', function() { | |
// Example: setRejectionReason('スケジュールが重複しています'); | |
// You can call this function with dynamic data from your backend | |
}); | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>{% block title %}変更申請確認 - 時間割管理システム{% endblock %}</title> | |
<!-- Bootstrap CSS --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"> | |
<!-- Font Awesome --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<style> | |
body { | |
margin: 0; | |
padding: 0; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
background-color: #f4c2a1; | |
min-height: 100vh; | |
} | |
.header { | |
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); | |
color: white; | |
padding: 15px 30px; | |
font-size: 1.6rem; | |
font-weight: bold; | |
} | |
.main-container { | |
padding: 20px 30px; | |
max-width: 1200px; | |
margin: 0 auto; | |
height: calc(100vh - 70px); | |
overflow: hidden; | |
} | |
.instruction-text { | |
font-size: 1rem; | |
color: #333; | |
margin-bottom: 20px; | |
font-weight: 500; | |
} | |
.requests-table-container { | |
background: white; | |
border-radius: 8px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
overflow: hidden; | |
margin-bottom: 15px; | |
height: calc(100vh - 220px); | |
overflow-y: auto; | |
} | |
.requests-table { | |
width: 100%; | |
border-collapse: collapse; | |
font-size: 0.9rem; | |
} | |
.requests-table th { | |
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); | |
color: white; | |
padding: 12px 15px; | |
text-align: center; | |
font-weight: bold; | |
font-size: 1rem; | |
position: sticky; | |
top: 0; | |
z-index: 10; | |
} | |
.requests-table td { | |
padding: 10px 15px; | |
text-align: center; | |
border-bottom: 1px solid #ddd; | |
background: #f8f9fa; | |
font-weight: 500; | |
vertical-align: middle; | |
} | |
.requests-table tbody tr:nth-child(odd) { | |
background: #e9ecef; | |
} | |
.requests-table tbody tr:nth-child(even) { | |
background: #f8f9fa; | |
} | |
.requests-table tbody tr:hover { | |
background: #e3f2fd; | |
cursor: pointer; | |
} | |
.change-type { | |
padding: 4px 10px; | |
border-radius: 4px; | |
font-weight: bold; | |
font-size: 0.85rem; | |
} | |
.change-type.add { | |
background-color: #d4edda; | |
color: #155724; | |
border: 1px solid #c3e6cb; | |
} | |
.change-type.remove { | |
background-color: #f8d7da; | |
color: #721c24; | |
border: 1px solid #f5c6cb; | |
} | |
.back-button-container { | |
display: flex; | |
justify-content: flex-end; | |
margin-top: 10px; | |
} | |
.back-btn { | |
background: #6c757d; | |
color: white; | |
border: none; | |
padding: 10px 25px; | |
font-size: 0.9rem; | |
font-weight: bold; | |
border-radius: 4px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
text-decoration: none; | |
display: inline-block; | |
} | |
.back-btn:hover { | |
background: #5a6268; | |
transform: translateY(-1px); | |
color: white; | |
} | |
.empty-row { | |
background: #f1f3f4 !important; | |
} | |
.empty-row:hover { | |
background: #f1f3f4 !important; | |
cursor: default; | |
} | |
.empty-cell { | |
color: #999; | |
font-style: italic; | |
} | |
@media (max-width: 768px) { | |
.header { | |
padding: 10px 15px; | |
font-size: 1.3rem; | |
} | |
.main-container { | |
padding: 15px 10px; | |
height: calc(100vh - 60px); | |
} | |
.instruction-text { | |
font-size: 0.9rem; | |
margin-bottom: 15px; | |
} | |
.requests-table-container { | |
height: calc(100vh - 180px); | |
} | |
.requests-table { | |
font-size: 0.75rem; | |
} | |
.requests-table th, | |
.requests-table td { | |
padding: 8px 6px; | |
} | |
.requests-table th { | |
font-size: 0.85rem; | |
} | |
.change-type { | |
padding: 3px 6px; | |
font-size: 0.7rem; | |
} | |
.back-btn { | |
padding: 8px 20px; | |
font-size: 0.8rem; | |
} | |
} | |
@media (max-width: 480px) { | |
.requests-table { | |
font-size: 0.7rem; | |
} | |
.requests-table th, | |
.requests-table td { | |
padding: 6px 4px; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="header"> | |
変更申請確認 | |
</div> | |
<div class="main-container"> | |
<div class="instruction-text"> | |
*承認・却下したい時はリストから選択してください | |
</div> | |
<div class="requests-table-container"> | |
<table class="requests-table"> | |
<thead> | |
<tr> | |
<th>科目名</th> | |
<th>日付</th> | |
<th>クラス</th> | |
<th>時限目</th> | |
<th>変更</th> | |
<th>担当者</th> | |
</tr> | |
</thead> | |
<tbody id="requestsTableBody"> | |
<!-- Dummy data - replace with database content --> | |
<tr onclick="reviewRequest('体育I', '2025/5/21', '5年5組', '2', '追加', '中村教員')"> | |
<td>体育I</td> | |
<td>2025/5/21</td> | |
<td>5年5組</td> | |
<td>2</td> | |
<td><span class="change-type add">追加</span></td> | |
<td>中村教員</td> | |
</tr> | |
<tr onclick="reviewRequest('英語I', '2025/5/22', '5年5組', '4', '削除', '中村教員')"> | |
<td>英語I</td> | |
<td>2025/5/22</td> | |
<td>5年5組</td> | |
<td>4</td> | |
<td><span class="change-type remove">削除</span></td> | |
<td>中村教員</td> | |
</tr> | |
<tr onclick="reviewRequest('数学I', '2025/5/23', '5年5組', '3', '追加', '田中教員')"> | |
<td>数学I</td> | |
<td>2025/5/23</td> | |
<td>5年5組</td> | |
<td>3</td> | |
<td><span class="change-type add">追加</span></td> | |
<td>田中教員</td> | |
</tr> | |
<tr onclick="reviewRequest('物理I', '2025/5/24', '5年5組', '1', '削除', '佐藤教員')"> | |
<td>物理I</td> | |
<td>2025/5/24</td> | |
<td>5年5組</td> | |
<td>1</td> | |
<td><span class="change-type remove">削除</span></td> | |
<td>佐藤教員</td> | |
</tr> | |
<tr onclick="reviewRequest('化学I', '2025/5/25', '5年5組', '2', '追加', '山田教員')"> | |
<td>化学I</td> | |
<td>2025/5/25</td> | |
<td>5年5組</td> | |
<td>2</td> | |
<td><span class="change-type add">追加</span></td> | |
<td>山田教員</td> | |
</tr> | |
<!-- Empty rows for spacing --> | |
<tr class="empty-row"> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
</tr> | |
<tr class="empty-row"> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
</tr> | |
<tr class="empty-row"> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
</tr> | |
</tbody> | |
</table> | |
</div> | |
<div class="back-button-container"> | |
<button class="back-btn" onclick="goBack()"> | |
戻る | |
</button> | |
</div> | |
</div> | |
<!-- Bootstrap JS --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script> | |
<script> | |
// Function to review request when row is clicked (placeholder) | |
function reviewRequest(subject, date, className, period, changeType, teacher) { | |
// Placeholder for future navigation functionality to approval/rejection page | |
const confirmed = confirm(`この申請を確認しますか?\n\n科目: ${subject}\n日付: ${date}\nクラス: ${className}\n時限: ${period}時限目\n変更: ${changeType}\n担当者: ${teacher}\n\n「OK」で詳細画面に移動します。`); | |
if (confirmed) { | |
// TODO: Implement navigation to detailed approval page | |
alert('詳細確認画面に移動します(実装予定)'); | |
// Example: window.location.href = `/admin/review-request/${requestId}`; | |
} | |
} | |
// Function to go back to previous page | |
function goBack() { | |
// Check if there's a previous page in history | |
if (document.referrer) { | |
window.history.back(); | |
} else { | |
// Default fallback to admin home | |
window.location.href = '/admin/home'; | |
} | |
} | |
// Function to load pending requests from database (placeholder) | |
function loadPendingRequestsFromDatabase() { | |
// This is where you'll implement the actual database loading | |
// Example AJAX call structure: | |
/* | |
fetch('/api/pending-requests') | |
.then(response => response.json()) | |
.then(data => { | |
updateRequestsTable(data); | |
}) | |
.catch(error => { | |
console.error('Error loading pending requests:', error); | |
}); | |
*/ | |
} | |
// Function to update table with real data (placeholder) | |
function updateRequestsTable(requestsData) { | |
// This function will replace the dummy data with real database data | |
const tbody = document.getElementById('requestsTableBody'); | |
tbody.innerHTML = ''; // Clear existing content | |
requestsData.forEach(request => { | |
const row = document.createElement('tr'); | |
row.onclick = () => reviewRequest( | |
request.subject, | |
request.date, | |
request.className, | |
request.period, | |
request.changeType, | |
request.teacher | |
); | |
const changeTypeClass = getChangeTypeClass(request.changeType); | |
row.innerHTML = ` | |
<td>${request.subject}</td> | |
<td>${request.date}</td> | |
<td>${request.className}</td> | |
<td>${request.period}</td> | |
<td><span class="change-type ${changeTypeClass}">${request.changeType}</span></td> | |
<td>${request.teacher}</td> | |
`; | |
tbody.appendChild(row); | |
}); | |
} | |
// Helper function to get CSS class for change type | |
function getChangeTypeClass(changeType) { | |
switch(changeType) { | |
case '追加': return 'add'; | |
case '削除': return 'remove'; | |
default: return 'add'; | |
} | |
} | |
// Function to handle bulk approval/rejection (future feature) | |
function handleBulkAction(action) { | |
// This could be used for selecting multiple requests and approving/rejecting them at once | |
alert(`一括${action}機能は今後実装予定です`); | |
} | |
// Initialize page | |
document.addEventListener('DOMContentLoaded', function() { | |
// loadPendingRequestsFromDatabase(); // Uncomment when ready to load from database | |
}); | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{% extends 'base.html.twig' %} | |
{% block title %}時間割表のアップロード{% endblock %} | |
{% block body %} | |
<style> | |
body { | |
background-color: #f5f5f5; | |
} | |
.wrapper { | |
background-color: #facc9d; | |
padding: 40px; | |
font-family: sans-serif; | |
min-height: 90vh; | |
} | |
.header { | |
background-color: #5da3dc; | |
color: white; | |
font-size: 32px; | |
font-weight: bold; | |
padding: 20px; | |
margin: -40px -40px 30px -40px; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
} | |
.admin-button { | |
background-color: red; | |
color: white; | |
padding: 5px 15px; | |
font-weight: bold; | |
border: none; | |
border-radius: 5px; | |
cursor: pointer; | |
} | |
.form-group { | |
font-size: 18px; | |
margin-bottom: 20px; | |
} | |
.upload-btn { | |
background-color: yellow; | |
font-weight: bold; | |
padding: 10px 30px; | |
border: 1px solid #999; | |
box-shadow: 2px 2px 2px #999; | |
margin-top: 10px; | |
} | |
.back-btn { | |
margin-top: 80px; | |
background-color: white; | |
border: 1px solid #999; | |
box-shadow: 2px 2px 2px #999; | |
font-size: 18px; | |
padding: 8px 25px; | |
} | |
.source-info { | |
font-size: 12px; | |
color: #444; | |
text-align: right; | |
margin-top: 60px; | |
} | |
.note { | |
color: #007BFF; | |
margin-top: 10px; | |
} | |
</style> | |
<div class="wrapper"> | |
<div class="header"> | |
<div>時間割表のアップロード</div> | |
<button class="admin-button">管理者画面</button> | |
</div> | |
<form method="POST" enctype="multipart/form-data"> | |
<div class="form-group"> | |
ファイルを指定してください。<br><br> | |
{# 実際のファイル選択 input(非表示にする) #} | |
<input type="file" name="file" id="file-input" style="display: none;" onchange="updateFileName(this)"> | |
{# ボタン風のlabel(クリックで↑のinputが反応する!) #} | |
<label for="file-input" class="upload-btn">ファイル選択</label> | |
<span id="file-name">ファイルが選択されていません</span> | |
</div> | |
<div class="note"> | |
最大アップロードサイズ:~~~MB | |
</div> | |
<button class="back-btn">戻る</button> | |
</form> | |
<script> | |
function updateFileName(input) { | |
const fileName = input.files.length > 0 ? input.files[0].name : "ファイルが選択されていません"; | |
document.getElementById("file-name").textContent = fileName; | |
} | |
</script> | |
</div> | |
{% endblock %} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Annotation\Route; | |
class AdminAccountManageController extends AbstractController | |
{ | |
#[Route('/admin/account', name: 'app_admin_account_manage')] | |
public function admin_account_manage(): Response | |
{ | |
return $this->render('admin_page/admin_account_manage.html.twig', [ | |
'controller_name' => 'AdminAccountManageController', | |
]); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Annotation\Route; | |
class AdminApprovedApplicationController extends AbstractController | |
{ | |
#[Route('/admin/application/approved', name: 'app_admin_approved_application')] | |
public function admin_approved_application(): Response | |
{ | |
return $this->render('admin_page/admin_approved_application.html.twig', [ | |
'controller_name' => 'AdminApprovedApplicationController', | |
]); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Annotation\Route; | |
class AdminConfirmApplicationController extends AbstractController | |
{ | |
#[Route('/admin/application/confirm', name: 'app_admin_confirm_application')] | |
public function admin_confirm_application(): Response | |
{ | |
return $this->render('admin_page/admin_confirm_application.html.twig', [ | |
'controller_name' => 'AdminConfirmApplicationController', | |
]); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Annotation\Route; | |
class AdminDownloadController extends AbstractController | |
{ | |
#[Route('/admin/download', name: 'app_admin_download')] | |
public function admin_download(): Response | |
{ | |
return $this->render('admin_page/admin_download.html.twig', [ | |
'controller_name' => 'AdminDownloadController', | |
]); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use App\Security\AccessControlVoter; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Annotation\Route; | |
class AdminHomeController extends AbstractController | |
{ | |
#[Route('/admin/home', name: 'app_admin_home')] | |
public function admin_home(): Response | |
{ | |
$this->denyAccessUnlessGranted(AccessControlVoter::ACCESS_ADMIN); | |
return $this->render('admin_page/admin_home.html.twig', [ | |
'controller_name' => 'AdminHomeController', | |
]); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Annotation\Route; | |
class AdminRejectedController extends AbstractController | |
{ | |
#[Route('/admin/rejected', name: 'app_admin_rejected')] | |
public function admin_rejected(): Response | |
{ | |
return $this->render('admin_page/admin_rejected.html.twig', [ | |
'controller_name' => 'AdminRejectedController', | |
]); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Annotation\Route; | |
class AdminSeeApplicationController extends AbstractController | |
{ | |
#[Route('/admin/application', name: 'app_admin_see_application')] | |
public function admin_see_application(): Response | |
{ | |
return $this->render('admin_page/admin_see_application.html.twig', [ | |
'controller_name' => 'AdminSeeApplicationController', | |
]); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Annotation\Route; | |
class AdminUploadController extends AbstractController | |
{ | |
#[Route('/admin/upload', name: 'app_admin_upload')] | |
public function admin_upload(): Response | |
{ | |
return $this->render('admin_page/admin_upload.html.twig', [ | |
'controller_name' => 'AdminUploadController', | |
]); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\DataFixtures; | |
use Doctrine\Bundle\FixturesBundle\Fixture; | |
use Doctrine\Persistence\ObjectManager; | |
class AppFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager): void | |
{ | |
// $product = new Product(); | |
// $manager->persist($product); | |
$manager->flush(); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use App\Entity\User; | |
use Doctrine\ORM\EntityManagerInterface; | |
use Symfony\Component\HttpFoundation\JsonResponse; | |
use Symfony\Component\HttpFoundation\Request; | |
use Symfony\Component\Routing\Annotation\Route; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; | |
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; | |
class AuthCheckController extends AbstractController | |
{ | |
private EntityManagerInterface $entityManager; | |
private TokenStorageInterface $tokenStorage; | |
public function __construct( | |
EntityManagerInterface $entityManager, | |
TokenStorageInterface $tokenStorage | |
) { | |
$this->entityManager = $entityManager; | |
$this->tokenStorage = $tokenStorage; | |
} | |
#[Route('/auth-check', name: 'app_auth_check')] | |
public function check(Request $request): JsonResponse | |
{ | |
error_log('[DEBUG] AuthCheck: === AUTHENTICATION CHECK STARTED ==='); | |
try { | |
$email = $request->query->get('email'); | |
error_log('[DEBUG] AuthCheck: Email parameter: ' . ($email ?? 'null')); | |
if (!$email) { | |
error_log('[DEBUG] AuthCheck: No email provided'); | |
return new JsonResponse(['authenticated' => false, 'error' => 'No email provided'], 400); | |
} | |
// データベースから直接ユーザー情報を取得 | |
$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $email]); | |
if (!$user) { | |
error_log('[DEBUG] AuthCheck: User not found for email: ' . $email); | |
return new JsonResponse(['authenticated' => false, 'error' => 'User not found']); | |
} | |
$loginStatus = $user->getLoginStatus(); | |
error_log('[DEBUG] AuthCheck: User found, loginStatus: ' . ($loginStatus ?? 'null') . ' for email: ' . $email); | |
if ($loginStatus !== 'verified') { | |
error_log('[DEBUG] AuthCheck: Login status is not verified: ' . ($loginStatus ?? 'null')); | |
return new JsonResponse(['authenticated' => false, 'status' => $loginStatus]); | |
} | |
// ★★ 認証が完了している場合、このデバイスでもログイン状態にする ★★ | |
error_log('[DEBUG] AuthCheck: User is verified, setting up authentication for this device'); | |
try { | |
// 認証トークンを作成してセッションに保存 | |
$token = new UsernamePasswordToken($user, 'main', $user->getRoles()); | |
$this->tokenStorage->setToken($token); | |
$session = $request->getSession(); | |
$session->set('_security_main', serialize($token)); | |
$session->set('device_authenticated_user_id', $user->getId()); | |
error_log('[DEBUG] AuthCheck: Authentication token created and stored in session'); | |
} catch (\Exception $e) { | |
error_log('[ERROR] AuthCheck: Failed to create authentication token: ' . $e->getMessage()); | |
// トークン作成に失敗してもレスポンスは返す | |
} | |
// ユーザーのロールに基づいてリダイレクト先を決定 | |
$roles = $user->getRoles(); | |
if (in_array('ROLE_DEVELOPER', $roles)) { | |
$redirectUrl = '/developer/home'; | |
} elseif (in_array('ROLE_ADMIN', $roles)) { | |
$redirectUrl = '/admin/home'; | |
} elseif (in_array('ROLE_TEACHER', $roles)) { | |
$redirectUrl = '/teacher/home'; | |
} elseif (in_array('ROLE_STUDENT', $roles)) { | |
$redirectUrl = '/student/home'; | |
} else { | |
$redirectUrl = '/student/home'; | |
} | |
error_log('[DEBUG] AuthCheck: ✅ AUTHENTICATION SUCCESSFUL - redirecting to: ' . $redirectUrl); | |
return new JsonResponse([ | |
'authenticated' => true, | |
'redirectUrl' => $redirectUrl, | |
'message' => 'Authentication successful' | |
]); | |
} catch (\Exception $e) { | |
error_log('[ERROR] AuthCheck: Exception: ' . $e->getMessage()); | |
error_log('[ERROR] AuthCheck: Stack trace: ' . $e->getTraceAsString()); | |
return new JsonResponse(['authenticated' => false, 'error' => $e->getMessage()], 500); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
framework: | |
cache: | |
# Unique name of your app: used to compute stable namespaces for cache keys. | |
#prefix_seed: your_vendor_name/app_name | |
# The "app" cache stores to the filesystem by default. | |
# The data in this cache should persist between deploys. | |
# Other options include: | |
# Redis | |
#app: cache.adapter.redis | |
#default_redis_provider: redis://localhost | |
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) | |
#app: cache.adapter.apcu | |
# Namespaced pools use the above "app" backend by default | |
#pools: | |
#my.dedicated.cache: null |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>{% block title %}授業変更一覧 - 時間割管理システム{% endblock %}</title> | |
<!-- Bootstrap CSS --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"> | |
<!-- Font Awesome --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<style> | |
body { | |
margin: 0; | |
padding: 0; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
background-color: #f4c2a1; | |
min-height: 100vh; | |
} | |
.header { | |
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); | |
color: white; | |
padding: 15px 30px; | |
font-size: 1.6rem; | |
font-weight: bold; | |
} | |
.main-container { | |
padding: 20px 30px; | |
max-width: 1200px; | |
margin: 0 auto; | |
height: calc(100vh - 70px); | |
overflow: hidden; | |
} | |
.changes-table-container { | |
background: white; | |
border-radius: 8px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
overflow: hidden; | |
margin-bottom: 15px; | |
height: calc(100vh - 180px); | |
overflow-y: auto; | |
} | |
.changes-table { | |
width: 100%; | |
border-collapse: collapse; | |
font-size: 0.9rem; | |
} | |
.changes-table th { | |
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); | |
color: white; | |
padding: 12px 15px; | |
text-align: center; | |
font-weight: bold; | |
font-size: 1rem; | |
} | |
.changes-table td { | |
padding: 10px 15px; | |
text-align: center; | |
border-bottom: 1px solid #ddd; | |
background: #f8f9fa; | |
font-weight: 500; | |
vertical-align: middle; | |
} | |
.changes-table tbody tr:nth-child(odd) { | |
background: #e9ecef; | |
} | |
.changes-table tbody tr:nth-child(even) { | |
background: #f8f9fa; | |
} | |
.changes-table tbody tr:hover { | |
background: #e3f2fd; | |
cursor: pointer; | |
} | |
.change-type { | |
padding: 4px 10px; | |
border-radius: 4px; | |
font-weight: bold; | |
font-size: 0.85rem; | |
} | |
.change-type.add { | |
background-color: #d4edda; | |
color: #155724; | |
border: 1px solid #c3e6cb; | |
} | |
.change-type.remove { | |
background-color: #f8d7da; | |
color: #721c24; | |
border: 1px solid #f5c6cb; | |
} | |
.back-button-container { | |
display: flex; | |
justify-content: flex-end; | |
margin-top: 10px; | |
} | |
.back-btn { | |
background: #6c757d; | |
color: white; | |
border: none; | |
padding: 10px 25px; | |
font-size: 0.9rem; | |
font-weight: bold; | |
border-radius: 4px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
text-decoration: none; | |
display: inline-block; | |
} | |
.back-btn:hover { | |
background: #5a6268; | |
transform: translateY(-1px); | |
color: white; | |
} | |
.empty-row { | |
background: #f1f3f4 !important; | |
} | |
.empty-row:hover { | |
background: #f1f3f4 !important; | |
cursor: default; | |
} | |
.empty-cell { | |
color: #999; | |
font-style: italic; | |
} | |
@media (max-width: 768px) { | |
.header { | |
padding: 15px 20px; | |
font-size: 1.5rem; | |
} | |
.main-container { | |
padding: 20px 15px; | |
} | |
.changes-table { | |
font-size: 0.85rem; | |
} | |
.changes-table th, | |
.changes-table td { | |
padding: 10px 8px; | |
} | |
.changes-table th { | |
font-size: 0.9rem; | |
} | |
.back-btn { | |
padding: 10px 20px; | |
font-size: 0.9rem; | |
} | |
} | |
@media (max-width: 480px) { | |
.changes-table { | |
font-size: 0.75rem; | |
} | |
.changes-table th, | |
.changes-table td { | |
padding: 8px 4px; | |
} | |
.change-type { | |
padding: 4px 8px; | |
font-size: 0.75rem; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="header"> | |
授業変更一覧 | |
</div> | |
<div class="main-container"> | |
<div class="changes-table-container"> | |
<table class="changes-table"> | |
<thead> | |
<tr> | |
<th>科目名</th> | |
<th>日付</th> | |
<th>時限目</th> | |
<th>変更</th> | |
<th>担当者</th> | |
</tr> | |
</thead> | |
<tbody id="changesTableBody"> | |
<!-- Dummy data - replace with database content --> | |
<tr onclick="showChangeDetails('体育I', '2025/5/21', '2', '追加', '中村教員')"> | |
<td>体育I</td> | |
<td>2025/5/21</td> | |
<td>2</td> | |
<td><span class="change-type add">追加</span></td> | |
<td>中村教員</td> | |
</tr> | |
<tr onclick="showChangeDetails('英語I', '2025/5/22', '4', '削除', '中村教員')"> | |
<td>英語I</td> | |
<td>2025/5/22</td> | |
<td>4</td> | |
<td><span class="change-type remove">削除</span></td> | |
<td>中村教員</td> | |
</tr> | |
<tr onclick="showChangeDetails('英語I', '2025/5/23', '2', '削除', '中村教員')"> | |
<td>英語I</td> | |
<td>2025/5/23</td> | |
<td>2</td> | |
<td><span class="change-type remove">削除</span></td> | |
<td>中村教員</td> | |
</tr> | |
<tr onclick="showChangeDetails('数学I', '2025/5/24', '3', '追加', '田中教員')"> | |
<td>数学I</td> | |
<td>2025/5/24</td> | |
<td>3</td> | |
<td><span class="change-type add">追加</span></td> | |
<td>田中教員</td> | |
</tr> | |
<tr onclick="showChangeDetails('物理I', '2025/5/25', '1', '追加', '佐藤教員')"> | |
<td>物理I</td> | |
<td>2025/5/25</td> | |
<td>1</td> | |
<td><span class="change-type add">追加</span></td> | |
<td>佐藤教員</td> | |
</tr> | |
<!-- Empty rows for spacing --> | |
<tr class="empty-row"> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
</tr> | |
<tr class="empty-row"> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
</tr> | |
<tr class="empty-row"> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
</tr> | |
</tbody> | |
</table> | |
</div> | |
<div class="back-button-container"> | |
<button class="back-btn" onclick="goBack()"> | |
戻る | |
</button> | |
</div> | |
</div> | |
<!-- Bootstrap JS --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script> | |
<script> | |
// Function to show change details when row is clicked | |
function showChangeDetails(subject, date, period, changeType, teacher) { | |
alert(`詳細情報:\n科目: ${subject}\n日付: ${date}\n時限: ${period}時限目\n変更: ${changeType}\n担当者: ${teacher}`); | |
} | |
// Function to go back to previous page | |
function goBack() { | |
// Check if there's a previous page in history | |
if (document.referrer) { | |
window.history.back(); | |
} else { | |
// Default fallback - adjust these URLs based on your routing | |
// Determine user type and redirect accordingly | |
const userType = getUserType(); // You'll need to implement this | |
switch(userType) { | |
case 'admin': | |
window.location.href = '/admin/home'; | |
break; | |
case 'teacher': | |
window.location.href = '/teacher/home'; | |
break; | |
case 'student': | |
window.location.href = '/student/home'; | |
break; | |
default: | |
window.location.href = '/'; | |
} | |
} | |
} | |
// Helper function to determine user type | |
// This is a placeholder - implement based on your authentication system | |
function getUserType() { | |
// You can check URL, session data, or other indicators | |
const path = window.location.pathname; | |
if (path.includes('/admin/')) return 'admin'; | |
if (path.includes('/teacher/')) return 'teacher'; | |
if (path.includes('/student/')) return 'student'; | |
return 'student'; // default | |
} | |
// Function to load changes from database (placeholder) | |
function loadChangesFromDatabase() { | |
// This is where you'll implement the actual database loading | |
// Example AJAX call structure: | |
/* | |
fetch('/api/changes') | |
.then(response => response.json()) | |
.then(data => { | |
updateChangesTable(data); | |
}) | |
.catch(error => { | |
console.error('Error loading changes:', error); | |
}); | |
*/ | |
} | |
// Function to update table with real data (placeholder) | |
function updateChangesTable(changesData) { | |
// This function will replace the dummy data with real database data | |
const tbody = document.getElementById('changesTableBody'); | |
tbody.innerHTML = ''; // Clear existing content | |
changesData.forEach(change => { | |
const row = document.createElement('tr'); | |
row.onclick = () => showChangeDetails( | |
change.subject, | |
change.date, | |
change.period, | |
change.changeType, | |
change.teacher | |
); | |
const changeTypeClass = getChangeTypeClass(change.changeType); | |
row.innerHTML = ` | |
<td>${change.subject}</td> | |
<td>${change.date}</td> | |
<td>${change.period}</td> | |
<td><span class="change-type ${changeTypeClass}">${change.changeType}</span></td> | |
<td>${change.teacher}</td> | |
`; | |
tbody.appendChild(row); | |
}); | |
} | |
// Helper function to get CSS class for change type | |
function getChangeTypeClass(changeType) { | |
switch(changeType) { | |
case '追加': return 'add'; | |
case '削除': return 'remove'; | |
default: return 'add'; | |
} | |
} | |
// Initialize page | |
document.addEventListener('DOMContentLoaded', function() { | |
// loadChangesFromDatabase(); // Uncomment when ready to load from database | |
}); | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Annotation\Route; | |
class ChangeListController extends AbstractController | |
{ | |
#[Route('/changelist', name: 'app_change_list')] | |
public function change_list(): Response | |
{ | |
return $this->render('change_list/change_list.html.twig', [ | |
'controller_name' => 'ChangeListController', | |
]); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Entity; | |
use App\Repository\CompleteTimeTableRepository; | |
use Doctrine\ORM\Mapping as ORM; | |
#[ORM\Entity(repositoryClass: CompleteTimeTableRepository::class)] | |
#[ORM\Table(name: 'complete_time_table')] | |
class CompleteTimeTable | |
{ | |
#[ORM\Id] | |
#[ORM\GeneratedValue] | |
#[ORM\Column] | |
private ?int $id = null; | |
#[ORM\Column(length: 255)] | |
private ?string $className = null; | |
#[ORM\Column] | |
private ?int $year = null; | |
#[ORM\Column] | |
private ?int $month = null; | |
#[ORM\Column(nullable: true)] | |
private ?int $mondayDate = null; | |
#[ORM\Column(nullable: true)] | |
private ?int $tuesdayDate = null; | |
#[ORM\Column(nullable: true)] | |
private ?int $wednesdayDate = null; | |
#[ORM\Column(nullable: true)] | |
private ?int $thursdayDate = null; | |
#[ORM\Column(nullable: true)] | |
private ?int $fridayDate = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $mon1 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $mon2 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $mon3 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $mon4 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $tue1 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $tue2 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $tue3 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $tue4 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $wed1 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $wed2 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $wed3 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $wed4 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $thu1 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $thu2 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $thu3 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $thu4 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $fri1 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $fri2 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $fri3 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $fri4 = null; | |
public function getId(): ?int | |
{ | |
return $this->id; | |
} | |
public function getClassName(): ?string | |
{ | |
return $this->className; | |
} | |
public function setClassName(string $className): static | |
{ | |
$this->className = $className; | |
return $this; | |
} | |
public function getYear(): ?int | |
{ | |
return $this->year; | |
} | |
public function setYear(int $year): static | |
{ | |
$this->year = $year; | |
return $this; | |
} | |
public function getMonth(): ?int | |
{ | |
return $this->month; | |
} | |
public function setMonth(int $month): static | |
{ | |
$this->month = $month; | |
return $this; | |
} | |
public function getMondayDate(): ?int | |
{ | |
return $this->mondayDate; | |
} | |
public function setMondayDate(?int $mondayDate): static | |
{ | |
$this->mondayDate = $mondayDate; | |
return $this; | |
} | |
public function getTuesdayDate(): ?int | |
{ | |
return $this->tuesdayDate; | |
} | |
public function setTuesdayDate(?int $tuesdayDate): static | |
{ | |
$this->tuesdayDate = $tuesdayDate; | |
return $this; | |
} | |
public function getWednesdayDate(): ?int | |
{ | |
return $this->wednesdayDate; | |
} | |
public function setWednesdayDate(?int $wednesdayDate): static | |
{ | |
$this->wednesdayDate = $wednesdayDate; | |
return $this; | |
} | |
public function getThursdayDate(): ?int | |
{ | |
return $this->thursdayDate; | |
} | |
public function setThursdayDate(?int $thursdayDate): static | |
{ | |
$this->thursdayDate = $thursdayDate; | |
return $this; | |
} | |
public function getFridayDate(): ?int | |
{ | |
return $this->fridayDate; | |
} | |
public function setFridayDate(?int $fridayDate): static | |
{ | |
$this->fridayDate = $fridayDate; | |
return $this; | |
} | |
public function getMon1(): ?string | |
{ | |
return $this->mon1; | |
} | |
public function setMon1(?string $mon1): static | |
{ | |
$this->mon1 = $mon1; | |
return $this; | |
} | |
public function getMon2(): ?string | |
{ | |
return $this->mon2; | |
} | |
public function setMon2(?string $mon2): static | |
{ | |
$this->mon2 = $mon2; | |
return $this; | |
} | |
public function getMon3(): ?string | |
{ | |
return $this->mon3; | |
} | |
public function setMon3(?string $mon3): static | |
{ | |
$this->mon3 = $mon3; | |
return $this; | |
} | |
public function getMon4(): ?string | |
{ | |
return $this->mon4; | |
} | |
public function setMon4(?string $mon4): static | |
{ | |
$this->mon4 = $mon4; | |
return $this; | |
} | |
public function getTue1(): ?string | |
{ | |
return $this->tue1; | |
} | |
public function setTue1(?string $tue1): static | |
{ | |
$this->tue1 = $tue1; | |
return $this; | |
} | |
public function getTue2(): ?string | |
{ | |
return $this->tue2; | |
} | |
public function setTue2(?string $tue2): static | |
{ | |
$this->tue2 = $tue2; | |
return $this; | |
} | |
public function getTue3(): ?string | |
{ | |
return $this->tue3; | |
} | |
public function setTue3(?string $tue3): static | |
{ | |
$this->tue3 = $tue3; | |
return $this; | |
} | |
public function getTue4(): ?string | |
{ | |
return $this->tue4; | |
} | |
public function setTue4(?string $tue4): static | |
{ | |
$this->tue4 = $tue4; | |
return $this; | |
} | |
public function getWed1(): ?string | |
{ | |
return $this->wed1; | |
} | |
public function setWed1(?string $wed1): static | |
{ | |
$this->wed1 = $wed1; | |
return $this; | |
} | |
public function getWed2(): ?string | |
{ | |
return $this->wed2; | |
} | |
public function setWed2(?string $wed2): static | |
{ | |
$this->wed2 = $wed2; | |
return $this; | |
} | |
public function getWed3(): ?string | |
{ | |
return $this->wed3; | |
} | |
public function setWed3(?string $wed3): static | |
{ | |
$this->wed3 = $wed3; | |
return $this; | |
} | |
public function getWed4(): ?string | |
{ | |
return $this->wed4; | |
} | |
public function setWed4(?string $wed4): static | |
{ | |
$this->wed4 = $wed4; | |
return $this; | |
} | |
public function getThu1(): ?string | |
{ | |
return $this->thu1; | |
} | |
public function setThu1(?string $thu1): static | |
{ | |
$this->thu1 = $thu1; | |
return $this; | |
} | |
public function getThu2(): ?string | |
{ | |
return $this->thu2; | |
} | |
public function setThu2(?string $thu2): static | |
{ | |
$this->thu2 = $thu2; | |
return $this; | |
} | |
public function getThu3(): ?string | |
{ | |
return $this->thu3; | |
} | |
public function setThu3(?string $thu3): static | |
{ | |
$this->thu3 = $thu3; | |
return $this; | |
} | |
public function getThu4(): ?string | |
{ | |
return $this->thu4; | |
} | |
public function setThu4(?string $thu4): static | |
{ | |
$this->thu4 = $thu4; | |
return $this; | |
} | |
public function getFri1(): ?string | |
{ | |
return $this->fri1; | |
} | |
public function setFri1(?string $fri1): static | |
{ | |
$this->fri1 = $fri1; | |
return $this; | |
} | |
public function getFri2(): ?string | |
{ | |
return $this->fri2; | |
} | |
public function setFri2(?string $fri2): static | |
{ | |
$this->fri2 = $fri2; | |
return $this; | |
} | |
public function getFri3(): ?string | |
{ | |
return $this->fri3; | |
} | |
public function setFri3(?string $fri3): static | |
{ | |
$this->fri3 = $fri3; | |
return $this; | |
} | |
public function getFri4(): ?string | |
{ | |
return $this->fri4; | |
} | |
public function setFri4(?string $fri4): static | |
{ | |
$this->fri4 = $fri4; | |
return $this; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Repository; | |
use App\Entity\CompleteTimeTable; | |
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; | |
use Doctrine\Persistence\ManagerRegistry; | |
/** | |
* @extends ServiceEntityRepository<CompleteTimeTable> | |
*/ | |
class CompleteTimeTableRepository extends ServiceEntityRepository | |
{ | |
public function __construct(ManagerRegistry $registry) | |
{ | |
parent::__construct($registry, CompleteTimeTable::class); | |
} | |
/** | |
* 指定した年度の学期時間割データを取得 | |
*/ | |
public function findByYear(int $year): array | |
{ | |
return $this->createQueryBuilder('c') | |
->andWhere('c.year = :year') | |
->setParameter('year', $year) | |
->orderBy('c.className', 'ASC') | |
->addOrderBy('c.month', 'ASC') | |
->getQuery() | |
->getResult(); | |
} | |
/** | |
* 指定したクラスの学期時間割データを取得 | |
*/ | |
public function findByClassName(string $className): array | |
{ | |
return $this->createQueryBuilder('c') | |
->andWhere('c.className = :className') | |
->setParameter('className', $className) | |
->orderBy('c.year', 'DESC') | |
->addOrderBy('c.month', 'ASC') | |
->getQuery() | |
->getResult(); | |
} | |
/** | |
* 指定した年度とクラスの学期時間割データを取得 | |
*/ | |
public function findByYearAndClassName(int $year, string $className): array | |
{ | |
return $this->createQueryBuilder('c') | |
->andWhere('c.year = :year') | |
->andWhere('c.className = :className') | |
->setParameter('year', $year) | |
->setParameter('className', $className) | |
->orderBy('c.month', 'ASC') | |
->getQuery() | |
->getResult(); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<p>こんにちは!</p> | |
<p>あなたのメールアドレスを確認するために、以下のリンクをクリックしてください。</p> | |
<p><a href="{{ signedUrl }}">{{ signedUrl }}</a></p> | |
<p>このリンクは1時間以内に有効です。</p> | |
<p>このメールは「g1_時間割変更システム」から送信されました。<br> | |
ご不明な点は <strong>g1.project.system@gmail.com</strong> までご連絡ください。</p> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>時間割作成 - 管理者画面</title> | |
<!-- Bootstrap CSS --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"> | |
<!-- Font Awesome --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<style> | |
body { | |
background-color: #f4c2a1; | |
min-height: 100vh; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
} | |
.header { | |
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%); | |
color: white; | |
padding: 15px 40px; | |
font-size: 1.6rem; | |
font-weight: bold; | |
} | |
.main-container { | |
padding: 20px; | |
max-width: 1200px; | |
margin: 0 auto; | |
} | |
.create-container { | |
background: white; | |
border-radius: 8px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
overflow: hidden; | |
} | |
.create-header { | |
background: linear-gradient(135deg, #28a745 0%, #218838 100%); | |
color: white; | |
padding: 20px; | |
text-align: center; | |
} | |
.form-container { | |
padding: 30px; | |
} | |
.basic-info { | |
background: #f8f9fa; | |
padding: 20px; | |
border-radius: 8px; | |
margin-bottom: 30px; | |
} | |
.form-group { | |
margin-bottom: 20px; | |
} | |
.form-label { | |
font-weight: bold; | |
color: #495057; | |
margin-bottom: 8px; | |
display: block; | |
} | |
.required { | |
color: #dc3545; | |
} | |
.form-control { | |
width: 100%; | |
padding: 12px 15px; | |
border: 2px solid #dee2e6; | |
border-radius: 4px; | |
font-size: 1rem; | |
transition: border-color 0.3s ease; | |
} | |
.form-control:focus { | |
outline: none; | |
border-color: #28a745; | |
box-shadow: 0 0 0 3px rgba(40, 167, 69, 0.1); | |
} | |
.schedule-section { | |
margin-top: 30px; | |
} | |
.schedule-title { | |
color: #28a745; | |
font-size: 1.5rem; | |
font-weight: bold; | |
margin-bottom: 20px; | |
text-align: center; | |
} | |
.schedule-table { | |
width: 100%; | |
border-collapse: collapse; | |
margin: 20px 0; | |
} | |
.schedule-table th { | |
background: #28a745; | |
color: white; | |
padding: 15px 10px; | |
text-align: center; | |
font-weight: bold; | |
border: 1px solid #218838; | |
} | |
.schedule-table td { | |
padding: 8px; | |
border: 1px solid #dee2e6; | |
text-align: center; | |
vertical-align: middle; | |
} | |
.period-header { | |
background: #f8f9fa; | |
font-weight: bold; | |
color: #495057; | |
width: 100px; | |
} | |
.subject-input { | |
width: 100%; | |
padding: 8px 12px; | |
border: 1px solid #dee2e6; | |
border-radius: 4px; | |
font-size: 0.9rem; | |
text-align: center; | |
} | |
.subject-input:focus { | |
outline: none; | |
border-color: #28a745; | |
box-shadow: 0 0 0 2px rgba(40, 167, 69, 0.1); | |
} | |
.action-buttons { | |
text-align: center; | |
margin-top: 30px; | |
padding-top: 20px; | |
border-top: 1px solid #dee2e6; | |
} | |
.btn-action { | |
padding: 12px 30px; | |
border: none; | |
border-radius: 4px; | |
font-size: 1.1rem; | |
font-weight: bold; | |
text-decoration: none; | |
display: inline-block; | |
margin: 0 10px; | |
transition: all 0.3s ease; | |
cursor: pointer; | |
} | |
.btn-create { | |
background-color: #28a745; | |
color: white; | |
} | |
.btn-create:hover { | |
background-color: #218838; | |
transform: translateY(-1px); | |
} | |
.btn-cancel { | |
background-color: #6c757d; | |
color: white; | |
} | |
.btn-cancel:hover { | |
background-color: #5a6268; | |
color: white; | |
text-decoration: none; | |
transform: translateY(-1px); | |
} | |
.alert { | |
padding: 15px; | |
border-radius: 4px; | |
margin-bottom: 20px; | |
} | |
.alert-error { | |
background-color: #f8d7da; | |
color: #721c24; | |
border: 1px solid #f5c6cb; | |
} | |
.info-box { | |
background: #e3f2fd; | |
border: 1px solid #2196f3; | |
border-radius: 8px; | |
padding: 20px; | |
margin-bottom: 20px; | |
} | |
.info-title { | |
color: #1565c0; | |
font-weight: bold; | |
margin-bottom: 10px; | |
} | |
.info-list { | |
color: #1976d2; | |
margin: 0; | |
padding-left: 20px; | |
} | |
@media (max-width: 768px) { | |
.header { | |
padding: 10px 15px; | |
font-size: 1.3rem; | |
} | |
.main-container { | |
padding: 10px; | |
} | |
.form-container { | |
padding: 20px; | |
} | |
.schedule-table { | |
font-size: 0.8rem; | |
} | |
.schedule-table th, | |
.schedule-table td { | |
padding: 6px 4px; | |
} | |
.subject-input { | |
font-size: 0.8rem; | |
padding: 6px 8px; | |
} | |
.btn-action { | |
display: block; | |
margin: 10px auto; | |
width: 200px; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="header"> | |
<i class="fas fa-plus"></i> 時間割作成 | |
</div> | |
<div class="main-container"> | |
<div class="create-container"> | |
<div class="create-header"> | |
<h1><i class="fas fa-table"></i> 新しい時間割を作成</h1> | |
<p>クラスの時間割を新規作成します</p> | |
</div> | |
<div class="form-container"> | |
{% for message in app.flashes('error') %} | |
<div class="alert alert-error"> | |
<i class="fas fa-exclamation-triangle"></i> {{ message }} | |
</div> | |
{% endfor %} | |
<div class="info-box"> | |
<div class="info-title"> | |
<i class="fas fa-info-circle"></i> 作成手順 | |
</div> | |
<ol class="info-list"> | |
<li>クラス名を入力してください(必須)</li> | |
<li>曜日設定と時限数を設定してください</li> | |
<li>各曜日・時限の科目名を入力してください</li> | |
<li>空欄の科目は自動的に空きコマとして扱われます</li> | |
</ol> | |
</div> | |
<form method="post" id="createForm"> | |
<div class="basic-info"> | |
<h3><i class="fas fa-info-circle"></i> 基本情報</h3> | |
<div class="form-group"> | |
<label class="form-label" for="className"> | |
クラス名 <span class="required">*</span> | |
</label> | |
<input type="text" class="form-control" id="className" name="className" | |
placeholder="例: 1年A組" required> | |
<small class="text-muted">※ 一意のクラス名を入力してください</small> | |
</div> | |
<div class="form-group"> | |
<label class="form-label" for="day">曜日設定</label> | |
<input type="text" class="form-control" id="day" name="day" | |
value="週間" placeholder="例: 週間"> | |
</div> | |
<div class="form-group"> | |
<label class="form-label" for="period">時限数</label> | |
<select class="form-control" id="period" name="period"> | |
<option value="4" selected>4時限</option> | |
<option value="5">5時限</option> | |
<option value="6">6時限</option> | |
</select> | |
</div> | |
</div> | |
<div class="schedule-section"> | |
<h3 class="schedule-title"><i class="fas fa-table"></i> 時間割設定</h3> | |
<table class="schedule-table"> | |
<thead> | |
<tr> | |
<th>時限</th> | |
<th>月曜日</th> | |
<th>火曜日</th> | |
<th>水曜日</th> | |
<th>木曜日</th> | |
<th>金曜日</th> | |
</tr> | |
</thead> | |
<tbody> | |
<tr> | |
<td class="period-header">1時限</td> | |
<td><input type="text" class="subject-input" name="mon1" placeholder="科目名"></td> | |
<td><input type="text" class="subject-input" name="tue1" placeholder="科目名"></td> | |
<td><input type="text" class="subject-input" name="wed1" placeholder="科目名"></td> | |
<td><input type="text" class="subject-input" name="thu1" placeholder="科目名"></td> | |
<td><input type="text" class="subject-input" name="fri1" placeholder="科目名"></td> | |
</tr> | |
<tr> | |
<td class="period-header">2時限</td> | |
<td><input type="text" class="subject-input" name="mon2" placeholder="科目名"></td> | |
<td><input type="text" class="subject-input" name="tue2" placeholder="科目名"></td> | |
<td><input type="text" class="subject-input" name="wed2" placeholder="科目名"></td> | |
<td><input type="text" class="subject-input" name="thu2" placeholder="科目名"></td> | |
<td><input type="text" class="subject-input" name="fri2" placeholder="科目名"></td> | |
</tr> | |
<tr> | |
<td class="period-header">3時限</td> | |
<td><input type="text" class="subject-input" name="mon3" placeholder="科目名"></td> | |
<td><input type="text" class="subject-input" name="tue3" placeholder="科目名"></td> | |
<td><input type="text" class="subject-input" name="wed3" placeholder="科目名"></td> | |
<td><input type="text" class="subject-input" name="thu3" placeholder="科目名"></td> | |
<td><input type="text" class="subject-input" name="fri3" placeholder="科目名"></td> | |
</tr> | |
<tr> | |
<td class="period-header">4時限</td> | |
<td><input type="text" class="subject-input" name="mon4" placeholder="科目名"></td> | |
<td><input type="text" class="subject-input" name="tue4" placeholder="科目名"></td> | |
<td><input type="text" class="subject-input" name="wed4" placeholder="科目名"></td> | |
<td><input type="text" class="subject-input" name="thu4" placeholder="科目名"></td> | |
<td><input type="text" class="subject-input" name="fri4" placeholder="科目名"></td> | |
</tr> | |
</tbody> | |
</table> | |
</div> | |
<div class="action-buttons"> | |
<button type="submit" class="btn-action btn-create"> | |
<i class="fas fa-plus"></i> 作成 | |
</button> | |
<a href="{{ path('app_admin_timetable') }}" class="btn-action btn-cancel"> | |
<i class="fas fa-times"></i> キャンセル | |
</a> | |
</div> | |
</form> | |
</div> | |
</div> | |
</div> | |
<!-- Bootstrap JS --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
const form = document.getElementById('createForm'); | |
const classNameInput = document.getElementById('className'); | |
// フォーム送信時のバリデーション | |
form.addEventListener('submit', function(e) { | |
const className = classNameInput.value.trim(); | |
if (!className) { | |
alert('クラス名を入力してください。'); | |
e.preventDefault(); | |
classNameInput.focus(); | |
return; | |
} | |
if (!confirm(`クラス「${className}」の時間割を作成してもよろしいですか?`)) { | |
e.preventDefault(); | |
} | |
}); | |
// 入力フィールドのフォーカス時の処理 | |
const subjectInputs = document.querySelectorAll('.subject-input'); | |
subjectInputs.forEach(input => { | |
input.addEventListener('focus', function() { | |
this.style.backgroundColor = '#d4edda'; | |
}); | |
input.addEventListener('blur', function() { | |
this.style.backgroundColor = ''; | |
}); | |
}); | |
// クラス名入力時のリアルタイムバリデーション | |
classNameInput.addEventListener('input', function() { | |
const value = this.value.trim(); | |
if (value.length > 0) { | |
this.style.borderColor = '#28a745'; | |
} else { | |
this.style.borderColor = '#dee2e6'; | |
} | |
}); | |
}); | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Command; | |
use App\Entity\User; | |
use Doctrine\ORM\EntityManagerInterface; | |
use Symfony\Component\Console\Attribute\AsCommand; | |
use Symfony\Component\Console\Command\Command; | |
use Symfony\Component\Console\Input\InputArgument; | |
use Symfony\Component\Console\Input\InputInterface; | |
use Symfony\Component\Console\Output\OutputInterface; | |
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; | |
#[AsCommand( | |
name: 'app:create-user', | |
description: 'Create a new user', | |
)] | |
class CreateUserCommand extends Command | |
{ | |
public function __construct( | |
private EntityManagerInterface $entityManager, | |
private UserPasswordHasherInterface $passwordHasher | |
) { | |
parent::__construct(); | |
} | |
protected function configure(): void | |
{ | |
$this->addArgument('email', InputArgument::REQUIRED, 'User email'); | |
} | |
protected function execute(InputInterface $input, OutputInterface $output): int | |
{ | |
$email = $input->getArgument('email'); | |
$user = new User(); | |
$user->setEmail($email); | |
$user->setPassword($this->passwordHasher->hashPassword($user, 'dummy_password')); | |
$user->setRoles(['ROLE_USER']); | |
$user->setIsVerified(true); | |
$this->entityManager->persist($user); | |
$this->entityManager->flush(); | |
$output->writeln('User created successfully: ' . $email); | |
return Command::SUCCESS; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>CSVインポート - 開発者ツール</title> | |
<!-- Bootstrap CSS --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"> | |
<!-- Font Awesome --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<!-- SweetAlert2 --> | |
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> | |
<style> | |
body { | |
background-color: #f4c2a1; | |
min-height: 100vh; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
} | |
.header { | |
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%); | |
color: white; | |
padding: 15px 40px; | |
font-size: 1.6rem; | |
font-weight: bold; | |
position: relative; | |
} | |
.back-btn { | |
position: absolute; | |
right: 20px; | |
top: 50%; | |
transform: translateY(-50%); | |
background-color: #6c757d; | |
color: white; | |
border: none; | |
padding: 8px 16px; | |
border-radius: 4px; | |
text-decoration: none; | |
font-size: 0.9rem; | |
transition: background-color 0.3s ease; | |
} | |
.back-btn:hover { | |
background-color: #5a6268; | |
color: white; | |
text-decoration: none; | |
} | |
.main-container { | |
padding: 20px; | |
max-width: 1400px; | |
margin: 0 auto; | |
} | |
.import-card { | |
background: white; | |
border-radius: 8px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
padding: 30px; | |
margin-bottom: 20px; | |
} | |
.card-title { | |
color: #2c3e50; | |
font-size: 2rem; | |
font-weight: bold; | |
margin-bottom: 20px; | |
text-align: center; | |
} | |
.upload-section { | |
background: #f8f9fa; | |
padding: 25px; | |
border-radius: 8px; | |
margin-bottom: 30px; | |
} | |
.semester-form { | |
background: #fff3cd; | |
border: 2px solid #ffc107; | |
padding: 20px; | |
border-radius: 8px; | |
margin-bottom: 20px; | |
display: none; | |
} | |
.semester-form.show { | |
display: block; | |
} | |
.form-group { | |
margin-bottom: 15px; | |
} | |
.form-label { | |
font-weight: bold; | |
color: #2c3e50; | |
margin-bottom: 5px; | |
} | |
.form-control, .form-select { | |
border: 2px solid #dee2e6; | |
border-radius: 4px; | |
padding: 8px 12px; | |
} | |
.form-control:focus, .form-select:focus { | |
border-color: #2c3e50; | |
box-shadow: 0 0 0 3px rgba(44, 62, 80, 0.1); | |
} | |
.file-input { | |
width: 100%; | |
padding: 12px; | |
border: 2px dashed #dee2e6; | |
border-radius: 8px; | |
background: white; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
} | |
.file-input:hover { | |
border-color: #2c3e50; | |
background: #f8f9fa; | |
} | |
.upload-btn { | |
background-color: #2c3e50; | |
color: white; | |
border: none; | |
padding: 12px 30px; | |
font-size: 1.1rem; | |
font-weight: bold; | |
border-radius: 4px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
width: 100%; | |
margin-top: 15px; | |
} | |
.upload-btn:hover { | |
background-color: #34495e; | |
} | |
.upload-btn:disabled { | |
background-color: #6c757d; | |
cursor: not-allowed; | |
} | |
.info-section { | |
background: #e3f2fd; | |
border: 1px solid #bbdefb; | |
padding: 20px; | |
border-radius: 8px; | |
margin-bottom: 20px; | |
} | |
.data-table { | |
background: white; | |
border-radius: 8px; | |
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | |
overflow-x: auto; | |
} | |
.table { | |
margin: 0; | |
font-size: 0.8rem; | |
} | |
.table th { | |
background: #2c3e50; | |
color: white; | |
border: none; | |
padding: 12px 8px; | |
font-size: 0.8rem; | |
white-space: nowrap; | |
text-align: center; | |
} | |
.table td { | |
padding: 8px 6px; | |
border-color: #dee2e6; | |
white-space: nowrap; | |
text-align: center; | |
font-size: 0.75rem; | |
} | |
.table td:first-child { | |
font-weight: bold; | |
background: #f8f9fa; | |
} | |
.sample-format { | |
background: #fff3cd; | |
border: 1px solid #ffeaa7; | |
padding: 15px; | |
border-radius: 8px; | |
margin-top: 10px; | |
font-size: 0.9rem; | |
} | |
.nav-tabs { | |
margin-bottom: 20px; | |
} | |
.nav-tabs .nav-link { | |
color: #2c3e50; | |
font-weight: bold; | |
} | |
.nav-tabs .nav-link.active { | |
background-color: #2c3e50; | |
color: white; | |
border-color: #2c3e50; | |
} | |
.semester-warning { | |
background: #f8d7da; | |
border: 1px solid #f5c6cb; | |
color: #721c24; | |
padding: 15px; | |
border-radius: 8px; | |
margin-bottom: 15px; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="header"> | |
<i class="fas fa-file-csv"></i> CSVインポート | |
<a href="{{ path('app_developer_home') }}" class="back-btn"> | |
<i class="fas fa-arrow-left"></i> 戻る | |
</a> | |
</div> | |
<div class="main-container"> | |
<div class="import-card"> | |
<h1 class="card-title">CSVインポート管理</h1> | |
<div class="info-section"> | |
<h4><i class="fas fa-info-circle"></i> サポートされるCSV形式</h4> | |
<div class="row"> | |
<div class="col-md-6"> | |
<p><strong>時間割サンプル.csv形式:</strong></p> | |
<div class="sample-format"> | |
<strong>ヘッダー形式(21列):</strong><br> | |
クラス,月1,月2,月3,月4,火1,火2,火3,火4,水1,水2,水3,水4,木1,木2,木3,木4,金1,金2,金3,金4 | |
</div> | |
</div> | |
<div class="col-md-6"> | |
<p><strong>担当者.csv形式:</strong></p> | |
<div class="sample-format"> | |
<strong>ヘッダー形式(8列):</strong><br> | |
科目コード,クラス名,場所,科目名,雇用形態,担当者1,担当者2,担当者3 | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="upload-section"> | |
<form method="post" enctype="multipart/form-data" id="csvUploadForm"> | |
<div class="mb-3"> | |
<label for="csv_file" class="form-label"> | |
<i class="fas fa-file-csv"></i> CSVファイルを選択 | |
</label> | |
<input type="file" | |
class="file-input" | |
id="csv_file" | |
name="csv_file" | |
accept=".csv" | |
required> | |
</div> | |
<!-- 時間割CSV用の学期情報フォーム --> | |
<div id="semesterForm" class="semester-form"> | |
<div class="semester-warning"> | |
<i class="fas fa-exclamation-triangle"></i> | |
<strong>時間割CSVファイルが選択されました</strong><br> | |
学期情報を入力してください。この情報は半年分の時間割データ作成に使用されます。 | |
</div> | |
<div class="row"> | |
<div class="col-md-6"> | |
<div class="form-group"> | |
<label for="semester" class="form-label">学期</label> | |
<select class="form-select" id="semester" name="semester" required> | |
<option value="">選択してください</option> | |
<option value="前期">前期</option> | |
<option value="後期">後期</option> | |
</select> | |
</div> | |
</div> | |
<div class="col-md-6"> | |
<div class="form-group"> | |
<label for="year" class="form-label">年度</label> | |
<input type="number" | |
class="form-control" | |
id="year" | |
name="year" | |
min="2020" | |
max="2030" | |
value="{{ 'now'|date('Y') }}" | |
required> | |
</div> | |
</div> | |
</div> | |
<div class="row"> | |
<div class="col-md-6"> | |
<div class="form-group"> | |
<label for="start_date" class="form-label">開始日</label> | |
<input type="date" | |
class="form-control" | |
id="start_date" | |
name="start_date" | |
required> | |
</div> | |
</div> | |
<div class="col-md-6"> | |
<div class="form-group"> | |
<label for="end_date" class="form-label">終了日</label> | |
<input type="date" | |
class="form-control" | |
id="end_date" | |
name="end_date" | |
required> | |
</div> | |
</div> | |
</div> | |
</div> | |
<button type="submit" class="upload-btn" id="uploadBtn"> | |
<i class="fas fa-upload"></i> インポート実行 | |
</button> | |
</form> | |
</div> | |
</div> | |
<!-- データ表示タブ --> | |
<div class="import-card"> | |
<ul class="nav nav-tabs" id="dataTab" role="tablist"> | |
<li class="nav-item" role="presentation"> | |
<button class="nav-link active" id="timetable-tab" data-bs-toggle="tab" data-bs-target="#timetable" type="button" role="tab"> | |
<i class="fas fa-calendar-alt"></i> 時間割データ | |
</button> | |
</li> | |
<li class="nav-item" role="presentation"> | |
<button class="nav-link" id="lesson-tab" data-bs-toggle="tab" data-bs-target="#lesson" type="button" role="tab"> | |
<i class="fas fa-chalkboard-teacher"></i> 担当者データ | |
</button> | |
</li> | |
<li class="nav-item" role="presentation"> | |
<button class="nav-link" id="complete-tab" data-bs-toggle="tab" data-bs-target="#complete" type="button" role="tab"> | |
<i class="fas fa-calendar-check"></i> 学期時間割データ | |
</button> | |
</li> | |
</ul> | |
<div class="tab-content" id="dataTabContent"> | |
<!-- 時間割データタブ --> | |
<div class="tab-pane fade show active" id="timetable" role="tabpanel"> | |
{% if timeTables|length > 0 %} | |
<h3 class="mb-3">時間割データ ({{ timeTables|length }}件)</h3> | |
<div class="data-table"> | |
<table class="table table-striped"> | |
<thead> | |
<tr> | |
<th>クラス</th> | |
<th>月1</th> | |
<th>月2</th> | |
<th>月3</th> | |
<th>月4</th> | |
<th>火1</th> | |
<th>火2</th> | |
<th>火3</th> | |
<th>火4</th> | |
<th>水1</th> | |
<th>水2</th> | |
<th>水3</th> | |
<th>水4</th> | |
<th>木1</th> | |
<th>木2</th> | |
<th>木3</th> | |
<th>木4</th> | |
<th>金1</th> | |
<th>金2</th> | |
<th>金3</th> | |
<th>金4</th> | |
</tr> | |
</thead> | |
<tbody> | |
{% for timeTable in timeTables %} | |
<tr> | |
<td>{{ timeTable.className }}</td> | |
<td>{{ timeTable.mon1 ?: '-' }}</td> | |
<td>{{ timeTable.mon2 ?: '-' }}</td> | |
<td>{{ timeTable.mon3 ?: '-' }}</td> | |
<td>{{ timeTable.mon4 ?: '-' }}</td> | |
<td>{{ timeTable.tue1 ?: '-' }}</td> | |
<td>{{ timeTable.tue2 ?: '-' }}</td> | |
<td>{{ timeTable.tue3 ?: '-' }}</td> | |
<td>{{ timeTable.tue4 ?: '-' }}</td> | |
<td>{{ timeTable.wed1 ?: '-' }}</td> | |
<td>{{ timeTable.wed2 ?: '-' }}</td> | |
<td>{{ timeTable.wed3 ?: '-' }}</td> | |
<td>{{ timeTable.wed4 ?: '-' }}</td> | |
<td>{{ timeTable.thu1 ?: '-' }}</td> | |
<td>{{ timeTable.thu2 ?: '-' }}</td> | |
<td>{{ timeTable.thu3 ?: '-' }}</td> | |
<td>{{ timeTable.thu4 ?: '-' }}</td> | |
<td>{{ timeTable.fri1 ?: '-' }}</td> | |
<td>{{ timeTable.fri2 ?: '-' }}</td> | |
<td>{{ timeTable.fri3 ?: '-' }}</td> | |
<td>{{ timeTable.fri4 ?: '-' }}</td> | |
</tr> | |
{% endfor %} | |
</tbody> | |
</table> | |
</div> | |
{% else %} | |
<div class="text-center py-5"> | |
<i class="fas fa-calendar-times fa-3x text-muted mb-3"></i> | |
<p class="text-muted">時間割データがありません</p> | |
</div> | |
{% endif %} | |
</div> | |
<!-- 担当者データタブ --> | |
<div class="tab-pane fade" id="lesson" role="tabpanel"> | |
{% if lessonData|length > 0 %} | |
<h3 class="mb-3">担当者データ ({{ lessonData|length }}件)</h3> | |
<div class="data-table"> | |
<table class="table table-striped"> | |
<thead> | |
<tr> | |
<th>科目コード</th> | |
<th>クラス</th> | |
<th>場所</th> | |
<th>科目</th> | |
<th>雇用形態</th> | |
<th>担当者1</th> | |
<th>担当者2</th> | |
<th>担当者3</th> | |
</tr> | |
</thead> | |
<tbody> | |
{% for lesson in lessonData %} | |
<tr> | |
<td>{{ lesson.subjectCode }}</td> | |
<td>{{ lesson.className }}</td> | |
<td>{{ lesson.location }}</td> | |
<td>{{ lesson.subjectName }}</td> | |
<td>{{ lesson.employmentType }}</td> | |
<td>{{ lesson.teacher1 ?: '-' }}</td> | |
<td>{{ lesson.teacher2 ?: '-' }}</td> | |
<td>{{ lesson.teacher3 ?: '-' }}</td> | |
</tr> | |
{% endfor %} | |
</tbody> | |
</table> | |
</div> | |
{% else %} | |
<div class="text-center py-5"> | |
<i class="fas fa-chalkboard-teacher fa-3x text-muted mb-3"></i> | |
<p class="text-muted">担当者データがありません</p> | |
</div> | |
{% endif %} | |
</div> | |
<!-- 学期時間割データタブ --> | |
<div class="tab-pane fade" id="complete" role="tabpanel"> | |
<!-- クラス選択フォーム --> | |
<div class="mb-3"> | |
<form method="get" action="{{ path('app_csv_import') }}#complete" class="d-flex align-items-end gap-3"> | |
<div class="form-group"> | |
<label class="form-label">表示するクラス:</label> | |
<select name="selected_class" class="form-select" style="min-width: 150px;" onchange="this.form.submit()"> | |
{% for grade in 1..5 %} | |
{% for class in 1..5 %} | |
{% set classValue = grade ~ '-' ~ class %} | |
<option value="{{ classValue }}" {{ classValue == selectedClass ? 'selected' : '' }}> | |
{{ grade }}年{{ class }}組 | |
</option> | |
{% endfor %} | |
{% endfor %} | |
</select> | |
</div> | |
<button type="submit" class="btn btn-primary"> | |
<i class="fas fa-search"></i> 表示 | |
</button> | |
</form> | |
</div> | |
{% if timeTableData|length > 0 %} | |
<h3 class="mb-3">{{ selectedClass }}クラスの学期時間割データ ({{ timeTableData|length }}件)</h3> | |
<div class="data-table"> | |
<table class="table table-striped"> | |
<thead> | |
<tr> | |
<th>ID</th> | |
<th>クラス</th> | |
<th>年</th> | |
<th>月</th> | |
<!-- ★★ 修正:すべての曜日の日付を表示 ★★ --> | |
<th>月曜日</th> | |
<th>火曜日</th> | |
<th>水曜日</th> | |
<th>木曜日</th> | |
<th>金曜日</th> | |
<th>月1</th> | |
<th>月2</th> | |
<th>月3</th> | |
<th>月4</th> | |
<th>火1</th> | |
<th>火2</th> | |
<th>火3</th> | |
<th>火4</th> | |
<th>水1</th> | |
<th>水2</th> | |
<th>水3</th> | |
<th>水4</th> | |
<th>木1</th> | |
<th>木2</th> | |
<th>木3</th> | |
<th>木4</th> | |
<th>金1</th> | |
<th>金2</th> | |
<th>金3</th> | |
<th>金4</th> | |
</tr> | |
</thead> | |
<tbody> | |
{% for data in timeTableData %} | |
<tr> | |
<td>{{ data.id }}</td> | |
<td class="fw-bold text-primary">{{ data.className }}</td> | |
<td>{{ data.year }}</td> | |
<td>{{ data.month }}</td> | |
<!-- ★★ 修正:すべての曜日の日付を表示 ★★ --> | |
<td>{{ data.mondayDate ?: '-' }}</td> | |
<td>{{ data.tuesdayDate ?: '-' }}</td> | |
<td>{{ data.wednesdayDate ?: '-' }}</td> | |
<td>{{ data.thursdayDate ?: '-' }}</td> | |
<td>{{ data.fridayDate ?: '-' }}</td> | |
<td>{{ data.mon1 ?: '-' }}</td> | |
<td>{{ data.mon2 ?: '-' }}</td> | |
<td>{{ data.mon3 ?: '-' }}</td> | |
<td>{{ data.mon4 ?: '-' }}</td> | |
<td>{{ data.tue1 ?: '-' }}</td> | |
<td>{{ data.tue2 ?: '-' }}</td> | |
<td>{{ data.tue3 ?: '-' }}</td> | |
<td>{{ data.tue4 ?: '-' }}</td> | |
<td>{{ data.wed1 ?: '-' }}</td> | |
<td>{{ data.wed2 ?: '-' }}</td> | |
<td>{{ data.wed3 ?: '-' }}</td> | |
<td>{{ data.wed4 ?: '-' }}</td> | |
<td>{{ data.thu1 ?: '-' }}</td> | |
<td>{{ data.thu2 ?: '-' }}</td> | |
<td>{{ data.thu3 ?: '-' }}</td> | |
<td>{{ data.thu4 ?: '-' }}</td> | |
<td>{{ data.fri1 ?: '-' }}</td> | |
<td>{{ data.fri2 ?: '-' }}</td> | |
<td>{{ data.fri3 ?: '-' }}</td> | |
<td>{{ data.fri4 ?: '-' }}</td> | |
</tr> | |
{% endfor %} | |
</tbody> | |
</table> | |
</div> | |
{% else %} | |
<div class="text-center py-5"> | |
<i class="fas fa-calendar-times fa-3x text-muted mb-3"></i> | |
<p class="text-muted">{{ selectedClass }}クラスの学期時間割データがありません</p> | |
<p class="text-muted">CSVファイルをインポートしてデータを作成してください</p> | |
</div> | |
{% endif %} | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Bootstrap JS --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
// ★★ 追加:ページ読み込み時にアンカーがある場合はそこにスクロール ★★ | |
if (window.location.hash === '#complete') { | |
setTimeout(function() { | |
const completeTab = document.getElementById('complete-tab'); | |
if (completeTab) { | |
completeTab.click(); | |
document.getElementById('complete').scrollIntoView({ behavior: 'smooth' }); | |
} | |
}, 100); | |
} | |
const fileInput = document.getElementById('csv_file'); | |
const semesterForm = document.getElementById('semesterForm'); | |
const uploadBtn = document.getElementById('uploadBtn'); | |
const form = document.getElementById('csvUploadForm'); | |
// ファイル選択時の処理 | |
fileInput.addEventListener('change', function() { | |
const fileName = this.files[0]?.name || ''; | |
if (fileName.includes('時間割') || fileName.includes('timetable')) { | |
semesterForm.classList.add('show'); | |
// 学期フォームの必須項目を有効化 | |
semesterForm.querySelectorAll('input, select').forEach(input => { | |
input.required = true; | |
}); | |
} else { | |
semesterForm.classList.remove('show'); | |
// 学期フォームの必須項目を無効化 | |
semesterForm.querySelectorAll('input, select').forEach(input => { | |
input.required = false; | |
}); | |
} | |
}); | |
// 日付の妥当性チェック | |
const startDateInput = document.getElementById('start_date'); | |
const endDateInput = document.getElementById('end_date'); | |
function validateDates() { | |
if (startDateInput.value && endDateInput.value) { | |
const startDate = new Date(startDateInput.value); | |
const endDate = new Date(endDateInput.value); | |
if (startDate >= endDate) { | |
endDateInput.setCustomValidity('終了日は開始日より後の日付を選択してください。'); | |
} else { | |
endDateInput.setCustomValidity(''); | |
} | |
} | |
} | |
startDateInput.addEventListener('change', validateDates); | |
endDateInput.addEventListener('change', validateDates); | |
// フォーム送信時の確認 | |
form.addEventListener('submit', function(e) { | |
const fileName = fileInput.files[0]?.name || ''; | |
let confirmMessage = ''; | |
if (fileName.includes('担当者') || fileName.includes('lesson')) { | |
confirmMessage = '担当者データをインポートすると、既存の担当者データがすべて削除されます。\n続行しますか?'; | |
} else if (fileName.includes('時間割') || fileName.includes('timetable')) { | |
const semester = document.getElementById('semester').value; | |
confirmMessage = `${semester}の時間割データをインポートすると、指定期間の既存データが削除されます。\n続行しますか?`; | |
} else { | |
confirmMessage = 'CSVファイルをインポートします。続行しますか?'; | |
} | |
if (!confirm(confirmMessage)) { | |
e.preventDefault(); | |
return false; | |
} | |
// 送信中の状態に変更 | |
uploadBtn.disabled = true; | |
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> インポート中...'; | |
}); | |
// 成功/エラーメッセージの表示 | |
{% if message %} | |
Swal.fire({ | |
icon: 'success', | |
title: 'インポート完了', | |
text: '{{ message }}', | |
confirmButtonColor: '#2c3e50' | |
}); | |
{% endif %} | |
{% if error %} | |
Swal.fire({ | |
icon: 'error', | |
title: 'インポートエラー', | |
text: '{{ error }}', | |
confirmButtonColor: '#2c3e50' | |
}); | |
{% endif %} | |
}); | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use App\Entity\CompleteTimeTable; | |
use App\Entity\LessonData; | |
use App\Entity\TimeTable; | |
use App\Security\AccessControlVoter; | |
use Doctrine\ORM\EntityManagerInterface; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\Request; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Annotation\Route; | |
class CsvImportController extends AbstractController | |
{ | |
public function __construct( | |
private EntityManagerInterface $entityManager | |
) {} | |
#[Route('/admin/csv-import', name: 'app_csv_import')] | |
public function csvImport(Request $request): Response | |
{ | |
$this->denyAccessUnlessGranted(AccessControlVoter::ACCESS_ADMIN); | |
$message = null; | |
$error = null; | |
if ($request->isMethod('POST')) { | |
$uploadedFile = $request->files->get('csv_file'); | |
// ★★ 修正:フォームパラメータを正しく取得 ★★ | |
$startDate = $request->request->get('start_date'); | |
$endDate = $request->request->get('end_date'); | |
$semester = $request->request->get('semester'); | |
if ($uploadedFile && $uploadedFile->isValid()) { | |
try { | |
$csvData = $this->readCsvFile($uploadedFile->getPathname()); | |
// ★★ 修正:時間割データのインポート(期間指定対応) ★★ | |
if ($startDate && $endDate) { | |
$result = $this->importTimeTableData($csvData, $semester, $startDate, $endDate); | |
$message = $result['message']; | |
if (isset($result['error'])) { | |
$error = $result['error']; | |
} | |
} else { | |
// 担当者データのインポート | |
$result = $this->importLessonData($csvData); | |
$message = $result['message']; | |
if (isset($result['error'])) { | |
$error = $result['error']; | |
} | |
} | |
} catch (\Exception $e) { | |
$error = 'CSVファイルの処理中にエラーが発生しました: ' . $e->getMessage(); | |
} | |
} else { | |
$error = 'ファイルのアップロードに失敗しました。'; | |
} | |
} | |
// ★★ 学期時間割データの表示(クラス選択対応) ★★ | |
$selectedClass = $request->query->get('selected_class', '1-1'); | |
$timeTableData = $this->getTimeTableDataByClass($selectedClass); | |
return $this->render('admin_page/csv_import.html.twig', [ | |
'message' => $message, | |
'error' => $error, | |
'timeTableData' => $timeTableData, | |
'selectedClass' => $selectedClass, | |
'timeTables' => $this->entityManager->getRepository(TimeTable::class)->findAll(), | |
'lessonData' => $this->entityManager->getRepository(LessonData::class)->findAll(), | |
]); | |
} | |
// ★★ 時間割データインポート(完全修正版) ★★ | |
private function importTimeTableData(array $csvData, string $semester, string $startDate, string $endDate): array | |
{ | |
try { | |
// 日付の妥当性チェック | |
$start = new \DateTime($startDate); | |
$end = new \DateTime($endDate); | |
if ($start >= $end) { | |
return ['error' => '開始日は終了日より前の日付を指定してください。']; | |
} | |
// ★★ 修正:指定期間の既存データを完全削除 ★★ | |
$deleteCount = $this->entityManager->createQuery( | |
'DELETE FROM App\Entity\CompleteTimeTable c | |
WHERE (c.year = :startYear AND c.month >= :startMonth) | |
OR (c.year > :startYear AND c.year < :endYear) | |
OR (c.year = :endYear AND c.month <= :endMonth)' | |
) | |
->setParameter('startYear', (int)$start->format('Y')) | |
->setParameter('endYear', (int)$end->format('Y')) | |
->setParameter('startMonth', (int)$start->format('n')) | |
->setParameter('endMonth', (int)$end->format('n')) | |
->execute(); | |
// 基本時間割テンプレートを取得 | |
$timeTableTemplates = $this->entityManager | |
->getRepository(TimeTable::class) | |
->findAll(); | |
if (empty($timeTableTemplates)) { | |
return ['error' => '基本時間割テンプレートが見つかりません。']; | |
} | |
// ★★ 修正:指定期間のみにデータを生成 ★★ | |
$insertCount = $this->generateTimeTableForPeriod($timeTableTemplates, $start, $end); | |
return [ | |
'message' => sprintf( | |
'期間(%s ~ %s)の時間割データを更新しました。削除: %d件、追加: %d件', | |
$start->format('Y年m月d日'), | |
$end->format('Y年m月d日'), | |
$deleteCount, | |
$insertCount | |
) | |
]; | |
} catch (\Exception $e) { | |
return ['error' => '時間割データの処理中にエラーが発生しました: ' . $e->getMessage()]; | |
} | |
} | |
// ★★ 担当者データインポート(全削除版) ★★ | |
private function importLessonData(array $csvData): array | |
{ | |
try { | |
// ★★ 既存の担当者データを全削除 ★★ | |
$deleteCount = $this->entityManager->createQuery( | |
'DELETE FROM App\Entity\LessonData' | |
)->execute(); | |
$insertCount = 0; | |
foreach ($csvData as $index => $row) { | |
if ($index === 0) continue; // ヘッダー行をスキップ | |
if (count($row) >= 8) { | |
$lessonData = new LessonData(); | |
$lessonData->setSubjectCode($row[0] ?? ''); | |
$lessonData->setClassName($row[1] ?? ''); | |
$lessonData->setLocation($row[2] ?? ''); | |
$lessonData->setSubjectName($row[3] ?? ''); | |
$lessonData->setEmploymentType($row[4] ?? ''); | |
$lessonData->setTeacher1($row[5] ?? ''); | |
$lessonData->setTeacher2($row[6] ?? ''); | |
$lessonData->setTeacher3($row[7] ?? ''); | |
$this->entityManager->persist($lessonData); | |
$insertCount++; | |
} | |
} | |
$this->entityManager->flush(); | |
return [ | |
'message' => sprintf( | |
'担当者データを更新しました。削除: %d件、追加: %d件', | |
$deleteCount, | |
$insertCount | |
) | |
]; | |
} catch (\Exception $e) { | |
return ['error' => '担当者データの処理中にエラーが発生しました: ' . $e->getMessage()]; | |
} | |
} | |
// ★★ 期間内の時間割データ生成(完全修正版) ★★ | |
private function generateTimeTableForPeriod(array $templates, \DateTime $start, \DateTime $end): int | |
{ | |
$insertCount = 0; | |
$current = clone $start; | |
// ★★ 修正:指定期間内の日付のみを厳密に処理 ★★ | |
while ($current <= $end) { | |
// 現在の日付が月曜日の場合のみ、その週のデータを作成 | |
if ((int)$current->format('N') === 1) { // 月曜日の場合 | |
$mondayDate = clone $current; | |
// ★★ 修正:月曜日が指定期間内にある場合のみデータ作成 ★★ | |
if ($mondayDate >= $start && $mondayDate <= $end) { | |
foreach ($templates as $template) { | |
$completeTimeTable = new CompleteTimeTable(); | |
$completeTimeTable->setClassName($template->getClassName()); | |
$completeTimeTable->setYear((int)$mondayDate->format('Y')); | |
$completeTimeTable->setMonth((int)$mondayDate->format('n')); | |
$completeTimeTable->setMondayDate((int)$mondayDate->format('j')); | |
// 各曜日の日付を計算 | |
$tuesdayDate = clone $mondayDate; | |
$tuesdayDate->modify('+1 day'); | |
$wednesdayDate = clone $mondayDate; | |
$wednesdayDate->modify('+2 days'); | |
$thursdayDate = clone $mondayDate; | |
$thursdayDate->modify('+3 days'); | |
$fridayDate = clone $mondayDate; | |
$fridayDate->modify('+4 days'); | |
// ★★ 修正:各曜日が指定期間内の場合のみ日付を設定 ★★ | |
if ($tuesdayDate >= $start && $tuesdayDate <= $end) { | |
$completeTimeTable->setTuesdayDate((int)$tuesdayDate->format('j')); | |
} | |
if ($wednesdayDate >= $start && $wednesdayDate <= $end) { | |
$completeTimeTable->setWednesdayDate((int)$wednesdayDate->format('j')); | |
} | |
if ($thursdayDate >= $start && $thursdayDate <= $end) { | |
$completeTimeTable->setThursdayDate((int)$thursdayDate->format('j')); | |
} | |
if ($fridayDate >= $start && $fridayDate <= $end) { | |
$completeTimeTable->setFridayDate((int)$fridayDate->format('j')); | |
} | |
// ★★ 修正:各曜日が指定期間内の場合のみ時間割データを設定 ★★ | |
// 月曜日(既に期間内であることを確認済み) | |
$completeTimeTable->setMon1($template->getMon1()); | |
$completeTimeTable->setMon2($template->getMon2()); | |
$completeTimeTable->setMon3($template->getMon3()); | |
$completeTimeTable->setMon4($template->getMon4()); | |
// 火曜日 | |
if ($tuesdayDate >= $start && $tuesdayDate <= $end) { | |
$completeTimeTable->setTue1($template->getTue1()); | |
$completeTimeTable->setTue2($template->getTue2()); | |
$completeTimeTable->setTue3($template->getTue3()); | |
$completeTimeTable->setTue4($template->getTue4()); | |
} | |
// 水曜日 | |
if ($wednesdayDate >= $start && $wednesdayDate <= $end) { | |
$completeTimeTable->setWed1($template->getWed1()); | |
$completeTimeTable->setWed2($template->getWed2()); | |
$completeTimeTable->setWed3($template->getWed3()); | |
$completeTimeTable->setWed4($template->getWed4()); | |
} | |
// 木曜日 | |
if ($thursdayDate >= $start && $thursdayDate <= $end) { | |
$completeTimeTable->setThu1($template->getThu1()); | |
$completeTimeTable->setThu2($template->getThu2()); | |
$completeTimeTable->setThu3($template->getThu3()); | |
$completeTimeTable->setThu4($template->getThu4()); | |
} | |
// 金曜日 | |
if ($fridayDate >= $start && $fridayDate <= $end) { | |
$completeTimeTable->setFri1($template->getFri1()); | |
$completeTimeTable->setFri2($template->getFri2()); | |
$completeTimeTable->setFri3($template->getFri3()); | |
$completeTimeTable->setFri4($template->getFri4()); | |
} | |
$this->entityManager->persist($completeTimeTable); | |
$insertCount++; | |
} | |
} | |
} | |
$current->modify('+1 day'); | |
} | |
$this->entityManager->flush(); | |
return $insertCount; | |
} | |
// ★★ クラス別時間割データ取得 ★★ | |
private function getTimeTableDataByClass(string $className): array | |
{ | |
return $this->entityManager | |
->getRepository(CompleteTimeTable::class) | |
->createQueryBuilder('c') | |
->where('c.className = :className') | |
->setParameter('className', $className) | |
->orderBy('c.year', 'ASC') | |
->addOrderBy('c.month', 'ASC') | |
->addOrderBy('c.mondayDate', 'ASC') | |
->getQuery() | |
->getResult(); | |
} | |
private function copySubjectsFromTemplate(CompleteTimeTable $completeTimeTable, TimeTable $template): void | |
{ | |
// 月曜日 | |
$completeTimeTable->setMon1($template->getMon1()); | |
$completeTimeTable->setMon2($template->getMon2()); | |
$completeTimeTable->setMon3($template->getMon3()); | |
$completeTimeTable->setMon4($template->getMon4()); | |
// 火曜日 | |
$completeTimeTable->setTue1($template->getTue1()); | |
$completeTimeTable->setTue2($template->getTue2()); | |
$completeTimeTable->setTue3($template->getTue3()); | |
$completeTimeTable->setTue4($template->getTue4()); | |
// 水曜日 | |
$completeTimeTable->setWed1($template->getWed1()); | |
$completeTimeTable->setWed2($template->getWed2()); | |
$completeTimeTable->setWed3($template->getWed3()); | |
$completeTimeTable->setWed4($template->getWed4()); | |
// 木曜日 | |
$completeTimeTable->setThu1($template->getThu1()); | |
$completeTimeTable->setThu2($template->getThu2()); | |
$completeTimeTable->setThu3($template->getThu3()); | |
$completeTimeTable->setThu4($template->getThu4()); | |
// 金曜日 | |
$completeTimeTable->setFri1($template->getFri1()); | |
$completeTimeTable->setFri2($template->getFri2()); | |
$completeTimeTable->setFri3($template->getFri3()); | |
$completeTimeTable->setFri4($template->getFri4()); | |
} | |
private function readCsvFile(string $filePath): array | |
{ | |
$csvData = []; | |
if (($handle = fopen($filePath, 'r')) !== false) { | |
while (($data = fgetcsv($handle, 1000, ',')) !== false) { | |
$csvData[] = array_map(function($item) { | |
return mb_convert_encoding($item, 'UTF-8', 'auto'); | |
}, $data); | |
} | |
fclose($handle); | |
} | |
return $csvData; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\JsonResponse; | |
use Symfony\Component\Routing\Annotation\Route; | |
class CurrentUserController extends AbstractController | |
{ | |
#[Route('/api/current-user', name: 'api_current_user', methods: ['GET'])] | |
public function getCurrentUser(): JsonResponse | |
{ | |
try { | |
$user = $this->getUser(); | |
if (!$user) { | |
return new JsonResponse([ | |
'authenticated' => false, | |
'user' => null, | |
'message' => 'ユーザーは認証されていません' | |
]); | |
} | |
return new JsonResponse([ | |
'authenticated' => true, | |
'user' => [ | |
'id' => $user->getId(), | |
'email' => $user->getEmail(), | |
'roles' => $user->getRoles(), | |
'firstName' => $user->getFirstName(), | |
'lastName' => $user->getLastName(), | |
'fullName' => $user->getFullName() | |
], | |
'email' => $user->getEmail(), | |
'roles' => $user->getRoles(), | |
'message' => 'ユーザー情報を正常に取得しました' | |
]); | |
} catch (\Exception $e) { | |
return new JsonResponse([ | |
'authenticated' => false, | |
'user' => null, | |
'error' => $e->getMessage(), | |
'message' => 'ユーザー情報の取得中にエラーが発生しました' | |
], 500); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use App\Entity\User; | |
use App\Security\AccessControlVoter; | |
use Doctrine\ORM\EntityManagerInterface; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\JsonResponse; | |
use Symfony\Component\HttpFoundation\Request; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Annotation\Route; | |
class DataBaseController extends AbstractController | |
{ | |
public function __construct( | |
private EntityManagerInterface $entityManager | |
) {} | |
#[Route('/database/user', name: 'app_user_database')] | |
public function userDatabase(): Response | |
{ | |
$this->denyAccessUnlessGranted(AccessControlVoter::ACCESS_DATABASE); | |
// 全てのユーザーを取得 | |
$users = $this->entityManager->getRepository(User::class)->findAll(); | |
return $this->render('database/user_database.html.twig', [ | |
'users' => $users, | |
]); | |
} | |
#[Route('/database/user/delete/{id}', name: 'app_delete_user', methods: ['POST'])] | |
public function deleteUser(int $id): JsonResponse | |
{ | |
$this->denyAccessUnlessGranted(AccessControlVoter::ACCESS_DATABASE); | |
try { | |
$user = $this->entityManager->getRepository(User::class)->find($id); | |
if (!$user) { | |
return new JsonResponse([ | |
'success' => false, | |
'message' => 'ユーザーが見つかりません。' | |
], 404); | |
} | |
// 開発者アカウントの削除防止 | |
if ($user->getEmail() === 'kentamagicshow@gmail.com') { | |
return new JsonResponse([ | |
'success' => false, | |
'message' => '開発者アカウントは削除できません。' | |
], 403); | |
} | |
$email = $user->getEmail(); // ログ用に保存 | |
$this->entityManager->remove($user); | |
$this->entityManager->flush(); | |
return new JsonResponse([ | |
'success' => true, | |
'message' => $email . ' を削除しました。' | |
]); | |
} catch (\Exception $e) { | |
return new JsonResponse([ | |
'success' => false, | |
'message' => '削除中にエラーが発生しました: ' . $e->getMessage() | |
], 500); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
when@dev: | |
debug: | |
# Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser. | |
# See the "server:dump" command to start a new server. | |
dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>{% block title %}開発者ホーム - 時間割管理システム{% endblock %}</title> | |
<!-- Bootstrap CSS --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"> | |
<!-- Font Awesome --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<style> | |
body { | |
background: linear-gradient(135deg, #f9f4e6 0%, #f2e6d0 50%, #ede0c8 100%); | |
min-height: 100vh; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
margin: 0; | |
padding: 0; | |
} | |
.header { | |
background: linear-gradient(135deg, #e6d2b0 0%, #dcc5a0 50%, #d2b890 100%); | |
color: #1a1a1a; | |
padding: 20px 40px; | |
font-size: 1.8rem; | |
font-weight: bold; | |
position: relative; | |
box-shadow: 0 2px 15px rgba(26, 26, 26, 0.12); | |
} | |
.header .user-info { | |
position: absolute; | |
right: 40px; | |
top: 50%; | |
transform: translateY(-50%); | |
font-size: 1rem; | |
font-weight: normal; | |
color: #000000; | |
} | |
.main-container { | |
padding: 30px; | |
max-width: 1200px; | |
margin: 0 auto; | |
} | |
.welcome-card { | |
background: linear-gradient(135deg, #ffffff 0%, #fefbf5 30%, #fdf8f0 70%, #fcf5ea 100%); | |
border-radius: 20px; | |
padding: 35px; | |
margin-bottom: 30px; | |
box-shadow: 0 8px 25px rgba(26, 26, 26, 0.1); | |
text-align: center; | |
border: 1px solid #f0e2cc; | |
} | |
.welcome-card h1 { | |
color: #000000; | |
font-size: 2.5rem; | |
margin-bottom: 15px; | |
font-weight: 700; | |
} | |
.welcome-card p { | |
color: #1a1a1a; | |
font-size: 1.1rem; | |
margin-bottom: 0; | |
} | |
.developer-icon { | |
color: #c49a6b; | |
font-size: 4rem; | |
margin-bottom: 20px; | |
} | |
.developer-badge { | |
background: linear-gradient(135deg, #e6d9f0 0%, #d9c7e8 100%); | |
color: #000000; | |
padding: 12px 42px; | |
border-radius: 40px; | |
font-size: 1.5rem; | |
font-weight: 700; | |
display: inline-block; | |
margin-bottom: 35px; | |
border: 1px solid #d0bde0; | |
} | |
.features-grid { | |
display: grid; | |
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); | |
gap: 25px; | |
margin-top: 30px; | |
} | |
.feature-card { | |
background: linear-gradient(135deg, #ffffff 0%, #fefcf8 50%, #fdf9f3 100%); | |
border-radius: 16px; | |
padding: 30px; | |
box-shadow: 0 6px 20px rgba(26, 26, 26, 0.08); | |
transition: transform 0.3s ease, box-shadow 0.3s ease; | |
text-decoration: none; | |
color: inherit; | |
border: 1px solid #f3e6d5; | |
} | |
.feature-card:hover { | |
transform: translateY(-8px); | |
box-shadow: 0 12px 35px rgba(26, 26, 26, 0.15); | |
text-decoration: none; | |
color: inherit; | |
} | |
.feature-icon { | |
font-size: 2.5rem; | |
margin-bottom: 18px; | |
color: #b8956f; | |
} | |
.feature-title { | |
font-size: 1.3rem; | |
font-weight: 700; | |
margin-bottom: 12px; | |
color: #000000; | |
} | |
.feature-description { | |
color: #1a1a1a; | |
font-size: 0.95rem; | |
line-height: 1.6; | |
} | |
.logout-section { | |
margin-top: 40px; | |
text-align: center; | |
} | |
.logout-btn { | |
background: linear-gradient(135deg, #eed8b7 0%, #e6d0af 50%, #dec8a7 100%); | |
color: #000000; | |
border: 1px solid #d2b890; | |
padding: 14px 32px; | |
font-size: 1.1rem; | |
font-weight: 700; | |
border-radius: 25px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
text-decoration: none; | |
display: inline-block; | |
} | |
.logout-btn:hover { | |
background: linear-gradient(135deg, #e6d0af 0%, #dec8a7 50%, #d6c09f 100%); | |
transform: translateY(-2px); | |
color: #000000; | |
text-decoration: none; | |
box-shadow: 0 4px 15px rgba(26, 26, 26, 0.18); | |
} | |
.device-info { | |
background: linear-gradient(135deg, #fbf7ed 0%, #f9f4e6 50%, #f7f1df 100%); | |
border: 1px solid #f0e2cc; | |
padding: 18px; | |
border-radius: 12px; | |
margin: 20px 0; | |
font-size: 0.9rem; | |
color: #000000; | |
} | |
/* 特別なカードのスタイル - オレンジ系 */ | |
.feature-card.special-orange { | |
background: linear-gradient(135deg, #fff9f2 0%, #fff6ed 30%, #fef3e8 70%, #fdf0e3 100%); | |
border: 1px solid #f2dcc0; | |
} | |
.feature-card.special-orange .feature-icon { | |
color: #d4a574; | |
} | |
.feature-card.special-orange .feature-title { | |
color: #000000; | |
} | |
.feature-card.special-orange .feature-description { | |
color: #1a1a1a; | |
} | |
/* 特別なカードのスタイル - グリーン系 */ | |
.feature-card.special-green { | |
background: linear-gradient(135deg, #f8faf6 0%, #f5f8f2 30%, #f2f6ee 70%, #eff4ea 100%); | |
border: 1px solid #e4eedd; | |
} | |
.feature-card.special-green .feature-icon { | |
color: #9bb89b; | |
} | |
.feature-card.special-green .feature-title { | |
color: #000000; | |
} | |
.feature-card.special-green .feature-description { | |
color: #1a1a1a; | |
} | |
/* データベースカード - ブルー系 */ | |
.feature-card.database-card { | |
background: linear-gradient(135deg, #f6f9fc 0%, #f3f7fb 30%, #f0f5fa 70%, #edf3f9 100%); | |
border: 1px solid #dde9f3; | |
} | |
.feature-card.database-card .feature-icon { | |
color: #7ba7d1; | |
} | |
/* 管理者カード - パープル系 */ | |
.feature-card.admin-card { | |
background: linear-gradient(135deg, #faf8fc 0%, #f7f5fb 30%, #f4f2fa 70%, #f1eff9 100%); | |
border: 1px solid #e8e2f3; | |
} | |
.feature-card.admin-card .feature-icon { | |
color: #a68cc5; | |
} | |
/* 教員カード - ピンク系 */ | |
.feature-card.teacher-card { | |
background: linear-gradient(135deg, #fcf8f9 0%, #faf5f7 30%, #f8f2f5 70%, #f6eff3 100%); | |
border: 1px solid #f0e2e8; | |
} | |
.feature-card.teacher-card .feature-icon { | |
color: #c5a6b8; | |
} | |
/* 学生カード - イエロー系 */ | |
.feature-card.student-card { | |
background: linear-gradient(135deg, #fcfaf6 0%, #faf8f3 30%, #f8f6f0 70%, #f6f4ed 100%); | |
border: 1px solid #f0eadd; | |
} | |
.feature-card.student-card .feature-icon { | |
color: #c5b896; | |
} | |
@media (max-width: 768px) { | |
.header { | |
padding: 15px 20px; | |
font-size: 1.5rem; | |
} | |
.header .user-info { | |
position: static; | |
transform: none; | |
margin-top: 10px; | |
text-align: center; | |
} | |
.main-container { | |
padding: 20px 15px; | |
} | |
.welcome-card { | |
padding: 25px; | |
} | |
.welcome-card h1 { | |
font-size: 2rem; | |
} | |
.features-grid { | |
grid-template-columns: 1fr; | |
gap: 20px; | |
} | |
.feature-card { | |
padding: 25px; | |
} | |
} | |
@media (max-width: 480px) { | |
.welcome-card h1 { | |
font-size: 1.8rem; | |
} | |
.developer-icon { | |
font-size: 3rem; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="header"> | |
開発者ダッシュボード | |
<div class="user-info"> | |
<i class="fas fa-user-cog"></i> {{ app.user.email }} | |
</div> | |
</div> | |
<div class="main-container"> | |
<div class="welcome-card"> | |
<div class="developer-badge"> | |
開発者モード | |
</div> | |
<p>システム管理と開発ツールにアクセスできます</p> | |
<div class="device-info"> | |
<i class="fas fa-info-circle"></i> | |
<strong>マルチデバイス対応:</strong> 複数のデバイスで同時にログインできるようになりました | |
</div> | |
</div> | |
<div class="features-grid"> | |
<a href="{{ path('app_user_database') }}" class="feature-card database-card"> | |
<div class="feature-icon"> | |
<i class="fas fa-database"></i> | |
</div> | |
<div class="feature-title">ユーザーデータベース</div> | |
<div class="feature-description">登録ユーザーの管理と削除</div> | |
</a> | |
<a href="{{ path('app_admin_home') }}" class="feature-card admin-card"> | |
<div class="feature-icon"> | |
<i class="fas fa-user-shield"></i> | |
</div> | |
<div class="feature-title">管理者機能</div> | |
<div class="feature-description">管理者権限でのシステム操作</div> | |
</a> | |
<a href="{{ path('app_teacher_home') }}" class="feature-card teacher-card"> | |
<div class="feature-icon"> | |
<i class="fas fa-chalkboard-teacher"></i> | |
</div> | |
<div class="feature-title">教員機能</div> | |
<div class="feature-description">教員権限でのシステム操作</div> | |
</a> | |
<a href="{{ path('app_student_home') }}" class="feature-card student-card"> | |
<div class="feature-icon"> | |
<i class="fas fa-graduation-cap"></i> | |
</div> | |
<div class="feature-title">学生機能</div> | |
<div class="feature-description">学生権限でのシステム操作</div> | |
</a> | |
<div class="feature-card special-orange"> | |
<div class="feature-icon"> | |
<i class="fas fa-tools"></i> | |
</div> | |
<div class="feature-title">開発ツール</div> | |
<div class="feature-description">システム開発とデバッグ機能</div> | |
</div> | |
<div class="feature-card special-green"> | |
<div class="feature-icon"> | |
<i class="fas fa-chart-line"></i> | |
</div> | |
<div class="feature-title">システム監視</div> | |
<div class="feature-description">パフォーマンスとログの監視</div> | |
</div> | |
</div> | |
<div class="logout-section"> | |
<a href="{{ path('app_logout') }}" class="logout-btn"> | |
<i class="fas fa-sign-out-alt"></i> ログアウト | |
</a> | |
</div> | |
</div> | |
<div class="menu-card"> | |
<div class="menu-icon"> | |
<i class="fas fa-file-csv"></i> | |
</div> | |
<h3 class="menu-title">CSVインポート</h3> | |
<p class="menu-description"> | |
時間割データをCSVファイルから<br> | |
一括インポートできます | |
</p> | |
<a href="{{ path('app_csv_import') }}" class="menu-link"> | |
<i class="fas fa-upload"></i> CSVインポート | |
</a> | |
</div> | |
<!-- Bootstrap JS --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\DataFixtures; | |
use App\Entity\User; | |
use Doctrine\Bundle\FixturesBundle\Fixture; | |
use Doctrine\Persistence\ObjectManager; | |
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; | |
class DeveloperFixtures extends Fixture | |
{ | |
private UserPasswordHasherInterface $passwordHasher; | |
public function __construct(UserPasswordHasherInterface $passwordHasher) | |
{ | |
$this->passwordHasher = $passwordHasher; | |
} | |
public function load(ObjectManager $manager): void | |
{ | |
// 既存のユーザーをチェック | |
$existingUser = $manager->getRepository(User::class)->findOneBy(['email' => 'kentamagicshow@gmail.com']); | |
if (!$existingUser) { | |
$developer = new User(); | |
$developer->setEmail('kentamagicshow@gmail.com'); | |
$developer->setRoles(['ROLE_DEVELOPER']); | |
$hashedPassword = $this->passwordHasher->hashPassword($developer, 'APUWRabN3RbQJWsM'); | |
$developer->setPassword($hashedPassword); | |
$manager->persist($developer); | |
$manager->flush(); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Annotation\Route; | |
class DeveloperHomeController extends AbstractController | |
{ | |
#[Route('/developer/home', name: 'app_developer_home')] | |
public function index(): Response | |
{ | |
return $this->render('developer/developer_home.html.twig', [ | |
'controller_name' => 'DeveloperHomeController', | |
]); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
services: | |
database: | |
image: postgres:16-alpine | |
environment: | |
POSTGRES_DB: symfony_db | |
POSTGRES_USER: symfony_user | |
POSTGRES_PASSWORD: symfony_password | |
ports: | |
- "5432:5432" | |
volumes: | |
- postgres_data:/var/lib/postgresql/data | |
mailer: | |
image: axllent/mailpit | |
ports: | |
- "1025:1025" | |
- "8025:8025" | |
php: | |
build: | |
context: . | |
dockerfile: docker/php/Dockerfile | |
volumes: | |
- ./symfony:/var/www/symfony | |
depends_on: | |
- database | |
nginx: | |
image: nginx:1.25-alpine | |
ports: | |
- "8000:80" | |
volumes: | |
- ./symfony:/var/www/symfony | |
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf | |
depends_on: | |
- php | |
volumes: | |
postgres_data: | |
networks: | |
default: | |
driver: bridge | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
doctrine: | |
dbal: | |
url: '%env(resolve:DATABASE_URL)%' | |
# IMPORTANT: You MUST configure your server version, | |
# either here or in the DATABASE_URL env var (see .env file) | |
#server_version: '16' | |
use_savepoints: true | |
orm: | |
auto_generate_proxy_classes: true | |
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware | |
auto_mapping: true | |
mappings: | |
App: | |
is_bundle: false | |
dir: '%kernel.project_dir%/src/Entity' | |
prefix: 'App\Entity' | |
alias: App | |
when@test: | |
doctrine: | |
dbal: | |
# "TEST_TOKEN" is typically set by ParaTest | |
dbname_suffix: '_test%env(default::TEST_TOKEN)%' | |
when@prod: | |
doctrine: | |
orm: | |
auto_generate_proxy_classes: false | |
proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies' | |
query_cache_driver: | |
type: pool | |
pool: doctrine.system_cache_pool | |
result_cache_driver: | |
type: pool | |
pool: doctrine.result_cache_pool | |
framework: | |
cache: | |
pools: | |
doctrine.result_cache_pool: | |
adapter: cache.app | |
doctrine.system_cache_pool: | |
adapter: cache.system |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
doctrine_migrations: | |
migrations_paths: | |
'DoctrineMigrations': '%kernel.project_dir%/migrations' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>時間割編集 - {{ timeTable.className }}</title> | |
<!-- Bootstrap CSS --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"> | |
<!-- Font Awesome --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<style> | |
body { | |
background-color: #f4c2a1; | |
min-height: 100vh; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
} | |
.header { | |
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%); | |
color: white; | |
padding: 15px 40px; | |
font-size: 1.6rem; | |
font-weight: bold; | |
} | |
.main-container { | |
padding: 20px; | |
max-width: 1200px; | |
margin: 0 auto; | |
} | |
.edit-container { | |
background: white; | |
border-radius: 8px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
overflow: hidden; | |
} | |
.edit-header { | |
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%); | |
color: white; | |
padding: 20px; | |
text-align: center; | |
} | |
.form-container { | |
padding: 30px; | |
} | |
.basic-info { | |
background: #f8f9fa; | |
padding: 20px; | |
border-radius: 8px; | |
margin-bottom: 30px; | |
} | |
.form-group { | |
margin-bottom: 20px; | |
} | |
.form-label { | |
font-weight: bold; | |
color: #495057; | |
margin-bottom: 8px; | |
display: block; | |
} | |
.form-control { | |
width: 100%; | |
padding: 10px 15px; | |
border: 2px solid #dee2e6; | |
border-radius: 4px; | |
font-size: 1rem; | |
transition: border-color 0.3s ease; | |
} | |
.form-control:focus { | |
outline: none; | |
border-color: #dc3545; | |
box-shadow: 0 0 0 3px rgba(220, 53, 69, 0.1); | |
} | |
.schedule-section { | |
margin-top: 30px; | |
} | |
.schedule-title { | |
color: #dc3545; | |
font-size: 1.5rem; | |
font-weight: bold; | |
margin-bottom: 20px; | |
text-align: center; | |
} | |
.schedule-table { | |
width: 100%; | |
border-collapse: collapse; | |
margin: 20px 0; | |
} | |
.schedule-table th { | |
background: #dc3545; | |
color: white; | |
padding: 15px 10px; | |
text-align: center; | |
font-weight: bold; | |
border: 1px solid #c82333; | |
} | |
.schedule-table td { | |
padding: 8px; | |
border: 1px solid #dee2e6; | |
text-align: center; | |
vertical-align: middle; | |
} | |
.period-header { | |
background: #f8f9fa; | |
font-weight: bold; | |
color: #495057; | |
width: 100px; | |
} | |
.subject-input { | |
width: 100%; | |
padding: 8px 12px; | |
border: 1px solid #dee2e6; | |
border-radius: 4px; | |
font-size: 0.9rem; | |
text-align: center; | |
} | |
.subject-input:focus { | |
outline: none; | |
border-color: #dc3545; | |
box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.1); | |
} | |
.action-buttons { | |
text-align: center; | |
margin-top: 30px; | |
padding-top: 20px; | |
border-top: 1px solid #dee2e6; | |
} | |
.btn-action { | |
padding: 12px 30px; | |
border: none; | |
border-radius: 4px; | |
font-size: 1.1rem; | |
font-weight: bold; | |
text-decoration: none; | |
display: inline-block; | |
margin: 0 10px; | |
transition: all 0.3s ease; | |
cursor: pointer; | |
} | |
.btn-save { | |
background-color: #28a745; | |
color: white; | |
} | |
.btn-save:hover { | |
background-color: #218838; | |
transform: translateY(-1px); | |
} | |
.btn-cancel { | |
background-color: #6c757d; | |
color: white; | |
} | |
.btn-cancel:hover { | |
background-color: #5a6268; | |
color: white; | |
text-decoration: none; | |
transform: translateY(-1px); | |
} | |
.readonly-field { | |
background-color: #e9ecef; | |
color: #6c757d; | |
cursor: not-allowed; | |
} | |
@media (max-width: 768px) { | |
.header { | |
padding: 10px 15px; | |
font-size: 1.3rem; | |
} | |
.main-container { | |
padding: 10px; | |
} | |
.form-container { | |
padding: 20px; | |
} | |
.schedule-table { | |
font-size: 0.8rem; | |
} | |
.schedule-table th, | |
.schedule-table td { | |
padding: 6px 4px; | |
} | |
.subject-input { | |
font-size: 0.8rem; | |
padding: 6px 8px; | |
} | |
.btn-action { | |
display: block; | |
margin: 10px auto; | |
width: 200px; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="header"> | |
<i class="fas fa-edit"></i> 時間割編集 | |
</div> | |
<div class="main-container"> | |
<div class="edit-container"> | |
<div class="edit-header"> | |
<h1><i class="fas fa-graduation-cap"></i> {{ timeTable.className }}</h1> | |
<p>時間割編集</p> | |
</div> | |
<div class="form-container"> | |
<form method="post" id="editForm"> | |
<div class="basic-info"> | |
<h3><i class="fas fa-info-circle"></i> 基本情報</h3> | |
<div class="form-group"> | |
<label class="form-label">クラス名</label> | |
<input type="text" class="form-control readonly-field" value="{{ timeTable.className }}" readonly> | |
<small class="text-muted">※ クラス名は変更できません</small> | |
</div> | |
<div class="form-group"> | |
<label class="form-label" for="day">曜日設定</label> | |
<input type="text" class="form-control" id="day" name="day" value="{{ timeTable.day }}" placeholder="例: 週間"> | |
</div> | |
<div class="form-group"> | |
<label class="form-label" for="period">時限数</label> | |
<select class="form-control" id="period" name="period"> | |
<option value="4" {{ timeTable.period == 4 ? 'selected' : '' }}>4時限</option> | |
<option value="5" {{ timeTable.period == 5 ? 'selected' : '' }}>5時限</option> | |
<option value="6" {{ timeTable.period == 6 ? 'selected' : '' }}>6時限</option> | |
</select> | |
</div> | |
</div> | |
<div class="schedule-section"> | |
<h3 class="schedule-title"><i class="fas fa-table"></i> 時間割設定</h3> | |
<table class="schedule-table"> | |
<thead> | |
<tr> | |
<th>時限</th> | |
<th>月曜日</th> | |
<th>火曜日</th> | |
<th>水曜日</th> | |
<th>木曜日</th> | |
<th>金曜日</th> | |
</tr> | |
</thead> | |
<tbody> | |
<tr> | |
<td class="period-header">1時限</td> | |
<td><input type="text" class="subject-input" name="mon1" value="{{ timeTable.mon1 }}" placeholder="科目名"></td> | |
<td><input type="text" class="subject-input" name="tue1" value="{{ timeTable.tue1 }}" placeholder="科目名"></td> | |
<td><input type="text" class="subject-input" name="wed1" value="{{ timeTable.wed1 }}" placeholder="科目名"></td> | |
<td><input type="text" class="subject-input" name="thu1" value="{{ timeTable.thu1 }}" placeholder="科目名"></td> | |
<td><input type="text" class="subject-input" name="fri1" value="{{ timeTable.fri1 }}" placeholder="科目名"></td> | |
</tr> | |
<tr> | |
<td class="period-header">2時限</td> | |
<td><input type="text" class="subject-input" name="mon2" value="{{ timeTable.mon2 }}" placeholder="科目名"></td> | |
<td><input type="text" class="subject-input" name="tue2" value="{{ timeTable.tue2 }}" placeholder="科目名"></td> | |
<td><input type="text" class="subject-input" name="wed2" value="{{ timeTable.wed2 }}" placeholder="科目名"></td> | |
<td><input type="text" class="subject-input" name="thu2" value="{{ timeTable.thu2 }}" placeholder="科目名"></td> | |
<td><input type="text" class="subject-input" name="fri2" value="{{ timeTable.fri2 }}" placeholder="科目名"></td> | |
</tr> | |
<tr> | |
<td class="period-header">3時限</td> | |
<td><input type="text" class="subject-input" name="mon3" value="{{ timeTable.mon3 }}" placeholder="科目名"></td> | |
<td><input type="text" class="subject-input" name="tue3" value="{{ timeTable.tue3 }}" placeholder="科目名"></td> | |
<td><input type="text" class="subject-input" name="wed3" value="{{ timeTable.wed3 }}" placeholder="科目名"></td> | |
<td><input type="text" class="subject-input" name="thu3" value="{{ timeTable.thu3 }}" placeholder="科目名"></td> | |
<td><input type="text" class="subject-input" name="fri3" value="{{ timeTable.fri3 }}" placeholder="科目名"></td> | |
</tr> | |
<tr> | |
<td class="period-header">4時限</td> | |
<td><input type="text" class="subject-input" name="mon4" value="{{ timeTable.mon4 }}" placeholder="科目名"></td> | |
<td><input type="text" class="subject-input" name="tue4" value="{{ timeTable.tue4 }}" placeholder="科目名"></td> | |
<td><input type="text" class="subject-input" name="wed4" value="{{ timeTable.wed4 }}" placeholder="科目名"></td> | |
<td><input type="text" class="subject-input" name="thu4" value="{{ timeTable.thu4 }}" placeholder="科目名"></td> | |
<td><input type="text" class="subject-input" name="fri4" value="{{ timeTable.fri4 }}" placeholder="科目名"></td> | |
</tr> | |
</tbody> | |
</table> | |
</div> | |
<div class="action-buttons"> | |
<button type="submit" class="btn-action btn-save"> | |
<i class="fas fa-save"></i> 保存 | |
</button> | |
<a href="{{ path('app_admin_timetable_show', {'className': timeTable.className}) }}" class="btn-action btn-cancel"> | |
<i class="fas fa-times"></i> キャンセル | |
</a> | |
</div> | |
</form> | |
</div> | |
</div> | |
</div> | |
<!-- Bootstrap JS --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
const form = document.getElementById('editForm'); | |
// フォーム送信時の確認 | |
form.addEventListener('submit', function(e) { | |
if (!confirm('時間割を保存してもよろしいですか?')) { | |
e.preventDefault(); | |
} | |
}); | |
// 入力フィールドのフォーカス時の処理 | |
const subjectInputs = document.querySelectorAll('.subject-input'); | |
subjectInputs.forEach(input => { | |
input.addEventListener('focus', function() { | |
this.style.backgroundColor = '#fff3cd'; | |
}); | |
input.addEventListener('blur', function() { | |
this.style.backgroundColor = ''; | |
}); | |
}); | |
}); | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>ログイン認証 - 時間割管理システム</title> | |
<style> | |
body { | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
line-height: 1.6; | |
color: #333; | |
max-width: 600px; | |
margin: 0 auto; | |
padding: 20px; | |
background-color: #f9f9f9; | |
} | |
.email-container { | |
background-color: white; | |
padding: 30px; | |
border-radius: 8px; | |
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); | |
} | |
.header { | |
text-align: center; | |
margin-bottom: 30px; | |
padding-bottom: 20px; | |
border-bottom: 2px solid #2e86c1; | |
} | |
.header h1 { | |
color: #2e86c1; | |
margin: 0; | |
font-size: 24px; | |
} | |
.content { | |
margin-bottom: 30px; | |
} | |
.content p { | |
margin-bottom: 15px; | |
font-size: 16px; | |
} | |
.login-button { | |
text-align: center; | |
margin: 30px 0; | |
} | |
.login-button a { | |
display: inline-block; | |
background-color: #2e86c1; | |
color: white; | |
padding: 15px 30px; | |
text-decoration: none; | |
border-radius: 5px; | |
font-weight: bold; | |
font-size: 16px; | |
} | |
.login-button a:hover { | |
background-color: #1f5f8b; | |
} | |
.alternative-link { | |
background-color: #f8f9fa; | |
padding: 15px; | |
border-radius: 5px; | |
margin: 20px 0; | |
border-left: 4px solid #2e86c1; | |
} | |
.alternative-link p { | |
margin: 0; | |
font-size: 14px; | |
color: #666; | |
} | |
.alternative-link a { | |
color: #2e86c1; | |
word-break: break-all; | |
} | |
.footer { | |
text-align: center; | |
margin-top: 30px; | |
padding-top: 20px; | |
border-top: 1px solid #eee; | |
font-size: 14px; | |
color: #666; | |
} | |
.security-notice { | |
background-color: #fff3cd; | |
border: 1px solid #ffeaa7; | |
padding: 15px; | |
border-radius: 5px; | |
margin: 20px 0; | |
} | |
.security-notice p { | |
margin: 0; | |
font-size: 14px; | |
color: #856404; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="email-container"> | |
<div class="header"> | |
<h1>時間割管理システム</h1> | |
<p>ログイン認証のお知らせ</p> | |
</div> | |
<div class="content"> | |
<p>こんにちは、</p> | |
<p>時間割管理システムへのログイン認証を行うため、以下のボタンをクリックしてください。</p> | |
<div class="login-button"> | |
<a href="{{ signedUrl }}">ログイン認証を完了する</a> | |
</div> | |
<div class="security-notice"> | |
<p><strong>セキュリティに関するお知らせ:</strong></p> | |
<p>このリンクは安全な認証システムを使用しています。ブラウザで「安全ではない」と表示される場合がありますが、これは一時的なテスト環境のためです。「詳細設定」→「サイトにアクセスする」で進んでください。</p> | |
</div> | |
<div class="alternative-link"> | |
<p><strong>ボタンが機能しない場合は、以下のリンクをコピーしてブラウザに貼り付けてください:</strong></p> | |
<p><a href="{{ signedUrl }}">{{ signedUrl }}</a></p> | |
</div> | |
<p>このメールに心当たりがない場合は、このメールを無視してください。</p> | |
<p>リンクの有効期限は1時間です。</p> | |
</div> | |
<div class="footer"> | |
<p>このメールは時間割管理システムから自動送信されています。</p> | |
<p>返信は不要です。</p> | |
<p>© 2025 時間割管理システム</p> | |
</div> | |
</div> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Security; | |
use App\Entity\User; | |
use Doctrine\ORM\EntityManagerInterface; | |
use Symfony\Bridge\Twig\Mime\TemplatedEmail; | |
use Symfony\Component\HttpFoundation\Request; | |
use Symfony\Component\Mailer\MailerInterface; | |
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; | |
use Symfony\Component\Routing\RouterInterface; | |
use Symfony\Component\Security\Core\User\UserInterface; | |
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; | |
use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface; | |
use SymfonyCasts\Bundle\VerifyEmail\Model\VerifyEmailSignatureComponents; | |
use SymfonyCasts\Bundle\VerifyEmail\VerifyEmailHelperInterface; | |
class EmailVerifier | |
{ | |
private string $baseUrl; | |
public function __construct( | |
private VerifyEmailHelperInterface $verifyEmailHelper, | |
private MailerInterface $mailer, | |
private EntityManagerInterface $entityManager, | |
private ParameterBagInterface $params, | |
private RouterInterface $router | |
) { | |
$this->baseUrl = rtrim($this->params->get('app.base_url'), '/'); | |
error_log('[DEBUG] Loaded APP_BASE_URL: ' . $this->baseUrl); | |
} | |
public function sendEmailConfirmation(string $verifyEmailRouteName, UserInterface $user, TemplatedEmail $email): void | |
{ | |
error_log('[DEBUG] Using base URL: ' . $this->baseUrl); | |
try { | |
// 絶対URLを生成(署名付き) | |
$signature = $this->verifyEmailHelper->generateSignature( | |
$verifyEmailRouteName, | |
$user->getId(), | |
$user->getEmail(), | |
['id' => $user->getId()], | |
$this->baseUrl | |
); | |
// 最終的な署名付きURL | |
$signedUrl = $signature->getSignedUrl(); | |
// localhost→ngrokへの補正 | |
$signedUrl = preg_replace('/^http:\/\/localhost(:8000)?/', $this->baseUrl, $signedUrl); | |
$context = $email->getContext(); | |
$context['signedUrl'] = $signedUrl; | |
$context['expiresAtMessageKey'] = $signature->getExpirationMessageKey(); | |
$context['expiresAtMessageData'] = $signature->getExpirationMessageData(); | |
$email->context($context); | |
error_log('[DEBUG] Final signed URL: ' . $signedUrl); | |
$this->mailer->send($email); | |
} catch (\Exception $e) { | |
error_log('[ERROR] EmailVerifier: Failed to send email: ' . $e->getMessage()); | |
throw $e; | |
} | |
} | |
/** | |
* @throws VerifyEmailExceptionInterface | |
*/ | |
public function handleEmailConfirmation(Request $request, UserInterface $user): void | |
{ | |
try { | |
// ★★ 署名検証をスキップして、直接ユーザーを認証 ★★ | |
error_log('[DEBUG] EmailVerifier: Skipping signature validation for development'); | |
// 開発環境では署名検証をスキップ | |
$user->setIsVerified(true); | |
$this->entityManager->persist($user); | |
$this->entityManager->flush(); | |
error_log('[DEBUG] EmailVerifier: User verified successfully (dev mode)'); | |
} catch (\Exception $e) { | |
error_log('[ERROR] EmailVerifier: Verification failed: ' . $e->getMessage()); | |
throw $e; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- symfony/templates/magic_link_login/error.html.twig --> | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>認証エラー</title> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
</head> | |
<body> | |
<div style="text-align: center; padding: 50px;"> | |
<h1>❌ 認証エラー</h1> | |
<p>{{ error }}</p> | |
<a href="{{ path('app_magic_login') }}">ログイン画面に戻る</a> | |
</div> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\ErrorHandler\Exception\FlattenException; | |
use Symfony\Component\HttpFoundation\Request; | |
use Symfony\Component\HttpFoundation\Response; | |
class ErrorController extends AbstractController | |
{ | |
public function show(FlattenException $exception, Request $request): Response | |
{ | |
// 403エラー(アクセス拒否)の場合はカスタムページを表示 | |
if ($exception->getStatusCode() === 403) { | |
return $this->render('bundles/TwigBundle/Exception/error403.html.twig', [ | |
'exception' => $exception, | |
'status_code' => 403, | |
'status_text' => 'Forbidden' | |
], new Response('', 403)); | |
} | |
// その他のエラーはデフォルトの処理 | |
return $this->render('bundles/TwigBundle/Exception/error.html.twig', [ | |
'exception' => $exception, | |
'status_code' => $exception->getStatusCode(), | |
'status_text' => Response::$statusTexts[$exception->getStatusCode()] ?? 'Unknown error' | |
], new Response('', $exception->getStatusCode())); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
when@dev: | |
_errors: | |
resource: '@FrameworkBundle/Resources/config/routing/errors.xml' | |
prefix: /_error |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# カラーコードの定義 | |
RED='\033[0;31m' | |
GREEN='\033[0;32m' | |
YELLOW='\033[1;33m' | |
BLUE='\033[0;34m' | |
PURPLE='\033[0;35m' | |
CYAN='\033[0;36m' | |
WHITE='\033[1;37m' | |
BOLD='\033[1m' | |
NC='\033[0m' # No Color (リセット) | |
echo "=========================================" | |
echo " G1 Project Full Development Setup" | |
echo "=========================================" | |
echo "" | |
# Docker Desktop起動確認 | |
echo -e "${BOLD}${RED}Please OPEN Docker Desktop${NC}" | |
echo "" | |
# ユーザーの確認を待つ | |
while true; do | |
read -p "Type 'OK' when ready: " DOCKER_CONFIRM | |
if [ "$DOCKER_CONFIRM" = "OK" ] || [ "$DOCKER_CONFIRM" = "ok" ]; then | |
echo "" | |
break | |
else | |
echo -e "${RED}Please type 'OK' to continue${NC}" | |
fi | |
done | |
# ブランチ名の入力を求める | |
read -p "Enter branch name (default: main): " BRANCH_NAME | |
BRANCH_NAME=${BRANCH_NAME:-main} | |
echo "" | |
echo -e "${GREEN}Starting full development environment setup...${NC}" | |
echo -e "${CYAN}Branch: $BRANCH_NAME${NC}" | |
echo "" | |
# g1_projectディレクトリに移動 | |
echo -e "${BLUE}Moving to project directory...${NC}" | |
cd ~/g1_project | |
# Gitの操作 | |
echo -e "${BLUE}Updating Git repository...${NC}" | |
echo -e "${WHITE} - Checking out branch: $BRANCH_NAME${NC}" | |
git checkout $BRANCH_NAME | |
echo -e "${WHITE} - Pulling latest changes...${NC}" | |
git pull origin $BRANCH_NAME | |
# Docker環境の構築 | |
echo -e "${BLUE}Building Docker environment...${NC}" | |
docker compose up -d --build | |
# 権限の設定 | |
echo -e "${BLUE}Setting up permissions...${NC}" | |
echo -e "${WHITE} - Setting user permissions...${NC}" | |
sudo chown -R $USER:$USER ~/g1_project/symfony | |
echo -e "${WHITE} - Setting web server permissions...${NC}" | |
docker compose exec --user root php chown -R www-data:www-data /var/www/symfony/var | |
echo "" | |
echo -e "${GREEN}✅ Docker environment setup completed!${NC}" | |
echo "" | |
echo -e "${YELLOW}Ngrok URL Configuration${NC}" | |
echo "=========================================" | |
echo "" | |
echo -e "${BOLD}${RED}Please follow these steps:${NC}" | |
echo -e "${BOLD}${YELLOW}1. Open a new WSL terminal${NC}" | |
echo -e "${BOLD}${YELLOW}2. Run: ${CYAN}ngrok http 8000${NC}" | |
echo -e "${BOLD}${YELLOW}3. Copy the ngrok URL from the terminal${NC}" | |
echo "" | |
# ngrok URLの入力を待つ | |
read -p "Enter ngrok URL (e.g., https://xxxx-xxx-xxx-xxx.ngrok-free.app): " NGROK_URL | |
if [ -z "$NGROK_URL" ]; then | |
echo -e "${YELLOW}No ngrok URL provided. Skipping URL configuration.${NC}" | |
echo -e "${WHITE}You can run ./update_ngrok.sh later to configure the URL.${NC}" | |
else | |
echo "" | |
echo -e "${BLUE}Configuring ngrok URL...${NC}" | |
# .envファイルを更新 | |
sed -i "s|APP_BASE_URL=.*|APP_BASE_URL=$NGROK_URL|" symfony/.env | |
# .env.localファイルを更新 | |
if [ -f "symfony/.env.local" ]; then | |
sed -i "s|APP_BASE_URL=.*|APP_BASE_URL=$NGROK_URL|" symfony/.env.local | |
sed -i "s|APP_URL=.*|APP_URL=$NGROK_URL|" symfony/.env.local | |
fi | |
# ★★ スクリプト内でエイリアスを定義して使用 ★★ | |
echo -e "${WHITE} - Setting up dc command for this session...${NC}" | |
shopt -s expand_aliases # エイリアスを有効化 | |
alias dc='docker compose exec php php bin/console' | |
echo -e "${WHITE} - Clearing cache...${NC}" | |
docker compose exec php php bin/console cache:clear | |
echo -e "${WHITE} - Resetting user login status...${NC}" | |
docker compose exec php php bin/console doctrine:query:sql "UPDATE \"user\" SET login_status = NULL" | |
echo -e "${GREEN}✅ Ngrok URL configured: ${CYAN}$NGROK_URL${NC}" | |
fi | |
echo "" | |
echo -e "${GREEN}✅ Full setup completed!${NC}" | |
echo "" | |
echo -e "${PURPLE}Access your application at:${NC}" | |
if [ ! -z "$NGROK_URL" ]; then | |
echo -e "${BOLD}${CYAN} $NGROK_URL/magic-login${NC}" | |
else | |
echo -e "${YELLOW} Configure ngrok URL first with ./update_ngrok.sh${NC}" | |
fi | |
echo "" | |
echo -e "${BLUE}Opening VS Code...${NC}" | |
code . | |
# ★★ dcエイリアスを.bashrcに追加(永続化) ★★ | |
echo "" | |
echo -e "${BLUE}Setting up dc command alias for future sessions...${NC}" | |
if ! grep -q "alias dc=" ~/.bashrc; then | |
echo "" >> ~/.bashrc | |
echo "# G1 Project Docker Compose alias" >> ~/.bashrc | |
echo "alias dc='docker compose exec php php bin/console'" >> ~/.bashrc | |
echo -e "${GREEN}✅ dc command alias added to .bashrc${NC}" | |
echo -e "${YELLOW}Note: dc command will be available in new terminal sessions${NC}" | |
else | |
echo -e "${YELLOW}dc command alias already exists in .bashrc${NC}" | |
fi | |
echo "" | |
echo "=========================================" | |
echo -e "${BOLD} Setup Summary${NC}" | |
echo "=========================================" | |
echo -e "${GREEN}✅ Git repository updated (branch: $BRANCH_NAME)${NC}" | |
echo -e "${GREEN}✅ Docker containers built and started${NC}" | |
echo -e "${GREEN}✅ Permissions configured${NC}" | |
if [ ! -z "$NGROK_URL" ]; then | |
echo -e "${GREEN}✅ Ngrok URL configured${NC}" | |
echo -e "${GREEN}✅ Database reset${NC}" | |
echo -e "${GREEN}✅ Cache cleared${NC}" | |
fi | |
echo -e "${GREEN}✅ VS Code opened${NC}" | |
echo -e "${GREEN}✅ dc command alias configured${NC}" | |
echo "" | |
echo -e "${BOLD}${GREEN}Setup Completed! Ready to use!${NC}" | |
echo "=========================================" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Command; | |
use App\Entity\CompleteTimeTable; | |
use App\Entity\TimeTable; | |
use Doctrine\ORM\EntityManagerInterface; | |
use Symfony\Component\Console\Attribute\AsCommand; | |
use Symfony\Component\Console\Command\Command; | |
use Symfony\Component\Console\Input\InputArgument; | |
use Symfony\Component\Console\Input\InputInterface; | |
use Symfony\Component\Console\Output\OutputInterface; | |
use Symfony\Component\Console\Style\SymfonyStyle; | |
#[AsCommand( | |
name: 'app:generate-academic-calendar', | |
description: '学年度の完全時間割カレンダーを生成します', | |
)] | |
class GenerateAcademicCalendarCommand extends Command | |
{ | |
public function __construct( | |
private EntityManagerInterface $entityManager | |
) { | |
parent::__construct(); | |
} | |
protected function configure(): void | |
{ | |
$this | |
->addArgument('year', InputArgument::REQUIRED, '学年度(例:2025)') | |
->setHelp('指定された学年度の完全時間割カレンダーを生成します。'); | |
} | |
protected function execute(InputInterface $input, OutputInterface $output): int | |
{ | |
$io = new SymfonyStyle($input, $output); | |
$year = (int) $input->getArgument('year'); | |
$io->title("学年度 {$year} の完全時間割カレンダー生成"); | |
try { | |
// 既存データの削除確認 | |
$existingCount = $this->entityManager | |
->getRepository(CompleteTimeTable::class) | |
->count(['year' => $year]); | |
if ($existingCount > 0) { | |
if (!$io->confirm("学年度 {$year} の既存データ({$existingCount}件)を削除して再生成しますか?", false)) { | |
$io->warning('処理をキャンセルしました。'); | |
return Command::SUCCESS; | |
} | |
// 既存データを削除 | |
$this->entityManager->createQuery( | |
'DELETE FROM App\Entity\CompleteTimeTable c WHERE c.year = :year' | |
)->setParameter('year', $year)->execute(); | |
$io->success("既存データ {$existingCount} 件を削除しました。"); | |
} | |
// 基本時間割テンプレートを取得 | |
$timeTableTemplates = $this->entityManager | |
->getRepository(TimeTable::class) | |
->findAll(); | |
if (empty($timeTableTemplates)) { | |
$io->error('基本時間割テンプレートが見つかりません。先にTimeTableデータを登録してください。'); | |
return Command::FAILURE; | |
} | |
$io->info(sprintf('基本時間割テンプレート: %d クラス分を発見', count($timeTableTemplates))); | |
// 学年度の開始日と終了日を計算 | |
$academicStartDate = new \DateTime("{$year}-04-01"); // 4月1日開始 | |
$academicEndDate = new \DateTime(($year + 1) . "-03-31"); // 翌年3月31日終了 | |
$io->info("期間: {$academicStartDate->format('Y-m-d')} ~ {$academicEndDate->format('Y-m-d')}"); | |
$totalGenerated = 0; | |
$progressBar = $io->createProgressBar(); | |
// 各クラスごとに処理 | |
foreach ($timeTableTemplates as $template) { | |
$className = $template->getClassName(); | |
$io->section("クラス: {$className} の処理中..."); | |
$classGenerated = $this->generateClassCalendar( | |
$template, | |
$year, | |
$academicStartDate, | |
$academicEndDate, | |
$progressBar | |
); | |
$totalGenerated += $classGenerated; | |
$io->info("クラス {$className}: {$classGenerated} 週分を生成"); | |
} | |
$progressBar->finish(); | |
$io->newLine(2); | |
// データベースに一括保存 | |
$this->entityManager->flush(); | |
$io->success([ | |
"学年度 {$year} の完全時間割カレンダーを生成しました!", | |
"総生成件数: {$totalGenerated} 週分", | |
"対象クラス数: " . count($timeTableTemplates), | |
]); | |
return Command::SUCCESS; | |
} catch (\Exception $e) { | |
$io->error([ | |
'エラーが発生しました:', | |
$e->getMessage(), | |
'ファイル: ' . $e->getFile() . ':' . $e->getLine() | |
]); | |
return Command::FAILURE; | |
} | |
} | |
private function generateClassCalendar( | |
TimeTable $template, | |
int $year, | |
\DateTime $startDate, | |
\DateTime $endDate, | |
$progressBar | |
): int { | |
$generated = 0; | |
$currentDate = clone $startDate; | |
// 最初の月曜日を見つける | |
while ($currentDate->format('N') !== '1') { | |
$currentDate->modify('+1 day'); | |
} | |
while ($currentDate <= $endDate) { | |
// 週の日付を計算 | |
$monday = clone $currentDate; | |
$tuesday = clone $monday; $tuesday->modify('+1 day'); | |
$wednesday = clone $monday; $wednesday->modify('+2 days'); | |
$thursday = clone $monday; $thursday->modify('+3 days'); | |
$friday = clone $monday; $friday->modify('+4 days'); | |
// 金曜日が学年度内にある場合のみ生成 | |
if ($friday <= $endDate) { | |
$completeTimeTable = new CompleteTimeTable(); | |
$completeTimeTable->setClassName($template->getClassName()); | |
$completeTimeTable->setYear((int)$monday->format('Y')); | |
$completeTimeTable->setMonth((int)$monday->format('n')); | |
$completeTimeTable->setMondayDate((int)$monday->format('j')); | |
$completeTimeTable->setTuesdayDate((int)$tuesday->format('j')); | |
$completeTimeTable->setThursdayDate((int)$thursday->format('j')); | |
$completeTimeTable->setFridayDate((int)$friday->format('j')); | |
// 水曜日の処理(月をまたぐ場合は null) | |
if ($wednesday->format('n') === $monday->format('n')) { | |
$completeTimeTable->setWednesdayDate((int)$wednesday->format('j')); | |
} else { | |
$completeTimeTable->setWednesdayDate(null); | |
} | |
// 各曜日・時限の科目をコピー | |
$this->copySubjectsFromTemplate($completeTimeTable, $template); | |
$this->entityManager->persist($completeTimeTable); | |
$generated++; | |
} | |
// 次の週へ | |
$currentDate->modify('+7 days'); | |
$progressBar->advance(); | |
} | |
return $generated; | |
} | |
private function copySubjectsFromTemplate(CompleteTimeTable $completeTimeTable, TimeTable $template): void | |
{ | |
// 月曜日 | |
$completeTimeTable->setMon1($template->getMon1()); | |
$completeTimeTable->setMon2($template->getMon2()); | |
$completeTimeTable->setMon3($template->getMon3()); | |
$completeTimeTable->setMon4($template->getMon4()); | |
// 火曜日 | |
$completeTimeTable->setTue1($template->getTue1()); | |
$completeTimeTable->setTue2($template->getTue2()); | |
$completeTimeTable->setTue3($template->getTue3()); | |
$completeTimeTable->setTue4($template->getTue4()); | |
// 水曜日 | |
$completeTimeTable->setWed1($template->getWed1()); | |
$completeTimeTable->setWed2($template->getWed2()); | |
$completeTimeTable->setWed3($template->getWed3()); | |
$completeTimeTable->setWed4($template->getWed4()); | |
// 木曜日 | |
$completeTimeTable->setThu1($template->getThu1()); | |
$completeTimeTable->setThu2($template->getThu2()); | |
$completeTimeTable->setThu3($template->getThu3()); | |
$completeTimeTable->setThu4($template->getThu4()); | |
// 金曜日 | |
$completeTimeTable->setFri1($template->getFri1()); | |
$completeTimeTable->setFri2($template->getFri2()); | |
$completeTimeTable->setFri3($template->getFri3()); | |
$completeTimeTable->setFri4($template->getFri4()); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{% extends 'base.html.twig' %} | |
{% block title %}Hello HelloController!{% endblock %} | |
{% block body %} | |
<style> | |
.example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; } | |
.example-wrapper code { background: #F5F5F5; padding: 2px 6px; } | |
</style> | |
<div class="example-wrapper"> | |
<h1>Hello {{ controller_name }}! ✅</h1> | |
This friendly message is coming from: | |
<ul> | |
<li>Your controller at <code><a href="{{ '/var/www/symfony/src/Controller/HelloController.php'|file_link(0) }}">src/Controller/HelloController.php</a></code></li> | |
<li>Your template at <code><a href="{{ '/var/www/symfony/templates/hello/index.html.twig'|file_link(0) }}">templates/hello/index.html.twig</a></code></li> | |
</ul> | |
</div> | |
{% endblock %} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Annotation\Route; | |
class HelloController extends AbstractController | |
{ | |
#[Route('/hello', name: 'app_hello')] | |
public function hello(): Response | |
{ | |
return $this->render('hello/hello.html.twig', [ | |
'controller_name' => 'HelloController', | |
]); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>{% block title %}ホーム - 時間割管理システム{% endblock %}</title> | |
<!-- Bootstrap CSS --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"> | |
<!-- Font Awesome --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<style> | |
body { | |
margin: 0; | |
padding: 0; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
background-color: #f4c2a1; | |
} | |
.header { | |
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); | |
color: white; | |
padding: 15px 40px; | |
font-size: 1.6rem; | |
font-weight: bold; | |
position: relative; | |
} | |
.main-container { | |
padding: 20px; | |
display: flex; | |
gap: 20px; | |
height: calc(100vh - 70px); | |
overflow: hidden; | |
} | |
.timetable-section { | |
flex: 1; | |
} | |
.timetable-container { | |
background: white; | |
border-radius: 8px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
overflow: hidden; | |
} | |
.timetable { | |
width: 100%; | |
border-collapse: collapse; | |
font-size: 0.9rem; | |
} | |
.timetable th { | |
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); | |
color: white; | |
padding: 10px 6px; | |
text-align: center; | |
font-weight: bold; | |
font-size: 0.9rem; | |
} | |
.timetable th.time-header { | |
background: #e8e8e8; | |
color: #333; | |
width: 120px; | |
} | |
.timetable td { | |
padding: 8px 6px; | |
text-align: center; | |
border: 1px solid #ddd; | |
background: #f8f9fa; | |
font-weight: 500; | |
vertical-align: middle; | |
font-size: 0.85rem; | |
} | |
.timetable td.time-cell { | |
background: #e8e8e8; | |
font-weight: bold; | |
color: #555; | |
font-size: 0.8rem; | |
} | |
.timetable td.subject { | |
background: white; | |
cursor: pointer; | |
transition: background-color 0.2s ease; | |
} | |
.timetable td.subject:hover { | |
background: #e3f2fd; | |
} | |
.timetable td.empty { | |
color: #999; | |
font-size: 1.2rem; | |
} | |
.controls-section { | |
width: 250px; | |
display: flex; | |
flex-direction: column; | |
gap: 10px; | |
} | |
.control-group { | |
background: white; | |
padding: 15px; | |
border-radius: 8px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
} | |
.selector-container { | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
gap: 20px; | |
margin: 15px 0; | |
} | |
.selector-group { | |
display: flex; | |
align-items: center; | |
gap: 10px; | |
} | |
.selector-label { | |
font-weight: bold; | |
font-size: 1.1rem; | |
min-width: 40px; | |
} | |
.custom-select { | |
position: relative; | |
display: inline-block; | |
} | |
.select-button { | |
background: white; | |
border: 2px solid #ddd; | |
padding: 6px 35px 6px 12px; | |
font-size: 1rem; | |
font-weight: bold; | |
cursor: pointer; | |
border-radius: 4px; | |
min-width: 70px; | |
text-align: center; | |
position: relative; | |
} | |
.select-button::after { | |
content: ''; | |
position: absolute; | |
right: 10px; | |
top: 50%; | |
transform: translateY(-50%); | |
width: 0; | |
height: 0; | |
border-left: 6px solid transparent; | |
border-right: 6px solid transparent; | |
border-top: 8px solid #4a90e2; | |
} | |
.select-dropdown { | |
position: absolute; | |
bottom: 100%; | |
left: 0; | |
right: 0; | |
background: white; | |
border: 2px solid #ddd; | |
border-bottom: none; | |
border-radius: 4px 4px 0 0; | |
z-index: 1000; | |
display: none; | |
} | |
.select-option { | |
padding: 8px 12px; | |
cursor: pointer; | |
border-bottom: 1px solid #eee; | |
font-weight: bold; | |
font-size: 0.9rem; | |
} | |
.select-option:hover { | |
background: #f0f8ff; | |
} | |
.select-option:last-child { | |
border-bottom: none; | |
} | |
.week-info { | |
text-align: center; | |
font-size: 1rem; | |
font-weight: bold; | |
color: #333; | |
margin: 15px 0 10px 0; | |
padding: 8px; | |
background: white; | |
border-radius: 4px; | |
border: 2px solid #ddd; | |
} | |
.action-buttons { | |
display: flex; | |
flex-direction: column; | |
gap: 10px; | |
} | |
.action-btn { | |
background: #f1c40f; | |
color: #333; | |
border: none; | |
padding: 12px 15px; | |
font-size: 0.9rem; | |
font-weight: bold; | |
border-radius: 4px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
text-align: center; | |
} | |
.action-btn:hover { | |
background: #d4ac0d; | |
transform: translateY(-1px); | |
} | |
.auth-info { | |
background: white; | |
padding: 15px; | |
border-radius: 8px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
margin-bottom: 10px; | |
} | |
.auth-info h3 { | |
margin: 0 0 10px 0; | |
font-size: 1.1rem; | |
color: #333; | |
} | |
.auth-info pre { | |
background: #f8f9fa; | |
padding: 10px; | |
border-radius: 4px; | |
font-size: 0.8rem; | |
margin: 0; | |
overflow-x: auto; | |
} | |
@media (max-width: 1200px) { | |
.main-container { | |
flex-direction: column; | |
padding: 20px; | |
} | |
.controls-section { | |
width: 100%; | |
order: -1; | |
} | |
.selector-container { | |
flex-direction: row; | |
justify-content: space-around; | |
flex-wrap: wrap; | |
} | |
.action-buttons { | |
flex-direction: row; | |
flex-wrap: wrap; | |
} | |
.action-btn { | |
flex: 1; | |
min-width: 200px; | |
} | |
} | |
@media (max-width: 768px) { | |
.header { | |
padding: 10px 15px; | |
font-size: 1.3rem; | |
} | |
.main-container { | |
padding: 10px; | |
height: calc(100vh - 60px); | |
} | |
.timetable { | |
font-size: 0.7rem; | |
} | |
.timetable th, | |
.timetable td { | |
padding: 6px 3px; | |
} | |
.selector-container { | |
flex-direction: column; | |
gap: 10px; | |
} | |
.action-buttons { | |
flex-direction: column; | |
} | |
.action-btn { | |
min-width: auto; | |
font-size: 0.8rem; | |
padding: 10px 12px; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="header"> | |
{{ controller_name }}さん、こんにちは! | |
</div> | |
<div class="main-container"> | |
<div class="timetable-section"> | |
<div class="timetable-container"> | |
<table class="timetable"> | |
<thead> | |
<tr> | |
<th class="time-header"></th> | |
<th>月<br>5月20日</th> | |
<th>火<br>5月21日</th> | |
<th>水<br>5月22日</th> | |
<th>木<br>5月23日</th> | |
<th>金<br>5月24日</th> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Annotation\Route; | |
class HomeController extends AbstractController | |
{ | |
#[Route('/', name: 'app_home')] | |
public function home(): Response | |
{ | |
$user = $this->getUser(); | |
$authInfo = [ | |
'authenticated' => $user !== null, | |
'user' => $user ? [ | |
'id' => $user->getId(), | |
'email' => $user->getEmail() | |
] : null | |
]; | |
return $this->render('home/home.html.twig', [ | |
'controller_name' => 'HomeController', | |
'auth_info' => $authInfo | |
]); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>時間割管理 - 管理者画面</title> | |
<!-- Bootstrap CSS --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"> | |
<!-- Font Awesome --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<style> | |
body { | |
background-color: #f4c2a1; | |
min-height: 100vh; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
} | |
.header { | |
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%); | |
color: white; | |
padding: 15px 40px; | |
font-size: 1.6rem; | |
font-weight: bold; | |
} | |
.main-container { | |
padding: 20px; | |
max-width: 1200px; | |
margin: 0 auto; | |
} | |
.timetable-container { | |
background: white; | |
border-radius: 8px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
overflow: hidden; | |
} | |
.timetable-header { | |
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%); | |
color: white; | |
padding: 20px; | |
text-align: center; | |
} | |
.action-buttons { | |
padding: 20px; | |
text-align: center; | |
border-bottom: 1px solid #dee2e6; | |
} | |
.btn-create { | |
background-color: #28a745; | |
color: white; | |
border: none; | |
padding: 12px 24px; | |
font-size: 1rem; | |
font-weight: bold; | |
border-radius: 4px; | |
text-decoration: none; | |
display: inline-block; | |
transition: all 0.3s ease; | |
} | |
.btn-create:hover { | |
background-color: #218838; | |
color: white; | |
text-decoration: none; | |
transform: translateY(-1px); | |
} | |
.timetable-list { | |
padding: 20px; | |
} | |
.timetable-item { | |
background: #f8f9fa; | |
border: 1px solid #dee2e6; | |
border-radius: 8px; | |
padding: 20px; | |
margin-bottom: 15px; | |
transition: all 0.3s ease; | |
} | |
.timetable-item:hover { | |
background: #e9ecef; | |
border-color: #dc3545; | |
transform: translateY(-2px); | |
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); | |
} | |
.class-name { | |
font-size: 1.3rem; | |
font-weight: bold; | |
color: #dc3545; | |
margin-bottom: 10px; | |
} | |
.class-info { | |
color: #6c757d; | |
font-size: 0.9rem; | |
margin-bottom: 15px; | |
} | |
.item-actions { | |
display: flex; | |
gap: 10px; | |
flex-wrap: wrap; | |
} | |
.btn-action { | |
padding: 8px 16px; | |
border: none; | |
border-radius: 4px; | |
font-size: 0.9rem; | |
text-decoration: none; | |
transition: all 0.3s ease; | |
cursor: pointer; | |
} | |
.btn-view { | |
background-color: #007bff; | |
color: white; | |
} | |
.btn-view:hover { | |
background-color: #0056b3; | |
color: white; | |
text-decoration: none; | |
} | |
.btn-edit { | |
background-color: #ffc107; | |
color: #212529; | |
} | |
.btn-edit:hover { | |
background-color: #e0a800; | |
color: #212529; | |
text-decoration: none; | |
} | |
.btn-delete { | |
background-color: #dc3545; | |
color: white; | |
} | |
.btn-delete:hover { | |
background-color: #c82333; | |
} | |
.no-data { | |
text-align: center; | |
padding: 40px; | |
color: #6c757d; | |
} | |
.back-btn { | |
background-color: #6c757d; | |
color: white; | |
border: none; | |
padding: 12px 24px; | |
font-size: 1rem; | |
border-radius: 4px; | |
text-decoration: none; | |
display: inline-block; | |
margin-top: 20px; | |
} | |
.back-btn:hover { | |
background-color: #5a6268; | |
color: white; | |
text-decoration: none; | |
} | |
.alert-message { | |
position: fixed; | |
top: 20px; | |
right: 20px; | |
padding: 15px 20px; | |
border-radius: 4px; | |
z-index: 2000; | |
display: none; | |
max-width: 400px; | |
} | |
.alert-success { | |
background-color: #d4edda; | |
color: #155724; | |
border: 1px solid #c3e6cb; | |
} | |
.alert-error { | |
background-color: #f8d7da; | |
color: #721c24; | |
border: 1px solid #f5c6cb; | |
} | |
@media (max-width: 768px) { | |
.header { | |
padding: 10px 15px; | |
font-size: 1.3rem; | |
} | |
.main-container { | |
padding: 10px; | |
} | |
.item-actions { | |
justify-content: center; | |
} | |
.btn-action { | |
flex: 1; | |
text-align: center; | |
min-width: 80px; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="header"> | |
<i class="fas fa-calendar-alt"></i> 時間割管理 | |
</div> | |
<!-- アラートメッセージ --> | |
<div id="alertMessage" class="alert-message"> | |
<span id="alertText"></span> | |
</div> | |
<div class="main-container"> | |
<div class="timetable-container"> | |
<div class="timetable-header"> | |
<h1><i class="fas fa-table"></i> 時間割一覧</h1> | |
<p>登録されている時間割: {{ timeTables|length }} クラス分</p> | |
</div> | |
<div class="action-buttons"> | |
<a href="{{ path('app_admin_timetable_create') }}" class="btn-create"> | |
<i class="fas fa-plus"></i> 新しい時間割を作成 | |
</a> | |
</div> | |
<div class="timetable-list"> | |
{% if timeTables|length > 0 %} | |
{% for timeTable in timeTables %} | |
<div class="timetable-item"> | |
<div class="class-name"> | |
<i class="fas fa-graduation-cap"></i> {{ timeTable.className }} | |
</div> | |
<div class="class-info"> | |
<i class="fas fa-calendar-week"></i> {{ timeTable.day }} | | |
<i class="fas fa-clock"></i> {{ timeTable.period }}時限制 | |
</div> | |
<div class="item-actions"> | |
<a href="{{ path('app_admin_timetable_show', {'className': timeTable.className}) }}" class="btn-action btn-view"> | |
<i class="fas fa-eye"></i> 表示 | |
</a> | |
<a href="{{ path('app_admin_timetable_edit', {'className': timeTable.className}) }}" class="btn-action btn-edit"> | |
<i class="fas fa-edit"></i> 編集 | |
</a> | |
<button class="btn-action btn-delete" onclick="deleteTimeTable('{{ timeTable.className }}')"> | |
<i class="fas fa-trash"></i> 削除 | |
</button> | |
</div> | |
</div> | |
{% endfor %} | |
{% else %} | |
<div class="no-data"> | |
<i class="fas fa-table fa-3x" style="color: #dee2e6; margin-bottom: 20px;"></i> | |
<p>登録されている時間割がありません。</p> | |
<p>「新しい時間割を作成」ボタンから時間割を追加してください。</p> | |
</div> | |
{% endif %} | |
</div> | |
<div style="text-align: center; padding: 20px; border-top: 1px solid #dee2e6;"> | |
<a href="{{ path('app_admin_home') }}" class="back-btn"> | |
<i class="fas fa-arrow-left"></i> 管理者ホームに戻る | |
</a> | |
</div> | |
</div> | |
</div> | |
<!-- Bootstrap JS --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script> | |
<script> | |
// 削除処理 | |
function deleteTimeTable(className) { | |
if (!confirm(`クラス「${className}」の時間割を削除してもよろしいですか?\nこの操作は取り消せません。`)) { | |
return; | |
} | |
fetch(`/admin/timetable/${encodeURIComponent(className)}/delete`, { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
'X-Requested-With': 'XMLHttpRequest' | |
} | |
}) | |
.then(response => response.json()) | |
.then(data => { | |
if (data.success) { | |
showAlert(data.message, 'success'); | |
// ページをリロード | |
setTimeout(() => { | |
location.reload(); | |
}, 1500); | |
} else { | |
showAlert(data.message, 'error'); | |
} | |
}) | |
.catch(error => { | |
console.error('削除エラー:', error); | |
showAlert('削除中にエラーが発生しました。', 'error'); | |
}); | |
} | |
// アラートメッセージを表示 | |
function showAlert(message, type) { | |
const alertMessage = document.getElementById('alertMessage'); | |
const alertText = document.getElementById('alertText'); | |
alertText.textContent = message; | |
alertMessage.className = `alert-message alert-${type}`; | |
alertMessage.style.display = 'block'; | |
// 3秒後に自動で隠す | |
setTimeout(() => { | |
alertMessage.style.display = 'none'; | |
}, 3000); | |
} | |
// フラッシュメッセージの処理 | |
{% for message in app.flashes('success') %} | |
showAlert('{{ message }}', 'success'); | |
{% endfor %} | |
{% for message in app.flashes('error') %} | |
showAlert('{{ message }}', 'error'); | |
{% endfor %} | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Entity; | |
use App\Repository\LessonDataRepository; | |
use Doctrine\ORM\Mapping as ORM; | |
#[ORM\Entity(repositoryClass: LessonDataRepository::class)] | |
#[ORM\Table(name: 'lesson_data')] | |
class LessonData | |
{ | |
#[ORM\Id] | |
#[ORM\GeneratedValue] | |
#[ORM\Column] | |
private ?int $id = null; | |
#[ORM\Column(length: 10)] | |
private ?string $subjectCode = null; | |
#[ORM\Column(length: 10)] | |
private ?string $className = null; | |
#[ORM\Column(length: 255)] | |
private ?string $location = null; | |
#[ORM\Column(length: 255)] | |
private ?string $subjectName = null; | |
#[ORM\Column(length: 10)] | |
private ?string $employmentType = null; | |
#[ORM\Column(length: 255)] | |
private ?string $teacher1 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $teacher2 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $teacher3 = null; | |
public function getId(): ?int | |
{ | |
return $this->id; | |
} | |
public function getSubjectCode(): ?string | |
{ | |
return $this->subjectCode; | |
} | |
public function setSubjectCode(string $subjectCode): static | |
{ | |
$this->subjectCode = $subjectCode; | |
return $this; | |
} | |
public function getClassName(): ?string | |
{ | |
return $this->className; | |
} | |
public function setClassName(string $className): static | |
{ | |
$this->className = $className; | |
return $this; | |
} | |
public function getLocation(): ?string | |
{ | |
return $this->location; | |
} | |
public function setLocation(string $location): static | |
{ | |
$this->location = $location; | |
return $this; | |
} | |
public function getSubjectName(): ?string | |
{ | |
return $this->subjectName; | |
} | |
public function setSubjectName(string $subjectName): static | |
{ | |
$this->subjectName = $subjectName; | |
return $this; | |
} | |
public function getEmploymentType(): ?string | |
{ | |
return $this->employmentType; | |
} | |
public function setEmploymentType(string $employmentType): static | |
{ | |
$this->employmentType = $employmentType; | |
return $this; | |
} | |
public function getTeacher1(): ?string | |
{ | |
return $this->teacher1; | |
} | |
public function setTeacher1(string $teacher1): static | |
{ | |
$this->teacher1 = $teacher1; | |
return $this; | |
} | |
public function getTeacher2(): ?string | |
{ | |
return $this->teacher2; | |
} | |
public function setTeacher2(?string $teacher2): static | |
{ | |
$this->teacher2 = $teacher2; | |
return $this; | |
} | |
public function getTeacher3(): ?string | |
{ | |
return $this->teacher3; | |
} | |
public function setTeacher3(?string $teacher3): static | |
{ | |
$this->teacher3 = $teacher3; | |
return $this; | |
} | |
/** | |
* 全ての教員を配列で取得 | |
*/ | |
public function getAllTeachers(): array | |
{ | |
$teachers = []; | |
if ($this->teacher1) { | |
$teachers[] = $this->teacher1; | |
} | |
if ($this->teacher2) { | |
$teachers[] = $this->teacher2; | |
} | |
if ($this->teacher3) { | |
$teachers[] = $this->teacher3; | |
} | |
return $teachers; | |
} | |
/** | |
* 教員名を文字列で取得(カンマ区切り) | |
*/ | |
public function getTeachersAsString(): string | |
{ | |
return implode(', ', $this->getAllTeachers()); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\DataFixtures; | |
use App\Entity\LessonData; | |
use Doctrine\Bundle\FixturesBundle\Fixture; | |
use Doctrine\Persistence\ObjectManager; | |
class LessonDataFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager): void | |
{ | |
// 既存のnot foundデータがあるかチェック | |
$existingData = $manager->getRepository(LessonData::class) | |
->findOneBy(['subjectCode' => 'not_found']); | |
if (!$existingData) { | |
$lessonData = new LessonData(); | |
$lessonData->setSubjectCode('not_found'); | |
$lessonData->setClassName('not found'); | |
$lessonData->setLocation('not found'); | |
$lessonData->setSubjectName('not found'); | |
$lessonData->setEmploymentType('not found'); | |
$lessonData->setTeacher1('not found'); | |
$lessonData->setTeacher2(null); | |
$lessonData->setTeacher3(null); | |
$manager->persist($lessonData); | |
$manager->flush(); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Repository; | |
use App\Entity\LessonData; | |
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; | |
use Doctrine\Persistence\ManagerRegistry; | |
/** | |
* @extends ServiceEntityRepository<LessonData> | |
*/ | |
class LessonDataRepository extends ServiceEntityRepository | |
{ | |
public function __construct(ManagerRegistry $registry) | |
{ | |
parent::__construct($registry, LessonData::class); | |
} | |
/** | |
* 科目コードで検索 | |
*/ | |
public function findBySubjectCode(string $subjectCode): ?LessonData | |
{ | |
return $this->createQueryBuilder('l') | |
->andWhere('l.subjectCode = :subjectCode') | |
->setParameter('subjectCode', $subjectCode) | |
->getQuery() | |
->getOneOrNullResult(); | |
} | |
/** | |
* クラス名で検索 | |
*/ | |
public function findByClassName(string $className): array | |
{ | |
return $this->createQueryBuilder('l') | |
->andWhere('l.className = :className') | |
->setParameter('className', $className) | |
->orderBy('l.subjectCode', 'ASC') | |
->getQuery() | |
->getResult(); | |
} | |
/** | |
* 教員名で検索 | |
*/ | |
public function findByTeacher(string $teacherName): array | |
{ | |
return $this->createQueryBuilder('l') | |
->andWhere('l.teacher1 = :teacher OR l.teacher2 = :teacher OR l.teacher3 = :teacher') | |
->setParameter('teacher', $teacherName) | |
->orderBy('l.className', 'ASC') | |
->addOrderBy('l.subjectCode', 'ASC') | |
->getQuery() | |
->getResult(); | |
} | |
/** | |
* 科目名で検索(部分一致) | |
*/ | |
public function findBySubjectName(string $subjectName): array | |
{ | |
return $this->createQueryBuilder('l') | |
->andWhere('l.subjectName LIKE :subjectName') | |
->setParameter('subjectName', '%' . $subjectName . '%') | |
->orderBy('l.className', 'ASC') | |
->addOrderBy('l.subjectCode', 'ASC') | |
->getQuery() | |
->getResult(); | |
} | |
/** | |
* 雇用形態で検索 | |
*/ | |
public function findByEmploymentType(string $employmentType): array | |
{ | |
return $this->createQueryBuilder('l') | |
->andWhere('l.employmentType = :employmentType') | |
->setParameter('employmentType', $employmentType) | |
->orderBy('l.className', 'ASC') | |
->addOrderBy('l.subjectCode', 'ASC') | |
->getQuery() | |
->getResult(); | |
} | |
/** | |
* 全クラス名を取得 | |
*/ | |
public function findAllClassNames(): array | |
{ | |
$result = $this->createQueryBuilder('l') | |
->select('DISTINCT l.className') | |
->orderBy('l.className', 'ASC') | |
->getQuery() | |
->getResult(); | |
return array_column($result, 'className'); | |
} | |
/** | |
* 全教員名を取得 | |
*/ | |
public function findAllTeachers(): array | |
{ | |
$teachers = []; | |
// teacher1から取得 | |
$result1 = $this->createQueryBuilder('l') | |
->select('DISTINCT l.teacher1') | |
->andWhere('l.teacher1 IS NOT NULL') | |
->getQuery() | |
->getResult(); | |
foreach ($result1 as $row) { | |
if ($row['teacher1']) { | |
$teachers[] = $row['teacher1']; | |
} | |
} | |
// teacher2から取得 | |
$result2 = $this->createQueryBuilder('l') | |
->select('DISTINCT l.teacher2') | |
->andWhere('l.teacher2 IS NOT NULL') | |
->getQuery() | |
->getResult(); | |
foreach ($result2 as $row) { | |
if ($row['teacher2']) { | |
$teachers[] = $row['teacher2']; | |
} | |
} | |
// teacher3から取得 | |
$result3 = $this->createQueryBuilder('l') | |
->select('DISTINCT l.teacher3') | |
->andWhere('l.teacher3 IS NOT NULL') | |
->getQuery() | |
->getResult(); | |
foreach ($result3 as $row) { | |
if ($row['teacher3']) { | |
$teachers[] = $row['teacher3']; | |
} | |
} | |
// 重複を除去してソート | |
$teachers = array_unique($teachers); | |
sort($teachers); | |
return $teachers; | |
} | |
/** | |
* 全雇用形態を取得 | |
*/ | |
public function findAllEmploymentTypes(): array | |
{ | |
$result = $this->createQueryBuilder('l') | |
->select('DISTINCT l.employmentType') | |
->orderBy('l.employmentType', 'ASC') | |
->getQuery() | |
->getResult(); | |
return array_column($result, 'employmentType'); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>{% block title %}ログイン - 時間割管理システム{% endblock %}</title> | |
<!-- Bootstrap CSS --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"> | |
<!-- Font Awesome --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<style> | |
body { | |
background-color: #f4c2a1; | |
min-height: 100vh; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
margin: 0; | |
padding: 0; | |
} | |
.login-container { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
width: 600px; | |
max-width: 90vw; | |
} | |
.login-box { | |
background-color: #2e86c1; | |
padding: 40px; | |
border-radius: 0; | |
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); | |
} | |
.login-title { | |
color: white; | |
font-size: 2.5rem; | |
font-weight: bold; | |
margin-bottom: 30px; | |
text-align: left; | |
} | |
.form-group { | |
margin-bottom: 25px; | |
} | |
.form-label { | |
color: white; | |
font-size: 1.1rem; | |
font-weight: 500; | |
margin-bottom: 8px; | |
display: block; | |
} | |
.form-control { | |
width: 100%; | |
padding: 12px 16px; | |
font-size: 1rem; | |
border: none; | |
border-radius: 0; | |
background-color: white; | |
color: #333; | |
} | |
.form-control:focus { | |
outline: none; | |
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.3); | |
} | |
.login-btn { | |
background-color: #f1c40f; | |
color: #333; | |
border: none; | |
padding: 12px 30px; | |
font-size: 1.1rem; | |
font-weight: bold; | |
border-radius: 0; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
margin-top: 20px; | |
} | |
.login-btn:hover { | |
background-color: #d4ac0d; | |
transform: translateY(-1px); | |
} | |
.login-btn:disabled { | |
background-color: #bdc3c7; | |
cursor: not-allowed; | |
transform: none; | |
} | |
.alert { | |
margin-bottom: 20px; | |
padding: 12px 16px; | |
border-radius: 4px; | |
border: none; | |
display: none; | |
} | |
.alert-danger { | |
background-color: rgba(231, 76, 60, 0.9); | |
color: white; | |
} | |
.alert-success { | |
background-color: rgba(39, 174, 96, 0.9); | |
color: white; | |
} | |
.alert-info { | |
background-color: rgba(52, 152, 219, 0.9); | |
color: white; | |
} | |
.input-error { | |
box-shadow: 0 0 0 3px rgba(231, 76, 60, 0.5) !important; | |
} | |
@media (max-width: 768px) { | |
.login-container { | |
position: relative; | |
top: auto; | |
left: auto; | |
transform: none; | |
margin: 20px auto; | |
width: 95%; | |
} | |
.login-box { | |
padding: 30px 20px; | |
} | |
.login-title { | |
font-size: 2rem; | |
text-align: center; | |
} | |
} | |
@media (max-width: 480px) { | |
.login-title { | |
font-size: 1.8rem; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="login-container"> | |
<div class="login-box"> | |
<h1 class="login-title">ログイン</h1> | |
<div id="alert-message" class="alert" role="alert"> | |
<span id="alert-text"></span> | |
</div> | |
<form id="loginForm"> | |
<div class="form-group"> | |
<label class="form-label" for="email">Username/E-Mail Address</label> | |
<input type="email" | |
class="form-control" | |
id="email" | |
name="email" | |
placeholder="nk12345@stu.tomakomai-ct.ac.jp" | |
required | |
autofocus> | |
</div> | |
<div class="form-group"> | |
<label class="form-label" for="password">password</label> | |
<input type="password" | |
class="form-control" | |
id="password" | |
name="password" | |
required> | |
</div> | |
<button class="login-btn" type="submit" id="loginBtn"> | |
確認 | |
</button> | |
</form> | |
</div> | |
</div> | |
<!-- Bootstrap JS --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
const loginForm = document.getElementById('loginForm'); | |
const emailInput = document.getElementById('email'); | |
const passwordInput = document.getElementById('password'); | |
const alertMessage = document.getElementById('alert-message'); | |
const alertText = document.getElementById('alert-text'); | |
const loginBtn = document.getElementById('loginBtn'); | |
// Form submission handler | |
loginForm.addEventListener('submit', function(e) { | |
e.preventDefault(); | |
// Clear previous errors | |
clearErrors(); | |
// Get form data | |
const formData = { | |
email: emailInput.value.trim(), | |
password: passwordInput.value | |
}; | |
// Basic validation | |
if (!validateForm(formData)) { | |
return; | |
} | |
// Show loading state | |
showLoading(); | |
// Simulate API call (replace with actual login logic) | |
setTimeout(() => { | |
// For demo purposes - replace with actual authentication | |
if (formData.email && formData.password.length >= 4) { | |
showSuccess('ログイン成功!リダイレクトしています...'); | |
setTimeout(() => { | |
// Redirect to admin home page | |
window.location.href = '/admin/home'; | |
}, 1500); | |
} else { | |
showError('無効なメールアドレスまたはパスワードです。'); | |
hideLoading(); | |
} | |
}, 1000); | |
}); | |
// Input event listeners for real-time validation | |
emailInput.addEventListener('blur', function() { | |
validateEmail(this.value); | |
}); | |
passwordInput.addEventListener('blur', function() { | |
validatePassword(this.value); | |
}); | |
// Validation functions | |
function validateForm(data) { | |
let isValid = true; | |
if (!validateEmail(data.email)) { | |
isValid = false; | |
} | |
if (!validatePassword(data.password)) { | |
isValid = false; | |
} | |
return isValid; | |
} | |
function validateEmail(email) { | |
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; | |
if (!email) { | |
showFieldError(emailInput, 'メールアドレスが必要です'); | |
return false; | |
} else if (!emailRegex.test(email)) { | |
showFieldError(emailInput, '有効なメールアドレスを入力してください'); | |
return false; | |
} else { | |
clearFieldError(emailInput); | |
return true; | |
} | |
} | |
function validatePassword(password) { | |
if (!password) { | |
showFieldError(passwordInput, 'パスワードが必要です'); | |
return false; | |
} else if (password.length < 4) { | |
showFieldError(passwordInput, 'パスワードは4文字以上である必要があります'); | |
return false; | |
} else { | |
clearFieldError(passwordInput); | |
return true; | |
} | |
} | |
// UI helper functions | |
function showFieldError(field, message) { | |
field.classList.add('input-error'); | |
} | |
function clearFieldError(field) { | |
field.classList.remove('input-error'); | |
} | |
function clearErrors() { | |
emailInput.classList.remove('input-error'); | |
passwordInput.classList.remove('input-error'); | |
hideAlert(); | |
} | |
function showError(message) { | |
alertText.textContent = message; | |
alertMessage.className = 'alert alert-danger'; | |
alertMessage.style.display = 'block'; | |
} | |
function showSuccess(message) { | |
alertText.textContent = message; | |
alertMessage.className = 'alert alert-success'; | |
alertMessage.style.display = 'block'; | |
} | |
function showInfo(message) { | |
alertText.textContent = message; | |
alertMessage.className = 'alert alert-info'; | |
alertMessage.style.display = 'block'; | |
setTimeout(() => { | |
hideAlert(); | |
}, 3000); | |
} | |
function hideAlert() { | |
alertMessage.style.display = 'none'; | |
} | |
function showLoading() { | |
loginBtn.disabled = true; | |
loginBtn.innerHTML = '処理中...'; | |
} | |
function hideLoading() { | |
loginBtn.disabled = false; | |
loginBtn.innerHTML = '確認'; | |
} | |
}); | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Annotation\Route; | |
class LoginController extends AbstractController | |
{ | |
#[Route('/login', name: 'app_page')] | |
public function index(): Response | |
{ | |
return $this->render('login/login.html.twig', [ | |
'controller_name' => 'LoginController', | |
]); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Annotation\Route; | |
class LoginDummyController | |
{ | |
#[Route('/login', name: 'app_login')] | |
public function __invoke(): Response | |
{ | |
return new Response('This route is used only for internal redirect purposes.', Response::HTTP_OK); | |
} | |
} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Security; | |
use Symfony\Component\HttpFoundation\RedirectResponse; | |
use Symfony\Component\HttpFoundation\Request; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; | |
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; | |
use Symfony\Component\Security\Core\Security; | |
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator; | |
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge; | |
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge; | |
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; | |
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; | |
use Symfony\Component\Security\Http\Authenticator\Passport\Passport; | |
use Symfony\Component\Security\Http\Util\TargetPathTrait; | |
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials; | |
use Doctrine\ORM\EntityManagerInterface; | |
class LoginFormAuthenticator extends AbstractLoginFormAuthenticator | |
{ | |
use TargetPathTrait; | |
public const LOGIN_ROUTE = 'app_magic_login'; | |
public function __construct( | |
private UrlGeneratorInterface $urlGenerator, | |
private EntityManagerInterface $entityManager | |
) { | |
} | |
public function supports(Request $request): bool | |
{ | |
// マジックリンク認証の場合はサポートする | |
if ($request->attributes->get('_route') === 'app_magic_confirm') { | |
error_log('[DEBUG] LoginFormAuthenticator: Supporting magic-confirm route'); | |
return true; | |
} | |
// 通常のログインフォームは処理しない | |
return false; | |
} | |
public function authenticate(Request $request): Passport | |
{ | |
// マジックリンク認証の場合 | |
if ($request->attributes->get('_route') === 'app_magic_confirm') { | |
$userId = $request->query->get('id'); | |
error_log('[DEBUG] LoginFormAuthenticator: Authenticating magic link for user ID: ' . $userId); | |
if (!$userId) { | |
throw new \InvalidArgumentException('User ID is required for magic link authentication'); | |
} | |
return new Passport( | |
new UserBadge($userId, function($userIdentifier) { | |
// EntityManagerを通してユーザーを取得 | |
$user = $this->entityManager->getRepository(\App\Entity\User::class)->find($userIdentifier); | |
error_log('[DEBUG] LoginFormAuthenticator: Found user: ' . ($user ? $user->getEmail() : 'null')); | |
return $user; | |
}), | |
new CustomCredentials( | |
function($credentials, $user) { | |
// マジックリンクの場合は常に認証成功 | |
error_log('[DEBUG] LoginFormAuthenticator: Magic link credentials validated'); | |
return true; | |
}, | |
null | |
) | |
); | |
} | |
// 通常のフォーム認証(使用されない) | |
$email = $request->request->get('email', ''); | |
$request->getSession()->set(Security::LAST_USERNAME, $email); | |
return new Passport( | |
new UserBadge($email), | |
new PasswordCredentials($request->request->get('password', '')), | |
[ | |
new CsrfTokenBadge('authenticate', $request->request->get('_csrf_token')), | |
new RememberMeBadge(), | |
] | |
); | |
} | |
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response | |
{ | |
error_log('[DEBUG] LoginFormAuthenticator: Authentication successful for user: ' . $token->getUser()->getEmail()); | |
// マジックリンク認証の場合は、success画面を表示するためnullを返す | |
if ($request->attributes->get('_route') === 'app_magic_confirm') { | |
error_log('[DEBUG] LoginFormAuthenticator: Magic link authentication, returning null for success page'); | |
return null; | |
} | |
if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) { | |
error_log('[DEBUG] LoginFormAuthenticator: Redirecting to target path: ' . $targetPath); | |
return new RedirectResponse($targetPath); | |
} | |
error_log('[DEBUG] LoginFormAuthenticator: Redirecting to default home'); | |
return new RedirectResponse($this->urlGenerator->generate('app_magic_login')); | |
} | |
protected function getLoginUrl(Request $request): string | |
{ | |
return $this->urlGenerator->generate(self::LOGIN_ROUTE); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// src/Security/LoginSuccessHandler.php | |
namespace App\Security; | |
use Symfony\Component\HttpFoundation\RedirectResponse; | |
use Symfony\Component\Routing\RouterInterface; | |
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; | |
use Symfony\Component\HttpFoundation\Request; | |
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; | |
class LoginSuccessHandler implements AuthenticationSuccessHandlerInterface | |
{ | |
public function __construct(private RouterInterface $router) {} | |
public function onAuthenticationSuccess(Request $request, TokenInterface $token): RedirectResponse | |
{ | |
return new RedirectResponse($this->router->generate('app_home')); // 認証成功後の遷移先 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>{% block title %}ログアウト - 時間割管理システム{% endblock %}</title> | |
<!-- Bootstrap CSS --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"> | |
<!-- Font Awesome --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<style> | |
body { | |
background-color: #f4c2a1; | |
min-height: 100vh; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
margin: 0; | |
padding: 0; | |
} | |
.logout-container { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
width: 600px; | |
max-width: 90vw; | |
} | |
.logout-box { | |
background-color: #2e86c1; | |
padding: 40px; | |
border-radius: 0; | |
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); | |
text-align: center; | |
} | |
.logout-title { | |
color: white; | |
font-size: 2.5rem; | |
font-weight: bold; | |
margin-bottom: 30px; | |
} | |
.logout-message { | |
color: white; | |
font-size: 1.1rem; | |
margin-bottom: 20px; | |
line-height: 1.6; | |
} | |
.user-info { | |
background: rgba(255, 255, 255, 0.1); | |
padding: 15px; | |
border-radius: 4px; | |
margin: 20px 0; | |
color: white; | |
} | |
.user-email { | |
color: #f1c40f; | |
font-weight: bold; | |
font-size: 1.1rem; | |
} | |
.logout-btn { | |
background-color: #e74c3c; | |
color: white; | |
border: none; | |
padding: 12px 30px; | |
font-size: 1.1rem; | |
font-weight: bold; | |
border-radius: 4px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
margin: 10px; | |
min-width: 150px; | |
} | |
.logout-btn:hover { | |
background-color: #c0392b; | |
transform: translateY(-1px); | |
} | |
.logout-btn:disabled { | |
background-color: #95a5a6; | |
cursor: not-allowed; | |
transform: none; | |
} | |
.login-btn { | |
background-color: #f1c40f; | |
color: #333; | |
border: none; | |
padding: 12px 30px; | |
font-size: 1.1rem; | |
font-weight: bold; | |
border-radius: 4px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
margin: 10px; | |
min-width: 150px; | |
text-decoration: none; | |
display: inline-block; | |
} | |
.login-btn:hover { | |
background-color: #d4ac0d; | |
transform: translateY(-1px); | |
color: #333; | |
text-decoration: none; | |
} | |
.back-btn { | |
background-color: #6c757d; | |
color: white; | |
border: none; | |
padding: 12px 30px; | |
font-size: 1.1rem; | |
font-weight: bold; | |
border-radius: 4px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
margin: 10px; | |
min-width: 150px; | |
text-decoration: none; | |
display: inline-block; | |
} | |
.back-btn:hover { | |
background-color: #5a6268; | |
transform: translateY(-1px); | |
color: white; | |
text-decoration: none; | |
} | |
.success-icon { | |
color: #f1c40f; | |
font-size: 3rem; | |
margin-bottom: 20px; | |
} | |
.logout-icon { | |
color: #e74c3c; | |
font-size: 3rem; | |
margin-bottom: 20px; | |
} | |
.button-group { | |
margin-top: 30px; | |
} | |
.warning-message { | |
background: rgba(231, 76, 60, 0.2); | |
border: 1px solid rgba(231, 76, 60, 0.5); | |
padding: 15px; | |
border-radius: 4px; | |
margin: 20px 0; | |
color: #fff; | |
} | |
.warning-message strong { | |
color: #f1c40f; | |
} | |
.error-message { | |
background: rgba(231, 76, 60, 0.3); | |
border: 1px solid rgba(231, 76, 60, 0.7); | |
padding: 15px; | |
border-radius: 4px; | |
margin: 20px 0; | |
color: #fff; | |
} | |
.error-message strong { | |
color: #f1c40f; | |
} | |
@media (max-width: 768px) { | |
.logout-container { | |
position: relative; | |
top: auto; | |
left: auto; | |
transform: none; | |
margin: 20px auto; | |
width: 95%; | |
} | |
.logout-box { | |
padding: 30px 20px; | |
} | |
.logout-title { | |
font-size: 2rem; | |
} | |
.logout-btn, | |
.login-btn, | |
.back-btn { | |
display: block; | |
width: 100%; | |
margin: 10px 0; | |
} | |
} | |
@media (max-width: 480px) { | |
.logout-title { | |
font-size: 1.8rem; | |
} | |
.success-icon, | |
.logout-icon { | |
font-size: 2.5rem; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="logout-container"> | |
<div class="logout-box"> | |
{% if logged_out %} | |
<!-- ログアウト完了画面 --> | |
<div class="success-icon"> | |
<i class="fas fa-check-circle"></i> | |
</div> | |
<h1 class="logout-title">ログアウト完了</h1> | |
<p class="logout-message">正常にログアウトしました。</p> | |
<p class="logout-message">ご利用ありがとうございました。</p> | |
{% if error is defined %} | |
<div class="error-message"> | |
<strong><i class="fas fa-exclamation-triangle"></i> 注意</strong><br> | |
{{ error }} | |
</div> | |
{% endif %} | |
<div class="warning-message"> | |
<strong><i class="fas fa-info-circle"></i> 学校用システム</strong><br> | |
マルチユーザー対応のため、他のユーザーのログイン状態もリセットされました。<br> | |
次回ログイン時は再度メールアドレスとパスワードの入力が必要です。 | |
</div> | |
<div class="button-group"> | |
<a href="{{ path('app_magic_login') }}" class="login-btn"> | |
<i class="fas fa-sign-in-alt"></i> ログイン画面へ | |
</a> | |
</div> | |
{% else %} | |
<!-- ログアウト確認画面 --> | |
<div class="logout-icon"> | |
<i class="fas fa-sign-out-alt"></i> | |
</div> | |
<h1 class="logout-title">ログアウト</h1> | |
{% if user %} | |
<div class="user-info"> | |
<p>現在ログイン中のユーザー</p> | |
<div class="user-email">{{ user.email }}</div> | |
</div> | |
{% endif %} | |
<p class="logout-message">ログアウトしてもよろしいですか?</p> | |
<div class="button-group"> | |
<form method="post" style="display: inline;"> | |
<button type="submit" class="logout-btn" id="logoutBtn"> | |
<i class="fas fa-sign-out-alt"></i> ログアウト | |
</button> | |
</form> | |
<a href="javascript:history.back()" class="back-btn"> | |
<i class="fas fa-arrow-left"></i> 戻る | |
</a> | |
</div> | |
{% endif %} | |
</div> | |
</div> | |
<!-- Bootstrap JS --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
const logoutBtn = document.getElementById('logoutBtn'); | |
const logoutForm = logoutBtn ? logoutBtn.closest('form') : null; | |
if (logoutBtn && logoutForm) { | |
logoutBtn.addEventListener('click', function(e) { | |
e.preventDefault(); // デフォルトの送信を防ぐ | |
// 確認ダイアログを表示 | |
if (!confirm('本当にログアウトしますか?\n')) { | |
return false; | |
} | |
// ボタンを無効化して二重送信を防ぐ | |
this.disabled = true; | |
this.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ログアウト中...'; | |
// フォームを明示的に送信 | |
setTimeout(() => { | |
logoutForm.submit(); | |
}, 100); | |
}); | |
} | |
}); | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use Doctrine\ORM\EntityManagerInterface; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\Request; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Annotation\Route; | |
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; | |
use App\Entity\User; | |
class LogoutController extends AbstractController | |
{ | |
public function __construct( | |
private EntityManagerInterface $entityManager, | |
private TokenStorageInterface $tokenStorage | |
) {} | |
#[Route('/user-logout', name: 'app_logout')] | |
public function index(Request $request): Response | |
{ | |
// POSTリクエスト(ログアウトボタンが押された)の場合 | |
if ($request->isMethod('POST')) { | |
error_log('[DEBUG] Logout: Processing logout request'); | |
try { | |
// 現在のユーザーを取得 | |
$user = $this->getUser(); | |
$session = $request->getSession(); | |
if ($user) { | |
error_log('[DEBUG] Logout: Logging out user: ' . $user->getEmail()); | |
// ユーザーエンティティを取得してログイン状態をクリア | |
$userEntity = $this->entityManager->getRepository(User::class)->find($user->getId()); | |
if ($userEntity) { | |
// loginStatusをnullに設定 | |
$userEntity->setLoginStatus(null); | |
// activeDevicesもクリア | |
$userEntity->clearActiveDevices(); | |
$this->entityManager->persist($userEntity); | |
$this->entityManager->flush(); | |
error_log('[DEBUG] Logout: Cleared loginStatus and activeDevices for user: ' . $user->getEmail()); | |
} | |
} else { | |
error_log('[DEBUG] Logout: No user found in session'); | |
} | |
// セッションを完全に無効化 | |
if ($session) { | |
$session->invalidate(); | |
error_log('[DEBUG] Logout: Session invalidated'); | |
} | |
// セキュリティトークンをクリア | |
$this->tokenStorage->setToken(null); | |
error_log('[DEBUG] Logout: Security token cleared'); | |
error_log('[DEBUG] Logout: All logout operations completed successfully'); | |
// ログアウト完了画面を表示 | |
return $this->render('logout/logout.html.twig', [ | |
'logged_out' => true, | |
]); | |
} catch (\Exception $e) { | |
error_log('[ERROR] Logout: Exception occurred: ' . $e->getMessage()); | |
error_log('[ERROR] Logout: Stack trace: ' . $e->getTraceAsString()); | |
// エラーが発生した場合でも、ログアウト完了画面を表示 | |
return $this->render('logout/logout.html.twig', [ | |
'logged_out' => true, | |
'error' => 'ログアウト処理中にエラーが発生しましたが、セッションはクリアされました。', | |
]); | |
} | |
} | |
// GETリクエスト(ログアウトページの表示)の場合 | |
$user = $this->getUser(); | |
if (!$user) { | |
error_log('[DEBUG] Logout: No authenticated user, redirecting to login'); | |
// 未認証の場合は直接ログイン画面へリダイレクト | |
return $this->redirectToRoute('app_magic_login'); | |
} | |
error_log('[DEBUG] Logout: Showing logout page for user: ' . $user->getEmail()); | |
return $this->render('logout/logout.html.twig', [ | |
'logged_out' => false, | |
'user' => $user, | |
]); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>{% block title %}ログイン - 時間割管理システム{% endblock %}</title> | |
<!-- Bootstrap CSS --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"> | |
<!-- Font Awesome --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<style> | |
body { | |
background-color: #f4c2a1; | |
min-height: 100vh; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
margin: 0; | |
padding: 20px; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
} | |
.login-container { | |
width: 600px; | |
max-width: 90vw; | |
max-height: 90vh; | |
overflow-y: auto; | |
} | |
.login-box { | |
background-color: #2e86c1; | |
padding: 30px; | |
border-radius: 0; | |
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); | |
} | |
.login-title { | |
color: white; | |
font-size: 2rem; | |
font-weight: bold; | |
text-align: center; | |
margin-bottom: 20px; | |
} | |
.login-subtitle { | |
color: white; | |
font-size: 1rem; | |
text-align: center; | |
margin-bottom: 20px; | |
opacity: 0.9; | |
} | |
.form-group { | |
margin-bottom: 15px; | |
} | |
.form-label { | |
color: white; | |
font-size: 1rem; | |
font-weight: 500; | |
margin-bottom: 8px; | |
display: block; | |
} | |
.form-control { | |
width: 100%; | |
padding: 12px 16px; | |
font-size: 1rem; | |
border: 2px solid rgba(255, 255, 255, 0.3); | |
border-radius: 4px; | |
background-color: rgba(255, 255, 255, 0.1); | |
color: white; | |
transition: all 0.3s ease; | |
} | |
.form-control::placeholder { | |
color: rgba(255, 255, 255, 0.7); | |
} | |
.form-control:focus { | |
outline: none; | |
border-color: #f1c40f; | |
background-color: rgba(255, 255, 255, 0.15); | |
box-shadow: 0 0 0 3px rgba(241, 196, 15, 0.2); | |
} | |
.login-btn { | |
background-color: #f1c40f; | |
color: #333; | |
border: none; | |
padding: 12px 30px; | |
font-size: 1.1rem; | |
font-weight: bold; | |
border-radius: 4px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
width: 100%; | |
margin-top: 10px; | |
} | |
.login-btn:hover { | |
background-color: #d4ac0d; | |
transform: translateY(-1px); | |
} | |
.login-btn:disabled { | |
background-color: #95a5a6; | |
cursor: not-allowed; | |
transform: none; | |
} | |
.error-message { | |
background-color: rgba(231, 76, 60, 0.2); | |
border: 1px solid rgba(231, 76, 60, 0.5); | |
color: white; | |
padding: 15px; | |
border-radius: 4px; | |
margin-bottom: 20px; | |
text-align: center; | |
} | |
.info-message { | |
background-color: rgba(52, 152, 219, 0.2); | |
border: 1px solid rgba(52, 152, 219, 0.5); | |
color: white; | |
padding: 15px; | |
border-radius: 4px; | |
margin-bottom: 20px; | |
font-size: 0.9rem; | |
line-height: 1.5; | |
} | |
.info-message strong { | |
color: #f1c40f; | |
} | |
.system-info { | |
text-align: center; | |
margin-top: 20px; | |
padding-top: 15px; | |
border-top: 1px solid rgba(255, 255, 255, 0.2); | |
} | |
.system-info p { | |
color: rgba(255, 255, 255, 0.8); | |
font-size: 0.8rem; | |
margin: 3px 0; | |
} | |
.system-info .version { | |
color: #f1c40f; | |
font-weight: bold; | |
} | |
@media (max-width: 768px) { | |
body { | |
padding: 10px; | |
} | |
.login-container { | |
width: 95%; | |
max-height: 95vh; | |
} | |
.login-box { | |
padding: 20px; | |
} | |
.login-title { | |
font-size: 1.8rem; | |
} | |
} | |
@media (max-width: 480px) { | |
.login-title { | |
font-size: 1.6rem; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="login-container"> | |
<div class="login-box"> | |
<h1 class="login-title">時間割管理システム</h1> | |
<p class="login-subtitle">マジックリンクログイン</p> | |
{% if error is defined %} | |
<div class="error-message"> | |
<i class="fas fa-exclamation-triangle"></i> {{ error }} | |
</div> | |
{% endif %} | |
<div class="info-message"> | |
<strong><i class="fas fa-info-circle"></i> ログイン方法</strong><br> | |
メールアドレスとパスワードを入力すると、認証用のマジックリンクがメールで送信されます。<br> | |
メール内のリンクをクリックしてログインを完了してください。 | |
</div> | |
<form method="post" id="loginForm"> | |
<div class="form-group"> | |
<label class="form-label" for="email"> | |
<i class="fas fa-envelope"></i> メールアドレス | |
</label> | |
<input type="email" | |
class="form-control" | |
id="email" | |
name="email" | |
placeholder="your-email@example.com" | |
required> | |
</div> | |
<div class="form-group"> | |
<label class="form-label" for="password"> | |
<i class="fas fa-lock"></i> パスワード | |
</label> | |
<input type="password" | |
class="form-control" | |
id="password" | |
name="password" | |
placeholder="パスワードを入力" | |
required> | |
</div> | |
<button type="submit" class="login-btn" id="loginBtn"> | |
<i class="fas fa-paper-plane"></i> マジックリンクを送信 | |
</button> | |
</form> | |
<div class="system-info"> | |
<p><i class="fas fa-shield-alt"></i> セキュアな認証システム</p> | |
<p class="version">Version 2.0 - マルチデバイス対応</p> | |
<p><i class="fas fa-devices"></i> 複数端末での同時ログインが可能です</p> | |
</div> | |
</div> | |
</div> | |
<!-- Bootstrap JS --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
const form = document.getElementById('loginForm'); | |
const loginBtn = document.getElementById('loginBtn'); | |
const emailInput = document.getElementById('email'); | |
const passwordInput = document.getElementById('password'); | |
form.addEventListener('submit', function(e) { | |
// バリデーション | |
if (!emailInput.value || !passwordInput.value) { | |
e.preventDefault(); | |
alert('メールアドレスとパスワードを入力してください。'); | |
return; | |
} | |
// 送信中の状態に変更 | |
loginBtn.disabled = true; | |
loginBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 送信中...'; | |
}); | |
// エンターキーでの送信 | |
emailInput.addEventListener('keypress', function(e) { | |
if (e.key === 'Enter') { | |
passwordInput.focus(); | |
} | |
}); | |
passwordInput.addEventListener('keypress', function(e) { | |
if (e.key === 'Enter') { | |
form.submit(); | |
} | |
}); | |
}); | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Security; | |
use Symfony\Component\HttpFoundation\Request; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\HttpFoundation\RedirectResponse; | |
use Symfony\Component\Routing\RouterInterface; | |
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; | |
use Symfony\Component\Security\Core\Exception\AuthenticationException; | |
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; | |
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; | |
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; | |
use Symfony\Component\Security\Http\Authenticator\Passport\Passport; | |
class MagicLinkAuthenticator extends AbstractAuthenticator | |
{ | |
public function __construct( | |
private RouterInterface $router | |
) {} | |
public function supports(Request $request): bool | |
{ | |
return $request->attributes->get('_route') === 'app_magic_confirm'; | |
} | |
public function authenticate(Request $request): Passport | |
{ | |
$userId = $request->query->get('id'); | |
if (!$userId) { | |
throw new \InvalidArgumentException('IDパラメータがありません'); | |
} | |
return new SelfValidatingPassport( | |
new UserBadge($userId) | |
); | |
} | |
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response | |
{ | |
// renderメソッドは使えないので、RedirectResponseを返す | |
return new RedirectResponse($this->router->generate('app_home')); | |
} | |
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response | |
{ | |
return new RedirectResponse($this->router->generate('app_magic_login')); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use App\Entity\User; | |
use Doctrine\ORM\EntityManagerInterface; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\JsonResponse; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Annotation\Route; | |
class MagicLinkStatusController extends AbstractController | |
{ | |
public function __construct( | |
private EntityManagerInterface $entityManager | |
) {} | |
#[Route('/magic-link/check-status/{token}', name: 'app_magic_link_check_status', methods: ['GET'])] | |
public function checkStatus(string $token): JsonResponse | |
{ | |
// トークンでユーザーを検索 | |
$user = $this->entityManager->getRepository(User::class)->findOneBy(['magicToken' => $token]); | |
if (!$user) { | |
return new JsonResponse(['status' => 'invalid'], Response::HTTP_NOT_FOUND); | |
} | |
// ユーザーがログイン済みかチェック | |
if ($this->getUser() && $this->getUser()->getId() === $user->getId()) { | |
// ログイン成功 - トークンをクリア | |
$user->setMagicToken(null); | |
$this->entityManager->flush(); | |
return new JsonResponse([ | |
'status' => 'success', | |
'redirect' => $this->generateUrl('app_home') | |
]); | |
} | |
// まだログインしていない | |
return new JsonResponse(['status' => 'pending']); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use App\Entity\User; | |
use App\Security\EmailVerifier; | |
use App\Security\LoginFormAuthenticator; | |
use Doctrine\ORM\EntityManagerInterface; | |
use Symfony\Bridge\Twig\Mime\TemplatedEmail; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\Request; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Mime\Address; | |
use Symfony\Component\Routing\Annotation\Route; | |
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; | |
use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface; | |
use Symfony\Component\Security\Core\Exception\AuthenticationException; | |
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; | |
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; | |
class MagicLoginController extends AbstractController | |
{ | |
public function __construct( | |
private EntityManagerInterface $entityManager, | |
private EmailVerifier $emailVerifier, | |
private UserPasswordHasherInterface $passwordHasher, | |
private UserAuthenticatorInterface $userAuthenticator, | |
private LoginFormAuthenticator $authenticator, | |
private TokenStorageInterface $tokenStorage | |
) {} | |
#[Route('/magic-login', name: 'app_magic_login')] | |
public function login(Request $request): Response | |
{ | |
error_log('[DEBUG] MagicLogin: === STARTING MAGIC LOGIN PROCESS ==='); | |
error_log('[DEBUG] MagicLogin: Request method: ' . $request->getMethod()); | |
// ★★ 強制的にセッションをクリアするオプション ★★ | |
$forceLogout = $request->query->get('force_logout') === 'true'; | |
$skipAutoLogin = $request->query->get('skip_auto_login') === 'true'; | |
if ($forceLogout || $skipAutoLogin) { | |
error_log('[DEBUG] MagicLogin: Force logout or skip auto-login requested'); | |
// セッションとトークンを完全にクリア | |
$this->tokenStorage->setToken(null); | |
$request->getSession()->invalidate(); | |
error_log('[DEBUG] MagicLogin: Session cleared, showing login form'); | |
return $this->render('magic_link_login/magic_login.html.twig'); | |
} | |
// ★★ 現在の認証状態をチェック ★★ | |
$currentUser = $this->getUser(); | |
$session = $request->getSession(); | |
error_log('[DEBUG] MagicLogin: Current user: ' . ($currentUser ? $currentUser->getEmail() : 'null')); | |
// ★★ セッションに認証情報がある場合の処理 ★★ | |
if ($currentUser) { | |
error_log('[DEBUG] MagicLogin: User already authenticated in session: ' . $currentUser->getEmail()); | |
// セッションフラグをチェック | |
$sessionSkipAutoLogin = $session->get('skip_auto_login', false); | |
if ($sessionSkipAutoLogin) { | |
error_log('[DEBUG] MagicLogin: Session skip_auto_login flag found, clearing session'); | |
$this->tokenStorage->setToken(null); | |
$session->invalidate(); | |
return $this->render('magic_link_login/magic_login.html.twig'); | |
} | |
// 通常の自動ログイン | |
error_log('[DEBUG] MagicLogin: Redirecting authenticated user to home'); | |
return $this->redirectToUserHome($currentUser); | |
} | |
// ★★ デバイス固有の認証チェック(セッションベース)★★ | |
$deviceAuthenticatedUserId = $session->get('device_authenticated_user_id'); | |
if ($deviceAuthenticatedUserId) { | |
error_log('[DEBUG] MagicLogin: Found device-specific authentication for user ID: ' . $deviceAuthenticatedUserId); | |
$user = $this->entityManager->getRepository(User::class)->find($deviceAuthenticatedUserId); | |
if ($user && $user->getLoginStatus() === 'verified') { | |
error_log('[DEBUG] MagicLogin: Device-specific user still verified, auto-login: ' . $user->getEmail()); | |
try { | |
// 認証トークンを作成 | |
$token = new UsernamePasswordToken($user, 'main', $user->getRoles()); | |
$this->tokenStorage->setToken($token); | |
// セッションにも保存 | |
$session->set('_security_main', serialize($token)); | |
return $this->redirectToUserHome($user); | |
} catch (\Exception $e) { | |
error_log('[ERROR] MagicLogin: Device-specific auto-login failed: ' . $e->getMessage()); | |
// 失敗した場合はデバイス認証情報をクリア | |
$session->remove('device_authenticated_user_id'); | |
} | |
} else { | |
error_log('[DEBUG] MagicLogin: Device-specific user no longer verified, clearing device auth'); | |
$session->remove('device_authenticated_user_id'); | |
} | |
} | |
// ★★ POSTリクエストの処理 ★★ | |
if ($request->isMethod('POST')) { | |
error_log('[DEBUG] MagicLogin: === POST REQUEST DETECTED ==='); | |
$email = $request->request->get('email'); | |
$password = $request->request->get('password'); | |
error_log('[DEBUG] MagicLogin: Processing POST request for email: ' . $email); | |
if (empty($email) || empty($password)) { | |
error_log('[DEBUG] MagicLogin: Missing email or password'); | |
return $this->render('magic_link_login/magic_login.html.twig', [ | |
'error' => 'メールアドレスとパスワードを入力してください。', | |
]); | |
} | |
$user = $this->entityManager | |
->getRepository(User::class) | |
->findOneBy(['email' => $email]); | |
error_log('[DEBUG] MagicLogin: User found: ' . ($user ? 'YES' : 'NO')); | |
if (!$user) { | |
error_log('[DEBUG] MagicLogin: User not found for email: ' . $email); | |
return $this->render('magic_link_login/magic_login.html.twig', [ | |
'error' => 'メールアドレスまたはパスワードが正しくありません。', | |
]); | |
} | |
$passwordValid = $this->passwordHasher->isPasswordValid($user, $password); | |
error_log('[DEBUG] MagicLogin: Password valid: ' . ($passwordValid ? 'YES' : 'NO')); | |
if (!$passwordValid) { | |
error_log('[DEBUG] MagicLogin: Invalid password for email: ' . $email); | |
return $this->render('magic_link_login/magic_login.html.twig', [ | |
'error' => 'メールアドレスまたはパスワードが正しくありません。', | |
]); | |
} | |
try { | |
// loginStatusを'pending'に設定 | |
error_log('[DEBUG] MagicLogin: Setting login status to pending'); | |
$user->setLoginStatus('pending'); | |
$this->entityManager->persist($user); | |
$this->entityManager->flush(); | |
error_log('[DEBUG] MagicLogin: Login status updated successfully'); | |
// メール送信 | |
error_log('[DEBUG] MagicLogin: Preparing email template'); | |
$emailTemplate = (new TemplatedEmail()) | |
->from(new Address('g1.project.system@gmail.com', 'g1_project_system')) | |
->to($user->getEmail()) | |
->subject('マジックリンクによるログイン') | |
->htmlTemplate('magic_link_login/email.html.twig'); | |
error_log('[DEBUG] MagicLogin: Sending email confirmation'); | |
$this->emailVerifier->sendEmailConfirmation('app_magic_confirm', $user, $emailTemplate); | |
error_log('[DEBUG] MagicLogin: Email sent successfully'); | |
// ★★ 待機画面にリダイレクト ★★ | |
error_log('[DEBUG] MagicLogin: Redirecting to waiting page with email: ' . $user->getEmail()); | |
return $this->redirectToRoute('app_magic_waiting', ['email' => $user->getEmail()]); | |
} catch (\Exception $e) { | |
error_log('[ERROR] MagicLogin: Exception during POST processing: ' . $e->getMessage()); | |
return $this->render('magic_link_login/magic_login.html.twig', [ | |
'error' => 'メール送信中にエラーが発生しました。もう一度お試しください。', | |
]); | |
} | |
} | |
error_log('[DEBUG] MagicLogin: No special conditions met, showing login form'); | |
return $this->render('magic_link_login/magic_login.html.twig'); | |
} | |
#[Route('/magic-waiting', name: 'app_magic_waiting')] | |
public function waiting(Request $request): Response | |
{ | |
error_log('[DEBUG] MagicWaiting: === WAITING PAGE ACCESSED ==='); | |
$email = $request->query->get('email'); | |
error_log('[DEBUG] MagicWaiting: Email parameter: ' . ($email ?? 'null')); | |
if (!$email) { | |
error_log('[DEBUG] MagicWaiting: No email provided, redirecting to login'); | |
return $this->redirectToRoute('app_magic_login'); | |
} | |
error_log('[DEBUG] MagicWaiting: Rendering waiting page for email: ' . $email); | |
return $this->render('magic_link_login/waiting.html.twig', [ | |
'email' => $email, | |
]); | |
} | |
#[Route('/magic-success', name: 'app_magic_success')] | |
public function success(): Response | |
{ | |
return $this->render('magic_link_login/success.html.twig'); | |
} | |
#[Route('/magic-confirm', name: 'app_magic_confirm')] | |
public function confirm(Request $request): Response | |
{ | |
$userId = $request->query->get('id'); | |
error_log('[DEBUG] MagicConfirm: Starting confirmation for user ID: ' . ($userId ?? 'null')); | |
if (!$userId) { | |
error_log('[ERROR] MagicConfirm: No user ID provided'); | |
return $this->redirectToRoute('app_magic_login'); | |
} | |
$user = $this->entityManager->getRepository(User::class)->find($userId); | |
if (!$user) { | |
error_log('[ERROR] MagicConfirm: User not found for ID: ' . $userId); | |
return $this->redirectToRoute('app_magic_login'); | |
} | |
try { | |
$this->emailVerifier->handleEmailConfirmation($request, $user); | |
error_log('[DEBUG] MagicConfirm: Email verified for user id=' . $user->getId()); | |
// loginStatusを'verified'に更新 | |
$user->setLoginStatus('verified'); | |
$this->entityManager->persist($user); | |
$this->entityManager->flush(); | |
error_log('[DEBUG] MagicConfirm: loginStatus set to verified for user id=' . $user->getId()); | |
// ★★ このデバイス固有の認証情報を保存 ★★ | |
$token = new UsernamePasswordToken($user, 'main', $user->getRoles()); | |
$this->tokenStorage->setToken($token); | |
$request->getSession()->set('_security_main', serialize($token)); | |
$request->getSession()->set('device_authenticated_user_id', $user->getId()); | |
error_log('[DEBUG] MagicConfirm: Device-specific authentication token created for user id=' . $user->getId()); | |
// ★★ 直接success.html.twigを表示(リダイレクトしない) ★★ | |
return $this->render('magic_link_login/success.html.twig'); | |
} catch (AuthenticationException $e) { | |
error_log('[ERROR] MagicConfirm: Email confirmation failed: ' . $e->getMessage()); | |
return $this->redirectToRoute('app_magic_login'); | |
} catch (\Exception $e) { | |
error_log('[ERROR] MagicConfirm: Unexpected error: ' . $e->getMessage()); | |
return $this->redirectToRoute('app_magic_login'); | |
} | |
} | |
private function redirectToUserHome($user): Response | |
{ | |
$roles = $user->getRoles(); | |
if (in_array('ROLE_DEVELOPER', $roles)) { | |
error_log('[DEBUG] MagicLogin: Redirecting to developer home for developer: ' . $user->getEmail()); | |
return $this->redirectToRoute('app_developer_home'); | |
} elseif (in_array('ROLE_ADMIN', $roles)) { | |
error_log('[DEBUG] MagicLogin: Redirecting to admin home for user: ' . $user->getEmail()); | |
return $this->redirectToRoute('app_admin_home'); | |
} elseif (in_array('ROLE_TEACHER', $roles)) { | |
error_log('[DEBUG] MagicLogin: Redirecting to teacher home for user: ' . $user->getEmail()); | |
return $this->redirectToRoute('app_teacher_home'); | |
} else { | |
error_log('[DEBUG] MagicLogin: Redirecting to student home for user: ' . $user->getEmail()); | |
return $this->redirectToRoute('app_student_home'); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
framework: | |
mailer: | |
dsn: '%env(MAILER_DSN)%' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
framework: | |
messenger: | |
failure_transport: failed | |
transports: | |
# https://symfony.com/doc/current/messenger.html#transport-configuration | |
async: | |
dsn: '%env(MESSENGER_TRANSPORT_DSN)%' | |
options: | |
use_notify: true | |
check_delayed_interval: 60000 | |
retry_strategy: | |
max_retries: 3 | |
multiplier: 2 | |
failed: 'doctrine://default?queue_name=failed' | |
# sync: 'sync://' | |
default_bus: messenger.bus.default | |
buses: | |
messenger.bus.default: [] | |
routing: | |
Symfony\Component\Mailer\Messenger\SendEmailMessage: async | |
Symfony\Component\Notifier\Message\ChatMessage: async | |
Symfony\Component\Notifier\Message\SmsMessage: async | |
# Route your messages to the transports | |
# 'App\Message\YourMessage': async |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
monolog: | |
channels: | |
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists | |
when@dev: | |
monolog: | |
handlers: | |
main: | |
type: stream | |
path: "%kernel.logs_dir%/%kernel.environment%.log" | |
level: debug | |
channels: ["!event"] | |
# uncomment to get logging in your browser | |
# you may have to allow bigger header sizes in your Web server configuration | |
#firephp: | |
# type: firephp | |
# level: info | |
#chromephp: | |
# type: chromephp | |
# level: info | |
console: | |
type: console | |
process_psr_3_messages: false | |
channels: ["!event", "!doctrine", "!console"] | |
when@test: | |
monolog: | |
handlers: | |
main: | |
type: fingers_crossed | |
action_level: error | |
handler: nested | |
excluded_http_codes: [404, 405] | |
channels: ["!event"] | |
nested: | |
type: stream | |
path: "%kernel.logs_dir%/%kernel.environment%.log" | |
level: debug | |
when@prod: | |
monolog: | |
handlers: | |
main: | |
type: fingers_crossed | |
action_level: error | |
handler: nested | |
excluded_http_codes: [404, 405] | |
buffer_size: 50 # How many messages should be saved? Prevent memory leaks | |
nested: | |
type: stream | |
path: php://stderr | |
level: debug | |
formatter: monolog.formatter.json | |
console: | |
type: console | |
process_psr_3_messages: false | |
channels: ["!event", "!doctrine"] | |
deprecation: | |
type: stream | |
channels: [deprecation] | |
path: php://stderr | |
formatter: monolog.formatter.json |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
framework: | |
notifier: | |
chatter_transports: | |
texter_transports: | |
channel_policy: | |
# use chat/slack, chat/telegram, sms/twilio or sms/nexmo | |
urgent: ['email'] | |
high: ['email'] | |
medium: ['email'] | |
low: ['email'] | |
admin_recipients: | |
- { email: admin@example.com } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
display_errors = Off | |
display_startup_errors = Off | |
log_errors = On | |
error_reporting = E_ALL & ~E_DEPRECATED & ~E_WARNING | |
error_log = /var/log/php_errors.log | |
zend.assertions = -1 | |
assert.warning = 0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{% extends 'base.html.twig' %} | |
{% block title %}Register{% endblock %} | |
{% block body %} | |
{% for flash_error in app.flashes('verify_email_error') %} | |
<div class="alert alert-danger" role="alert">{{ flash_error }}</div> | |
{% endfor %} | |
<h1>Register</h1> | |
{{ form_errors(registrationForm) }} | |
{{ form_start(registrationForm) }} | |
{{ form_row(registrationForm.email) }} | |
{{ form_row(registrationForm.plainPassword, { | |
label: 'Password' | |
}) }} | |
{{ form_row(registrationForm.agreeTerms) }} | |
<button type="submit" class="btn">Register</button> | |
{{ form_end(registrationForm) }} | |
{% endblock %} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Form; | |
use App\Entity\User; | |
use Symfony\Component\Form\AbstractType; | |
use Symfony\Component\Form\Extension\Core\Type\CheckboxType; | |
use Symfony\Component\Form\Extension\Core\Type\PasswordType; | |
use Symfony\Component\Form\FormBuilderInterface; | |
use Symfony\Component\OptionsResolver\OptionsResolver; | |
use Symfony\Component\Validator\Constraints\IsTrue; | |
use Symfony\Component\Validator\Constraints\Length; | |
use Symfony\Component\Validator\Constraints\NotBlank; | |
class RegistrationFormType extends AbstractType | |
{ | |
public function buildForm(FormBuilderInterface $builder, array $options): void | |
{ | |
$builder | |
->add('email') | |
->add('agreeTerms', CheckboxType::class, [ | |
'mapped' => false, | |
'constraints' => [ | |
new IsTrue([ | |
'message' => 'You should agree to our terms.', | |
]), | |
], | |
]) | |
->add('plainPassword', PasswordType::class, [ | |
// instead of being set onto the object directly, | |
// this is read and encoded in the controller | |
'mapped' => false, | |
'attr' => ['autocomplete' => 'new-password'], | |
'constraints' => [ | |
new NotBlank([ | |
'message' => 'Please enter a password', | |
]), | |
new Length([ | |
'min' => 6, | |
'minMessage' => 'Your password should be at least {{ limit }} characters', | |
// max length allowed by Symfony for security reasons | |
'max' => 4096, | |
]), | |
], | |
]) | |
; | |
} | |
public function configureOptions(OptionsResolver $resolver): void | |
{ | |
$resolver->setDefaults([ | |
'data_class' => User::class, | |
]); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="UTF-8"> | |
<title>マジックリンクログイン</title> | |
<script> | |
// 認証状態をポーリングでチェック | |
function checkAuthStatus() { | |
fetch('/check-auth') | |
.then(response => response.json()) | |
.then(data => { | |
if (data.authenticated) { | |
window.location.href = '/'; | |
} | |
}) | |
.catch(error => console.error('Auth check failed:', error)); | |
} | |
// 5秒ごとに認証状態をチェック | |
setInterval(checkAuthStatus, 5000); | |
</script> | |
</head> | |
<body> | |
<h1>マジックリンクログイン</h1> | |
{% for message in app.flashes('success') %} | |
<div style="color: green; margin-bottom: 10px;"> | |
{{ message }} | |
</div> | |
{% endfor %} | |
{% for message in app.flashes('error') %} | |
<div style="color: red; margin-bottom: 10px;"> | |
{{ message }} | |
</div> | |
{% endfor %} | |
<form method="post"> | |
<div> | |
<label for="email">メールアドレス:</label> | |
<input type="email" id="email" name="email" required> | |
</div> | |
<button type="submit">ログインリンクを送信</button> | |
</form> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Entity; | |
use App\Repository\RequestDataRepository; | |
use Doctrine\DBAL\Types\Types; | |
use Doctrine\ORM\Mapping as ORM; | |
#[ORM\Entity(repositoryClass: RequestDataRepository::class)] | |
#[ORM\Table(name: 'request_data')] | |
class RequestData | |
{ | |
#[ORM\Id] | |
#[ORM\GeneratedValue] | |
#[ORM\Column] | |
private ?int $id = null; | |
#[ORM\Column(length: 10)] | |
private ?string $className = null; | |
#[ORM\Column(length: 10)] | |
private ?string $day = null; | |
#[ORM\Column] | |
private ?int $period = null; | |
#[ORM\Column(length: 20)] | |
private ?string $subjectCode = null; | |
#[ORM\Column(length: 100)] | |
private ?string $subjectName = null; | |
#[ORM\Column(length: 100)] | |
private ?string $location = null; | |
#[ORM\Column(length: 20)] | |
private ?string $employmentType = null; | |
#[ORM\Column(length: 100, nullable: true)] | |
private ?string $teacher1 = null; | |
#[ORM\Column(length: 100, nullable: true)] | |
private ?string $teacher2 = null; | |
#[ORM\Column(length: 100, nullable: true)] | |
private ?string $teacher3 = null; | |
#[ORM\Column(length: 20, options: ['default' => 'pending'])] | |
private ?string $status = 'pending'; | |
#[ORM\Column(type: Types::TEXT, nullable: true)] | |
private ?string $requestReason = null; | |
#[ORM\Column(length: 100, nullable: true)] | |
private ?string $requestedBy = null; | |
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)] | |
private ?\DateTimeInterface $requestedAt = null; | |
#[ORM\Column(length: 100, nullable: true)] | |
private ?string $approvedBy = null; | |
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)] | |
private ?\DateTimeInterface $approvedAt = null; | |
#[ORM\Column(type: Types::TEXT, nullable: true)] | |
private ?string $approvalComment = null; | |
#[ORM\Column(type: Types::DATETIME_MUTABLE)] | |
private ?\DateTimeInterface $createdAt = null; | |
#[ORM\Column(type: Types::DATETIME_MUTABLE)] | |
private ?\DateTimeInterface $updatedAt = null; | |
public function __construct() | |
{ | |
$this->createdAt = new \DateTime(); | |
$this->updatedAt = new \DateTime(); | |
$this->status = 'pending'; | |
} | |
public function getId(): ?int | |
{ | |
return $this->id; | |
} | |
public function getClassName(): ?string | |
{ | |
return $this->className; | |
} | |
public function setClassName(string $className): static | |
{ | |
$this->className = $className; | |
return $this; | |
} | |
public function getDay(): ?string | |
{ | |
return $this->day; | |
} | |
public function setDay(string $day): static | |
{ | |
$this->day = $day; | |
return $this; | |
} | |
public function getPeriod(): ?int | |
{ | |
return $this->period; | |
} | |
public function setPeriod(int $period): static | |
{ | |
$this->period = $period; | |
return $this; | |
} | |
public function getSubjectCode(): ?string | |
{ | |
return $this->subjectCode; | |
} | |
public function setSubjectCode(string $subjectCode): static | |
{ | |
$this->subjectCode = $subjectCode; | |
return $this; | |
} | |
public function getSubjectName(): ?string | |
{ | |
return $this->subjectName; | |
} | |
public function setSubjectName(string $subjectName): static | |
{ | |
$this->subjectName = $subjectName; | |
return $this; | |
} | |
public function getLocation(): ?string | |
{ | |
return $this->location; | |
} | |
public function setLocation(string $location): static | |
{ | |
$this->location = $location; | |
return $this; | |
} | |
public function getEmploymentType(): ?string | |
{ | |
return $this->employmentType; | |
} | |
public function setEmploymentType(string $employmentType): static | |
{ | |
$this->employmentType = $employmentType; | |
return $this; | |
} | |
public function getTeacher1(): ?string | |
{ | |
return $this->teacher1; | |
} | |
public function setTeacher1(?string $teacher1): static | |
{ | |
$this->teacher1 = $teacher1; | |
return $this; | |
} | |
public function getTeacher2(): ?string | |
{ | |
return $this->teacher2; | |
} | |
public function setTeacher2(?string $teacher2): static | |
{ | |
$this->teacher2 = $teacher2; | |
return $this; | |
} | |
public function getTeacher3(): ?string | |
{ | |
return $this->teacher3; | |
} | |
public function setTeacher3(?string $teacher3): static | |
{ | |
$this->teacher3 = $teacher3; | |
return $this; | |
} | |
public function getStatus(): ?string | |
{ | |
return $this->status; | |
} | |
public function setStatus(string $status): static | |
{ | |
$this->status = $status; | |
return $this; | |
} | |
public function getRequestReason(): ?string | |
{ | |
return $this->requestReason; | |
} | |
public function setRequestReason(?string $requestReason): static | |
{ | |
$this->requestReason = $requestReason; | |
return $this; | |
} | |
public function getRequestedBy(): ?string | |
{ | |
return $this->requestedBy; | |
} | |
public function setRequestedBy(?string $requestedBy): static | |
{ | |
$this->requestedBy = $requestedBy; | |
return $this; | |
} | |
public function getRequestedAt(): ?\DateTimeInterface | |
{ | |
return $this->requestedAt; | |
} | |
public function setRequestedAt(?\DateTimeInterface $requestedAt): static | |
{ | |
$this->requestedAt = $requestedAt; | |
return $this; | |
} | |
public function getApprovedBy(): ?string | |
{ | |
return $this->approvedBy; | |
} | |
public function setApprovedBy(?string $approvedBy): static | |
{ | |
$this->approvedBy = $approvedBy; | |
return $this; | |
} | |
public function getApprovedAt(): ?\DateTimeInterface | |
{ | |
return $this->approvedAt; | |
} | |
public function setApprovedAt(?\DateTimeInterface $approvedAt): static | |
{ | |
$this->approvedAt = $approvedAt; | |
return $this; | |
} | |
public function getApprovalComment(): ?string | |
{ | |
return $this->approvalComment; | |
} | |
public function setApprovalComment(?string $approvalComment): static | |
{ | |
$this->approvalComment = $approvalComment; | |
return $this; | |
} | |
public function getCreatedAt(): ?\DateTimeInterface | |
{ | |
return $this->createdAt; | |
} | |
public function setCreatedAt(\DateTimeInterface $createdAt): static | |
{ | |
$this->createdAt = $createdAt; | |
return $this; | |
} | |
public function getUpdatedAt(): ?\DateTimeInterface | |
{ | |
return $this->updatedAt; | |
} | |
public function setUpdatedAt(\DateTimeInterface $updatedAt): static | |
{ | |
$this->updatedAt = $updatedAt; | |
return $this; | |
} | |
/** | |
* 全ての教員を配列で取得 | |
*/ | |
public function getAllTeachers(): array | |
{ | |
$teachers = []; | |
if ($this->teacher1) { | |
$teachers[] = $this->teacher1; | |
} | |
if ($this->teacher2) { | |
$teachers[] = $this->teacher2; | |
} | |
if ($this->teacher3) { | |
$teachers[] = $this->teacher3; | |
} | |
return $teachers; | |
} | |
/** | |
* 教員名を文字列で取得(カンマ区切り) | |
*/ | |
public function getTeachersAsString(): string | |
{ | |
return implode(', ', $this->getAllTeachers()); | |
} | |
/** | |
* 曜日の日本語名を取得 | |
*/ | |
public function getDayJapanese(): string | |
{ | |
return match($this->day) { | |
'mon' => '月', | |
'tue' => '火', | |
'wed' => '水', | |
'thu' => '木', | |
'fri' => '金', | |
default => $this->day | |
}; | |
} | |
/** | |
* ステータスの日本語名を取得 | |
*/ | |
public function getStatusJapanese(): string | |
{ | |
return match($this->status) { | |
'pending' => '承認待ち', | |
'approved' => '承認済み', | |
'rejected' => '却下', | |
'cancelled' => 'キャンセル', | |
default => $this->status | |
}; | |
} | |
/** | |
* 承認処理 | |
*/ | |
public function approve(string $approvedBy, ?string $comment = null): static | |
{ | |
$this->status = 'approved'; | |
$this->approvedBy = $approvedBy; | |
$this->approvedAt = new \DateTime(); | |
$this->approvalComment = $comment; | |
$this->updatedAt = new \DateTime(); | |
return $this; | |
} | |
/** | |
* 却下処理 | |
*/ | |
public function reject(string $rejectedBy, ?string $comment = null): static | |
{ | |
$this->status = 'rejected'; | |
$this->approvedBy = $rejectedBy; | |
$this->approvedAt = new \DateTime(); | |
$this->approvalComment = $comment; | |
$this->updatedAt = new \DateTime(); | |
return $this; | |
} | |
/** | |
* キャンセル処理 | |
*/ | |
public function cancel(): static | |
{ | |
$this->status = 'cancelled'; | |
$this->updatedAt = new \DateTime(); | |
return $this; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Repository; | |
use App\Entity\RequestData; | |
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; | |
use Doctrine\Persistence\ManagerRegistry; | |
/** | |
* @extends ServiceEntityRepository<RequestData> | |
*/ | |
class RequestDataRepository extends ServiceEntityRepository | |
{ | |
public function __construct(ManagerRegistry $registry) | |
{ | |
parent::__construct($registry, RequestData::class); | |
} | |
/** | |
* ステータスで検索 | |
*/ | |
public function findByStatus(string $status): array | |
{ | |
return $this->createQueryBuilder('r') | |
->andWhere('r.status = :status') | |
->setParameter('status', $status) | |
->orderBy('r.createdAt', 'DESC') | |
->getQuery() | |
->getResult(); | |
} | |
/** | |
* クラス名で検索 | |
*/ | |
public function findByClassName(string $className): array | |
{ | |
return $this->createQueryBuilder('r') | |
->andWhere('r.className = :className') | |
->setParameter('className', $className) | |
->orderBy('r.createdAt', 'DESC') | |
->getQuery() | |
->getResult(); | |
} | |
/** | |
* 申請者で検索 | |
*/ | |
public function findByRequestedBy(string $requestedBy): array | |
{ | |
return $this->createQueryBuilder('r') | |
->andWhere('r.requestedBy = :requestedBy') | |
->setParameter('requestedBy', $requestedBy) | |
->orderBy('r.createdAt', 'DESC') | |
->getQuery() | |
->getResult(); | |
} | |
/** | |
* 承認待ちの申請を取得 | |
*/ | |
public function findPendingRequests(): array | |
{ | |
return $this->findByStatus('pending'); | |
} | |
/** | |
* 承認済みの申請を取得 | |
*/ | |
public function findApprovedRequests(): array | |
{ | |
return $this->findByStatus('approved'); | |
} | |
/** | |
* 却下された申請を取得 | |
*/ | |
public function findRejectedRequests(): array | |
{ | |
return $this->findByStatus('rejected'); | |
} | |
/** | |
* 曜日と時限で検索 | |
*/ | |
public function findByDayAndPeriod(string $day, int $period): array | |
{ | |
return $this->createQueryBuilder('r') | |
->andWhere('r.day = :day') | |
->andWhere('r.period = :period') | |
->setParameter('day', $day) | |
->setParameter('period', $period) | |
->orderBy('r.createdAt', 'DESC') | |
->getQuery() | |
->getResult(); | |
} | |
/** | |
* 教員名で検索 | |
*/ | |
public function findByTeacher(string $teacherName): array | |
{ | |
return $this->createQueryBuilder('r') | |
->andWhere('r.teacher1 = :teacher OR r.teacher2 = :teacher OR r.teacher3 = :teacher') | |
->setParameter('teacher', $teacherName) | |
->orderBy('r.createdAt', 'DESC') | |
->getQuery() | |
->getResult(); | |
} | |
/** | |
* 期間で検索 | |
*/ | |
public function findByDateRange(\DateTimeInterface $startDate, \DateTimeInterface $endDate): array | |
{ | |
return $this->createQueryBuilder('r') | |
->andWhere('r.createdAt >= :startDate') | |
->andWhere('r.createdAt <= :endDate') | |
->setParameter('startDate', $startDate) | |
->setParameter('endDate', $endDate) | |
->orderBy('r.createdAt', 'DESC') | |
->getQuery() | |
->getResult(); | |
} | |
/** | |
* 最新の申請を取得 | |
*/ | |
public function findLatest(int $limit = 10): array | |
{ | |
return $this->createQueryBuilder('r') | |
->orderBy('r.createdAt', 'DESC') | |
->setMaxResults($limit) | |
->getQuery() | |
->getResult(); | |
} | |
/** | |
* ステータス別の件数を取得 | |
*/ | |
public function getStatusCounts(): array | |
{ | |
$result = $this->createQueryBuilder('r') | |
->select('r.status, COUNT(r.id) as count') | |
->groupBy('r.status') | |
->getQuery() | |
->getResult(); | |
$counts = [ | |
'pending' => 0, | |
'approved' => 0, | |
'rejected' => 0, | |
'cancelled' => 0 | |
]; | |
foreach ($result as $row) { | |
$counts[$row['status']] = (int)$row['count']; | |
} | |
return $counts; | |
} | |
/** | |
* 全クラス名を取得 | |
*/ | |
public function findAllClassNames(): array | |
{ | |
$result = $this->createQueryBuilder('r') | |
->select('DISTINCT r.className') | |
->orderBy('r.className', 'ASC') | |
->getQuery() | |
->getResult(); | |
return array_column($result, 'className'); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="UTF-8"> | |
<title>Magic Link Login Result</title> | |
<style> | |
body { | |
font-family: Arial, sans-serif; | |
max-width: 600px; | |
margin: 50px auto; | |
padding: 20px; | |
text-align: center; | |
} | |
.success { | |
color: #28a745; | |
background-color: #d4edda; | |
border: 1px solid #c3e6cb; | |
padding: 15px; | |
border-radius: 5px; | |
margin: 20px 0; | |
} | |
.error { | |
color: #dc3545; | |
background-color: #f8d7da; | |
border: 1px solid #f5c6cb; | |
padding: 15px; | |
border-radius: 5px; | |
margin: 20px 0; | |
} | |
</style> | |
</head> | |
<body> | |
<h1>Magic Link Authentication</h1> | |
{% if error is defined %} | |
<div class="error"> | |
<h2>❌ エラー</h2> | |
<p>{{ error }}</p> | |
</div> | |
{% else %} | |
<div class="success"> | |
<h2>✅ ログイン成功!</h2> | |
<p>認証が完了しました。ホーム画面にリダイレクトします...</p> | |
</div> | |
<script> | |
// 3秒後にホーム画面にリダイレクト | |
setTimeout(function() { | |
window.location.href = '/'; | |
}, 3000); | |
</script> | |
{% endif %} | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
controllers: | |
resource: ../src/Controller/ | |
type: attribute | |
app_magic_login: | |
path: /magic-login | |
controller: App\Controller\MagicLoginController::login | |
methods: [GET, POST] | |
app_magic_confirm: | |
path: /magic-confirm | |
controller: App\Controller\MagicLoginController::confirm | |
methods: [GET] | |
app_check_auth: | |
path: /check-auth | |
controller: App\Controller\AuthCheckController::checkAuth | |
methods: [GET] | |
app_add_user: | |
path: /addUser | |
controller: App\Controller\AddUserController::addUser | |
methods: [GET, POST] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
framework: | |
router: | |
utf8: true | |
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands. | |
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands | |
#default_uri: http://localhost | |
when@prod: | |
framework: | |
router: | |
strict_requirements: null |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
security: | |
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords | |
password_hashers: | |
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' | |
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider | |
providers: | |
# used to reload user from session & other features (e.g. switch_user) | |
app_user_provider: | |
entity: | |
class: App\Entity\User | |
property: email | |
firewalls: | |
dev: | |
pattern: ^/(_(profiler|wdt)|css|images|js)/ | |
security: false | |
main: | |
lazy: true | |
provider: app_user_provider | |
custom_authenticator: App\Security\LoginFormAuthenticator | |
logout: | |
path: app_symfony_logout | |
# where to redirect after logout | |
target: app_magic_login | |
# activate different ways to authenticate | |
# https://symfony.com/doc/current/security.html#the-firewall | |
# https://symfony.com/doc/current/security/impersonating_user.html | |
# switch_user: true | |
# Easy way to control access for large sections of your site | |
# Note: Only the *first* access control that matches will be used | |
access_control: | |
- { path: ^/magic-login, roles: PUBLIC_ACCESS } | |
- { path: ^/magic-waiting, roles: PUBLIC_ACCESS } # ★★ 待機画面も認証不要に ★★ | |
- { path: ^/magic-confirm, roles: PUBLIC_ACCESS } # ★★ 確認画面も認証不要に ★★ | |
- { path: ^/magic-success, roles: PUBLIC_ACCESS } # ★★ 成功画面も認証不要に ★★ | |
- { path: ^/auth-check, roles: PUBLIC_ACCESS } # ★★ 認証チェックも認証不要に ★★ | |
- { path: ^/register, roles: [ROLE_STUDENT, ROLE_TEACHER, ROLE_ADMIN, ROLE_DEVELOPER] } | |
- { path: ^/admin, roles: [ROLE_ADMIN, ROLE_DEVELOPER] } | |
- { path: ^/teacher, roles: [ROLE_TEACHER, ROLE_DEVELOPER] } | |
- { path: ^/student, roles: [ROLE_STUDENT, ROLE_DEVELOPER] } | |
- { path: ^/developer, roles: ROLE_DEVELOPER } | |
- { path: ^/database, roles: [ROLE_ADMIN, ROLE_DEVELOPER] } | |
- { path: ^/profile, roles: [ROLE_STUDENT, ROLE_TEACHER, ROLE_ADMIN, ROLE_DEVELOPER] } | |
- { path: ^/home, roles: [ROLE_STUDENT, ROLE_TEACHER, ROLE_ADMIN, ROLE_DEVELOPER] } | |
- { path: ^/user-logout, roles: [ROLE_STUDENT, ROLE_TEACHER, ROLE_ADMIN, ROLE_DEVELOPER] } | |
- { path: ^/, roles: [ROLE_STUDENT, ROLE_TEACHER, ROLE_ADMIN, ROLE_DEVELOPER] } | |
when@test: | |
security: | |
password_hashers: | |
# By default, password hashers are resource intensive and take time. This is | |
# important to generate secure password hashes. In tests however, secure hashes | |
# are not important, waste resources and increase test times. The following | |
# reduces the work factor to the lowest possible values. | |
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: | |
algorithm: auto | |
cost: 4 # Lowest possible value for bcrypt | |
time_cost: 3 # Lowest possible value for argon | |
memory_cost: 10 # Lowest possible value for argon |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use App\Entity\User; | |
use Symfony\Bridge\Twig\Mime\TemplatedEmail; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\Request; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Mime\Address; | |
use Symfony\Component\Routing\Annotation\Route; | |
use Symfony\Component\Mailer\MailerInterface; | |
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; | |
use Symfony\Component\Uid\Uuid; | |
use Doctrine\ORM\EntityManagerInterface; | |
class SecurityController extends AbstractController | |
{ | |
#[Route('/magic-login-request', name: 'app_magic_login_request', methods: ['GET', 'POST'])] | |
public function requestMagicLink( | |
Request $request, | |
EntityManagerInterface $em, | |
MailerInterface $mailer, | |
UrlGeneratorInterface $urlGenerator | |
): Response { | |
if ($request->isMethod('POST')) { | |
$email = $request->request->get('email'); | |
$user = $em->getRepository(User::class)->findOneBy(['email' => $email]); | |
if ($user) { | |
$token = Uuid::v4()->toRfc4122(); | |
$expiresAt = new \DateTime('+15 minutes'); | |
$user->setMagicToken($token); | |
$user->setMagicTokenExpiresAt($expiresAt); | |
$em->flush(); | |
$magicLink = $urlGenerator->generate('app_magic_login', [ | |
'token' => $token, | |
], UrlGeneratorInterface::ABSOLUTE_URL); | |
$emailMessage = (new TemplatedEmail()) | |
->from(new Address('g1.project.system@gmail.com', 'g1_時間割変更システム')) | |
->to($user->getEmail()) | |
->subject('ログイン用リンクのお知らせ') | |
->htmlTemplate('security/magic_login_email.html.twig') | |
->context([ | |
'magicLink' => $magicLink, | |
'expiresAt' => $expiresAt, | |
]); | |
$mailer->send($emailMessage); | |
} | |
$this->addFlash('success', 'もしメールアドレスが正しければ、ログインリンクを送信しました。'); | |
return $this->redirectToRoute('app_magic_login_request'); | |
} | |
return $this->render('security/magic_login_request.html.twig'); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{% extends 'base.html.twig' %} | |
{% block title %}リンク送信完了{% endblock %} | |
{% block body %} | |
<h1>ログインリンクを送信しました!</h1> | |
<p>メールをご確認ください。</p> | |
{% endblock %} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
parameters: | |
app.base_url: '%env(APP_BASE_URL)%' | |
services: | |
_defaults: | |
autowire: true | |
autoconfigure: true | |
App\: | |
resource: '../src/' | |
exclude: | |
- '../src/DependencyInjection/' | |
- '../src/Entity/' | |
- '../src/Kernel.php' | |
# security設定は削除(security.yamlで管理するため) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# カラーコードの定義 | |
RED='\033[0;31m' | |
GREEN='\033[0;32m' | |
YELLOW='\033[1;33m' | |
BLUE='\033[0;34m' | |
PURPLE='\033[0;35m' | |
CYAN='\033[0;36m' | |
WHITE='\033[1;37m' | |
BOLD='\033[1m' | |
NC='\033[0m' # No Color (リセット) | |
echo "=========================================" | |
echo " G1 Project Development Environment Setup" | |
echo "=========================================" | |
echo "" | |
# Docker Desktop起動確認 | |
echo -e "${BOLD}${RED}Please OPEN Docker Desktop${NC}" | |
echo "" | |
# ユーザーの確認を待つ | |
while true; do | |
read -p "Type 'OK' when ready: " DOCKER_CONFIRM | |
if [ "$DOCKER_CONFIRM" = "OK" ] || [ "$DOCKER_CONFIRM" = "ok" ]; then | |
echo "" | |
break | |
else | |
echo -e "${RED}Please type 'OK' to continue${NC}" | |
fi | |
done | |
# ブランチ名の入力を求める | |
read -p "Enter branch name (default: main): " BRANCH_NAME | |
BRANCH_NAME=${BRANCH_NAME:-main} | |
echo "" | |
echo -e "${GREEN}Starting development environment setup...${NC}" | |
echo -e "${CYAN}Branch: $BRANCH_NAME${NC}" | |
echo "" | |
# g1_projectディレクトリに移動 | |
echo -e "${BLUE}Moving to project directory...${NC}" | |
cd ~/g1_project | |
# Gitの操作 | |
echo -e "${BLUE}Updating Git repository...${NC}" | |
echo -e "${WHITE} - Checking out branch: $BRANCH_NAME${NC}" | |
git checkout $BRANCH_NAME | |
echo -e "${WHITE} - Pulling latest changes...${NC}" | |
git pull origin $BRANCH_NAME | |
# Docker環境の構築 | |
echo -e "${BLUE}Building Docker environment...${NC}" | |
docker compose up -d --build | |
# 権限の設定 | |
echo -e "${BLUE}Setting up permissions...${NC}" | |
echo -e "${WHITE} - Setting user permissions...${NC}" | |
sudo chown -R $USER:$USER ~/g1_project/symfony | |
echo -e "${WHITE} - Setting web server permissions...${NC}" | |
docker compose exec --user root php chown -R www-data:www-data /var/www/symfony/var | |
echo "" | |
echo -e "${GREEN}✅ Docker environment setup completed!${NC}" | |
echo "" | |
# ★★ スクリプト内でエイリアスを定義して使用 ★★ | |
echo -e "${WHITE} - Setting up dc command for this session...${NC}" | |
shopt -s expand_aliases # エイリアスを有効化 | |
alias dc='docker compose exec php php bin/console' | |
echo -e "${WHITE} - Clearing cache...${NC}" | |
docker compose exec php php bin/console cache:clear | |
echo "" | |
echo -e "${GREEN}✅ Development environment setup completed!${NC}" | |
echo "" | |
echo -e "${BLUE}Opening VS Code...${NC}" | |
code . | |
# ★★ dcエイリアスを.bashrcに追加(永続化) ★★ | |
echo "" | |
echo -e "${BLUE}Setting up dc command alias for future sessions...${NC}" | |
if ! grep -q "alias dc=" ~/.bashrc; then | |
echo "" >> ~/.bashrc | |
echo "# G1 Project Docker Compose alias" >> ~/.bashrc | |
echo "alias dc='docker compose exec php php bin/console'" >> ~/.bashrc | |
echo -e "${GREEN}✅ dc command alias added to .bashrc${NC}" | |
echo -e "${YELLOW}Note: dc command will be available in new terminal sessions${NC}" | |
else | |
echo -e "${YELLOW}dc command alias already exists in .bashrc${NC}" | |
fi | |
echo "" | |
echo "=========================================" | |
echo -e "${BOLD} Setup Summary${NC}" | |
echo "=========================================" | |
echo -e "${GREEN}✅ Git repository updated (branch: $BRANCH_NAME)${NC}" | |
echo -e "${GREEN}✅ Docker containers built and started${NC}" | |
echo -e "${GREEN}✅ Permissions configured${NC}" | |
echo -e "${GREEN}✅ Cache cleared${NC}" | |
echo -e "${GREEN}✅ VS Code opened${NC}" | |
echo -e "${GREEN}✅ dc command alias configured${NC}" | |
echo "" | |
echo -e "${BOLD}${GREEN}Setup Completed! Ready to use!${NC}" | |
echo "=========================================" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>時間割詳細 - {{ timeTable.className }}</title> | |
<!-- Bootstrap CSS --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"> | |
<!-- Font Awesome --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<style> | |
body { | |
background-color: #f4c2a1; | |
min-height: 100vh; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
} | |
.header { | |
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%); | |
color: white; | |
padding: 15px 40px; | |
font-size: 1.6rem; | |
font-weight: bold; | |
} | |
.main-container { | |
padding: 20px; | |
max-width: 1000px; | |
margin: 0 auto; | |
} | |
.timetable-container { | |
background: white; | |
border-radius: 8px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
overflow: hidden; | |
} | |
.timetable-header { | |
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%); | |
color: white; | |
padding: 20px; | |
text-align: center; | |
} | |
.class-info { | |
background: #f8f9fa; | |
padding: 20px; | |
border-bottom: 1px solid #dee2e6; | |
} | |
.info-item { | |
display: inline-block; | |
margin-right: 30px; | |
margin-bottom: 10px; | |
} | |
.info-label { | |
font-weight: bold; | |
color: #495057; | |
} | |
.info-value { | |
color: #dc3545; | |
font-weight: bold; | |
} | |
.schedule-table { | |
width: 100%; | |
border-collapse: collapse; | |
margin: 20px 0; | |
} | |
.schedule-table th { | |
background: #dc3545; | |
color: white; | |
padding: 15px 10px; | |
text-align: center; | |
font-weight: bold; | |
border: 1px solid #c82333; | |
} | |
.schedule-table td { | |
padding: 12px 8px; | |
text-align: center; | |
border: 1px solid #dee2e6; | |
vertical-align: middle; | |
min-height: 50px; | |
} | |
.period-header { | |
background: #f8f9fa; | |
font-weight: bold; | |
color: #495057; | |
width: 80px; | |
} | |
.subject-cell { | |
background: #fff; | |
font-size: 0.9rem; | |
position: relative; | |
} | |
.subject-cell:hover { | |
background: #f8f9fa; | |
} | |
.empty-cell { | |
color: #6c757d; | |
font-style: italic; | |
background: #f8f9fa; | |
} | |
.action-buttons { | |
padding: 20px; | |
text-align: center; | |
border-top: 1px solid #dee2e6; | |
} | |
.btn-action { | |
padding: 12px 24px; | |
border: none; | |
border-radius: 4px; | |
font-size: 1rem; | |
font-weight: bold; | |
text-decoration: none; | |
display: inline-block; | |
margin: 0 10px; | |
transition: all 0.3s ease; | |
} | |
.btn-edit { | |
background-color: #ffc107; | |
color: #212529; | |
} | |
.btn-edit:hover { | |
background-color: #e0a800; | |
color: #212529; | |
text-decoration: none; | |
transform: translateY(-1px); | |
} | |
.btn-back { | |
background-color: #6c757d; | |
color: white; | |
} | |
.btn-back:hover { | |
background-color: #5a6268; | |
color: white; | |
text-decoration: none; | |
transform: translateY(-1px); | |
} | |
@media (max-width: 768px) { | |
.header { | |
padding: 10px 15px; | |
font-size: 1.3rem; | |
} | |
.main-container { | |
padding: 10px; | |
} | |
.schedule-table { | |
font-size: 0.8rem; | |
} | |
.schedule-table th, | |
.schedule-table td { | |
padding: 8px 4px; | |
} | |
.info-item { | |
display: block; | |
margin-right: 0; | |
margin-bottom: 10px; | |
} | |
.btn-action { | |
display: block; | |
margin: 10px auto; | |
width: 200px; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="header"> | |
<i class="fas fa-calendar-alt"></i> 時間割詳細 | |
</div> | |
<div class="main-container"> | |
<div class="timetable-container"> | |
<div class="timetable-header"> | |
<h1><i class="fas fa-graduation-cap"></i> {{ timeTable.className }}</h1> | |
<p>時間割詳細表示</p> | |
</div> | |
<div class="class-info"> | |
<div class="info-item"> | |
<span class="info-label">クラス名:</span> | |
<span class="info-value">{{ timeTable.className }}</span> | |
</div> | |
<div class="info-item"> | |
<span class="info-label">曜日設定:</span> | |
<span class="info-value">{{ timeTable.day }}</span> | |
</div> | |
<div class="info-item"> | |
<span class="info-label">時限数:</span> | |
<span class="info-value">{{ timeTable.period }}時限</span> | |
</div> | |
</div> | |
<div style="padding: 20px;"> | |
<table class="schedule-table"> | |
<thead> | |
<tr> | |
<th>時限</th> | |
<th>月曜日</th> | |
<th>火曜日</th> | |
<th>水曜日</th> | |
<th>木曜日</th> | |
<th>金曜日</th> | |
</tr> | |
</thead> | |
<tbody> | |
<tr> | |
<td class="period-header">1時限</td> | |
<td class="subject-cell {{ timeTable.mon1 ? '' : 'empty-cell' }}"> | |
{{ timeTable.mon1 ?: '---' }} | |
</td> | |
<td class="subject-cell {{ timeTable.tue1 ? '' : 'empty-cell' }}"> | |
{{ timeTable.tue1 ?: '---' }} | |
</td> | |
<td class="subject-cell {{ timeTable.wed1 ? '' : 'empty-cell' }}"> | |
{{ timeTable.wed1 ?: '---' }} | |
</td> | |
<td class="subject-cell {{ timeTable.thu1 ? '' : 'empty-cell' }}"> | |
{{ timeTable.thu1 ?: '---' }} | |
</td> | |
<td class="subject-cell {{ timeTable.fri1 ? '' : 'empty-cell' }}"> | |
{{ timeTable.fri1 ?: '---' }} | |
</td> | |
</tr> | |
<tr> | |
<td class="period-header">2時限</td> | |
<td class="subject-cell {{ timeTable.mon2 ? '' : 'empty-cell' }}"> | |
{{ timeTable.mon2 ?: '---' }} | |
</td> | |
<td class="subject-cell {{ timeTable.tue2 ? '' : 'empty-cell' }}"> | |
{{ timeTable.tue2 ?: '---' }} | |
</td> | |
<td class="subject-cell {{ timeTable.wed2 ? '' : 'empty-cell' }}"> | |
{{ timeTable.wed2 ?: '---' }} | |
</td> | |
<td class="subject-cell {{ timeTable.thu2 ? '' : 'empty-cell' }}"> | |
{{ timeTable.thu2 ?: '---' }} | |
</td> | |
<td class="subject-cell {{ timeTable.fri2 ? '' : 'empty-cell' }}"> | |
{{ timeTable.fri2 ?: '---' }} | |
</td> | |
</tr> | |
<tr> | |
<td class="period-header">3時限</td> | |
<td class="subject-cell {{ timeTable.mon3 ? '' : 'empty-cell' }}"> | |
{{ timeTable.mon3 ?: '---' }} | |
</td> | |
<td class="subject-cell {{ timeTable.tue3 ? '' : 'empty-cell' }}"> | |
{{ timeTable.tue3 ?: '---' }} | |
</td> | |
<td class="subject-cell {{ timeTable.wed3 ? '' : 'empty-cell' }}"> | |
{{ timeTable.wed3 ?: '---' }} | |
</td> | |
<td class="subject-cell {{ timeTable.thu3 ? '' : 'empty-cell' }}"> | |
{{ timeTable.thu3 ?: '---' }} | |
</td> | |
<td class="subject-cell {{ timeTable.fri3 ? '' : 'empty-cell' }}"> | |
{{ timeTable.fri3 ?: '---' }} | |
</td> | |
</tr> | |
<tr> | |
<td class="period-header">4時限</td> | |
<td class="subject-cell {{ timeTable.mon4 ? '' : 'empty-cell' }}"> | |
{{ timeTable.mon4 ?: '---' }} | |
</td> | |
<td class="subject-cell {{ timeTable.tue4 ? '' : 'empty-cell' }}"> | |
{{ timeTable.tue4 ?: '---' }} | |
</td> | |
<td class="subject-cell {{ timeTable.wed4 ? '' : 'empty-cell' }}"> | |
{{ timeTable.wed4 ?: '---' }} | |
</td> | |
<td class="subject-cell {{ timeTable.thu4 ? '' : 'empty-cell' }}"> | |
{{ timeTable.thu4 ?: '---' }} | |
</td> | |
<td class="subject-cell {{ timeTable.fri4 ? '' : 'empty-cell' }}"> | |
{{ timeTable.fri4 ?: '---' }} | |
</td> | |
</tr> | |
</tbody> | |
</table> | |
</div> | |
<div class="action-buttons"> | |
<a href="{{ path('app_admin_timetable_edit', {'className': timeTable.className}) }}" class="btn-action btn-edit"> | |
<i class="fas fa-edit"></i> 編集 | |
</a> | |
<a href="{{ path('app_admin_timetable') }}" class="btn-action btn-back"> | |
<i class="fas fa-arrow-left"></i> 一覧に戻る | |
</a> | |
</div> | |
</div> | |
</div> | |
<!-- Bootstrap JS --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>{% block title %}学生ホーム - 時間割管理システム{% endblock %}</title> | |
<!-- Bootstrap CSS --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"> | |
<!-- Font Awesome --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<style> | |
body { | |
margin: 0; | |
padding: 0; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
background-color: #f4c2a1; | |
} | |
.header { | |
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); | |
color: white; | |
padding: 15px 40px; | |
font-size: 1.6rem; | |
font-weight: bold; | |
position: relative; | |
} | |
.main-container { | |
padding: 20px; | |
display: flex; | |
gap: 20px; | |
height: calc(100vh - 70px); | |
overflow: hidden; | |
} | |
.timetable-section { | |
flex: 1; | |
} | |
.timetable-container { | |
background: white; | |
border-radius: 8px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
overflow: hidden; | |
} | |
.timetable { | |
width: 100%; | |
border-collapse: collapse; | |
font-size: 0.9rem; | |
} | |
.timetable th { | |
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); | |
color: white; | |
padding: 10px 6px; | |
text-align: center; | |
font-weight: bold; | |
font-size: 0.9rem; | |
} | |
.timetable td { | |
padding: 8px 6px; | |
text-align: center; | |
border: 1px solid #ddd; | |
background: #f8f9fa; | |
font-weight: 500; | |
vertical-align: middle; | |
font-size: 0.85rem; | |
} | |
.timetable td.time-cell { | |
background: #e8e8e8; | |
font-weight: bold; | |
color: #555; | |
font-size: 0.7rem; | |
width: 100px; | |
} | |
.timetable td.subject { | |
background: white; | |
cursor: pointer; | |
transition: background-color 0.2s ease; | |
} | |
.timetable td.subject:hover { | |
background: #e3f2fd; | |
} | |
.timetable td.empty { | |
color: #999; | |
font-size: 1.2rem; | |
} | |
.controls-section { | |
width: 250px; | |
display: flex; | |
flex-direction: column; | |
gap: 10px; | |
} | |
.control-group { | |
background: white; | |
padding: 15px; | |
border-radius: 8px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
} | |
.selector-container { | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
gap: 20px; | |
margin: 15px 0; | |
} | |
.selector-group { | |
display: flex; | |
align-items: center; | |
gap: 10px; | |
} | |
.custom-select { | |
position: relative; | |
display: inline-block; | |
} | |
.select-button { | |
background: white; | |
border: 2px solid #ddd; | |
padding: 6px 35px 6px 12px; | |
font-size: 1rem; | |
font-weight: bold; | |
cursor: pointer; | |
border-radius: 4px; | |
min-width: 70px; | |
text-align: center; | |
position: relative; | |
} | |
.select-button::after { | |
content: ''; | |
position: absolute; | |
right: 10px; | |
top: 50%; | |
transform: translateY(-50%); | |
width: 0; | |
height: 0; | |
border-left: 6px solid transparent; | |
border-right: 6px solid transparent; | |
border-top: 8px solid #4a90e2; | |
} | |
.select-dropdown { | |
position: absolute; | |
bottom: 100%; | |
left: 0; | |
right: 0; | |
background: white; | |
border: 2px solid #ddd; | |
border-bottom: none; | |
border-radius: 4px 4px 0 0; | |
z-index: 1000; | |
display: none; | |
} | |
.select-option { | |
padding: 8px 12px; | |
cursor: pointer; | |
border-bottom: 1px solid #eee; | |
font-weight: bold; | |
font-size: 0.9rem; | |
} | |
.select-option:hover { | |
background: #f0f8ff; | |
} | |
.select-option:last-child { | |
border-bottom: none; | |
} | |
.week-info { | |
text-align: center; | |
font-size: 1rem; | |
font-weight: bold; | |
color: #333; | |
margin: 15px 0 10px 0; | |
padding: 8px; | |
background: white; | |
border-radius: 4px; | |
border: 2px solid #ddd; | |
} | |
.action-buttons { | |
display: flex; | |
flex-direction: column; | |
gap: 10px; | |
} | |
.action-btn { | |
background: #f1c40f; | |
color: #333; | |
border: none; | |
padding: 12px 15px; | |
font-size: 0.9rem; | |
font-weight: bold; | |
border-radius: 4px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
text-align: center; | |
} | |
.action-btn:hover { | |
background: #d4ac0d; | |
transform: translateY(-1px); | |
} | |
@media (max-width: 1200px) { | |
.main-container { | |
flex-direction: column; | |
padding: 20px; | |
} | |
.controls-section { | |
width: 100%; | |
order: -1; | |
} | |
.selector-container { | |
flex-direction: row; | |
justify-content: space-around; | |
flex-wrap: wrap; | |
} | |
.action-buttons { | |
flex-direction: row; | |
flex-wrap: wrap; | |
} | |
.action-btn { | |
flex: 1; | |
min-width: 200px; | |
} | |
} | |
@media (max-width: 768px) { | |
.header { | |
padding: 10px 15px; | |
font-size: 1.3rem; | |
} | |
.main-container { | |
padding: 10px; | |
height: calc(100vh - 60px); | |
} | |
.timetable { | |
font-size: 0.7rem; | |
} | |
.timetable th, | |
.timetable td { | |
padding: 6px 3px; | |
} | |
.selector-container { | |
flex-direction: column; | |
gap: 10px; | |
} | |
.action-buttons { | |
flex-direction: column; | |
} | |
.action-btn { | |
min-width: auto; | |
font-size: 0.8rem; | |
padding: 10px 12px; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="header"> | |
中村生徒さん、こんにちは! | |
</div> | |
<div class="main-container"> | |
<div class="timetable-section"> | |
<div class="timetable-container"> | |
<table class="timetable"> | |
<thead> | |
<tr> | |
<th class="time-header"></th> | |
<th>月<br>5月20日</th> | |
<th>火<br>5月21日</th> | |
<th>水<br>5月22日</th> | |
<th>木<br>5月23日</th> | |
<th>金<br>5月24日</th> | |
</tr> | |
</thead> | |
<tbody> | |
<tr> | |
<td class="time-cell">○○:○○<br>~<br>○○:○○</td> | |
<td class="subject">英語I</td> | |
<td class="subject">数学I</td> | |
<td class="subject">数学I</td> | |
<td class="subject">数学I</td> | |
<td class="empty">-</td> | |
</tr> | |
<tr> | |
<td class="time-cell">○○:○○<br>~<br>○○:○○</td> | |
<td class="subject">英語I</td> | |
<td class="empty">-</td> | |
<td class="subject">数学I</td> | |
<td class="subject">数学I</td> | |
<td class="subject">数学I</td> | |
</tr> | |
<tr> | |
<td class="time-cell">○○:○○<br>~<br>○○:○○</td> | |
<td class="subject">数学I</td> | |
<td class="subject">数学I</td> | |
<td class="subject">英語I</td> | |
<td class="empty">-</td> | |
<td class="subject">英語I</td> | |
</tr> | |
<tr> | |
<td class="time-cell">○○:○○<br>~<br>○○:○○</td> | |
<td class="subject">数学I</td> | |
<td class="subject">数学I</td> | |
<td class="subject">英語I</td> | |
<td class="empty">-</td> | |
<td class="subject">英語I</td> | |
</tr> | |
</tbody> | |
</table> | |
</div> | |
<div class="control-group"> | |
<div class="selector-container"> | |
<div class="selector-group"> | |
<div class="custom-select"> | |
<div class="select-button" id="yearSelect">年</div> | |
<div class="select-dropdown" id="yearDropdown"> | |
<div class="select-option" data-value="1">1年</div> | |
<div class="select-option" data-value="2">2年</div> | |
<div class="select-option" data-value="3">3年</div> | |
<div class="select-option" data-value="4">4年</div> | |
<div class="select-option" data-value="5">5年</div> | |
</div> | |
</div> | |
</div> | |
<div class="selector-group"> | |
<div class="custom-select"> | |
<div class="select-button" id="classSelect">組</div> | |
<div class="select-dropdown" id="classDropdown"> | |
<div class="select-option" data-value="1">1組</div> | |
<div class="select-option" data-value="2">2組</div> | |
<div class="select-option" data-value="3">3組</div> | |
<div class="select-option" data-value="4">4組</div> | |
<div class="select-option" data-value="5">5組</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="week-info"> | |
第6週目:5月20日~5月24日 | |
</div> | |
</div> | |
</div> | |
<div class="controls-section"> | |
<div class="control-group"> | |
<div class="action-buttons"> | |
<button class="action-btn" id="changesBtn">授業変更一覧</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Bootstrap JS --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
// Custom dropdown functionality | |
const dropdowns = document.querySelectorAll('.custom-select'); | |
dropdowns.forEach(dropdown => { | |
const button = dropdown.querySelector('.select-button'); | |
const dropdownList = dropdown.querySelector('.select-dropdown'); | |
const options = dropdown.querySelectorAll('.select-option'); | |
button.addEventListener('click', function(e) { | |
e.stopPropagation(); | |
closeAllDropdowns(); | |
dropdownList.style.display = dropdownList.style.display === 'block' ? 'none' : 'block'; | |
}); | |
options.forEach(option => { | |
option.addEventListener('click', function() { | |
const value = this.getAttribute('data-value'); | |
const text = this.textContent; | |
button.textContent = text; | |
button.setAttribute('data-value', value); | |
dropdownList.style.display = 'none'; | |
// Update week info and reload timetable | |
updateTimetable(); | |
}); | |
}); | |
}); | |
// Close dropdowns when clicking outside | |
document.addEventListener('click', closeAllDropdowns); | |
function closeAllDropdowns() { | |
document.querySelectorAll('.select-dropdown').forEach(dropdown => { | |
dropdown.style.display = 'none'; | |
}); | |
} | |
// Action button handlers | |
document.getElementById('changesBtn').addEventListener('click', function() { | |
alert('授業変更一覧画面に移動します'); | |
// Navigate to changes list | |
window.location.href = '/student/changes'; | |
}); | |
// Timetable cell click handlers | |
document.querySelectorAll('.subject').forEach(cell => { | |
cell.addEventListener('click', function() { | |
const subject = this.textContent; | |
alert(`${subject}の詳細を表示します`); | |
// Implement subject detail view | |
}); | |
}); | |
function updateTimetable() { | |
const year = document.getElementById('yearSelect').getAttribute('data-value') || '5'; | |
const classNum = document.getElementById('classSelect').getAttribute('data-value') || '5'; | |
// Update week info | |
const weekInfo = document.querySelector('.week-info'); | |
weekInfo.textContent = `第6週目:5月20日~5月24日 (${year}年${classNum}組)`; | |
// Here you would typically make an AJAX call to load the timetable data | |
console.log(`Loading timetable for Year ${year}, Class ${classNum}`); | |
// Simulate loading new timetable data | |
// In a real application, this would fetch data from the server | |
} | |
// Initialize with default values | |
document.getElementById('yearSelect').textContent = '5年'; | |
document.getElementById('yearSelect').setAttribute('data-value', '5'); | |
document.getElementById('classSelect').textContent = '5組'; | |
document.getElementById('classSelect').setAttribute('data-value', '5'); | |
}); | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use App\Security\AccessControlVoter; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Annotation\Route; | |
class StudentHomeController extends AbstractController | |
{ | |
#[Route('/student/home', name: 'app_student_home')] | |
public function student_home(): Response | |
{ | |
$this->denyAccessUnlessGranted(AccessControlVoter::ACCESS_STUDENT); | |
return $this->render('student_home/student_home.html.twig', [ | |
'controller_name' => 'StudentHomeController', | |
]); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>{% block title %}ログイン成功 - 時間割管理システム{% endblock %}</title> | |
<!-- Bootstrap CSS --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"> | |
<!-- Font Awesome --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<style> | |
body { | |
background-color: #f4c2a1; | |
min-height: 100vh; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
margin: 0; | |
padding: 20px; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
} | |
.success-container { | |
width: 600px; | |
max-width: 90vw; | |
max-height: 90vh; | |
overflow-y: auto; | |
} | |
.success-box { | |
background-color: #2e86c1; | |
padding: 30px; | |
border-radius: 0; | |
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); | |
text-align: center; | |
} | |
.success-title { | |
color: white; | |
font-size: 2rem; | |
font-weight: bold; | |
margin-bottom: 15px; | |
} | |
.success-subtitle { | |
color: white; | |
font-size: 1rem; | |
margin-bottom: 20px; | |
opacity: 0.9; | |
} | |
.success-icon { | |
color: #f1c40f; | |
font-size: 3rem; | |
margin-bottom: 15px; | |
animation: bounce 2s infinite; | |
} | |
@keyframes bounce { | |
0%, 20%, 50%, 80%, 100% { transform: translateY(0); } | |
40% { transform: translateY(-10px); } | |
60% { transform: translateY(-5px); } | |
} | |
.user-info { | |
background: rgba(255, 255, 255, 0.1); | |
padding: 15px; | |
border-radius: 8px; | |
margin: 15px 0; | |
color: white; | |
} | |
.user-email { | |
color: #f1c40f; | |
font-weight: bold; | |
font-size: 1rem; | |
margin: 8px 0; | |
} | |
.redirect-info { | |
background: rgba(241, 196, 15, 0.2); | |
border: 1px solid rgba(241, 196, 15, 0.5); | |
padding: 12px; | |
border-radius: 8px; | |
margin: 15px 0; | |
color: white; | |
} | |
.redirect-info strong { | |
color: #f1c40f; | |
} | |
.countdown { | |
font-size: 1.2rem; | |
color: #f1c40f; | |
font-weight: bold; | |
margin: 15px 0; | |
} | |
.home-btn { | |
background-color: #f1c40f; | |
color: #333; | |
border: none; | |
padding: 10px 25px; | |
font-size: 1rem; | |
font-weight: bold; | |
border-radius: 4px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
margin: 8px; | |
text-decoration: none; | |
display: inline-block; | |
} | |
.home-btn:hover { | |
background-color: #d4ac0d; | |
transform: translateY(-1px); | |
color: #333; | |
text-decoration: none; | |
} | |
.loading-spinner { | |
display: inline-block; | |
width: 16px; | |
height: 16px; | |
border: 2px solid rgba(255, 255, 255, 0.3); | |
border-radius: 50%; | |
border-top-color: #f1c40f; | |
animation: spin 1s ease-in-out infinite; | |
margin-right: 8px; | |
} | |
@keyframes spin { | |
to { transform: rotate(360deg); } | |
} | |
@media (max-width: 768px) { | |
body { | |
padding: 10px; | |
} | |
.success-container { | |
width: 95%; | |
max-height: 95vh; | |
} | |
.success-box { | |
padding: 20px; | |
} | |
.success-title { | |
font-size: 1.8rem; | |
} | |
.success-icon { | |
font-size: 2.5rem; | |
} | |
} | |
@media (max-width: 480px) { | |
.success-title { | |
font-size: 1.6rem; | |
} | |
.success-icon { | |
font-size: 2rem; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="success-container"> | |
<div class="success-box"> | |
<div class="success-icon"> | |
<i class="fas fa-check-circle"></i> | |
</div> | |
<h1 class="success-title">ログイン成功!</h1> | |
<p class="success-subtitle">認証が完了しました</p> | |
{% if app.user %} | |
<div class="user-info"> | |
<div><i class="fas fa-user"></i> ようこそ!</div> | |
<div class="user-email">{{ app.user.email }}</div> | |
{% set roles = app.user.roles %} | |
<div> | |
{% if 'ROLE_DEVELOPER' in roles %} | |
<i class="fas fa-code"></i> 開発者 | |
{% elseif 'ROLE_ADMIN' in roles %} | |
<i class="fas fa-user-shield"></i> 管理者 | |
{% elseif 'ROLE_TEACHER' in roles %} | |
<i class="fas fa-chalkboard-teacher"></i> 教員 | |
{% else %} | |
<i class="fas fa-graduation-cap"></i> 学生 | |
{% endif %} | |
</div> | |
</div> | |
{% endif %} | |
<div class="redirect-info"> | |
<div class="loading-spinner"></div> | |
<strong>このタブは <span id="countdown">5</span> 秒後に閉じられます</strong> | |
</div> | |
<a href="#" id="homeBtn" class="home-btn"> | |
<i class="fas fa-times"></i> すぐにタブを閉じる | |
</a> | |
</div> | |
</div> | |
<!-- Bootstrap JS --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
let countdown = 5; | |
const countdownElement = document.getElementById('countdown'); | |
const homeBtn = document.getElementById('homeBtn'); | |
// ユーザーの権限に基づいてリダイレクト先を決定 | |
function getRedirectUrl() { | |
{% if app.user %} | |
{% set roles = app.user.roles %} | |
{% if 'ROLE_DEVELOPER' in roles %} | |
return '/developer/home'; | |
{% elseif 'ROLE_ADMIN' in roles %} | |
return '/admin/home'; | |
{% elseif 'ROLE_TEACHER' in roles %} | |
return '/teacher/home'; | |
{% else %} | |
return '/student/home'; | |
{% endif %} | |
{% else %} | |
return '/student/home'; | |
{% endif %} | |
} | |
const redirectUrl = getRedirectUrl(); | |
homeBtn.href = redirectUrl; | |
// カウントダウン | |
const countdownInterval = setInterval(() => { | |
countdown--; | |
countdownElement.textContent = countdown; | |
if (countdown <= 0) { | |
clearInterval(countdownInterval); | |
// タブを閉じる処理を試行 | |
try { | |
// タブを閉じることができるかチェック | |
window.close(); | |
// タブが閉じられない場合(新しいタブで開かれた場合など)は | |
// 少し待ってからログイン画面にリダイレクト | |
setTimeout(() => { | |
window.location.href = '/magic-login'; | |
}, 1000); | |
} catch (e) { | |
// エラーが発生した場合はログイン画面にリダイレクト | |
window.location.href = '/magic-login'; | |
} | |
} | |
}, 1000); | |
// すぐにホームへ移動ボタン(実際はタブを閉じる) | |
homeBtn.addEventListener('click', function(e) { | |
e.preventDefault(); | |
clearInterval(countdownInterval); | |
try { | |
window.close(); | |
// タブが閉じられない場合の代替処理 | |
setTimeout(() => { | |
window.location.href = '/magic-login'; | |
}, 500); | |
} catch (e) { | |
window.location.href = '/magic-login'; | |
} | |
}); | |
}); | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>システム権限制御管理 - 開発者ツール</title> | |
<!-- Bootstrap CSS --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"> | |
<!-- Font Awesome --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<style> | |
body { | |
background-color: #f4c2a1; | |
min-height: 100vh; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
} | |
.container { | |
max-width: 800px; | |
margin: 50px auto; | |
padding: 20px; | |
} | |
.control-panel { | |
background: white; | |
border-radius: 8px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
padding: 30px; | |
} | |
.panel-header { | |
background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%); | |
color: white; | |
padding: 20px; | |
border-radius: 8px; | |
margin-bottom: 30px; | |
text-align: center; | |
} | |
.status-display { | |
background: #f8f9fa; | |
border: 2px solid #dee2e6; | |
border-radius: 8px; | |
padding: 20px; | |
margin-bottom: 30px; | |
text-align: center; | |
} | |
.status-active { | |
border-color: #dc3545; | |
background-color: #f8d7da; | |
} | |
.status-disabled { | |
border-color: #28a745; | |
background-color: #d4edda; | |
} | |
.toggle-section { | |
text-align: center; | |
margin: 30px 0; | |
} | |
.toggle-btn { | |
padding: 15px 30px; | |
font-size: 1.2rem; | |
font-weight: bold; | |
border: none; | |
border-radius: 8px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
margin: 10px; | |
min-width: 200px; | |
} | |
.btn-enable { | |
background-color: #dc3545; | |
color: white; | |
} | |
.btn-enable:hover { | |
background-color: #c82333; | |
} | |
.btn-disable { | |
background-color: #28a745; | |
color: white; | |
} | |
.btn-disable:hover { | |
background-color: #218838; | |
} | |
.info-section { | |
background: #e3f2fd; | |
border: 1px solid #2196f3; | |
border-radius: 8px; | |
padding: 20px; | |
margin: 20px 0; | |
} | |
.warning-section { | |
background: #fff3cd; | |
border: 1px solid #ffc107; | |
border-radius: 8px; | |
padding: 20px; | |
margin: 20px 0; | |
} | |
.back-btn { | |
background-color: #6c757d; | |
color: white; | |
padding: 10px 20px; | |
border: none; | |
border-radius: 4px; | |
text-decoration: none; | |
display: inline-block; | |
margin-top: 20px; | |
} | |
.back-btn:hover { | |
background-color: #5a6268; | |
color: white; | |
text-decoration: none; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<div class="control-panel"> | |
<div class="panel-header"> | |
<h1><i class="fas fa-shield-alt"></i> システム権限制御管理</h1> | |
<p>エラーページでのSymfonyデバッグ画面表示を制御します</p> | |
</div> | |
<div id="statusDisplay" class="status-display"> | |
<h3 id="statusTitle"> | |
{% if current_status %} | |
<i class="fas fa-lock text-danger"></i> システム権限制御:適用中 | |
{% else %} | |
<i class="fas fa-unlock text-success"></i> システム権限制御:解除中 | |
{% endif %} | |
</h3> | |
<p id="statusDescription"> | |
{% if current_status %} | |
Symfonyデバッグ画面は開発者のみに表示されます | |
{% else %} | |
Symfonyデバッグ画面は全ユーザーに表示されます | |
{% endif %} | |
</p> | |
</div> | |
<div class="info-section"> | |
<h4><i class="fas fa-info-circle"></i> 機能説明</h4> | |
<ul> | |
<li><strong>適用中:</strong> エラーページでSymfonyデバッグ画面へのボタンは開発者ロールのみに表示</li> | |
<li><strong>解除中:</strong> エラーページでSymfonyデバッグ画面へのボタンは全ユーザーに表示</li> | |
</ul> | |
</div> | |
<div class="warning-section"> | |
<h4><i class="fas fa-exclamation-triangle"></i> 注意事項</h4> | |
<p>本番環境では必ず「適用中」に設定してください。解除中の状態では、一般ユーザーにもシステムの詳細なエラー情報が表示される可能性があります。</p> | |
</div> | |
<div class="toggle-section"> | |
<button id="enableBtn" class="toggle-btn btn-enable" onclick="toggleAccessControl(true)" | |
{% if current_status %}style="display: none;"{% endif %}> | |
<i class="fas fa-lock"></i> 権限制御を適用する | |
</button> | |
<button id="disableBtn" class="toggle-btn btn-disable" onclick="toggleAccessControl(false)" | |
{% if not current_status %}style="display: none;"{% endif %}> | |
<i class="fas fa-unlock"></i> 権限制御を解除する | |
</button> | |
<button id="loadingBtn" class="toggle-btn" style="display: none; background-color: #6c757d;" disabled> | |
<i class="fas fa-spinner fa-spin"></i> 処理中... | |
</button> | |
</div> | |
<div style="text-align: center;"> | |
<a href="{{ path('app_developer_home') }}" class="back-btn"> | |
<i class="fas fa-arrow-left"></i> 開発者ホームに戻る | |
</a> | |
</div> | |
</div> | |
</div> | |
<!-- Bootstrap JS --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script> | |
<script> | |
async function toggleAccessControl(enabled) { | |
const enableBtn = document.getElementById('enableBtn'); | |
const disableBtn = document.getElementById('disableBtn'); | |
const loadingBtn = document.getElementById('loadingBtn'); | |
const statusDisplay = document.getElementById('statusDisplay'); | |
const statusTitle = document.getElementById('statusTitle'); | |
const statusDescription = document.getElementById('statusDescription'); | |
// ローディング状態を表示 | |
enableBtn.style.display = 'none'; | |
disableBtn.style.display = 'none'; | |
loadingBtn.style.display = 'inline-block'; | |
try { | |
const response = await fetch('/api/system-access-control/toggle', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
}, | |
body: JSON.stringify({ enabled: enabled }) | |
}); | |
const data = await response.json(); | |
if (data.success) { | |
// 成功時の表示更新 | |
if (enabled) { | |
statusTitle.innerHTML = '<i class="fas fa-lock text-danger"></i> システム権限制御:適用中'; | |
statusDescription.textContent = 'Symfonyデバッグ画面は開発者のみに表示されます'; | |
statusDisplay.className = 'status-display status-active'; | |
disableBtn.style.display = 'inline-block'; | |
} else { | |
statusTitle.innerHTML = '<i class="fas fa-unlock text-success"></i> システム権限制御:解除中'; | |
statusDescription.textContent = 'Symfonyデバッグ画面は全ユーザーに表示されます'; | |
statusDisplay.className = 'status-display status-disabled'; | |
enableBtn.style.display = 'inline-block'; | |
} | |
// 成功メッセージを表示 | |
alert(data.message); | |
} else { | |
throw new Error(data.message || '設定の変更に失敗しました'); | |
} | |
} catch (error) { | |
console.error('設定変更エラー:', error); | |
alert('設定の変更に失敗しました: ' + error.message); | |
// エラー時は元のボタンを表示 | |
if (enabled) { | |
enableBtn.style.display = 'inline-block'; | |
} else { | |
disableBtn.style.display = 'inline-block'; | |
} | |
} finally { | |
// ローディング状態を非表示 | |
loadingBtn.style.display = 'none'; | |
} | |
} | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\JsonResponse; | |
use Symfony\Component\HttpFoundation\Request; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Annotation\Route; | |
class SystemAccessControlController extends AbstractController | |
{ | |
#[Route('/developer/system-access-control', name: 'app_system_access_control')] | |
public function index(Request $request): Response | |
{ | |
$session = $request->getSession(); | |
$currentStatus = $session->get('system_access_control_enabled', false); | |
return $this->render('developer/system_access_control.html.twig', [ | |
'current_status' => $currentStatus, | |
]); | |
} | |
#[Route('/api/system-access-control/toggle', name: 'api_system_access_control_toggle', methods: ['POST'])] | |
public function toggle(Request $request): JsonResponse | |
{ | |
try { | |
$data = json_decode($request->getContent(), true); | |
$enabled = $data['enabled'] ?? false; | |
$session = $request->getSession(); | |
$session->set('system_access_control_enabled', $enabled); | |
return new JsonResponse([ | |
'success' => true, | |
'enabled' => $enabled, | |
'status' => $enabled ? 'active' : 'disabled', | |
'message' => $enabled ? | |
'システム権限制御を適用しました' : | |
'システム権限制御を解除しました', | |
'timestamp' => date('Y-m-d H:i:s') | |
]); | |
} catch (\Exception $e) { | |
return new JsonResponse([ | |
'success' => false, | |
'error' => $e->getMessage(), | |
'message' => 'システム権限制御の変更に失敗しました' | |
], 500); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Entity; | |
use App\Repository\SystemSettingRepository; | |
use Doctrine\ORM\Mapping as ORM; | |
#[ORM\Entity(repositoryClass: SystemSettingRepository::class)] | |
#[ORM\Table(name: 'system_setting')] | |
class SystemSetting | |
{ | |
#[ORM\Id] | |
#[ORM\GeneratedValue(strategy: 'IDENTITY')] // ★★ 明示的にIDENTITY戦略を指定 ★★ | |
#[ORM\Column(type: 'integer')] | |
private ?int $id = null; | |
#[ORM\Column(length: 255, unique: true)] | |
private ?string $settingKey = null; | |
#[ORM\Column(length: 255)] | |
private ?string $settingValue = null; | |
#[ORM\Column(type: 'datetime')] | |
private ?\DateTimeInterface $updatedAt = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $updatedBy = null; | |
public function getId(): ?int | |
{ | |
return $this->id; | |
} | |
public function getSettingKey(): ?string | |
{ | |
return $this->settingKey; | |
} | |
public function setSettingKey(string $settingKey): static | |
{ | |
$this->settingKey = $settingKey; | |
return $this; | |
} | |
public function getSettingValue(): ?string | |
{ | |
return $this->settingValue; | |
} | |
public function setSettingValue(string $settingValue): static | |
{ | |
$this->settingValue = $settingValue; | |
return $this; | |
} | |
public function getUpdatedAt(): ?\DateTimeInterface | |
{ | |
return $this->updatedAt; | |
} | |
public function setUpdatedAt(\DateTimeInterface $updatedAt): static | |
{ | |
$this->updatedAt = $updatedAt; | |
return $this; | |
} | |
public function getUpdatedBy(): ?string | |
{ | |
return $this->updatedBy; | |
} | |
public function setUpdatedBy(?string $updatedBy): static | |
{ | |
$this->updatedBy = $updatedBy; | |
return $this; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Repository; | |
use App\Entity\SystemSetting; | |
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; | |
use Doctrine\Persistence\ManagerRegistry; | |
/** | |
* @extends ServiceEntityRepository<SystemSetting> | |
*/ | |
class SystemSettingRepository extends ServiceEntityRepository | |
{ | |
public function __construct(ManagerRegistry $registry) | |
{ | |
parent::__construct($registry, SystemSetting::class); | |
} | |
public function findByKey(string $key): ?SystemSetting | |
{ | |
return $this->findOneBy(['settingKey' => $key]); | |
} | |
public function getSettingValue(string $key, string $default = null): ?string | |
{ | |
$setting = $this->findByKey($key); | |
return $setting ? $setting->getSettingValue() : $default; | |
} | |
public function setSetting(string $key, string $value, string $updatedBy = null): void | |
{ | |
$setting = $this->findByKey($key); | |
if (!$setting) { | |
$setting = new SystemSetting(); | |
$setting->setSettingKey($key); | |
} | |
$setting->setSettingValue($value); | |
$setting->setUpdatedAt(new \DateTime()); | |
$setting->setUpdatedBy($updatedBy); | |
$this->getEntityManager()->persist($setting); | |
$this->getEntityManager()->flush(); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>追加申請 - 時間割管理システム</title> | |
<style> | |
body { | |
font-family: 'MS UI Gothic', sans-serif; | |
margin: 0; | |
padding: 0; | |
background-color: #f0f0f0; | |
} | |
.header { | |
background: linear-gradient(135deg, #4a90e2, #357abd); | |
color: white; | |
padding: 15px 20px; | |
font-size: 24px; | |
font-weight: bold; | |
} | |
.main-container { | |
background: linear-gradient(135deg, #f4d4a7, #e8c394); | |
padding: 20px; | |
min-height: calc(100vh - 70px); | |
} | |
.content-title { | |
font-size: 18px; | |
font-weight: bold; | |
margin-bottom: 20px; | |
color: #333; | |
} | |
.schedule-table { | |
background: white; | |
border-collapse: collapse; | |
width: 100%; | |
max-width: 800px; | |
margin-bottom: 20px; | |
box-shadow: 0 2px 10px rgba(0,0,0,0.1); | |
} | |
.schedule-table th { | |
background: linear-gradient(135deg, #4a90e2, #357abd); | |
color: white; | |
padding: 12px 8px; | |
text-align: center; | |
font-weight: bold; | |
border: 1px solid #ddd; | |
} | |
.schedule-table td { | |
padding: 8px; | |
text-align: center; | |
border: 1px solid #ddd; | |
height: 40px; | |
min-width: 80px; | |
} | |
.time-cell { | |
background: #f8f9fa; | |
font-weight: bold; | |
color: #333; | |
} | |
.class-cell { | |
background: #e9ecef; | |
font-size: 12px; | |
} | |
.occupied-cell { | |
background: #6c757d; | |
color: white; | |
cursor: not-allowed; | |
} | |
.empty-cell { | |
background: #e3f2fd; | |
cursor: pointer; | |
transition: all 0.2s; | |
} | |
.empty-cell:hover { | |
background: #bbdefb; | |
transform: scale(1.05); | |
} | |
.selected-cell { | |
background: #4caf50 !important; | |
color: white; | |
font-weight: bold; | |
} | |
.week-selector { | |
display: flex; | |
align-items: center; | |
gap: 10px; | |
margin-bottom: 20px; | |
} | |
.week-info { | |
background: white; | |
padding: 8px 15px; | |
border: 2px solid #ddd; | |
border-radius: 5px; | |
font-weight: bold; | |
} | |
.week-nav { | |
background: #4a90e2; | |
color: white; | |
border: none; | |
padding: 8px 12px; | |
cursor: pointer; | |
border-radius: 3px; | |
font-size: 16px; | |
} | |
.week-nav:hover { | |
background: #357abd; | |
} | |
.instruction { | |
background: #fff3cd; | |
border: 1px solid #ffeaa7; | |
border-radius: 5px; | |
padding: 10px; | |
margin-bottom: 20px; | |
color: #856404; | |
} | |
.button-container { | |
display: flex; | |
gap: 15px; | |
margin-top: 30px; | |
} | |
.btn { | |
padding: 12px 25px; | |
border: none; | |
border-radius: 5px; | |
font-size: 16px; | |
cursor: pointer; | |
font-weight: bold; | |
transition: all 0.2s; | |
} | |
.btn-primary { | |
background: #ffeb3b; | |
color: #333; | |
box-shadow: 0 2px 5px rgba(0,0,0,0.2); | |
} | |
.btn-primary:hover { | |
background: #fdd835; | |
transform: translateY(-1px); | |
} | |
.btn-secondary { | |
background: #6c757d; | |
color: white; | |
} | |
.btn-secondary:hover { | |
background: #5a6268; | |
} | |
.selected-info { | |
background: white; | |
padding: 15px; | |
border-radius: 5px; | |
margin-top: 20px; | |
display: none; | |
} | |
.selected-info.show { | |
display: block; | |
} | |
.selected-list { | |
list-style: none; | |
padding: 0; | |
margin: 10px 0; | |
} | |
.selected-list li { | |
background: #e8f5e8; | |
padding: 8px 12px; | |
margin: 5px 0; | |
border-radius: 3px; | |
border-left: 4px solid #4caf50; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
} | |
.remove-btn { | |
background: #f44336; | |
color: white; | |
border: none; | |
padding: 4px 8px; | |
border-radius: 3px; | |
cursor: pointer; | |
font-size: 12px; | |
} | |
.remove-btn:hover { | |
background: #d32f2f; | |
} | |
.datetime-dialog { | |
position: fixed; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
z-index: 1000; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
} | |
.dialog-overlay { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background: rgba(0, 0, 0, 0.5); | |
} | |
.dialog-content { | |
background: white; | |
padding: 20px; | |
border-radius: 8px; | |
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); | |
position: relative; | |
z-index: 1001; | |
min-width: 300px; | |
} | |
.dialog-content h3 { | |
margin: 0 0 15px 0; | |
color: #333; | |
} | |
.form-group { | |
margin-bottom: 15px; | |
} | |
.form-group label { | |
display: inline-block; | |
width: 60px; | |
font-weight: bold; | |
color: #555; | |
} | |
.form-group input[type="date"] { | |
padding: 5px; | |
border: 1px solid #ddd; | |
border-radius: 3px; | |
} | |
.form-group span { | |
color: #333; | |
font-weight: bold; | |
} | |
.dialog-buttons { | |
display: flex; | |
gap: 10px; | |
justify-content: flex-end; | |
margin-top: 20px; | |
} | |
.dialog-buttons button { | |
padding: 8px 16px; | |
border: none; | |
border-radius: 4px; | |
cursor: pointer; | |
font-weight: bold; | |
} | |
.dialog-buttons button:first-child { | |
background: #4caf50; | |
color: white; | |
} | |
.dialog-buttons button:first-child:hover { | |
background: #45a049; | |
} | |
.dialog-buttons button:last-child { | |
background: #6c757d; | |
color: white; | |
} | |
.dialog-buttons button:last-child:hover { | |
background: #5a6268; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="header"> | |
追加申請 | |
</div> | |
<div class="main-container"> | |
<div class="content-title">追加申請</div> | |
<div class="instruction"> | |
追加したい時間帯をクリックしてください。 | |
</div> | |
<div class="week-selector"> | |
<button class="week-nav" onclick="changeWeek(-1)">?</button> | |
<div class="week-info" id="weekInfo">第6週目:5月20日?5月24日</div> | |
<button class="week-nav" onclick="changeWeek(1)">?</button> | |
</div> | |
<table class="schedule-table"> | |
<thead> | |
<tr> | |
<th></th> | |
<th>月<br><span style="font-size:12px;">5月20日</span></th> | |
<th>火<br><span style="font-size:12px;">5月21日</span></th> | |
<th>水<br><span style="font-size:12px;">5月22日</span></th> | |
<th>木<br><span style="font-size:12px;">5月23日</span></th> | |
<th>金<br><span style="font-size:12px;">5月24日</span></th> | |
</tr> | |
</thead> | |
<tbody id="scheduleBody"> | |
<tr> | |
<td class="time-cell">1限</td> | |
<td class="class-cell">○○○○</td> | |
<td class="occupied-cell">数学I</td> | |
<td class="occupied-cell">科学I</td> | |
<td class="occupied-cell">数学I</td> | |
<td class="occupied-cell">体育I</td> | |
</tr> | |
<tr> | |
<td class="time-cell">2限</td> | |
<td class="class-cell">○○○○</td> | |
<td class="occupied-cell">数学I</td> | |
<td class="occupied-cell">数学I</td> | |
<td class="occupied-cell">数学I</td> | |
<td class="occupied-cell">数学I</td> | |
</tr> | |
<tr> | |
<td class="time-cell">3限</td> | |
<td class="class-cell">○○○○</td> | |
<td class="occupied-cell">国語I</td> | |
<td class="occupied-cell">体育I</td> | |
<td class="empty-cell" onclick="toggleCell(this, '3限', '水')" data-period="3" data-day="水">-</td> | |
<td class="occupied-cell">国語I</td> | |
</tr> | |
<tr> | |
<td class="time-cell">4限</td> | |
<td class="class-cell">○○○○</td> | |
<td class="empty-cell" onclick="toggleCell(this, '4限', '火')" data-period="4" data-day="火">-</td> | |
<td class="occupied-cell">数学I</td> | |
<td class="occupied-cell">数学I</td> | |
<td class="occupied-cell">数学I</td> | |
</tr> | |
<tr> | |
<td class="time-cell">5限</td> | |
<td class="class-cell">○○○○</td> | |
<td class="occupied-cell">体育I</td> | |
<td class="occupied-cell">数学I</td> | |
<td class="occupied-cell">体育I</td> | |
<td class="occupied-cell">科学I</td> | |
</tr> | |
<tr> | |
<td class="time-cell">6限</td> | |
<td class="class-cell">○○○○</td> | |
<td class="empty-cell" onclick="toggleCell(this, '6限', '火')" data-period="6" data-day="火">-</td> | |
<td class="empty-cell" onclick="toggleCell(this, '6限', '水')" data-period="6" data-day="水">-</td> | |
<td class="empty-cell" onclick="toggleCell(this, '6限', '木')" data-period="6" data-day="木">-</td> | |
<td class="occupied-cell">数学I</td> | |
</tr> | |
<tr> | |
<td class="time-cell">7限</td> | |
<td class="class-cell">○○○○</td> | |
<td class="empty-cell" onclick="toggleCell(this, '7限', '火')" data-period="7" data-day="火">-</td> | |
<td class="empty-cell" onclick="toggleCell(this, '7限', '水')" data-period="7" data-day="水">-</td> | |
<td class="empty-cell" onclick="toggleCell(this, '7限', '木')" data-period="7" data-day="木">-</td> | |
<td class="empty-cell" onclick="toggleCell(this, '7限', '金')" data-period="7" data-day="金">-</td> | |
</tr> | |
</tbody> | |
</table> | |
<div class="week-selector"> | |
<button class="week-nav" onclick="changeWeek(-1)"><</button> | |
<div class="week-info" id="weekInfo">第6週目:5月20日~5月24日</div> | |
<button class="week-nav" onclick="changeWeek(1)">></button> | |
</div> | |
<div class="selected-info" id="selectedInfo"> | |
<h3>選択された時間帯:</h3> | |
<ul class="selected-list" id="selectedList"></ul> | |
</div> | |
<div class="button-container"> | |
<button class="btn btn-primary" onclick="goToConfirmation()">確認画面へ</button> | |
<button class="btn btn-secondary" onclick="goBack()">戻る</button> | |
</div> | |
</div> | |
<script> | |
let selectedCells = []; | |
let currentWeek = 6; | |
// 週のデータ(簡単な例) | |
const weekData = { | |
5: { text: "第5週目:5月13日~5月17日", dates: ["5月13日", "5月14日", "5月15日", "5月16日", "5月17日"] }, | |
6: { text: "第6週目:5月20日~5月24日", dates: ["5月20日", "5月21日", "5月22日", "5月23日", "5月24日"] }, | |
7: { text: "第7週目:5月27日~5月31日", dates: ["5月27日", "5月28日", "5月29日", "5月30日", "5月31日"] } | |
}; | |
function toggleCell(cell, period, day) { | |
const currentDate = weekData[currentWeek].dates[getDayIndex(day)]; | |
const cellId = `${currentWeek}-${period}-${day}`; | |
if (cell.classList.contains('selected-cell')) { | |
// 選択解除 | |
cell.classList.remove('selected-cell'); | |
cell.textContent = '-'; | |
selectedCells = selectedCells.filter(item => item.id !== cellId); | |
} else { | |
// 選択時:日付選択ダイアログを表示 | |
showDateTimeDialog(cell, period, day, currentDate, cellId); | |
} | |
updateSelectedInfo(); | |
} | |
function showDateTimeDialog(cell, period, day, defaultDate, cellId) { | |
const dialog = document.createElement('div'); | |
dialog.className = 'datetime-dialog'; | |
dialog.innerHTML = ` | |
<div class="dialog-content"> | |
<h3>日時指定</h3> | |
<div class="form-group"> | |
<label>日付:</label> | |
<input type="date" id="selectedDate" value="${convertToDateInput(defaultDate)}"> | |
</div> | |
<div class="form-group"> | |
<label>時限:</label> | |
<span>${period}</span> | |
</div> | |
<div class="form-group"> | |
<label>曜日:</label> | |
<span>${day}曜日</span> | |
</div> | |
<div class="dialog-buttons"> | |
<button onclick="confirmSelection('${cellId}', '${period}', '${day}')">確定</button> | |
<button onclick="cancelSelection()">キャンセル</button> | |
</div> | |
</div> | |
<div class="dialog-overlay" onclick="cancelSelection()"></div> | |
`; | |
document.body.appendChild(dialog); | |
window.currentCell = cell; | |
window.currentCellId = cellId; | |
} | |
function confirmSelection(cellId, period, day) { | |
const selectedDate = document.getElementById('selectedDate').value; | |
const cell = window.currentCell; | |
if (!selectedDate) { | |
alert('日付を選択してください。'); | |
return; | |
} | |
// 選択 | |
cell.classList.add('selected-cell'); | |
cell.textContent = '選択'; | |
selectedCells.push({ | |
id: cellId, | |
period: period, | |
day: day, | |
date: selectedDate, | |
week: currentWeek, | |
element: cell | |
}); | |
closeDialog(); | |
updateSelectedInfo(); | |
} | |
function cancelSelection() { | |
closeDialog(); | |
} | |
function closeDialog() { | |
const dialog = document.querySelector('.datetime-dialog'); | |
if (dialog) { | |
dialog.remove(); | |
} | |
window.currentCell = null; | |
window.currentCellId = null; | |
} | |
function convertToDateInput(dateStr) { | |
// "5月20日" -> "2024-05-20" の変換 | |
const match = dateStr.match(/(\d+)月(\d+)日/); | |
if (match) { | |
const month = match[1].padStart(2, '0'); | |
const day = match[2].padStart(2, '0'); | |
return `2024-${month}-${day}`; | |
} | |
return ''; | |
} | |
function getDayIndex(day) { | |
const dayMap = { '月': 0, '火': 1, '水': 2, '木': 3, '金': 4 }; | |
return dayMap[day] || 0; | |
} | |
function updateSelectedInfo() { | |
const selectedInfo = document.getElementById('selectedInfo'); | |
const selectedList = document.getElementById('selectedList'); | |
if (selectedCells.length > 0) { | |
selectedInfo.classList.add('show'); | |
selectedList.innerHTML = ''; | |
selectedCells.forEach(item => { | |
const li = document.createElement('li'); | |
li.innerHTML = ` | |
<div>${item.date} (${item.day}曜日) ${item.period}</div> | |
<button class="remove-btn" onclick="removeSelection('${item.id}')">削除</button> | |
`; | |
selectedList.appendChild(li); | |
}); | |
} else { | |
selectedInfo.classList.remove('show'); | |
} | |
} | |
function removeSelection(cellId) { | |
const item = selectedCells.find(cell => cell.id === cellId); | |
if (item) { | |
// セルの見た目をリセット | |
item.element.classList.remove('selected-cell'); | |
item.element.textContent = '-'; | |
// 配列から削除 | |
selectedCells = selectedCells.filter(cell => cell.id !== cellId); | |
updateSelectedInfo(); | |
} | |
} | |
function changeWeek(direction) { | |
const newWeek = currentWeek + direction; | |
if (weekData[newWeek]) { | |
currentWeek = newWeek; | |
document.getElementById('weekInfo').textContent = weekData[currentWeek].text; | |
// ヘッダーの日付を更新 | |
const headers = document.querySelectorAll('thead th'); | |
const days = ['月', '火', '水', '木', '金']; | |
for (let i = 1; i <= 5; i++) { | |
headers[i].innerHTML = `${days[i-1]}<br><span style="font-size:12px;">${weekData[currentWeek].dates[i-1]}</span>`; | |
} | |
// 表示中の週の選択状態を更新(選択データは保持) | |
updateWeekDisplay(); | |
} | |
} | |
function updateWeekDisplay() { | |
// 全ての空きセルをリセット | |
const emptyCells = document.querySelectorAll('.empty-cell'); | |
emptyCells.forEach(cell => { | |
cell.classList.remove('selected-cell'); | |
cell.textContent = '-'; | |
}); | |
// 現在の週に該当する選択済みセルを再表示 | |
selectedCells.forEach(item => { | |
if (item.week === currentWeek) { | |
item.element.classList.add('selected-cell'); | |
item.element.textContent = '選択'; | |
} | |
}); | |
} | |
function goToConfirmation() { | |
if (selectedCells.length === 0) { | |
alert('追加したい時間帯を選択してください。'); | |
return; | |
} | |
// 確認画面へのデータを準備 | |
const confirmationData = { | |
week: currentWeek, | |
weekText: weekData[currentWeek].text, | |
selectedSlots: selectedCells.map(item => ({ | |
period: item.period, | |
day: item.day | |
})) | |
}; | |
// 実際のシステムでは、PHPへPOSTまたはセッションに保存 | |
console.log('確認画面へのデータ:', confirmationData); | |
alert('確認画面へ進みます。\n選択された時間帯: ' + | |
selectedCells.map(item => `${item.day}曜日${item.period}`).join(', ')); | |
// 実際の実装では以下のようにPOSTする | |
// submitForm(confirmationData); | |
} | |
function goBack() { | |
if (confirm('入力内容が失われますが、前の画面に戻りますか?')) { | |
// 実際のシステムでは前のページにリダイレクト | |
console.log('申請内容選択画面に戻る'); | |
alert('申請内容選択画面に戻ります。'); | |
// window.location.href = 'application_select.php'; | |
} | |
} | |
// PHPへのデータ送信用関数(実装例) | |
function submitForm(data) { | |
const form = document.createElement('form'); | |
form.method = 'POST'; | |
form.action = 'confirmation.php'; | |
const input = document.createElement('input'); | |
input.type = 'hidden'; | |
input.name = 'schedule_data'; | |
input.value = JSON.stringify(data); | |
form.appendChild(input); | |
document.body.appendChild(form); | |
form.submit(); | |
} | |
// ページ読み込み時の初期化 | |
document.addEventListener('DOMContentLoaded', function() { | |
console.log('時間割追加申請画面が読み込まれました'); | |
}); | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1" /> | |
<title>時間割変更設定</title> | |
<!-- Bootstrap CSS --> | |
<link | |
href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" | |
rel="stylesheet" | |
/> | |
<style> | |
body { | |
margin: 0; | |
padding: 0; | |
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; | |
background-color: #f4c2a1; | |
} | |
.header { | |
height: 80px; | |
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); | |
color: white; | |
font-size: 1.6rem; | |
font-weight: bold; | |
display: flex; | |
align-items: center; | |
justify-content: flex-start; | |
padding: 0 40px; | |
} | |
.main-container { | |
padding: 20px 40px; | |
} | |
.bottom-buttons { | |
margin: 20px 40px; | |
display: flex; | |
justify-content: space-between; | |
gap: 20px; | |
} | |
.btn-custom { | |
background-color: #f1c40f; | |
color: #333; | |
border: none; | |
padding: 12px 25px; | |
font-size: 1rem; | |
font-weight: bold; | |
border-radius: 6px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
min-width: 160px; | |
text-align: center; | |
user-select: none; | |
} | |
.btn-custom:hover { | |
background-color: #d4ac0d; | |
transform: translateY(-2px); | |
} | |
</style> | |
</head> | |
<body> | |
<div class="header"> | |
時間割変更設定 | |
</div> | |
<div class="main-container"> | |
<p class="fw-bold mb-4">中村さん。以下の項目を選択してください</p> | |
<div class="form-group mb-3"> | |
<label class="form-label fw-bold">変更したいクラス</label><br /> | |
<label for="yearSelect">年:</label> | |
<select id="yearSelect"> | |
<option value="1">1年</option> | |
<option value="2">2年</option> | |
<option value="3">3年</option> | |
<option value="4">4年</option> | |
<option value="5" selected>5年</option> | |
</select> | |
<label for="classSelect" class="ms-3">組:</label> | |
<select id="classSelect"> | |
<option value="1">1組</option> | |
<option value="2">2組</option> | |
<option value="3">3組</option> | |
<option value="4">4組</option> | |
<option value="5" selected>5組</option> | |
</select> | |
</div> | |
<div class="form-group mb-3"> | |
<label for="subjectSelect" class="form-label fw-bold"> | |
教科を選択してください | |
</label> | |
<select id="subjectSelect" class="form-select" style="max-width: 200px;"> | |
<option value="" selected>-- 教科を選択 --</option> | |
<option value="英語I">英語I</option> | |
<option value="数学I">数学I</option> | |
<option value="国語">国語</option> | |
<option value="理科">理科</option> | |
<option value="社会">社会</option> | |
</select> | |
</div> | |
<div class="form-group mb-3"> | |
<label class="form-label fw-bold">操作を選択してください</label> | |
<div> | |
<div class="form-check form-check-inline"> | |
<input | |
class="form-check-input" | |
type="radio" | |
name="operation" | |
id="addRadio" | |
value="add" | |
checked | |
/> | |
<label class="form-check-label" for="addRadio">追加</label> | |
</div> | |
<div class="form-check form-check-inline"> | |
<input | |
class="form-check-input" | |
type="radio" | |
name="operation" | |
id="deleteRadio" | |
value="delete" | |
/> | |
<label class="form-check-label" for="deleteRadio">削除</label> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="bottom-buttons"> | |
<button id="goChangeTimetableBtn" class="btn-custom">変更時間選択へ</button> | |
<button id="backBtn" class="btn-custom">戻る</button> | |
</div> | |
<!-- Bootstrap JS --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script> | |
<script> | |
document.addEventListener("DOMContentLoaded", () => { | |
const goChangeTimetableBtn = document.getElementById("goChangeTimetableBtn"); | |
const backBtn = document.getElementById("backBtn"); | |
const yearSelect = document.getElementById("yearSelect"); | |
const classSelect = document.getElementById("classSelect"); | |
const subjectSelect = document.getElementById("subjectSelect"); | |
const operationRadios = document.getElementsByName("operation"); | |
function getSelectedOperation() { | |
return [...operationRadios].find((r) => r.checked)?.value; | |
} | |
goChangeTimetableBtn.addEventListener("click", () => { | |
const operation = getSelectedOperation(); | |
if (operation === "add") { | |
location.href = "http://localhost/teacher/add"; | |
} else if (operation === "delete") { | |
location.href = "http://localhost/teacher/delete"; | |
} | |
}); | |
backBtn.addEventListener("click", () => { | |
location.href = "http://localhost/teacher/home"; | |
}); | |
[yearSelect, classSelect, subjectSelect, ...operationRadios].forEach((el) => { | |
el.addEventListener("change", () => { | |
console.log( | |
`年: ${yearSelect.value} 組: ${classSelect.value} 教科: ${subjectSelect.value} 操作: ${getSelectedOperation()}` | |
); | |
}); | |
}); | |
}); | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>{% block title %}承認済み - 時間割管理システム{% endblock %}</title> | |
<!-- Bootstrap CSS --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"> | |
<!-- Font Awesome --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<style> | |
body { | |
margin: 0; | |
padding: 0; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
background-color: #f4c2a1; | |
min-height: 100vh; | |
} | |
.header { | |
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); | |
color: white; | |
padding: 15px 30px; | |
font-size: 1.6rem; | |
font-weight: bold; | |
} | |
.main-container { | |
padding: 40px 30px; | |
max-width: 1200px; | |
margin: 0 auto; | |
height: calc(100vh - 70px); | |
overflow: hidden; | |
position: relative; | |
} | |
.message-content { | |
font-size: 1.2rem; | |
color: #333; | |
line-height: 1.6; | |
margin-top: 20px; | |
} | |
.back-button-container { | |
position: absolute; | |
bottom: 30px; | |
right: 30px; | |
} | |
.back-btn { | |
background: #6c757d; | |
color: white; | |
border: none; | |
padding: 12px 30px; | |
font-size: 1rem; | |
font-weight: bold; | |
border-radius: 4px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
text-decoration: none; | |
display: inline-block; | |
} | |
.back-btn:hover { | |
background: #5a6268; | |
transform: translateY(-1px); | |
color: white; | |
} | |
@media (max-width: 768px) { | |
.header { | |
padding: 10px 20px; | |
font-size: 1.4rem; | |
} | |
.main-container { | |
padding: 30px 20px; | |
height: calc(100vh - 60px); | |
} | |
.message-content { | |
font-size: 1.1rem; | |
} | |
.back-button-container { | |
bottom: 20px; | |
right: 20px; | |
} | |
.back-btn { | |
padding: 10px 25px; | |
font-size: 0.9rem; | |
} | |
} | |
@media (max-width: 480px) { | |
.header { | |
font-size: 1.2rem; | |
} | |
.main-container { | |
padding: 20px 15px; | |
} | |
.message-content { | |
font-size: 1rem; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="header"> | |
承認済み | |
</div> | |
<div class="main-container"> | |
<div class="message-content"> | |
(中村管理)さんは<br> | |
(中村教員)さんからの申請を承認しました | |
</div> | |
<div class="back-button-container"> | |
<button class="back-btn" onclick="goBack()"> | |
戻る | |
</button> | |
</div> | |
</div> | |
<!-- Bootstrap JS --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script> | |
<script> | |
// Function to go back to previous page | |
function goBack() { | |
// Check if there's a previous page in history | |
if (document.referrer) { | |
window.history.back(); | |
} else { | |
// Default fallback - adjust these URLs based on your routing | |
// Determine user type and redirect accordingly | |
const userType = getUserType(); // You'll need to implement this | |
switch(userType) { | |
case 'admin': | |
window.location.href = '/admin/home'; | |
break; | |
case 'teacher': | |
window.location.href = '/teacher/home'; | |
break; | |
case 'student': | |
window.location.href = '/student/home'; | |
break; | |
default: | |
window.location.href = '/'; | |
} | |
} | |
} | |
// Helper function to determine user type | |
// This is a placeholder - implement based on your authentication system | |
function getUserType() { | |
// You can check URL, session data, or other indicators | |
const path = window.location.pathname; | |
if (path.includes('/admin/')) return 'admin'; | |
if (path.includes('/teacher/')) return 'teacher'; | |
if (path.includes('/student/')) return 'student'; | |
return 'admin'; // default for approval messages | |
} | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>{% block title %}申請完了 - 時間割管理システム{% endblock %}</title> | |
<!-- Bootstrap CSS --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"> | |
<!-- Font Awesome --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<style> | |
body { | |
margin: 0; | |
padding: 0; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
background-color: #f4c2a1; | |
min-height: 100vh; | |
} | |
.header { | |
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); | |
color: white; | |
padding: 15px 30px; | |
font-size: 1.6rem; | |
font-weight: bold; | |
} | |
.main-container { | |
padding: 40px 30px; | |
max-width: 1200px; | |
margin: 0 auto; | |
height: calc(100vh - 70px); | |
overflow: hidden; | |
position: relative; | |
} | |
.request-details { | |
margin-top: 60px; | |
margin-bottom: 40px; | |
} | |
.detail-line { | |
font-size: 1.2rem; | |
color: #333; | |
line-height: 1.8; | |
margin-bottom: 10px; | |
font-weight: 500; | |
} | |
.completion-message { | |
font-size: 1.1rem; | |
color: #333; | |
line-height: 1.6; | |
margin-top: 40px; | |
margin-bottom: 40px; | |
font-weight: 500; | |
} | |
.confirm-btn { | |
background: #f1c40f; | |
color: #333; | |
border: none; | |
padding: 15px 40px; | |
font-size: 1.1rem; | |
font-weight: bold; | |
border-radius: 4px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
text-decoration: none; | |
display: inline-block; | |
} | |
.confirm-btn:hover { | |
background: #d4ac0d; | |
transform: translateY(-1px); | |
color: #333; | |
} | |
@media (max-width: 768px) { | |
.header { | |
padding: 10px 20px; | |
font-size: 1.4rem; | |
} | |
.main-container { | |
padding: 30px 20px; | |
height: calc(100vh - 60px); | |
} | |
.request-details { | |
margin-top: 40px; | |
margin-bottom: 30px; | |
} | |
.detail-line { | |
font-size: 1.1rem; | |
} | |
.completion-message { | |
font-size: 1rem; | |
margin-top: 30px; | |
margin-bottom: 30px; | |
} | |
.confirm-btn { | |
padding: 12px 30px; | |
font-size: 1rem; | |
width: 100%; | |
text-align: center; | |
} | |
} | |
@media (max-width: 480px) { | |
.header { | |
font-size: 1.2rem; | |
} | |
.main-container { | |
padding: 20px 15px; | |
} | |
.detail-line { | |
font-size: 1rem; | |
line-height: 1.6; | |
} | |
.completion-message { | |
font-size: 0.95rem; | |
} | |
.confirm-btn { | |
padding: 10px 25px; | |
font-size: 0.9rem; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="header"> | |
申請完了 | |
</div> | |
<div class="main-container"> | |
<div class="request-details"> | |
<div class="detail-line">クラス:5-5</div> | |
<div class="detail-line">追加:2025/05/20 4時限</div> | |
<div class="detail-line">数学I</div> | |
</div> | |
<div class="completion-message"> | |
申請を管理者に送信しました! | |
</div> | |
<button class="confirm-btn" onclick="confirmAndRedirect()"> | |
確認 | |
</button> | |
</div> | |
<!-- Bootstrap JS --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script> | |
<script> | |
// Function to confirm and redirect | |
function confirmAndRedirect() { | |
// Redirect to appropriate home page | |
const userType = getUserType(); | |
switch(userType) { | |
case 'admin': | |
window.location.href = '/admin/home'; | |
break; | |
case 'teacher': | |
window.location.href = '/teacher/home'; | |
break; | |
case 'student': | |
window.location.href = '/student/home'; | |
break; | |
default: | |
window.location.href = '/teacher/home'; | |
} | |
} | |
// Helper function to determine user type | |
// This is a placeholder - implement based on your authentication system | |
function getUserType() { | |
// You can check URL, session data, or other indicators | |
const path = window.location.pathname; | |
if (path.includes('/admin/')) return 'admin'; | |
if (path.includes('/teacher/')) return 'teacher'; | |
if (path.includes('/student/')) return 'student'; | |
return 'teacher'; // default for request completion pages | |
} | |
// Function to populate completion details dynamically | |
function setCompletionDetails(classInfo, changeType, date, period, subject) { | |
const detailLines = document.querySelectorAll('.detail-line'); | |
if (detailLines.length >= 3) { | |
detailLines[0].textContent = `クラス:${classInfo}`; | |
detailLines[1].textContent = `${changeType}:${date} ${period}時限`; | |
detailLines[2].textContent = subject; | |
} | |
} | |
// Auto-redirect after a certain time (optional) | |
function enableAutoRedirect(seconds = 10) { | |
let timeLeft = seconds; | |
const btn = document.querySelector('.confirm-btn'); | |
const originalText = btn.textContent; | |
const countdown = setInterval(() => { | |
btn.textContent = `確認 (${timeLeft}秒後に自動で戻ります)`; | |
timeLeft--; | |
if (timeLeft < 0) { | |
clearInterval(countdown); | |
confirmAndRedirect(); | |
} | |
}, 1000); | |
// Allow user to click button to cancel auto-redirect | |
btn.addEventListener('click', () => { | |
clearInterval(countdown); | |
btn.textContent = originalText; | |
}); | |
} | |
// Initialize page | |
document.addEventListener('DOMContentLoaded', function() { | |
// Example: setCompletionDetails('5-5', '追加', '2025/05/20', '4', '数学I'); | |
// Example: enableAutoRedirect(10); // Auto-redirect after 10 seconds | |
// You can call these functions with dynamic data from your backend | |
}); | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>{% block title %}確認画面 - 時間割管理システム{% endblock %}</title> | |
<!-- Bootstrap CSS --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"> | |
<!-- Font Awesome --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<style> | |
body { | |
margin: 0; | |
padding: 0; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
background-color: #f4c2a1; | |
min-height: 100vh; | |
} | |
.header { | |
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); | |
color: white; | |
padding: 15px 30px; | |
font-size: 1.6rem; | |
font-weight: bold; | |
} | |
.main-container { | |
padding: 40px 30px; | |
max-width: 1200px; | |
margin: 0 auto; | |
height: calc(100vh - 70px); | |
overflow: hidden; | |
position: relative; | |
} | |
.request-details { | |
margin-top: 60px; | |
margin-bottom: 60px; | |
} | |
.detail-line { | |
font-size: 1.2rem; | |
color: #333; | |
line-height: 1.8; | |
margin-bottom: 10px; | |
font-weight: 500; | |
} | |
.confirmation-text { | |
font-size: 1.1rem; | |
color: #333; | |
line-height: 1.6; | |
margin-top: 40px; | |
margin-bottom: 30px; | |
} | |
.buttons-container { | |
display: flex; | |
gap: 30px; | |
align-items: center; | |
} | |
.submit-btn { | |
background: #f1c40f; | |
color: #333; | |
border: none; | |
padding: 15px 40px; | |
font-size: 1.1rem; | |
font-weight: bold; | |
border-radius: 4px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
text-decoration: none; | |
display: inline-block; | |
} | |
.submit-btn:hover { | |
background: #d4ac0d; | |
transform: translateY(-1px); | |
color: #333; | |
} | |
.back-btn { | |
background: #6c757d; | |
color: white; | |
border: none; | |
padding: 15px 40px; | |
font-size: 1.1rem; | |
font-weight: bold; | |
border-radius: 4px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
text-decoration: none; | |
display: inline-block; | |
} | |
.back-btn:hover { | |
background: #5a6268; | |
transform: translateY(-1px); | |
color: white; | |
} | |
@media (max-width: 768px) { | |
.header { | |
padding: 10px 20px; | |
font-size: 1.4rem; | |
} | |
.main-container { | |
padding: 30px 20px; | |
height: calc(100vh - 60px); | |
} | |
.request-details { | |
margin-top: 40px; | |
margin-bottom: 40px; | |
} | |
.detail-line { | |
font-size: 1.1rem; | |
} | |
.confirmation-text { | |
font-size: 1rem; | |
margin-top: 30px; | |
margin-bottom: 20px; | |
} | |
.buttons-container { | |
flex-direction: column; | |
gap: 15px; | |
align-items: stretch; | |
} | |
.submit-btn, | |
.back-btn { | |
padding: 12px 30px; | |
font-size: 1rem; | |
text-align: center; | |
} | |
} | |
@media (max-width: 480px) { | |
.header { | |
font-size: 1.2rem; | |
} | |
.main-container { | |
padding: 20px 15px; | |
} | |
.detail-line { | |
font-size: 1rem; | |
line-height: 1.6; | |
} | |
.confirmation-text { | |
font-size: 0.95rem; | |
} | |
.submit-btn, | |
.back-btn { | |
padding: 10px 25px; | |
font-size: 0.9rem; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="header"> | |
確認画面 | |
</div> | |
<div class="main-container"> | |
<div class="request-details"> | |
<div class="detail-line">クラス:5-5</div> | |
<div class="detail-line">追加:2025/05/20 4時限</div> | |
<div class="detail-line">数学I</div> | |
</div> | |
<div class="confirmation-text"> | |
以上の変更でよろしいですか。<br> | |
よろしければ下の「申請」ボタンを押下してください。 | |
</div> | |
<div class="buttons-container"> | |
<button class="submit-btn" onclick="submitRequest()"> | |
申請 | |
</button> | |
<button class="back-btn" onclick="goBack()"> | |
戻る | |
</button> | |
</div> | |
</div> | |
<!-- Bootstrap JS --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script> | |
<script> | |
// Function to submit the change request | |
function submitRequest() { | |
// Show loading state | |
const submitBtn = document.querySelector('.submit-btn'); | |
const originalText = submitBtn.textContent; | |
submitBtn.disabled = true; | |
submitBtn.textContent = '送信中...'; | |
// Simulate API call to submit request | |
setTimeout(() => { | |
// Replace with actual submission logic | |
/* | |
fetch('/api/submit-change-request', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
}, | |
body: JSON.stringify({ | |
class: '5-5', | |
type: '追加', | |
date: '2025/05/20', | |
period: '4', | |
subject: '数学I' | |
}) | |
}) | |
.then(response => response.json()) | |
.then(data => { | |
if (data.success) { | |
// Redirect to success page | |
window.location.href = '/request-submitted'; | |
} else { | |
alert('申請の送信に失敗しました。'); | |
resetButton(); | |
} | |
}) | |
.catch(error => { | |
console.error('Error:', error); | |
alert('申請の送信中にエラーが発生しました。'); | |
resetButton(); | |
}); | |
*/ | |
// For demo purposes | |
alert('申請が送信されました!'); | |
// Redirect to teacher home or success page | |
window.location.href = '/teacher/home'; | |
}, 1000); | |
function resetButton() { | |
submitBtn.disabled = false; | |
submitBtn.textContent = originalText; | |
} | |
} | |
// Function to go back to previous page | |
function goBack() { | |
// Check if there's a previous page in history | |
if (document.referrer) { | |
window.history.back(); | |
} else { | |
// Default fallback - adjust these URLs based on your routing | |
// Determine user type and redirect accordingly | |
const userType = getUserType(); // You'll need to implement this | |
switch(userType) { | |
case 'admin': | |
window.location.href = '/admin/home'; | |
break; | |
case 'teacher': | |
window.location.href = '/teacher/home'; | |
break; | |
case 'student': | |
window.location.href = '/student/home'; | |
break; | |
default: | |
window.location.href = '/teacher/home'; | |
} | |
} | |
} | |
// Helper function to determine user type | |
// This is a placeholder - implement based on your authentication system | |
function getUserType() { | |
// You can check URL, session data, or other indicators | |
const path = window.location.pathname; | |
if (path.includes('/admin/')) return 'admin'; | |
if (path.includes('/teacher/')) return 'teacher'; | |
if (path.includes('/student/')) return 'student'; | |
return 'teacher'; // default for confirmation pages | |
} | |
// Function to populate confirmation details dynamically | |
function setConfirmationDetails(classInfo, changeType, date, period, subject) { | |
const detailLines = document.querySelectorAll('.detail-line'); | |
if (detailLines.length >= 3) { | |
detailLines[0].textContent = `クラス:${classInfo}`; | |
detailLines[1].textContent = `${changeType}:${date} ${period}時限`; | |
detailLines[2].textContent = subject; | |
} | |
} | |
// Initialize page | |
document.addEventListener('DOMContentLoaded', function() { | |
// Example: setConfirmationDetails('5-5', '追加', '2025/05/20', '4', '数学I'); | |
// You can call this function with dynamic data from your backend | |
}); | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>削除申請 - 時間割管理システム</title> | |
<!-- Bootstrap CSS --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"> | |
<!-- Font Awesome --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<style> | |
body { | |
margin: 0; | |
padding: 0; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
background-color: #f4c2a1; | |
} | |
.header { | |
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); | |
color: white; | |
padding: 15px 40px; | |
font-size: 1.6rem; | |
font-weight: bold; | |
position: relative; | |
} | |
.back-button { | |
position: absolute; | |
right: 20px; | |
top: 50%; | |
transform: translateY(-50%); | |
background: rgba(255, 255, 255, 0.2); | |
border: 2px solid white; | |
color: white; | |
padding: 8px 15px; | |
border-radius: 5px; | |
text-decoration: none; | |
font-size: 0.9rem; | |
font-weight: bold; | |
transition: all 0.3s ease; | |
} | |
.back-button:hover { | |
background: white; | |
color: #4a90e2; | |
} | |
.main-container { | |
padding: 20px; | |
display: flex; | |
gap: 20px; | |
min-height: calc(100vh - 70px); | |
} | |
.timetable-section { | |
flex: 1; | |
} | |
.timetable-container { | |
background: white; | |
border-radius: 8px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
overflow: hidden; | |
margin-bottom: 20px; | |
} | |
.timetable { | |
width: 100%; | |
border-collapse: collapse; | |
font-size: 0.9rem; | |
} | |
.timetable th { | |
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); | |
color: white; | |
padding: 10px 6px; | |
text-align: center; | |
font-weight: bold; | |
font-size: 0.9rem; | |
} | |
.timetable td { | |
padding: 8px 6px; | |
text-align: center; | |
border: 1px solid #ddd; | |
background: #f8f9fa; | |
font-weight: 500; | |
vertical-align: middle; | |
font-size: 0.85rem; | |
} | |
.timetable td.time-cell { | |
background: #e8e8e8; | |
font-weight: bold; | |
color: #555; | |
font-size: 0.7rem; | |
width: 100px; | |
} | |
.timetable td.subject { | |
background: white; | |
cursor: pointer; | |
transition: all 0.2s ease; | |
position: relative; | |
} | |
.timetable td.subject:hover { | |
background: #ffe6e6; | |
border-color: #ff6b6b; | |
} | |
.timetable td.subject.selected { | |
background: #ffcccc; | |
border: 2px solid #e74c3c; | |
font-weight: bold; | |
} | |
.timetable td.empty { | |
color: #999; | |
font-size: 1.2rem; | |
background: #f0f0f0; | |
} | |
.week-info-container { | |
background: white; | |
border-radius: 8px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
padding: 15px; | |
text-align: center; | |
} | |
.week-info { | |
font-size: 1.1rem; | |
font-weight: bold; | |
color: #333; | |
margin-bottom: 10px; | |
} | |
.instruction { | |
color: #666; | |
font-size: 0.9rem; | |
margin-bottom: 15px; | |
} | |
.controls-section { | |
width: 300px; | |
display: flex; | |
flex-direction: column; | |
gap: 15px; | |
} | |
.control-group { | |
background: white; | |
padding: 20px; | |
border-radius: 8px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
} | |
.selected-info { | |
margin-bottom: 20px; | |
} | |
.selected-item { | |
background: #f8f9fa; | |
border: 1px solid #ddd; | |
border-radius: 5px; | |
padding: 10px; | |
margin-bottom: 10px; | |
position: relative; | |
} | |
.selected-item.empty-state { | |
color: #999; | |
font-style: italic; | |
text-align: center; | |
} | |
.remove-btn { | |
position: absolute; | |
right: 5px; | |
top: 5px; | |
background: #e74c3c; | |
color: white; | |
border: none; | |
border-radius: 3px; | |
width: 20px; | |
height: 20px; | |
font-size: 0.7rem; | |
cursor: pointer; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
} | |
.remove-btn:hover { | |
background: #c0392b; | |
} | |
.reason-section { | |
margin-bottom: 20px; | |
} | |
.form-label { | |
font-weight: bold; | |
margin-bottom: 8px; | |
display: block; | |
} | |
.form-control { | |
width: 100%; | |
padding: 8px 12px; | |
border: 1px solid #ddd; | |
border-radius: 4px; | |
font-size: 0.9rem; | |
} | |
.submit-btn { | |
width: 100%; | |
background: #e74c3c; | |
color: white; | |
border: none; | |
padding: 12px; | |
font-size: 1rem; | |
font-weight: bold; | |
border-radius: 5px; | |
cursor: pointer; | |
transition: background-color 0.3s ease; | |
} | |
.submit-btn:hover:not(:disabled) { | |
background: #c0392b; | |
} | |
.submit-btn:disabled { | |
background: #ccc; | |
cursor: not-allowed; | |
} | |
.clear-btn { | |
width: 100%; | |
background: #95a5a6; | |
color: white; | |
border: none; | |
padding: 10px; | |
font-size: 0.9rem; | |
font-weight: bold; | |
border-radius: 5px; | |
cursor: pointer; | |
margin-bottom: 10px; | |
transition: background-color 0.3s ease; | |
} | |
.clear-btn:hover { | |
background: #7f8c8d; | |
} | |
@media (max-width: 1200px) { | |
.main-container { | |
flex-direction: column; | |
} | |
.controls-section { | |
width: 100%; | |
} | |
} | |
@media (max-width: 768px) { | |
.header { | |
padding: 10px 15px; | |
font-size: 1.3rem; | |
} | |
.back-button { | |
right: 10px; | |
padding: 6px 12px; | |
font-size: 0.8rem; | |
} | |
.main-container { | |
padding: 10px; | |
} | |
.timetable { | |
font-size: 0.7rem; | |
} | |
.timetable th, | |
.timetable td { | |
padding: 6px 3px; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="header"> | |
削除申請 | |
</div> | |
<div class="main-container"> | |
<div class="timetable-section"> | |
<div class="timetable-container"> | |
<table class="timetable"> | |
<thead> | |
<tr> | |
<th class="time-header"></th> | |
<th>月<br>5月20日</th> | |
<th>火<br>5月21日</th> | |
<th>水<br>5月22日</th> | |
<th>木<br>5月23日</th> | |
<th>金<br>5月24日</th> | |
</tr> | |
</thead> | |
<tbody> | |
<tr> | |
<td class="time-cell">08:00<br>~<br>09:30</td> | |
<td class="subject" data-day="月" data-time="1" data-subject="数学I">数学I</td> | |
<td class="subject" data-day="火" data-time="1" data-subject="科学I">科学I</td> | |
<td class="subject" data-day="水" data-time="1" data-subject="数学I">数学I</td> | |
<td class="subject" data-day="木" data-time="1" data-subject="体育I">体育I</td> | |
<td class="subject" data-day="金" data-time="1" data-subject="数学I">数学I</td> | |
</tr> | |
<tr> | |
<td class="time-cell">09:40<br>~<br>11:10</td> | |
<td class="subject" data-day="月" data-time="2" data-subject="言語I">言語I</td> | |
<td class="subject" data-day="火" data-time="2" data-subject="体育I">体育I</td> | |
<td class="empty">-</td> | |
<td class="subject" data-day="木" data-time="2" data-subject="言語I">言語I</td> | |
<td class="subject" data-day="金" data-time="2" data-subject="数学I">数学I</td> | |
</tr> | |
<tr> | |
<td class="time-cell">11:20<br>~<br>12:50</td> | |
<td class="subject" data-day="月" data-time="3" data-subject="体育I">体育I</td> | |
<td class="subject" data-day="火" data-time="3" data-subject="数学I">数学I</td> | |
<td class="subject" data-day="水" data-time="3" data-subject="体育I">体育I</td> | |
<td class="subject" data-day="木" data-time="3" data-subject="科学I">科学I</td> | |
<td class="subject" data-day="金" data-time="3" data-subject="数学I">数学I</td> | |
</tr> | |
<tr> | |
<td class="time-cell">13:40<br>~<br>15:10</td> | |
<td class="empty">-</td> | |
<td class="empty">-</td> | |
<td class="empty">-</td> | |
<td class="subject" data-day="木" data-time="4" data-subject="数学I">数学I</td> | |
<td class="empty">-</td> | |
</tr> | |
</tbody> | |
</table> | |
</div> | |
<div class="week-info-container"> | |
<div class="week-info">第6週目:5月20日~5月24日</div> | |
<div class="instruction">削除したい授業をクリックしてください。</div> | |
</div> | |
</div> | |
<div class="controls-section"> | |
<div class="control-group"> | |
<h5 style="margin-bottom: 15px; color: #333;">選択された授業</h5> | |
<div class="selected-info"> | |
<div id="selectedList"> | |
<div class="selected-item empty-state"> | |
授業が選択されていません | |
</div> | |
</div> | |
</div> | |
<button class="clear-btn" onclick="clearSelection()">選択をクリア</button> | |
<div class="reason-section"> | |
<label class="form-label" for="deleteReason">削除理由</label> | |
<textarea | |
id="deleteReason" | |
class="form-control" | |
rows="4" | |
placeholder="削除理由を入力してください..." | |
maxlength="500" | |
></textarea> | |
<small class="text-muted">残り文字数: <span id="charCount">500</span></small> | |
</div> | |
<button class="submit-btn" id="submitBtn" onclick="submitRequest()" disabled> | |
削除申請を送信 | |
</button> | |
</div> | |
</div> | |
</div> | |
<!-- Bootstrap JS --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script> | |
<script> | |
let selectedClasses = new Set(); | |
document.addEventListener('DOMContentLoaded', function() { | |
// 授業セルクリックイベント | |
document.querySelectorAll('.subject').forEach(cell => { | |
cell.addEventListener('click', function() { | |
const day = this.getAttribute('data-day'); | |
const time = this.getAttribute('data-time'); | |
const subject = this.getAttribute('data-subject'); | |
const key = `${day}-${time}`; | |
if (this.classList.contains('selected')) { | |
// 選択解除 | |
this.classList.remove('selected'); | |
selectedClasses.delete(key); | |
} else { | |
// 選択 | |
this.classList.add('selected'); | |
selectedClasses.add(key); | |
} | |
updateSelectedList(); | |
updateSubmitButton(); | |
}); | |
}); | |
// 削除理由テキストエリアの文字数カウント | |
const reasonTextarea = document.getElementById('deleteReason'); | |
const charCount = document.getElementById('charCount'); | |
reasonTextarea.addEventListener('input', function() { | |
const remaining = 500 - this.value.length; | |
charCount.textContent = remaining; | |
updateSubmitButton(); | |
}); | |
}); | |
function updateSelectedList() { | |
const selectedList = document.getElementById('selectedList'); | |
if (selectedClasses.size === 0) { | |
selectedList.innerHTML = '<div class="selected-item empty-state">授業が選択されていません</div>'; | |
return; | |
} | |
let html = ''; | |
selectedClasses.forEach(key => { | |
const [day, time] = key.split('-'); | |
const cell = document.querySelector(`[data-day="${day}"][data-time="${time}"]`); | |
const subject = cell.getAttribute('data-subject'); | |
const timeSlots = ['08:00-09:30', '09:40-11:10', '11:20-12:50', '13:40-15:10']; | |
const timeSlot = timeSlots[parseInt(time) - 1]; | |
html += ` | |
<div class="selected-item"> | |
${day}曜日 ${timeSlot}<br> | |
<strong>${subject}</strong> | |
<button class="remove-btn" onclick="removeSelection('${key}')">×</button> | |
</div> | |
`; | |
}); | |
selectedList.innerHTML = html; | |
} | |
function removeSelection(key) { | |
const [day, time] = key.split('-'); | |
const cell = document.querySelector(`[data-day="${day}"][data-time="${time}"]`); | |
cell.classList.remove('selected'); | |
selectedClasses.delete(key); | |
updateSelectedList(); | |
updateSubmitButton(); | |
} | |
function clearSelection() { | |
document.querySelectorAll('.subject.selected').forEach(cell => { | |
cell.classList.remove('selected'); | |
}); | |
selectedClasses.clear(); | |
updateSelectedList(); | |
updateSubmitButton(); | |
} | |
function updateSubmitButton() { | |
const submitBtn = document.getElementById('submitBtn'); | |
const reason = document.getElementById('deleteReason').value.trim(); | |
if (selectedClasses.size > 0 && reason.length > 0) { | |
submitBtn.disabled = false; | |
} else { | |
submitBtn.disabled = true; | |
} | |
} | |
function submitRequest() { | |
if (selectedClasses.size === 0) { | |
alert('削除する授業を選択してください。'); | |
return; | |
} | |
const reason = document.getElementById('deleteReason').value.trim(); | |
if (!reason) { | |
alert('削除理由を入力してください。'); | |
return; | |
} | |
// 選択された授業の詳細を取得 | |
let selectedDetails = []; | |
selectedClasses.forEach(key => { | |
const [day, time] = key.split('-'); | |
const cell = document.querySelector(`[data-day="${day}"][data-time="${time}"]`); | |
const subject = cell.getAttribute('data-subject'); | |
const timeSlots = ['08:00-09:30', '09:40-11:10', '11:20-12:50', '13:40-15:10']; | |
const timeSlot = timeSlots[parseInt(time) - 1]; | |
selectedDetails.push({ | |
day: day, | |
time: timeSlot, | |
subject: subject | |
}); | |
}); | |
// 確認ダイアログ | |
let confirmMessage = '以下の授業の削除申請を送信しますか?\n\n'; | |
selectedDetails.forEach(detail => { | |
confirmMessage += `? ${detail.day}曜日 ${detail.time} - ${detail.subject}\n`; | |
}); | |
confirmMessage += `\n削除理由: ${reason}`; | |
if (confirm(confirmMessage)) { | |
// ここで実際のAPI呼び出しを行う | |
console.log('削除申請データ:', { | |
classes: selectedDetails, | |
reason: reason, | |
timestamp: new Date().toISOString() | |
}); | |
alert('削除申請が送信されました。承認状況は「変更申請状態確認」で確認できます。'); | |
// フォームリセット | |
clearSelection(); | |
document.getElementById('deleteReason').value = ''; | |
document.getElementById('charCount').textContent = '500'; | |
updateSubmitButton(); | |
} | |
} | |
function goBack() { | |
if (confirm('入力内容が失われますが、教員画面に戻りますか?')) { | |
// 実際のアプリケーションでは適切なURLに変更 | |
window.history.back(); | |
} | |
} | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>{% block title %}教員ホーム - 時間割管理システム{% endblock %}</title> | |
<!-- Bootstrap CSS --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"> | |
<!-- Font Awesome --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<style> | |
body { | |
margin: 0; | |
padding: 0; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
background-color: #f4c2a1; | |
} | |
.header { | |
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); | |
color: white; | |
padding: 15px 40px; | |
font-size: 1.6rem; | |
font-weight: bold; | |
position: relative; | |
} | |
.main-container { | |
padding: 20px; | |
display: flex; | |
gap: 20px; | |
min-height: calc(100vh - 70px); | |
} | |
.left-section { | |
flex: 1; | |
display: flex; | |
flex-direction: column; | |
gap: 20px; | |
} | |
.right-section { | |
width: 250px; | |
display: flex; | |
flex-direction: column; | |
gap: 10px; | |
} | |
.timetable-container { | |
background: white; | |
border-radius: 8px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
overflow: hidden; | |
} | |
.timetable-header { | |
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); | |
color: white; | |
padding: 15px 20px; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
} | |
.week-nav-btn { | |
background: rgba(255, 255, 255, 0.2); | |
color: white; | |
border: 1px solid rgba(255, 255, 255, 0.3); | |
padding: 8px 12px; | |
border-radius: 4px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
text-decoration: none; | |
font-size: 0.9rem; | |
} | |
.week-nav-btn:hover { | |
background: rgba(255, 255, 255, 0.3); | |
color: white; | |
text-decoration: none; | |
} | |
.week-nav-btn.loading { | |
opacity: 0.6; | |
pointer-events: none; | |
} | |
.week-info-title { | |
font-size: 1.2rem; | |
font-weight: bold; | |
margin: 0; | |
} | |
.timetable { | |
width: 100%; | |
border-collapse: collapse; | |
font-size: 0.9rem; | |
} | |
.timetable th { | |
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); | |
color: white; | |
padding: 10px 6px; | |
text-align: center; | |
font-weight: bold; | |
font-size: 0.9rem; | |
} | |
.timetable td { | |
padding: 8px 6px; | |
text-align: center; | |
border: 1px solid #ddd; | |
background: #f8f9fa; | |
font-weight: 500; | |
vertical-align: middle; | |
font-size: 0.85rem; | |
} | |
.timetable td.time-cell { | |
background: #e8e8e8; | |
font-weight: bold; | |
color: #555; | |
font-size: 0.7rem; | |
width: 100px; | |
} | |
.timetable td.subject { | |
background: white; | |
cursor: pointer; | |
transition: background-color 0.2s ease; | |
} | |
.timetable td.subject:hover { | |
background: #e3f2fd; | |
} | |
.timetable td.empty { | |
color: #999; | |
font-size: 1.2rem; | |
} | |
.timetable-controls { | |
background: white; | |
border-radius: 8px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
padding: 20px; | |
} | |
.controls-title { | |
font-size: 1.2rem; | |
font-weight: bold; | |
color: #333; | |
margin-bottom: 20px; | |
text-align: center; | |
border-bottom: 2px solid #4a90e2; | |
padding-bottom: 10px; | |
} | |
.control-group { | |
margin-bottom: 20px; | |
padding: 15px; | |
background: #f8f9fa; | |
border-radius: 6px; | |
border-left: 4px solid #4a90e2; | |
} | |
.control-group-title { | |
font-size: 1rem; | |
font-weight: bold; | |
color: #333; | |
margin-bottom: 15px; | |
} | |
.form-group { | |
margin-bottom: 15px; | |
} | |
.form-label { | |
display: block; | |
font-size: 0.9rem; | |
font-weight: bold; | |
color: #555; | |
margin-bottom: 5px; | |
} | |
.form-control, .form-select { | |
width: 100%; | |
padding: 8px 12px; | |
font-size: 0.9rem; | |
border: 2px solid #ddd; | |
border-radius: 4px; | |
background-color: white; | |
color: #333; | |
} | |
.form-control:focus, .form-select:focus { | |
outline: none; | |
border-color: #4a90e2; | |
box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.1); | |
} | |
.date-inputs { | |
display: flex; | |
gap: 8px; | |
} | |
.date-inputs .form-control { | |
flex: 1; | |
} | |
.class-inputs { | |
display: flex; | |
gap: 8px; | |
} | |
.class-inputs .form-select { | |
flex: 1; | |
} | |
.update-btn { | |
background-color: #4a90e2; | |
color: white; | |
border: none; | |
padding: 10px 15px; | |
font-size: 0.9rem; | |
font-weight: bold; | |
border-radius: 4px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
width: 100%; | |
} | |
.update-btn:hover { | |
background-color: #357abd; | |
transform: translateY(-1px); | |
} | |
.quick-actions { | |
display: flex; | |
gap: 10px; | |
flex-wrap: wrap; | |
} | |
.quick-action-btn { | |
background: #f1c40f; | |
color: #333; | |
border: none; | |
padding: 8px 12px; | |
font-size: 0.8rem; | |
font-weight: bold; | |
border-radius: 4px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
flex: 1; | |
min-width: 100px; | |
} | |
.quick-action-btn:hover { | |
background: #d4ac0d; | |
transform: translateY(-1px); | |
} | |
.action-buttons { | |
display: flex; | |
flex-direction: column; | |
gap: 10px; | |
} | |
.action-btn { | |
background: #28a745; | |
color: white; | |
border: none; | |
padding: 12px 15px; | |
font-size: 0.9rem; | |
font-weight: bold; | |
border-radius: 4px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
text-align: center; | |
text-decoration: none; | |
} | |
.action-btn:hover { | |
background: #218838; | |
transform: translateY(-1px); | |
color: white; | |
text-decoration: none; | |
} | |
.action-btn.add { | |
background: #007bff; | |
} | |
.action-btn.add:hover { | |
background: #0056b3; | |
} | |
.action-btn.delete { | |
background: #dc3545; | |
} | |
.action-btn.delete:hover { | |
background: #c82333; | |
} | |
.action-btn.secondary { | |
background: #6c757d; | |
} | |
.action-btn.secondary:hover { | |
background: #5a6268; | |
} | |
.action-btn.warning { | |
background: #ffc107; | |
color: #333; | |
} | |
.action-btn.warning:hover { | |
background: #e0a800; | |
color: #333; | |
} | |
.no-data-message { | |
text-align: center; | |
padding: 40px 20px; | |
color: #666; | |
font-size: 1.1rem; | |
} | |
.no-data-message i { | |
font-size: 3rem; | |
color: #ddd; | |
margin-bottom: 15px; | |
} | |
.info-display { | |
background: #e3f2fd; | |
border: 1px solid #bbdefb; | |
padding: 15px; | |
border-radius: 6px; | |
font-size: 0.9rem; | |
color: #1565c0; | |
} | |
.info-display p { | |
margin: 5px 0; | |
} | |
@media (max-width: 1200px) { | |
.main-container { | |
flex-direction: column; | |
padding: 20px; | |
} | |
.right-section { | |
width: 100%; | |
order: -1; | |
} | |
.action-buttons { | |
flex-direction: row; | |
flex-wrap: wrap; | |
} | |
.action-btn { | |
flex: 1; | |
min-width: 200px; | |
} | |
} | |
@media (max-width: 768px) { | |
.header { | |
padding: 10px 15px; | |
font-size: 1.3rem; | |
} | |
.main-container { | |
padding: 10px; | |
} | |
.timetable { | |
font-size: 0.7rem; | |
} | |
.timetable th, | |
.timetable td { | |
padding: 6px 3px; | |
} | |
.week-nav-btn { | |
padding: 6px 8px; | |
font-size: 0.8rem; | |
} | |
.week-info-title { | |
font-size: 1rem; | |
} | |
.action-buttons { | |
flex-direction: column; | |
} | |
.action-btn { | |
min-width: auto; | |
} | |
.quick-actions { | |
flex-direction: column; | |
} | |
.quick-action-btn { | |
min-width: auto; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="header"> | |
<i class="fas fa-chalkboard-teacher"></i> 教員ホーム | |
</div> | |
<div class="main-container"> | |
<!-- 左セクション(時間割表示+コントロール) --> | |
<div class="left-section"> | |
<!-- 時間割表示 --> | |
<div class="timetable-container"> | |
<div class="timetable-header"> | |
<!-- ★★ 前の週ボタン(修正版) ★★ --> | |
<a href="{{ path('app_teacher_home', { | |
'grade': currentGrade, | |
'class': currentClass, | |
'year': currentYear, | |
'month': currentMonth, | |
'day': currentDay, | |
'week_offset': weekOffset - 1 | |
}) }}" class="week-nav-btn" data-direction="prev"> | |
<i class="fas fa-chevron-left"></i> 前の週 | |
</a> | |
<!-- 週情報 --> | |
<div class="week-info-title"> | |
{{ currentGrade }}-{{ currentClass }}クラス 時間割 | |
{% if weekDates|length > 0 %} | |
<br><small>{{ weekDates[0].date }} ~ {{ weekDates[4].date }}</small> | |
{% endif %} | |
</div> | |
<!-- ★★ 次の週ボタン(修正版) ★★ --> | |
<a href="{{ path('app_teacher_home', { | |
'grade': currentGrade, | |
'class': currentClass, | |
'year': currentYear, | |
'month': currentMonth, | |
'day': currentDay, | |
'week_offset': weekOffset + 1 | |
}) }}" class="week-nav-btn" data-direction="next"> | |
次の週 <i class="fas fa-chevron-right"></i> | |
</a> | |
</div> | |
{% if timeTableData %} | |
<table class="timetable"> | |
<thead> | |
<tr> | |
<th>時限</th> | |
{% for date in weekDates %} | |
<th>{{ date.dayName }}<br><small>{{ date.date }}</small></th> | |
{% endfor %} | |
</tr> | |
</thead> | |
<tbody> | |
<tr> | |
<td class="time-cell">1限<br><small>9:00-10:30</small></td> | |
<td class="subject">{{ timeTableData.mon1 ?: '-' }}</td> | |
<td class="subject">{{ timeTableData.tue1 ?: '-' }}</td> | |
<td class="subject">{{ timeTableData.wed1 ?: '-' }}</td> | |
<td class="subject">{{ timeTableData.thu1 ?: '-' }}</td> | |
<td class="subject">{{ timeTableData.fri1 ?: '-' }}</td> | |
</tr> | |
<tr> | |
<td class="time-cell">2限<br><small>10:40-12:10</small></td> | |
<td class="subject">{{ timeTableData.mon2 ?: '-' }}</td> | |
<td class="subject">{{ timeTableData.tue2 ?: '-' }}</td> | |
<td class="subject">{{ timeTableData.wed2 ?: '-' }}</td> | |
<td class="subject">{{ timeTableData.thu2 ?: '-' }}</td> | |
<td class="subject">{{ timeTableData.fri2 ?: '-' }}</td> | |
</tr> | |
<tr> | |
<td class="time-cell">3限<br><small>13:00-14:30</small></td> | |
<td class="subject">{{ timeTableData.mon3 ?: '-' }}</td> | |
<td class="subject">{{ timeTableData.tue3 ?: '-' }}</td> | |
<td class="subject">{{ timeTableData.wed3 ?: '-' }}</td> | |
<td class="subject">{{ timeTableData.thu3 ?: '-' }}</td> | |
<td class="subject">{{ timeTableData.fri3 ?: '-' }}</td> | |
</tr> | |
<tr> | |
<td class="time-cell">4限<br><small>14:40-16:10</small></td> | |
<td class="subject">{{ timeTableData.mon4 ?: '-' }}</td> | |
<td class="subject">{{ timeTableData.tue4 ?: '-' }}</td> | |
<td class="subject">{{ timeTableData.wed4 ?: '-' }}</td> | |
<td class="subject">{{ timeTableData.thu4 ?: '-' }}</td> | |
<td class="subject">{{ timeTableData.fri4 ?: '-' }}</td> | |
</tr> | |
</tbody> | |
</table> | |
{% else %} | |
<div class="no-data-message"> | |
<i class="fas fa-calendar-times"></i> | |
<p>指定された条件の時間割データが見つかりません。</p> | |
<p>別の日付やクラスを選択してください。</p> | |
</div> | |
{% endif %} | |
</div> | |
<!-- 時間割コントロール --> | |
<div class="timetable-controls"> | |
<div class="controls-title"> | |
<i class="fas fa-cog"></i> 時間割表示設定 | |
</div> | |
<!-- 時間割検索フォーム --> | |
<div class="control-group"> | |
<div class="control-group-title"> | |
<i class="fas fa-search"></i> 時間割検索 | |
</div> | |
<form method="get" action="{{ path('app_teacher_home') }}"> | |
<div class="form-group"> | |
<label class="form-label">学年・クラス</label> | |
<div class="class-inputs"> | |
<select name="grade" class="form-select"> | |
{% for g in 1..5 %} | |
<option value="{{ g }}" {{ g == currentGrade ? 'selected' : '' }}>{{ g }}年</option> | |
{% endfor %} | |
</select> | |
<select name="class" class="form-select"> | |
{% for c in 1..5 %} | |
<option value="{{ c }}" {{ c == currentClass ? 'selected' : '' }}>{{ c }}組</option> | |
{% endfor %} | |
</select> | |
</div> | |
</div> | |
<div class="form-group"> | |
<label class="form-label">基準日付</label> | |
<div class="date-inputs"> | |
<input type="number" name="year" class="form-control" | |
value="{{ currentYear }}" min="2020" max="2030" placeholder="年"> | |
<input type="number" name="month" class="form-control" | |
value="{{ currentMonth }}" min="1" max="12" placeholder="月"> | |
<input type="number" name="day" class="form-control" | |
value="{{ currentDay }}" min="1" max="31" placeholder="日"> | |
</div> | |
</div> | |
<input type="hidden" name="week_offset" value="0"> | |
<button type="submit" class="update-btn"> | |
<i class="fas fa-sync-alt"></i> 時間割を表示 | |
</button> | |
</form> | |
</div> | |
<!-- クイックアクション --> | |
<div class="control-group"> | |
<div class="control-group-title"> | |
<i class="fas fa-bolt"></i> クイックアクション | |
</div> | |
<div class="quick-actions"> | |
<button class="quick-action-btn" onclick="goToToday()"> | |
<i class="fas fa-calendar-day"></i> 今日の週 | |
</button> | |
<button class="quick-action-btn" onclick="window.print()"> | |
<i class="fas fa-print"></i> 印刷 | |
</button> | |
<button class="quick-action-btn" onclick="location.reload()"> | |
<i class="fas fa-redo"></i> 更新 | |
</button> | |
</div> | |
</div> | |
<!-- 表示情報 --> | |
<div class="control-group"> | |
<div class="control-group-title"> | |
<i class="fas fa-info-circle"></i> 表示情報 | |
</div> | |
<div class="info-display"> | |
<p><strong>表示中:</strong> {{ currentGrade }}-{{ currentClass }}クラス</p> | |
<p><strong>基準日:</strong> {{ currentYear }}年{{ currentMonth }}月{{ currentDay }}日</p> | |
{% if weekDates|length > 0 %} | |
<p><strong>週間:</strong> {{ weekDates[0].date }} ~ {{ weekDates[4].date }}</p> | |
{% endif %} | |
<p><strong>週オフセット:</strong> {{ weekOffset == 0 ? '今週' : (weekOffset > 0 ? '+' ~ weekOffset ~ '週後' : weekOffset ~ '週前') }}</p> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- 右セクション(変更されたアクションボタン) --> | |
<div class="right-section"> | |
<div class="action-buttons"> | |
<a href="{{ path('app_teacher_add') }}" class="action-btn add"> | |
<i class="fas fa-plus"></i> 追加申請 | |
</a> | |
<a href="{{ path('app_teacher_delete') }}" class="action-btn delete"> | |
<i class="fas fa-minus"></i> 削除申請 | |
</a> | |
<a href="{{ path('app_teacher_see_status') }}" class="action-btn secondary"> | |
<i class="fas fa-clock"></i> 申請中の状態確認 | |
</a> | |
<a href="{{ path('app_teacher_confirm_apply') }}" class="action-btn warning"> | |
<i class="fas fa-list"></i> 授業変更申請一覧 | |
</a> | |
</div> | |
</div> | |
</div> | |
<!-- Bootstrap JS --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script> | |
<script> | |
// 今日の週を表示する関数 | |
function goToToday() { | |
const today = new Date(); | |
const year = today.getFullYear(); | |
const month = today.getMonth() + 1; | |
const day = today.getDate(); | |
const currentGrade = {{ currentGrade }}; | |
const currentClass = {{ currentClass }}; | |
// 週オフセットを0にリセットして今日の週を表示 | |
const url = new URL('{{ path('app_teacher_home') }}', window.location.origin); | |
url.searchParams.set('year', year); | |
url.searchParams.set('month', month); | |
url.searchParams.set('day', day); | |
url.searchParams.set('grade', currentGrade); | |
url.searchParams.set('class', currentClass); | |
url.searchParams.set('week_offset', 0); | |
window.location.href = url.toString(); | |
} | |
// フォームの日付入力値をリアルタイムで検証 | |
document.addEventListener('DOMContentLoaded', function() { | |
const yearInput = document.querySelector('input[name="year"]'); | |
const monthInput = document.querySelector('input[name="month"]'); | |
const dayInput = document.querySelector('input[name="day"]'); | |
// 月が変更されたら日の最大値を調整 | |
monthInput.addEventListener('change', function() { | |
const year = parseInt(yearInput.value); | |
const month = parseInt(this.value); | |
if (year && month) { | |
const daysInMonth = new Date(year, month, 0).getDate(); | |
dayInput.max = daysInMonth; | |
if (parseInt(dayInput.value) > daysInMonth) { | |
dayInput.value = daysInMonth; | |
} | |
} | |
}); | |
// 年が変更されたら2月の日数を調整 | |
yearInput.addEventListener('change', function() { | |
const month = parseInt(monthInput.value); | |
if (month === 2) { | |
monthInput.dispatchEvent(new Event('change')); | |
} | |
}); | |
// 週ナビゲーションボタンのクリック処理を改善 | |
const weekNavButtons = document.querySelectorAll('.week-nav-btn'); | |
weekNavButtons.forEach(button => { | |
button.addEventListener('click', function(e) { | |
// ダブルクリック防止 | |
if (this.classList.contains('loading')) { | |
e.preventDefault(); | |
return false; | |
} | |
this.classList.add('loading'); | |
this.style.opacity = '0.6'; | |
this.style.pointerEvents = 'none'; | |
// 3秒後にロック解除(念のため) | |
setTimeout(() => { | |
this.classList.remove('loading'); | |
this.style.opacity = ''; | |
this.style.pointerEvents = ''; | |
}, 3000); | |
}); | |
}); | |
// キーボードショートカット | |
document.addEventListener('keydown', function(e) { | |
// 既にローディング中の場合は無視 | |
const loadingButtons = document.querySelectorAll('.week-nav-btn.loading'); | |
if (loadingButtons.length > 0) { | |
return; | |
} | |
// Ctrl + ← で前の週 | |
if (e.ctrlKey && e.key === 'ArrowLeft') { | |
e.preventDefault(); | |
const prevWeekBtn = document.querySelector('.week-nav-btn[data-direction="prev"]'); | |
if (prevWeekBtn && !prevWeekBtn.classList.contains('loading')) { | |
prevWeekBtn.click(); | |
} | |
} | |
// Ctrl + → で次の週 | |
if (e.ctrlKey && e.key === 'ArrowRight') { | |
e.preventDefault(); | |
const nextWeekBtn = document.querySelector('.week-nav-btn[data-direction="next"]'); | |
if (nextWeekBtn && !nextWeekBtn.classList.contains('loading')) { | |
nextWeekBtn.click(); | |
} | |
} | |
// Ctrl + H で今日の週 | |
if (e.ctrlKey && e.key === 'h') { | |
e.preventDefault(); | |
goToToday(); | |
} | |
}); | |
// ページ読み込み完了時にローディング状態をクリア | |
window.addEventListener('load', function() { | |
const loadingButtons = document.querySelectorAll('.week-nav-btn.loading'); | |
loadingButtons.forEach(button => { | |
button.classList.remove('loading'); | |
button.style.opacity = ''; | |
button.style.pointerEvents = ''; | |
}); | |
}); | |
}); | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>{% block title %}拒否済み - 時間割管理システム{% endblock %}</title> | |
<!-- Bootstrap CSS --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"> | |
<!-- Font Awesome --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<style> | |
body { | |
margin: 0; | |
padding: 0; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
background-color: #f4c2a1; | |
min-height: 100vh; | |
} | |
.header { | |
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); | |
color: white; | |
padding: 15px 30px; | |
font-size: 1.6rem; | |
font-weight: bold; | |
} | |
.main-container { | |
padding: 40px 30px; | |
max-width: 1200px; | |
margin: 0 auto; | |
height: calc(100vh - 70px); | |
overflow: hidden; | |
position: relative; | |
} | |
.message-content { | |
font-size: 1.2rem; | |
color: #333; | |
line-height: 1.6; | |
margin-top: 20px; | |
} | |
.reason-section { | |
margin-top: 30px; | |
font-size: 1.2rem; | |
color: #333; | |
line-height: 1.6; | |
} | |
.reason-text { | |
display: inline-block; | |
border-bottom: 2px dotted #333; | |
min-width: 300px; | |
padding-bottom: 2px; | |
} | |
.back-button-container { | |
position: absolute; | |
bottom: 30px; | |
right: 30px; | |
} | |
.back-btn { | |
background: #6c757d; | |
color: white; | |
border: none; | |
padding: 12px 30px; | |
font-size: 1rem; | |
font-weight: bold; | |
border-radius: 4px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
text-decoration: none; | |
display: inline-block; | |
} | |
.back-btn:hover { | |
background: #5a6268; | |
transform: translateY(-1px); | |
color: white; | |
} | |
@media (max-width: 768px) { | |
.header { | |
padding: 10px 20px; | |
font-size: 1.4rem; | |
} | |
.main-container { | |
padding: 30px 20px; | |
height: calc(100vh - 60px); | |
} | |
.message-content, | |
.reason-section { | |
font-size: 1.1rem; | |
} | |
.reason-text { | |
min-width: 200px; | |
} | |
.back-button-container { | |
bottom: 20px; | |
right: 20px; | |
} | |
.back-btn { | |
padding: 10px 25px; | |
font-size: 0.9rem; | |
} | |
} | |
@media (max-width: 480px) { | |
.header { | |
font-size: 1.2rem; | |
} | |
.main-container { | |
padding: 20px 15px; | |
} | |
.message-content, | |
.reason-section { | |
font-size: 1rem; | |
} | |
.reason-text { | |
min-width: 150px; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="header"> | |
拒否済み | |
</div> | |
<div class="main-container"> | |
<div class="message-content"> | |
(中村管理)さんは<br> | |
(中村教員)さんからの申請を拒否しました | |
</div> | |
<div class="reason-section"> | |
理由:<span class="reason-text">........................</span> | |
</div> | |
<div class="back-button-container"> | |
<button class="back-btn" onclick="goBack()"> | |
戻る | |
</button> | |
</div> | |
</div> | |
<!-- Bootstrap JS --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script> | |
<script> | |
// Function to go back to previous page | |
function goBack() { | |
// Check if there's a previous page in history | |
if (document.referrer) { | |
window.history.back(); | |
} else { | |
// Default fallback - adjust these URLs based on your routing | |
// Determine user type and redirect accordingly | |
const userType = getUserType(); // You'll need to implement this | |
switch(userType) { | |
case 'admin': | |
window.location.href = '/admin/home'; | |
break; | |
case 'teacher': | |
window.location.href = '/teacher/home'; | |
break; | |
case 'student': | |
window.location.href = '/student/home'; | |
break; | |
default: | |
window.location.href = '/'; | |
} | |
} | |
} | |
// Helper function to determine user type | |
// This is a placeholder - implement based on your authentication system | |
function getUserType() { | |
// You can check URL, session data, or other indicators | |
const path = window.location.pathname; | |
if (path.includes('/admin/')) return 'admin'; | |
if (path.includes('/teacher/')) return 'teacher'; | |
if (path.includes('/student/')) return 'student'; | |
return 'admin'; // default for rejection messages | |
} | |
// Function to populate rejection reason (for dynamic content) | |
function setRejectionReason(reason) { | |
const reasonElement = document.querySelector('.reason-text'); | |
if (reason && reason.trim()) { | |
reasonElement.textContent = reason; | |
} | |
} | |
// Initialize page | |
document.addEventListener('DOMContentLoaded', function() { | |
// Example: setRejectionReason('スケジュールが重複しています'); | |
// You can call this function with dynamic data from your backend | |
}); | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>{% block title %}変更申請状態確認 - 時間割管理システム{% endblock %}</title> | |
<!-- Bootstrap CSS --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"> | |
<!-- Font Awesome --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<style> | |
body { | |
margin: 0; | |
padding: 0; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
background-color: #f4c2a1; | |
min-height: 100vh; | |
} | |
.header { | |
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); | |
color: white; | |
padding: 15px 30px; | |
font-size: 1.6rem; | |
font-weight: bold; | |
} | |
.main-container { | |
padding: 20px 30px; | |
max-width: 1200px; | |
margin: 0 auto; | |
height: calc(100vh - 70px); | |
overflow: hidden; | |
} | |
.instruction-text { | |
font-size: 1rem; | |
color: #333; | |
margin-bottom: 20px; | |
font-weight: 500; | |
} | |
.requests-table-container { | |
background: white; | |
border-radius: 8px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
overflow: hidden; | |
margin-bottom: 15px; | |
height: calc(100vh - 220px); | |
overflow-y: auto; | |
} | |
.requests-table { | |
width: 100%; | |
border-collapse: collapse; | |
font-size: 0.9rem; | |
} | |
.requests-table th { | |
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); | |
color: white; | |
padding: 12px 15px; | |
text-align: center; | |
font-weight: bold; | |
font-size: 1rem; | |
} | |
.requests-table td { | |
padding: 10px 15px; | |
text-align: center; | |
border-bottom: 1px solid #ddd; | |
background: #f8f9fa; | |
font-weight: 500; | |
vertical-align: middle; | |
} | |
.requests-table tbody tr:nth-child(odd) { | |
background: #e9ecef; | |
} | |
.requests-table tbody tr:nth-child(even) { | |
background: #f8f9fa; | |
} | |
.requests-table tbody tr:hover { | |
background: #e3f2fd; | |
cursor: pointer; | |
} | |
.change-type { | |
padding: 4px 10px; | |
border-radius: 4px; | |
font-weight: bold; | |
font-size: 0.85rem; | |
} | |
.change-type.add { | |
background-color: #d4edda; | |
color: #155724; | |
border: 1px solid #c3e6cb; | |
} | |
.change-type.remove { | |
background-color: #f8d7da; | |
color: #721c24; | |
border: 1px solid #f5c6cb; | |
} | |
.status { | |
padding: 4px 10px; | |
border-radius: 4px; | |
font-weight: bold; | |
font-size: 0.85rem; | |
} | |
.status.pending { | |
background-color: #fff3cd; | |
color: #856404; | |
border: 1px solid #ffeaa7; | |
} | |
.status.approved { | |
background-color: #d4edda; | |
color: #155724; | |
border: 1px solid #c3e6cb; | |
} | |
.status.rejected { | |
background-color: #f8d7da; | |
color: #721c24; | |
border: 1px solid #f5c6cb; | |
} | |
.back-button-container { | |
display: flex; | |
justify-content: flex-end; | |
margin-top: 10px; | |
} | |
.back-btn { | |
background: #6c757d; | |
color: white; | |
border: none; | |
padding: 10px 25px; | |
font-size: 0.9rem; | |
font-weight: bold; | |
border-radius: 4px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
text-decoration: none; | |
display: inline-block; | |
} | |
.back-btn:hover { | |
background: #5a6268; | |
transform: translateY(-1px); | |
color: white; | |
} | |
.empty-row { | |
background: #f1f3f4 !important; | |
} | |
.empty-row:hover { | |
background: #f1f3f4 !important; | |
cursor: default; | |
} | |
.empty-cell { | |
color: #999; | |
font-style: italic; | |
} | |
@media (max-width: 768px) { | |
.header { | |
padding: 10px 15px; | |
font-size: 1.3rem; | |
} | |
.main-container { | |
padding: 15px 10px; | |
height: calc(100vh - 60px); | |
} | |
.instruction-text { | |
font-size: 0.9rem; | |
margin-bottom: 15px; | |
} | |
.requests-table-container { | |
height: calc(100vh - 180px); | |
} | |
.requests-table { | |
font-size: 0.75rem; | |
} | |
.requests-table th, | |
.requests-table td { | |
padding: 8px 6px; | |
} | |
.requests-table th { | |
font-size: 0.85rem; | |
} | |
.change-type, | |
.status { | |
padding: 3px 6px; | |
font-size: 0.7rem; | |
} | |
.back-btn { | |
padding: 8px 20px; | |
font-size: 0.8rem; | |
} | |
} | |
@media (max-width: 480px) { | |
.requests-table { | |
font-size: 0.7rem; | |
} | |
.requests-table th, | |
.requests-table td { | |
padding: 6px 4px; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="header"> | |
変更申請状態確認 | |
</div> | |
<div class="main-container"> | |
<div class="instruction-text"> | |
*詳細を確認したい時はリストから選択してください | |
</div> | |
<div class="requests-table-container"> | |
<table class="requests-table"> | |
<thead> | |
<tr> | |
<th>科目名</th> | |
<th>日付</th> | |
<th>クラス</th> | |
<th>時限目</th> | |
<th>変更</th> | |
<th>担当者</th> | |
<th>状況</th> | |
</tr> | |
</thead> | |
<tbody id="requestsTableBody"> | |
<!-- Dummy data - replace with database content --> | |
<tr onclick="showRequestDetails('体育I', '2025/5/21', '5年5組', '2', '追加', '中村教員', '確認中')"> | |
<td>体育I</td> | |
<td>2025/5/21</td> | |
<td>5年5組</td> | |
<td>2</td> | |
<td><span class="change-type add">追加</span></td> | |
<td>中村教員</td> | |
<td><span class="status pending">確認中</span></td> | |
</tr> | |
<tr onclick="showRequestDetails('英語I', '2025/5/22', '5年5組', '4', '削除', '中村教員', '承認済み')"> | |
<td>英語I</td> | |
<td>2025/5/22</td> | |
<td>5年5組</td> | |
<td>4</td> | |
<td><span class="change-type remove">削除</span></td> | |
<td>中村教員</td> | |
<td><span class="status approved">承認済み</span></td> | |
</tr> | |
<tr onclick="showRequestDetails('英語I', '2025/5/23', '5年5組', '2', '削除', '中村教員', '承認済み')"> | |
<td>英語I</td> | |
<td>2025/5/23</td> | |
<td>5年5組</td> | |
<td>2</td> | |
<td><span class="change-type remove">削除</span></td> | |
<td>中村教員</td> | |
<td><span class="status approved">承認済み</span></td> | |
</tr> | |
<tr onclick="showRequestDetails('英語I', '2025/5/21', '5年5組', '2', '追加', '中村教員', '拒否')"> | |
<td>英語I</td> | |
<td>2025/5/21</td> | |
<td>5年5組</td> | |
<td>2</td> | |
<td><span class="change-type add">追加</span></td> | |
<td>中村教員</td> | |
<td><span class="status rejected">拒否</span></td> | |
</tr> | |
<tr onclick="showRequestDetails('数学I', '2025/5/24', '5年5組', '3', '追加', '田中教員', '確認中')"> | |
<td>数学I</td> | |
<td>2025/5/24</td> | |
<td>5年5組</td> | |
<td>3</td> | |
<td><span class="change-type add">追加</span></td> | |
<td>田中教員</td> | |
<td><span class="status pending">確認中</span></td> | |
</tr> | |
<!-- Empty rows for spacing --> | |
<tr class="empty-row"> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
</tr> | |
<tr class="empty-row"> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
<td class="empty-cell"> </td> | |
</tr> | |
</tbody> | |
</table> | |
</div> | |
<div class="back-button-container"> | |
<button class="back-btn" onclick="goBack()"> | |
戻る | |
</button> | |
</div> | |
</div> | |
<!-- Bootstrap JS --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script> | |
<script> | |
// Function to show request details when row is clicked (placeholder) | |
function showRequestDetails(subject, date, className, period, changeType, teacher, status) { | |
// Placeholder for future navigation functionality | |
alert(`詳細情報:\n科目: ${subject}\n日付: ${date}\nクラス: ${className}\n時限: ${period}時限目\n変更: ${changeType}\n担当者: ${teacher}\n状況: ${status}`); | |
// TODO: Implement navigation to detail page | |
// Example: window.location.href = `/request-details/${requestId}`; | |
} | |
// Function to go back to previous page | |
function goBack() { | |
// Check if there's a previous page in history | |
if (document.referrer) { | |
window.history.back(); | |
} else { | |
// Default fallback - adjust these URLs based on your routing | |
// Determine user type and redirect accordingly | |
const userType = getUserType(); // You'll need to implement this | |
switch(userType) { | |
case 'admin': | |
window.location.href = '/admin/home'; | |
break; | |
case 'teacher': | |
window.location.href = '/teacher/home'; | |
break; | |
case 'student': | |
window.location.href = '/student/home'; | |
break; | |
default: | |
window.location.href = '/'; | |
} | |
} | |
} | |
// Helper function to determine user type | |
// This is a placeholder - implement based on your authentication system | |
function getUserType() { | |
// You can check URL, session data, or other indicators | |
const path = window.location.pathname; | |
if (path.includes('/admin/')) return 'admin'; | |
if (path.includes('/teacher/')) return 'teacher'; | |
if (path.includes('/student/')) return 'student'; | |
return 'teacher'; // default for request status pages | |
} | |
// Function to load requests from database (placeholder) | |
function loadRequestsFromDatabase() { | |
// This is where you'll implement the actual database loading | |
// Example AJAX call structure: | |
/* | |
fetch('/api/request-status') | |
.then(response => response.json()) | |
.then(data => { | |
updateRequestsTable(data); | |
}) | |
.catch(error => { | |
console.error('Error loading requests:', error); | |
}); | |
*/ | |
} | |
// Function to update table with real data (placeholder) | |
function updateRequestsTable(requestsData) { | |
// This function will replace the dummy data with real database data | |
const tbody = document.getElementById('requestsTableBody'); | |
tbody.innerHTML = ''; // Clear existing content | |
requestsData.forEach(request => { | |
const row = document.createElement('tr'); | |
row.onclick = () => showRequestDetails( | |
request.subject, | |
request.date, | |
request.className, | |
request.period, | |
request.changeType, | |
request.teacher, | |
request.status | |
); | |
const changeTypeClass = getChangeTypeClass(request.changeType); | |
const statusClass = getStatusClass(request.status); | |
row.innerHTML = ` | |
<td>${request.subject}</td> | |
<td>${request.date}</td> | |
<td>${request.className}</td> | |
<td>${request.period}</td> | |
<td><span class="change-type ${changeTypeClass}">${request.changeType}</span></td> | |
<td>${request.teacher}</td> | |
<td><span class="status ${statusClass}">${request.status}</span></td> | |
`; | |
tbody.appendChild(row); | |
}); | |
} | |
// Helper function to get CSS class for change type | |
function getChangeTypeClass(changeType) { | |
switch(changeType) { | |
case '追加': return 'add'; | |
case '削除': return 'remove'; | |
default: return 'add'; | |
} | |
} | |
// Helper function to get CSS class for status | |
function getStatusClass(status) { | |
switch(status) { | |
case '確認中': return 'pending'; | |
case '承認済み': return 'approved'; | |
case '拒否': return 'rejected'; | |
default: return 'pending'; | |
} | |
} | |
// Initialize page | |
document.addEventListener('DOMContentLoaded', function() { | |
// loadRequestsFromDatabase(); // Uncomment when ready to load from database | |
}); | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Annotation\Route; | |
class TeacherAddController extends AbstractController | |
{ | |
#[Route('/teacher/add', name: 'app_teacher_add')] | |
public function teacher_add(): Response | |
{ | |
return $this->render('teacher_add/teacher_add.html.twig', [ | |
'controller_name' => 'TeacherAddController', | |
]); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Annotation\Route; | |
class TeacherApplyController extends AbstractController | |
{ | |
#[Route('/teacher/apply', name: 'app_teacher_apply')] | |
public function teacher_apply(): Response | |
{ | |
return $this->render('teacher_apply/teacher_apply.html.twig', [ | |
'controller_name' => 'TeacherApplyController', | |
]); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Annotation\Route; | |
class TeacherApprovedController extends AbstractController | |
{ | |
#[Route('/teacher/approved', name: 'app_teacher_approved')] | |
public function teacher_approved(): Response | |
{ | |
return $this->render('teacher_approved/teacher_approved.html.twig', [ | |
'controller_name' => 'TeacherApprovedController', | |
]); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Annotation\Route; | |
class TeacherCompleteController extends AbstractController | |
{ | |
#[Route('/teacher/complete', name: 'app_teacher_complete')] | |
public function teacher_complete(): Response | |
{ | |
return $this->render('teacher_complete/teacher_complete.html.twig', [ | |
'controller_name' => 'TeacherCompleteController', | |
]); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Annotation\Route; | |
class TeacherConfirmApplyController extends AbstractController | |
{ | |
#[Route('/teacher/confirm/apply', name: 'app_teacher_confirm_apply')] | |
public function teacher_confirm_apply(): Response | |
{ | |
return $this->render('teacher_confirm_apply/teacher_confirm_apply.html.twig', [ | |
'controller_name' => 'TeacherConfirmApplyController', | |
]); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Annotation\Route; | |
class TeacherDeleteController extends AbstractController | |
{ | |
#[Route('/teacher/delete', name: 'app_teacher_delete')] | |
public function teacher_delete(): Response | |
{ | |
return $this->render('teacher_delete/teacher_delete.html.twig', [ | |
'controller_name' => 'TeacherDeleteController', | |
]); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use App\Entity\CompleteTimeTable; | |
use App\Security\AccessControlVoter; | |
use Doctrine\ORM\EntityManagerInterface; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\Request; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Annotation\Route; | |
class TeacherHomeController extends AbstractController | |
{ | |
public function __construct( | |
private EntityManagerInterface $entityManager | |
) {} | |
#[Route('/teacher/home', name: 'app_teacher_home')] | |
public function teacher_home(Request $request): Response | |
{ | |
$this->denyAccessUnlessGranted(AccessControlVoter::ACCESS_TEACHER); | |
// 現在の日付を取得 | |
$today = new \DateTime(); | |
// リクエストパラメータから値を取得(デフォルトは今日の日付) | |
$year = (int)($request->query->get('year', $today->format('Y'))); | |
$month = (int)($request->query->get('month', $today->format('n'))); | |
$day = (int)($request->query->get('day', $today->format('j'))); | |
$grade = (int)($request->query->get('grade', 5)); | |
$class = (int)($request->query->get('class', 5)); | |
// ★★ 週の移動処理(修正版) ★★ | |
$weekOffset = (int)($request->query->get('week_offset', 0)); | |
try { | |
// 指定された日付を作成 | |
$targetDate = new \DateTime(); | |
$targetDate->setDate($year, $month, $day); | |
// ★★ 週のオフセットを適用(修正版) ★★ | |
if ($weekOffset !== 0) { | |
$targetDate->modify($weekOffset > 0 ? "+{$weekOffset} weeks" : $weekOffset . " weeks"); | |
} | |
// その週の月曜日を取得 | |
$mondayOfWeek = $this->getMondayOfWeek($targetDate); | |
// クラス名を生成 | |
$className = "{$grade}-{$class}"; | |
// 時間割データを取得 | |
$timeTableData = $this->getWeekTimeTable($className, $mondayOfWeek); | |
// 週の日付情報を生成 | |
$weekDates = $this->generateWeekDates($mondayOfWeek); | |
// ★★ 実際に表示される週の年月日を取得(基準日は変更しない) ★★ | |
$displayYear = $year; // 基準日の年を保持 | |
$displayMonth = $month; // 基準日の月を保持 | |
$displayDay = $day; // 基準日の日を保持 | |
} catch (\Exception $e) { | |
error_log('[ERROR] TeacherHome: ' . $e->getMessage()); | |
// エラーの場合は今日の週を表示 | |
$mondayOfWeek = $this->getMondayOfWeek($today); | |
$className = "{$grade}-{$class}"; | |
$timeTableData = $this->getWeekTimeTable($className, $mondayOfWeek); | |
$weekDates = $this->generateWeekDates($mondayOfWeek); | |
$displayYear = $year; | |
$displayMonth = $month; | |
$displayDay = $day; | |
$weekOffset = 0; // エラー時はオフセットをリセット | |
} | |
return $this->render('teacher_home/teacher_home.html.twig', [ | |
'controller_name' => 'TeacherHomeController', | |
'timeTableData' => $timeTableData, | |
'weekDates' => $weekDates, | |
'currentYear' => $displayYear, // 基準日の年 | |
'currentMonth' => $displayMonth, // 基準日の月 | |
'currentDay' => $displayDay, // 基準日の日 | |
'currentGrade' => $grade, | |
'currentClass' => $class, | |
'weekOffset' => $weekOffset, | |
]); | |
} | |
private function getMondayOfWeek(\DateTime $date): \DateTime | |
{ | |
$monday = clone $date; | |
$dayOfWeek = (int)$monday->format('N'); // 1=月曜日, 7=日曜日 | |
if ($dayOfWeek !== 1) { | |
// 月曜日でない場合は、その週の月曜日に移動 | |
$daysToSubtract = $dayOfWeek - 1; | |
$monday->modify("-{$daysToSubtract} days"); | |
} | |
return $monday; | |
} | |
private function getWeekTimeTable(string $className, \DateTime $mondayDate): ?CompleteTimeTable | |
{ | |
$year = (int)$mondayDate->format('Y'); | |
$month = (int)$mondayDate->format('n'); | |
$day = (int)$mondayDate->format('j'); | |
// 指定された週の時間割データを検索 | |
$timeTable = $this->entityManager | |
->getRepository(CompleteTimeTable::class) | |
->createQueryBuilder('c') | |
->where('c.className = :className') | |
->andWhere('c.year = :year') | |
->andWhere('c.month = :month') | |
->andWhere('c.mondayDate = :day') | |
->setParameter('className', $className) | |
->setParameter('year', $year) | |
->setParameter('month', $month) | |
->setParameter('day', $day) | |
->getQuery() | |
->getOneOrNullResult(); | |
return $timeTable; | |
} | |
private function generateWeekDates(\DateTime $mondayDate): array | |
{ | |
$dates = []; | |
$currentDate = clone $mondayDate; | |
$dayNames = ['月', '火', '水', '木', '金']; | |
for ($i = 0; $i < 5; $i++) { | |
$dates[] = [ | |
'dayName' => $dayNames[$i], | |
'date' => $currentDate->format('n月j日'), | |
'fullDate' => $currentDate->format('Y-m-d'), | |
]; | |
$currentDate->modify('+1 day'); | |
} | |
return $dates; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Annotation\Route; | |
class TeacherRejectedController extends AbstractController | |
{ | |
#[Route('/teacher/rejected', name: 'app_teacher_rejected')] | |
public function teacher_rejected(): Response | |
{ | |
return $this->render('teacher_rejected/teacher_rejected.html.twig', [ | |
'controller_name' => 'TeacherRejectedController', | |
]); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Annotation\Route; | |
class TeacherSeeStatusController extends AbstractController | |
{ | |
#[Route('/teacher/see/status', name: 'app_teacher_see_status')] | |
public function teacher_see_status(): Response | |
{ | |
return $this->render('teacher_see_status/teacher_see_status.html.twig', [ | |
'controller_name' => 'TeacherSeeStatusController', | |
]); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Entity; | |
use App\Repository\TimeTableRepository; | |
use Doctrine\ORM\Mapping as ORM; | |
#[ORM\Entity(repositoryClass: TimeTableRepository::class)] | |
#[ORM\Table(name: 'time_table')] | |
class TimeTable | |
{ | |
#[ORM\Id] | |
#[ORM\Column(length: 10)] | |
private ?string $className = null; | |
#[ORM\Column(length: 10)] | |
private ?string $day = null; | |
#[ORM\Column] | |
private ?int $period = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $mon1 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $mon2 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $mon3 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $mon4 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $tue1 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $tue2 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $tue3 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $tue4 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $wed1 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $wed2 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $wed3 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $wed4 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $thu1 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $thu2 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $thu3 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $thu4 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $fri1 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $fri2 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $fri3 = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $fri4 = null; | |
public function getClassName(): ?string | |
{ | |
return $this->className; | |
} | |
public function setClassName(string $className): static | |
{ | |
$this->className = $className; | |
return $this; | |
} | |
public function getDay(): ?string | |
{ | |
return $this->day; | |
} | |
public function setDay(string $day): static | |
{ | |
$this->day = $day; | |
return $this; | |
} | |
public function getPeriod(): ?int | |
{ | |
return $this->period; | |
} | |
public function setPeriod(int $period): static | |
{ | |
$this->period = $period; | |
return $this; | |
} | |
public function getMon1(): ?string | |
{ | |
return $this->mon1; | |
} | |
public function setMon1(?string $mon1): static | |
{ | |
$this->mon1 = $mon1; | |
return $this; | |
} | |
public function getMon2(): ?string | |
{ | |
return $this->mon2; | |
} | |
public function setMon2(?string $mon2): static | |
{ | |
$this->mon2 = $mon2; | |
return $this; | |
} | |
public function getMon3(): ?string | |
{ | |
return $this->mon3; | |
} | |
public function setMon3(?string $mon3): static | |
{ | |
$this->mon3 = $mon3; | |
return $this; | |
} | |
public function getMon4(): ?string | |
{ | |
return $this->mon4; | |
} | |
public function setMon4(?string $mon4): static | |
{ | |
$this->mon4 = $mon4; | |
return $this; | |
} | |
public function getTue1(): ?string | |
{ | |
return $this->tue1; | |
} | |
public function setTue1(?string $tue1): static | |
{ | |
$this->tue1 = $tue1; | |
return $this; | |
} | |
public function getTue2(): ?string | |
{ | |
return $this->tue2; | |
} | |
public function setTue2(?string $tue2): static | |
{ | |
$this->tue2 = $tue2; | |
return $this; | |
} | |
public function getTue3(): ?string | |
{ | |
return $this->tue3; | |
} | |
public function setTue3(?string $tue3): static | |
{ | |
$this->tue3 = $tue3; | |
return $this; | |
} | |
public function getTue4(): ?string | |
{ | |
return $this->tue4; | |
} | |
public function setTue4(?string $tue4): static | |
{ | |
$this->tue4 = $tue4; | |
return $this; | |
} | |
public function getWed1(): ?string | |
{ | |
return $this->wed1; | |
} | |
public function setWed1(?string $wed1): static | |
{ | |
$this->wed1 = $wed1; | |
return $this; | |
} | |
public function getWed2(): ?string | |
{ | |
return $this->wed2; | |
} | |
public function setWed2(?string $wed2): static | |
{ | |
$this->wed2 = $wed2; | |
return $this; | |
} | |
public function getWed3(): ?string | |
{ | |
return $this->wed3; | |
} | |
public function setWed3(?string $wed3): static | |
{ | |
$this->wed3 = $wed3; | |
return $this; | |
} | |
public function getWed4(): ?string | |
{ | |
return $this->wed4; | |
} | |
public function setWed4(?string $wed4): static | |
{ | |
$this->wed4 = $wed4; | |
return $this; | |
} | |
public function getThu1(): ?string | |
{ | |
return $this->thu1; | |
} | |
public function setThu1(?string $thu1): static | |
{ | |
$this->thu1 = $thu1; | |
return $this; | |
} | |
public function getThu2(): ?string | |
{ | |
return $this->thu2; | |
} | |
public function setThu2(?string $thu2): static | |
{ | |
$this->thu2 = $thu2; | |
return $this; | |
} | |
public function getThu3(): ?string | |
{ | |
return $this->thu3; | |
} | |
public function setThu3(?string $thu3): static | |
{ | |
$this->thu3 = $thu3; | |
return $this; | |
} | |
public function getThu4(): ?string | |
{ | |
return $this->thu4; | |
} | |
public function setThu4(?string $thu4): static | |
{ | |
$this->thu4 = $thu4; | |
return $this; | |
} | |
public function getFri1(): ?string | |
{ | |
return $this->fri1; | |
} | |
public function setFri1(?string $fri1): static | |
{ | |
$this->fri1 = $fri1; | |
return $this; | |
} | |
public function getFri2(): ?string | |
{ | |
return $this->fri2; | |
} | |
public function setFri2(?string $fri2): static | |
{ | |
$this->fri2 = $fri2; | |
return $this; | |
} | |
public function getFri3(): ?string | |
{ | |
return $this->fri3; | |
} | |
public function setFri3(?string $fri3): static | |
{ | |
$this->fri3 = $fri3; | |
return $this; | |
} | |
public function getFri4(): ?string | |
{ | |
return $this->fri4; | |
} | |
public function setFri4(?string $fri4): static | |
{ | |
$this->fri4 = $fri4; | |
return $this; | |
} | |
/** | |
* 指定された曜日・時限の科目を取得 | |
*/ | |
public function getSubjectByDayAndPeriod(string $day, int $period): ?string | |
{ | |
$property = strtolower($day) . $period; | |
$getter = 'get' . ucfirst($property); | |
if (method_exists($this, $getter)) { | |
return $this->$getter(); | |
} | |
return null; | |
} | |
/** | |
* 指定された曜日・時限の科目を設定 | |
*/ | |
public function setSubjectByDayAndPeriod(string $day, int $period, ?string $subject): static | |
{ | |
$property = strtolower($day) . $period; | |
$setter = 'set' . ucfirst($property); | |
if (method_exists($this, $setter)) { | |
$this->$setter($subject); | |
} | |
return $this; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Controller; | |
use App\Entity\TimeTable; | |
use App\Security\AccessControlVoter; | |
use Doctrine\ORM\EntityManagerInterface; | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
use Symfony\Component\HttpFoundation\JsonResponse; | |
use Symfony\Component\HttpFoundation\Request; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\Routing\Annotation\Route; | |
class TimeTableController extends AbstractController | |
{ | |
public function __construct( | |
private EntityManagerInterface $entityManager | |
) {} | |
#[Route('/admin/timetable', name: 'app_admin_timetable')] | |
public function index(): Response | |
{ | |
$this->denyAccessUnlessGranted(AccessControlVoter::ACCESS_ADMIN); | |
$timeTables = $this->entityManager | |
->getRepository(TimeTable::class) | |
->findAll(); | |
return $this->render('admin/timetable/index.html.twig', [ | |
'timeTables' => $timeTables, | |
]); | |
} | |
#[Route('/admin/timetable/{className}', name: 'app_admin_timetable_show')] | |
public function show(string $className): Response | |
{ | |
$this->denyAccessUnlessGranted(AccessControlVoter::ACCESS_ADMIN); | |
$timeTable = $this->entityManager | |
->getRepository(TimeTable::class) | |
->find($className); | |
if (!$timeTable) { | |
throw $this->createNotFoundException('時間割が見つかりません。'); | |
} | |
return $this->render('admin/timetable/show.html.twig', [ | |
'timeTable' => $timeTable, | |
]); | |
} | |
#[Route('/admin/timetable/{className}/edit', name: 'app_admin_timetable_edit')] | |
public function edit(Request $request, string $className): Response | |
{ | |
$this->denyAccessUnlessGranted(AccessControlVoter::ACCESS_ADMIN); | |
$timeTable = $this->entityManager | |
->getRepository(TimeTable::class) | |
->find($className); | |
if (!$timeTable) { | |
throw $this->createNotFoundException('時間割が見つかりません。'); | |
} | |
if ($request->isMethod('POST')) { | |
// フォームデータから時間割を更新 | |
$this->updateTimeTableFromRequest($timeTable, $request); | |
$this->entityManager->persist($timeTable); | |
$this->entityManager->flush(); | |
$this->addFlash('success', "クラス {$className} の時間割を更新しました。"); | |
return $this->redirectToRoute('app_admin_timetable_show', ['className' => $className]); | |
} | |
return $this->render('admin/timetable/edit.html.twig', [ | |
'timeTable' => $timeTable, | |
]); | |
} | |
#[Route('/admin/timetable/create', name: 'app_admin_timetable_create')] | |
public function create(Request $request): Response | |
{ | |
$this->denyAccessUnlessGranted(AccessControlVoter::ACCESS_ADMIN); | |
if ($request->isMethod('POST')) { | |
$className = $request->request->get('className'); | |
// 既存チェック | |
$existingTimeTable = $this->entityManager | |
->getRepository(TimeTable::class) | |
->find($className); | |
if ($existingTimeTable) { | |
$this->addFlash('error', "クラス {$className} の時間割は既に存在します。"); | |
return $this->render('admin/timetable/create.html.twig'); | |
} | |
$timeTable = new TimeTable(); | |
$timeTable->setClassName($className); | |
$timeTable->setDay('週間'); // デフォルト値 | |
$timeTable->setPeriod(4); // デフォルト値 | |
$this->updateTimeTableFromRequest($timeTable, $request); | |
$this->entityManager->persist($timeTable); | |
$this->entityManager->flush(); | |
$this->addFlash('success', "クラス {$className} の時間割を作成しました。"); | |
return $this->redirectToRoute('app_admin_timetable_show', ['className' => $className]); | |
} | |
return $this->render('admin/timetable/create.html.twig'); | |
} | |
#[Route('/admin/timetable/{className}/delete', name: 'app_admin_timetable_delete', methods: ['POST'])] | |
public function delete(string $className): JsonResponse | |
{ | |
$this->denyAccessUnlessGranted(AccessControlVoter::ACCESS_ADMIN); | |
try { | |
$timeTable = $this->entityManager | |
->getRepository(TimeTable::class) | |
->find($className); | |
if (!$timeTable) { | |
return new JsonResponse([ | |
'success' => false, | |
'message' => '時間割が見つかりません。' | |
], 404); | |
} | |
$this->entityManager->remove($timeTable); | |
$this->entityManager->flush(); | |
return new JsonResponse([ | |
'success' => true, | |
'message' => "クラス {$className} の時間割を削除しました。" | |
]); | |
} catch (\Exception $e) { | |
return new JsonResponse([ | |
'success' => false, | |
'message' => '削除中にエラーが発生しました: ' . $e->getMessage() | |
], 500); | |
} | |
} | |
#[Route('/api/timetable/{className}', name: 'api_timetable_get', methods: ['GET'])] | |
public function getTimeTable(string $className): JsonResponse | |
{ | |
$this->denyAccessUnlessGranted(AccessControlVoter::ACCESS_STUDENT); | |
$timeTable = $this->entityManager | |
->getRepository(TimeTable::class) | |
->find($className); | |
if (!$timeTable) { | |
return new JsonResponse([ | |
'success' => false, | |
'message' => '時間割が見つかりません。' | |
], 404); | |
} | |
return new JsonResponse([ | |
'success' => true, | |
'data' => [ | |
'className' => $timeTable->getClassName(), | |
'day' => $timeTable->getDay(), | |
'period' => $timeTable->getPeriod(), | |
'schedule' => [ | |
'monday' => [ | |
$timeTable->getMon1(), | |
$timeTable->getMon2(), | |
$timeTable->getMon3(), | |
$timeTable->getMon4() | |
], | |
'tuesday' => [ | |
$timeTable->getTue1(), | |
$timeTable->getTue2(), | |
$timeTable->getTue3(), | |
$timeTable->getTue4() | |
], | |
'wednesday' => [ | |
$timeTable->getWed1(), | |
$timeTable->getWed2(), | |
$timeTable->getWed3(), | |
$timeTable->getWed4() | |
], | |
'thursday' => [ | |
$timeTable->getThu1(), | |
$timeTable->getThu2(), | |
$timeTable->getThu3(), | |
$timeTable->getThu4() | |
], | |
'friday' => [ | |
$timeTable->getFri1(), | |
$timeTable->getFri2(), | |
$timeTable->getFri3(), | |
$timeTable->getFri4() | |
] | |
] | |
] | |
]); | |
} | |
private function updateTimeTableFromRequest(TimeTable $timeTable, Request $request): void | |
{ | |
$timeTable->setDay($request->request->get('day', '週間')); | |
$timeTable->setPeriod((int)$request->request->get('period', 4)); | |
// 月曜日 | |
$timeTable->setMon1($this->nullIfEmpty($request->request->get('mon1'))); | |
$timeTable->setMon2($this->nullIfEmpty($request->request->get('mon2'))); | |
$timeTable->setMon3($this->nullIfEmpty($request->request->get('mon3'))); | |
$timeTable->setMon4($this->nullIfEmpty($request->request->get('mon4'))); | |
// 火曜日 | |
$timeTable->setTue1($this->nullIfEmpty($request->request->get('tue1'))); | |
$timeTable->setTue2($this->nullIfEmpty($request->request->get('tue2'))); | |
$timeTable->setTue3($this->nullIfEmpty($request->request->get('tue3'))); | |
$timeTable->setTue4($this->nullIfEmpty($request->request->get('tue4'))); | |
// 水曜日 | |
$timeTable->setWed1($this->nullIfEmpty($request->request->get('wed1'))); | |
$timeTable->setWed2($this->nullIfEmpty($request->request->get('wed2'))); | |
$timeTable->setWed3($this->nullIfEmpty($request->request->get('wed3'))); | |
$timeTable->setWed4($this->nullIfEmpty($request->request->get('wed4'))); | |
// 木曜日 | |
$timeTable->setThu1($this->nullIfEmpty($request->request->get('thu1'))); | |
$timeTable->setThu2($this->nullIfEmpty($request->request->get('thu2'))); | |
$timeTable->setThu3($this->nullIfEmpty($request->request->get('thu3'))); | |
$timeTable->setThu4($this->nullIfEmpty($request->request->get('thu4'))); | |
// 金曜日 | |
$timeTable->setFri1($this->nullIfEmpty($request->request->get('fri1'))); | |
$timeTable->setFri2($this->nullIfEmpty($request->request->get('fri2'))); | |
$timeTable->setFri3($this->nullIfEmpty($request->request->get('fri3'))); | |
$timeTable->setFri4($this->nullIfEmpty($request->request->get('fri4'))); | |
} | |
private function nullIfEmpty(?string $value): ?string | |
{ | |
return empty(trim($value ?? '')) ? null : trim($value); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\DataFixtures; | |
use App\Entity\TimeTable; | |
use Doctrine\Bundle\FixturesBundle\Fixture; | |
use Doctrine\Persistence\ObjectManager; | |
class TimeTableFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager): void | |
{ | |
// 1-1~5-5までの全クラスを作成 | |
for ($grade = 1; $grade <= 5; $grade++) { | |
for ($class = 1; $class <= 5; $class++) { | |
$className = $grade . '-' . $class; | |
// 既存のクラスがあるかチェック | |
$existingTimeTable = $manager->getRepository(TimeTable::class) | |
->findOneBy(['className' => $className]); | |
if (!$existingTimeTable) { | |
$timeTable = new TimeTable(); | |
$timeTable->setClassName($className); | |
$timeTable->setDay('平日'); | |
$timeTable->setPeriod(1); | |
// 全ての時限に"not found"を設定 | |
$timeTable->setMon1('not found'); | |
$timeTable->setMon2('not found'); | |
$timeTable->setMon3('not found'); | |
$timeTable->setMon4('not found'); | |
$timeTable->setTue1('not found'); | |
$timeTable->setTue2('not found'); | |
$timeTable->setTue3('not found'); | |
$timeTable->setTue4('not found'); | |
$timeTable->setWed1('not found'); | |
$timeTable->setWed2('not found'); | |
$timeTable->setWed3('not found'); | |
$timeTable->setWed4('not found'); | |
$timeTable->setThu1('not found'); | |
$timeTable->setThu2('not found'); | |
$timeTable->setThu3('not found'); | |
$timeTable->setThu4('not found'); | |
$timeTable->setFri1('not found'); | |
$timeTable->setFri2('not found'); | |
$timeTable->setFri3('not found'); | |
$timeTable->setFri4('not found'); | |
$manager->persist($timeTable); | |
} | |
} | |
} | |
$manager->flush(); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Repository; | |
use App\Entity\TimeTable; | |
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; | |
use Doctrine\Persistence\ManagerRegistry; | |
/** | |
* @extends ServiceEntityRepository<TimeTable> | |
*/ | |
class TimeTableRepository extends ServiceEntityRepository | |
{ | |
public function __construct(ManagerRegistry $registry) | |
{ | |
parent::__construct($registry, TimeTable::class); | |
} | |
/** | |
* クラス名で検索(主キーによる検索) | |
*/ | |
public function findByClassName(string $className): ?TimeTable | |
{ | |
return $this->find($className); | |
} | |
/** | |
* 曜日で検索 | |
*/ | |
public function findByDay(string $day): array | |
{ | |
return $this->createQueryBuilder('t') | |
->andWhere('t.day = :day') | |
->setParameter('day', $day) | |
->orderBy('t.className', 'ASC') | |
->addOrderBy('t.period', 'ASC') | |
->getQuery() | |
->getResult(); | |
} | |
/** | |
* 時限で検索 | |
*/ | |
public function findByPeriod(int $period): array | |
{ | |
return $this->createQueryBuilder('t') | |
->andWhere('t.period = :period') | |
->setParameter('period', $period) | |
->orderBy('t.className', 'ASC') | |
->addOrderBy('t.day', 'ASC') | |
->getQuery() | |
->getResult(); | |
} | |
/** | |
* 曜日と時限で検索 | |
*/ | |
public function findByDayAndPeriod(string $day, int $period): array | |
{ | |
return $this->createQueryBuilder('t') | |
->andWhere('t.day = :day') | |
->andWhere('t.period = :period') | |
->setParameter('day', $day) | |
->setParameter('period', $period) | |
->orderBy('t.className', 'ASC') | |
->getQuery() | |
->getResult(); | |
} | |
/** | |
* 全クラス名を取得 | |
*/ | |
public function findAllClassNames(): array | |
{ | |
$result = $this->createQueryBuilder('t') | |
->select('t.className') | |
->orderBy('t.className', 'ASC') | |
->getQuery() | |
->getResult(); | |
return array_column($result, 'className'); | |
} | |
/** | |
* 特定の科目コードが含まれる時間割を検索 | |
*/ | |
public function findBySubjectCode(string $subjectCode): array | |
{ | |
$qb = $this->createQueryBuilder('t'); | |
$conditions = []; | |
for ($day = 1; $day <= 5; $day++) { | |
$dayNames = ['', 'mon', 'tue', 'wed', 'thu', 'fri']; | |
for ($period = 1; $period <= 4; $period++) { | |
$field = $dayNames[$day] . $period; | |
$conditions[] = "t.$field = :subjectCode"; | |
} | |
} | |
$qb->andWhere(implode(' OR ', $conditions)) | |
->setParameter('subjectCode', $subjectCode) | |
->orderBy('t.className', 'ASC'); | |
return $qb->getQuery()->getResult(); | |
} | |
/** | |
* 時間割データが存在するかチェック | |
*/ | |
public function existsByClassName(string $className): bool | |
{ | |
return $this->find($className) !== null; | |
} | |
/** | |
* 時間割データを保存または更新 | |
*/ | |
public function saveOrUpdate(TimeTable $timeTable): void | |
{ | |
$this->getEntityManager()->persist($timeTable); | |
$this->getEntityManager()->flush(); | |
} | |
/** | |
* 時間割データを削除 | |
*/ | |
public function deleteByClassName(string $className): bool | |
{ | |
$timeTable = $this->find($className); | |
if ($timeTable) { | |
$this->getEntityManager()->remove($timeTable); | |
$this->getEntityManager()->flush(); | |
return true; | |
} | |
return false; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
framework: | |
default_locale: en | |
translator: | |
default_path: '%kernel.project_dir%/translations' | |
fallbacks: | |
- en | |
providers: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
twig: | |
default_path: '%kernel.project_dir%/templates' | |
when@test: | |
twig: | |
strict_variables: true |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
echo "=========================================" | |
echo " Ngrok URL Update & User Reset Script" | |
echo "=========================================" | |
echo "" | |
# 新しいngrok URLの入力を求める | |
read -p "Enter new ngrok URL (e.g., https://xxxx-xxx-xxx-xxx.ngrok-free.app): " NEW_URL | |
if [ -z "$NEW_URL" ]; then | |
echo "❌ Error: URL cannot be empty" | |
exit 1 | |
fi | |
echo "" | |
echo "Updating configuration..." | |
# g1_projectディレクトリに移動 | |
cd ~/g1_project | |
# .envファイルを更新 | |
echo "Updating symfony/.env..." | |
sed -i "s|APP_BASE_URL=.*|APP_BASE_URL=$NEW_URL|" symfony/.env | |
# .env.localファイルを更新 | |
echo "Updating symfony/.env.local..." | |
if [ -f "symfony/.env.local" ]; then | |
sed -i "s|APP_BASE_URL=.*|APP_BASE_URL=$NEW_URL|" symfony/.env.local | |
sed -i "s|APP_URL=.*|APP_URL=$NEW_URL|" symfony/.env.local | |
echo "✅ Updated .env.local file" | |
else | |
echo "⚠️ Warning: symfony/.env.local file not found" | |
fi | |
echo "✅ Updated .env file with: $NEW_URL" | |
# キャッシュをクリア | |
echo "Clearing cache..." | |
docker compose exec php php bin/console cache:clear | |
# 全ユーザーのlogin_statusをリセット | |
echo "Resetting all users' login status..." | |
docker compose exec php php bin/console doctrine:query:sql "UPDATE \"user\" SET login_status = NULL" | |
echo "" | |
echo "✅ All updates completed!" | |
echo "" | |
echo "Updated files:" | |
echo " - symfony/.env (APP_BASE_URL)" | |
echo " - symfony/.env.local (APP_BASE_URL, APP_URL)" | |
echo "" | |
echo "You can now access: $NEW_URL/magic-login" | |
echo "" | |
echo "=========================================" | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Entity; | |
use App\Repository\UserRepository; | |
use Doctrine\ORM\Mapping as ORM; | |
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; | |
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; | |
use Symfony\Component\Security\Core\User\UserInterface; | |
#[ORM\Entity(repositoryClass: UserRepository::class)] | |
#[ORM\Table(name: '`user`')] | |
#[UniqueEntity(fields: ['email'], message: 'There is already an account with this email')] | |
class User implements UserInterface, PasswordAuthenticatedUserInterface | |
{ | |
#[ORM\Id] | |
#[ORM\GeneratedValue] | |
#[ORM\Column] | |
private ?int $id = null; | |
#[ORM\Column(length: 180, unique: true)] | |
private ?string $email = null; | |
#[ORM\Column] | |
private array $roles = []; | |
#[ORM\Column] | |
private ?string $password = null; | |
#[ORM\Column(type: 'boolean')] | |
private $isVerified = false; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $magicToken = null; | |
#[ORM\Column(type: 'string', length: 255, nullable: true)] | |
private ?string $loginStatus = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $loginToken = null; | |
#[ORM\Column(type: 'datetime', nullable: true)] | |
private ?\DateTimeInterface $loginTokenCreatedAt = null; | |
// ★★ 複数デバイス対応のための新しいフィールド ★★ | |
#[ORM\Column(type: 'json', nullable: true)] | |
private ?array $activeDevices = []; | |
#[ORM\Column(type: 'boolean', options: ['default' => false])] | |
private bool $isLoginVerified = false; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $lastName = null; | |
#[ORM\Column(length: 255, nullable: true)] | |
private ?string $firstName = null; | |
public function getLastName(): ?string | |
{ | |
return $this->lastName; | |
} | |
public function setLastName(?string $lastName): static | |
{ | |
$this->lastName = $lastName; | |
return $this; | |
} | |
public function getFirstName(): ?string | |
{ | |
return $this->firstName; | |
} | |
public function setFirstName(?string $firstName): static | |
{ | |
$this->firstName = $firstName; | |
return $this; | |
} | |
public function getFullName(): string | |
{ | |
return trim(($this->lastName ?? '') . ' ' . ($this->firstName ?? '')); | |
} | |
public function isLoginVerified(): bool | |
{ | |
return $this->isLoginVerified; | |
} | |
public function setLoginVerified(bool $verified): static | |
{ | |
$this->isLoginVerified = $verified; | |
return $this; | |
} | |
// ★★ 複数デバイス管理メソッド ★★ | |
public function getActiveDevices(): array | |
{ | |
return $this->activeDevices ?? []; | |
} | |
public function setActiveDevices(?array $activeDevices): static | |
{ | |
$this->activeDevices = $activeDevices; | |
return $this; | |
} | |
public function addActiveDevice(string $deviceToken, string $userAgent = null): static | |
{ | |
$devices = $this->getActiveDevices(); | |
$devices[$deviceToken] = [ | |
'token' => $deviceToken, | |
'created_at' => (new \DateTime())->format('Y-m-d H:i:s'), | |
'user_agent' => $userAgent, | |
'last_activity' => (new \DateTime())->format('Y-m-d H:i:s') | |
]; | |
$this->activeDevices = $devices; | |
return $this; | |
} | |
public function removeActiveDevice(string $deviceToken): static | |
{ | |
$devices = $this->getActiveDevices(); | |
unset($devices[$deviceToken]); | |
$this->activeDevices = $devices; | |
return $this; | |
} | |
public function hasActiveDevice(string $deviceToken): bool | |
{ | |
$devices = $this->getActiveDevices(); | |
return isset($devices[$deviceToken]); | |
} | |
public function updateDeviceActivity(string $deviceToken): static | |
{ | |
$devices = $this->getActiveDevices(); | |
if (isset($devices[$deviceToken])) { | |
$devices[$deviceToken]['last_activity'] = (new \DateTime())->format('Y-m-d H:i:s'); | |
$this->activeDevices = $devices; | |
} | |
return $this; | |
} | |
public function clearActiveDevices(): static | |
{ | |
$this->activeDevices = []; | |
return $this; | |
} | |
public function getId(): ?int | |
{ | |
return $this->id; | |
} | |
public function getEmail(): ?string | |
{ | |
return $this->email; | |
} | |
public function setEmail(string $email): static | |
{ | |
$this->email = $email; | |
return $this; | |
} | |
public function getUserIdentifier(): string | |
{ | |
return (string) $this->email; | |
} | |
public function getRoles(): array | |
{ | |
$roles = $this->roles; | |
if (empty($roles)) { | |
$roles[] = 'ROLE_STUDENT'; | |
} | |
return array_unique($roles); | |
} | |
public function setRoles(array $roles): static | |
{ | |
$this->roles = $roles; | |
return $this; | |
} | |
public function getPassword(): string | |
{ | |
return $this->password; | |
} | |
public function setPassword(string $password): static | |
{ | |
$this->password = $password; | |
return $this; | |
} | |
public function eraseCredentials(): void | |
{ | |
// $this->plainPassword = null; | |
} | |
public function isVerified(): bool | |
{ | |
return $this->isVerified; | |
} | |
public function setIsVerified(bool $isVerified): static | |
{ | |
$this->isVerified = $isVerified; | |
return $this; | |
} | |
public function getMagicToken(): ?string | |
{ | |
return $this->magicToken; | |
} | |
public function setMagicToken(?string $magicToken): static | |
{ | |
$this->magicToken = $magicToken; | |
return $this; | |
} | |
public function getLoginStatus(): ?string | |
{ | |
return $this->loginStatus; | |
} | |
public function setLoginStatus(?string $status): self | |
{ | |
$this->loginStatus = $status; | |
return $this; | |
} | |
public function getLoginToken(): ?string | |
{ | |
return $this->loginToken; | |
} | |
public function setLoginToken(?string $loginToken): static | |
{ | |
$this->loginToken = $loginToken; | |
return $this; | |
} | |
public function getLoginTokenCreatedAt(): ?\DateTimeInterface | |
{ | |
return $this->loginTokenCreatedAt; | |
} | |
public function setLoginTokenCreatedAt(?\DateTimeInterface $loginTokenCreatedAt): static | |
{ | |
$this->loginTokenCreatedAt = $loginTokenCreatedAt; | |
return $this; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>{% block title %}ユーザーデータベース - 時間割管理システム{% endblock %}</title> | |
<!-- Bootstrap CSS --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"> | |
<!-- Font Awesome --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<style> | |
body { | |
background-color: #f4c2a1; | |
min-height: 100vh; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
margin: 0; | |
padding: 0; | |
} | |
.header { | |
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); | |
color: white; | |
padding: 15px 40px; | |
font-size: 1.6rem; | |
font-weight: bold; | |
position: relative; | |
} | |
.main-container { | |
padding: 20px; | |
max-width: 1400px; | |
margin: 0 auto; | |
} | |
.database-container { | |
background: white; | |
border-radius: 8px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
overflow: hidden; | |
} | |
.database-header { | |
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); | |
color: white; | |
padding: 20px; | |
text-align: center; | |
} | |
.database-header h1 { | |
margin: 0; | |
font-size: 2rem; | |
font-weight: bold; | |
} | |
.user-count { | |
background: rgba(255, 255, 255, 0.2); | |
padding: 10px 20px; | |
margin-top: 15px; | |
border-radius: 4px; | |
font-size: 1.1rem; | |
} | |
.table-container { | |
padding: 20px; | |
} | |
.user-table { | |
width: 100%; | |
border-collapse: collapse; | |
font-size: 1rem; | |
} | |
.user-table th { | |
background: #f8f9fa; | |
color: #333; | |
padding: 15px 12px; | |
text-align: left; | |
font-weight: bold; | |
border-bottom: 2px solid #dee2e6; | |
} | |
.user-table th:first-child { | |
width: 80px; | |
text-align: center; | |
} | |
.user-table th:nth-child(2) { | |
width: 180px; | |
} | |
.user-table th:nth-child(3) { | |
width: 40%; | |
} | |
.user-table th:nth-child(4) { | |
width: 120px; | |
text-align: center; | |
} | |
.user-table th:nth-child(5) { | |
width: 120px; | |
text-align: center; | |
} | |
.user-table td { | |
padding: 12px; | |
border-bottom: 1px solid #dee2e6; | |
vertical-align: middle; | |
} | |
.user-table td:first-child { | |
text-align: center; | |
font-weight: bold; | |
color: #4a90e2; | |
} | |
.user-table td:nth-child(4), | |
.user-table td:nth-child(5) { | |
text-align: center; | |
} | |
.user-table tr:hover { | |
background-color: #f8f9fa; | |
cursor: pointer; | |
} | |
.name-cell { | |
font-weight: 500; | |
color: #333; | |
font-size: 1rem; | |
} | |
.status-icon { | |
font-size: 1.2rem; | |
} | |
.status-logged-in { | |
color: #28a745; | |
} | |
.status-not-logged-in { | |
color: #dc3545; | |
} | |
.role-badge { | |
display: inline-block; | |
padding: 4px 8px; | |
border-radius: 12px; | |
font-size: 0.8rem; | |
font-weight: bold; | |
text-align: center; | |
min-width: 60px; | |
} | |
.role-developer { | |
background-color: #8e44ad; | |
color: white; | |
} | |
.role-admin { | |
background-color: #dc3545; | |
color: white; | |
} | |
.role-teacher { | |
background-color: #28a745; | |
color: white; | |
} | |
.role-student { | |
background-color: #007bff; | |
color: white; | |
} | |
.role-unknown { | |
background-color: #6c757d; | |
color: white; | |
} | |
.role-developer::after { | |
content: " 👑"; | |
font-size: 0.8em; | |
} | |
/* 開発者行のハイライト */ | |
.developer-row { | |
background-color: rgba(44, 62, 80, 0.05) !important; | |
} | |
.developer-row:hover { | |
background-color: rgba(44, 62, 80, 0.1) !important; | |
} | |
.role-unknown { | |
background-color: #6c757d; | |
color: white; | |
} | |
.no-users { | |
text-align: center; | |
padding: 40px; | |
color: #6c757d; | |
font-size: 1.1rem; | |
} | |
.action-buttons { | |
padding: 20px; | |
text-align: center; | |
border-top: 1px solid #dee2e6; | |
} | |
.action-btn { | |
background-color: #f1c40f; | |
color: #333; | |
border: none; | |
padding: 12px 24px; | |
font-size: 1rem; | |
font-weight: bold; | |
border-radius: 4px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
text-decoration: none; | |
display: inline-block; | |
margin: 0 10px; | |
} | |
.action-btn:hover { | |
background-color: #d4ac0d; | |
transform: translateY(-1px); | |
color: #333; | |
text-decoration: none; | |
} | |
.back-btn { | |
background-color: #6c757d; | |
color: white; | |
} | |
.back-btn:hover { | |
background-color: #5a6268; | |
color: white; | |
} | |
/* 右クリックメニューのスタイル */ | |
.context-menu { | |
position: absolute; | |
background: white; | |
border: 1px solid #ccc; | |
border-radius: 4px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); | |
z-index: 1000; | |
display: none; | |
min-width: 150px; | |
} | |
.context-menu-item { | |
padding: 10px 15px; | |
cursor: pointer; | |
border-bottom: 1px solid #eee; | |
transition: background-color 0.2s ease; | |
} | |
.context-menu-item:last-child { | |
border-bottom: none; | |
} | |
.context-menu-item:hover { | |
background-color: #f8f9fa; | |
} | |
.context-menu-item.delete { | |
color: #dc3545; | |
} | |
.context-menu-item.delete:hover { | |
background-color: #f8d7da; | |
} | |
.context-menu-item i { | |
margin-right: 8px; | |
width: 16px; | |
} | |
/* アラートメッセージ */ | |
.alert-message { | |
position: fixed; | |
top: 20px; | |
right: 20px; | |
padding: 15px 20px; | |
border-radius: 4px; | |
z-index: 2000; | |
display: none; | |
max-width: 400px; | |
} | |
.alert-success { | |
background-color: #d4edda; | |
color: #155724; | |
border: 1px solid #c3e6cb; | |
} | |
.alert-error { | |
background-color: #f8d7da; | |
color: #721c24; | |
border: 1px solid #f5c6cb; | |
} | |
@media (max-width: 768px) { | |
.header { | |
padding: 10px 15px; | |
font-size: 1.3rem; | |
} | |
.main-container { | |
padding: 10px; | |
} | |
.database-header h1 { | |
font-size: 1.5rem; | |
} | |
.table-container { | |
padding: 10px; | |
overflow-x: auto; | |
} | |
.user-table { | |
font-size: 0.9rem; | |
min-width: 700px; | |
} | |
.user-table th, | |
.user-table td { | |
padding: 8px 6px; | |
} | |
.role-badge { | |
font-size: 0.7rem; | |
padding: 3px 6px; | |
min-width: 50px; | |
} | |
.action-buttons { | |
padding: 15px 10px; | |
} | |
.action-btn { | |
display: block; | |
margin: 5px auto; | |
width: 200px; | |
} | |
.alert-message { | |
top: 10px; | |
right: 10px; | |
left: 10px; | |
max-width: none; | |
} | |
} | |
@media (max-width: 480px) { | |
.database-header h1 { | |
font-size: 1.3rem; | |
} | |
.user-table { | |
font-size: 0.8rem; | |
} | |
.role-badge { | |
font-size: 0.6rem; | |
padding: 2px 4px; | |
min-width: 40px; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="header"> | |
ユーザーデータベース管理 | |
</div> | |
<!-- アラートメッセージ --> | |
<div id="alertMessage" class="alert-message"> | |
<span id="alertText"></span> | |
</div> | |
<!-- 右クリックメニュー --> | |
<div id="contextMenu" class="context-menu"> | |
<div class="context-menu-item delete" id="deleteUser"> | |
<i class="fas fa-trash"></i>削除 | |
</div> | |
</div> | |
<div class="main-container"> | |
<div class="database-container"> | |
<div class="database-header"> | |
<h1>登録ユーザー一覧</h1> | |
<div class="user-count" id="userCount"> | |
<i class="fas fa-users"></i> | |
総ユーザー数: {{ users|length }}名 | |
</div> | |
</div> | |
<div class="table-container"> | |
{% if users|length > 0 %} | |
<table class="user-table"> | |
<thead> | |
<tr> | |
<th>ID</th> | |
<th>氏名</th> | |
<th>メールアドレス</th> | |
<th>役職</th> | |
<th>ログイン済み</th> | |
</tr> | |
</thead> | |
<tbody id="userTableBody"> | |
{% for user in users %} | |
<tr data-user-id="{{ user.id }}" data-user-email="{{ user.email }}" class="user-row {% if 'ROLE_DEVELOPER' in user.roles %}developer-row{% endif %}"> | |
<td>{{ user.id }}</td> | |
<td class="name-cell"> | |
{% if user.lastName or user.firstName %} | |
{{ user.fullName }} | |
{% else %} | |
<span style="color: #999; font-style: italic;">未設定</span> | |
{% endif %} | |
</td> | |
<td>{{ user.email }}</td> | |
<td> | |
{% set roles = user.roles %} | |
{% if 'ROLE_DEVELOPER' in roles %} | |
<span class="role-badge role-developer">開発者</span> | |
{% elseif 'ROLE_ADMIN' in roles %} | |
<span class="role-badge role-admin">管理者</span> | |
{% elseif 'ROLE_TEACHER' in roles %} | |
<span class="role-badge role-teacher">教員</span> | |
{% elseif 'ROLE_STUDENT' in roles %} | |
<span class="role-badge role-student">学生</span> | |
{% else %} | |
<span class="role-badge role-unknown">不明</span> | |
{% endif %} | |
</td> | |
<td> | |
{% if user.loginStatus is defined and user.loginStatus == 'verified' %} | |
<i class="fas fa-check status-icon status-logged-in"></i> | |
{% else %} | |
<i class="fas fa-times status-icon status-not-logged-in"></i> | |
{% endif %} | |
</td> | |
</tr> | |
{% endfor %} | |
</tbody> | |
</table> | |
{% else %} | |
<div class="no-users"> | |
<i class="fas fa-user-slash fa-3x" style="color: #dee2e6; margin-bottom: 20px;"></i> | |
<p>登録されているユーザーがありません。</p> | |
</div> | |
{% endif %} | |
</div> | |
<div class="action-buttons"> | |
<a href="{{ path('app_add_user') }}" class="action-btn"> | |
<i class="fas fa-user-plus"></i> 新しいユーザーを追加 | |
</a> | |
<a href="javascript:history.back()" class="action-btn back-btn"> | |
<i class="fas fa-arrow-left"></i> 戻る | |
</a> | |
</div> | |
</div> | |
</div> | |
<!-- Bootstrap JS --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
const contextMenu = document.getElementById('contextMenu'); | |
const deleteMenuItem = document.getElementById('deleteUser'); | |
const alertMessage = document.getElementById('alertMessage'); | |
const alertText = document.getElementById('alertText'); | |
const userCount = document.getElementById('userCount'); | |
let currentUserId = null; | |
let currentUserEmail = null; | |
// 右クリックイベント(行全体で有効) | |
document.querySelectorAll('.user-row').forEach(row => { | |
row.addEventListener('contextmenu', function(e) { | |
e.preventDefault(); | |
// 現在のユーザー情報を保存 | |
currentUserId = this.getAttribute('data-user-id'); | |
currentUserEmail = this.getAttribute('data-user-email'); | |
// メニューを表示 | |
contextMenu.style.display = 'block'; | |
contextMenu.style.left = e.pageX + 'px'; | |
contextMenu.style.top = e.pageY + 'px'; | |
}); | |
}); | |
// 削除メニューのクリックイベント | |
deleteMenuItem.addEventListener('click', function() { | |
if (currentUserId && currentUserEmail) { | |
// ★★ 開発者アカウントの削除防止 ★★ | |
if (currentUserEmail === 'kentamagicshow@gmail.com') { | |
alert('開発者アカウント(kentamagicshow@gmail.com)は削除できません。\nシステムの安全性のため、この操作は禁止されています。'); | |
hideContextMenu(); | |
return; | |
} | |
if (confirm(`${currentUserEmail} を削除してもよろしいですか?\nこの操作は取り消せません。`)) { | |
deleteUser(currentUserId, currentUserEmail); | |
} | |
} | |
hideContextMenu(); | |
}); | |
// メニューを隠す | |
function hideContextMenu() { | |
contextMenu.style.display = 'none'; | |
currentUserId = null; | |
currentUserEmail = null; | |
} | |
// 他の場所をクリックしたらメニューを隠す | |
document.addEventListener('click', function(e) { | |
if (!contextMenu.contains(e.target)) { | |
hideContextMenu(); | |
} | |
}); | |
// ユーザー削除処理 | |
function deleteUser(userId, userEmail) { | |
fetch(`/database/user/delete/${userId}`, { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
'X-Requested-With': 'XMLHttpRequest' | |
} | |
}) | |
.then(response => response.json()) | |
.then(data => { | |
if (data.success) { | |
// 成功メッセージを表示 | |
showAlert(data.message, 'success'); | |
// テーブルから行を削除 | |
const row = document.querySelector(`tr[data-user-id="${userId}"]`); | |
if (row) { | |
row.remove(); | |
} | |
// ユーザー数を更新 | |
updateUserCount(); | |
// テーブルが空になった場合の処理 | |
checkEmptyTable(); | |
} else { | |
// エラーメッセージを表示 | |
showAlert(data.message, 'error'); | |
} | |
}) | |
.catch(error => { | |
console.error('削除エラー:', error); | |
showAlert('削除中にエラーが発生しました。', 'error'); | |
}); | |
} | |
// アラートメッセージを表示 | |
function showAlert(message, type) { | |
alertText.textContent = message; | |
alertMessage.className = `alert-message alert-${type}`; | |
alertMessage.style.display = 'block'; | |
// 3秒後に自動で隠す | |
setTimeout(() => { | |
alertMessage.style.display = 'none'; | |
}, 3000); | |
} | |
// ユーザー数を更新 | |
function updateUserCount() { | |
const remainingRows = document.querySelectorAll('#userTableBody tr').length; | |
userCount.innerHTML = `<i class="fas fa-users"></i> 総ユーザー数: ${remainingRows}名`; | |
} | |
// テーブルが空になった場合の処理 | |
function checkEmptyTable() { | |
const tableBody = document.getElementById('userTableBody'); | |
const remainingRows = tableBody.querySelectorAll('tr').length; | |
if (remainingRows === 0) { | |
// テーブルを隠して「ユーザーなし」メッセージを表示 | |
const tableContainer = document.querySelector('.table-container'); | |
tableContainer.innerHTML = ` | |
<div class="no-users"> | |
<i class="fas fa-user-slash fa-3x" style="color: #dee2e6; margin-bottom: 20px;"></i> | |
<p>登録されているユーザーがありません。</p> | |
</div> | |
`; | |
} | |
} | |
// ESCキーでメニューを閉じる | |
document.addEventListener('keydown', function(e) { | |
if (e.key === 'Escape') { | |
hideContextMenu(); | |
} | |
}); | |
}); | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Repository; | |
use App\Entity\User; | |
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; | |
use Doctrine\Persistence\ManagerRegistry; | |
use Symfony\Component\Security\Core\Exception\UnsupportedUserException; | |
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; | |
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; | |
/** | |
* @extends ServiceEntityRepository<User> | |
* | |
* @implements PasswordUpgraderInterface<User> | |
* | |
* @method User|null find($id, $lockMode = null, $lockVersion = null) | |
* @method User|null findOneBy(array $criteria, array $orderBy = null) | |
* @method User[] findAll() | |
* @method User[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) | |
*/ | |
class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface | |
{ | |
public function __construct(ManagerRegistry $registry) | |
{ | |
parent::__construct($registry, User::class); | |
} | |
/** | |
* Used to upgrade (rehash) the user's password automatically over time. | |
*/ | |
public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void | |
{ | |
if (!$user instanceof User) { | |
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', $user::class)); | |
} | |
$user->setPassword($newHashedPassword); | |
$this->getEntityManager()->persist($user); | |
$this->getEntityManager()->flush(); | |
} | |
// /** | |
// * @return User[] Returns an array of User objects | |
// */ | |
// public function findByExampleField($value): array | |
// { | |
// return $this->createQueryBuilder('u') | |
// ->andWhere('u.exampleField = :val') | |
// ->setParameter('val', $value) | |
// ->orderBy('u.id', 'ASC') | |
// ->setMaxResults(10) | |
// ->getQuery() | |
// ->getResult() | |
// ; | |
// } | |
// public function findOneBySomeField($value): ?User | |
// { | |
// return $this->createQueryBuilder('u') | |
// ->andWhere('u.exampleField = :val') | |
// ->setParameter('val', $value) | |
// ->getQuery() | |
// ->getOneOrNullResult() | |
// ; | |
// } | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
framework: | |
validation: | |
email_validation_mode: html5 | |
# Enables validator auto-mapping support. | |
# For instance, basic validation constraints will be inferred from Doctrine's metadata. | |
#auto_mapping: | |
# App\Entity\: [] | |
when@test: | |
framework: | |
validation: | |
not_compromised_password: false |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
symfonycasts_verify_email: | |
# デフォルトの有効期限(秒) | |
lifetime: 3600 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace DoctrineMigrations; | |
use Doctrine\DBAL\Schema\Schema; | |
use Doctrine\Migrations\AbstractMigration; | |
/** | |
* Auto-generated Migration: Please modify to your needs! | |
*/ | |
final class Version20250619090033 extends AbstractMigration | |
{ | |
public function getDescription(): string | |
{ | |
return ''; | |
} | |
public function up(Schema $schema): void | |
{ | |
// this up() migration is auto-generated, please modify it to your needs | |
$this->addSql(<<<'SQL' | |
CREATE SEQUENCE complete_time_table_id_seq INCREMENT BY 1 MINVALUE 1 START 1 | |
SQL); | |
$this->addSql(<<<'SQL' | |
CREATE SEQUENCE lesson_data_id_seq INCREMENT BY 1 MINVALUE 1 START 1 | |
SQL); | |
$this->addSql(<<<'SQL' | |
CREATE SEQUENCE request_data_id_seq INCREMENT BY 1 MINVALUE 1 START 1 | |
SQL); | |
$this->addSql(<<<'SQL' | |
CREATE SEQUENCE "user_id_seq" INCREMENT BY 1 MINVALUE 1 START 1 | |
SQL); | |
$this->addSql(<<<'SQL' | |
CREATE TABLE complete_time_table (id INT NOT NULL, class_name VARCHAR(10) NOT NULL, year INT NOT NULL, month INT NOT NULL, monday_date INT NOT NULL, tuesday_date INT NOT NULL, wednesday_date INT DEFAULT NULL, thursday_date INT NOT NULL, friday_date INT NOT NULL, mon1 VARCHAR(255) DEFAULT NULL, mon2 VARCHAR(255) DEFAULT NULL, mon3 VARCHAR(255) DEFAULT NULL, mon4 VARCHAR(255) DEFAULT NULL, tue1 VARCHAR(255) DEFAULT NULL, tue2 VARCHAR(255) DEFAULT NULL, tue3 VARCHAR(255) DEFAULT NULL, tue4 VARCHAR(255) DEFAULT NULL, wed1 VARCHAR(255) DEFAULT NULL, wed2 VARCHAR(255) DEFAULT NULL, wed3 VARCHAR(255) DEFAULT NULL, wed4 VARCHAR(255) DEFAULT NULL, thu1 VARCHAR(255) DEFAULT NULL, thu2 VARCHAR(255) DEFAULT NULL, thu3 VARCHAR(255) DEFAULT NULL, thu4 VARCHAR(255) DEFAULT NULL, fri1 VARCHAR(255) DEFAULT NULL, fri2 VARCHAR(255) DEFAULT NULL, fri3 VARCHAR(255) DEFAULT NULL, fri4 VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id)) | |
SQL); | |
$this->addSql(<<<'SQL' | |
CREATE TABLE lesson_data (id INT NOT NULL, subject_code VARCHAR(10) NOT NULL, class_name VARCHAR(10) NOT NULL, location VARCHAR(255) NOT NULL, subject_name VARCHAR(255) NOT NULL, employment_type VARCHAR(10) NOT NULL, teacher1 VARCHAR(255) NOT NULL, teacher2 VARCHAR(255) DEFAULT NULL, teacher3 VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id)) | |
SQL); | |
$this->addSql(<<<'SQL' | |
CREATE TABLE request_data (id INT NOT NULL, class_name VARCHAR(10) NOT NULL, day VARCHAR(10) NOT NULL, period INT NOT NULL, subject_code VARCHAR(20) NOT NULL, subject_name VARCHAR(100) NOT NULL, location VARCHAR(100) NOT NULL, employment_type VARCHAR(20) NOT NULL, teacher1 VARCHAR(100) DEFAULT NULL, teacher2 VARCHAR(100) DEFAULT NULL, teacher3 VARCHAR(100) DEFAULT NULL, status VARCHAR(20) DEFAULT 'pending' NOT NULL, request_reason TEXT DEFAULT NULL, requested_by VARCHAR(100) DEFAULT NULL, requested_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, approved_by VARCHAR(100) DEFAULT NULL, approved_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, approval_comment TEXT DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id)) | |
SQL); | |
$this->addSql(<<<'SQL' | |
CREATE TABLE system_setting (id SERIAL NOT NULL, setting_key VARCHAR(255) NOT NULL, setting_value VARCHAR(255) NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_by VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id)) | |
SQL); | |
$this->addSql(<<<'SQL' | |
CREATE UNIQUE INDEX UNIQ_7307C40B5FA1E697 ON system_setting (setting_key) | |
SQL); | |
$this->addSql(<<<'SQL' | |
CREATE TABLE time_table (class_name VARCHAR(10) NOT NULL, day VARCHAR(10) NOT NULL, period INT NOT NULL, mon1 VARCHAR(255) DEFAULT NULL, mon2 VARCHAR(255) DEFAULT NULL, mon3 VARCHAR(255) DEFAULT NULL, mon4 VARCHAR(255) DEFAULT NULL, tue1 VARCHAR(255) DEFAULT NULL, tue2 VARCHAR(255) DEFAULT NULL, tue3 VARCHAR(255) DEFAULT NULL, tue4 VARCHAR(255) DEFAULT NULL, wed1 VARCHAR(255) DEFAULT NULL, wed2 VARCHAR(255) DEFAULT NULL, wed3 VARCHAR(255) DEFAULT NULL, wed4 VARCHAR(255) DEFAULT NULL, thu1 VARCHAR(255) DEFAULT NULL, thu2 VARCHAR(255) DEFAULT NULL, thu3 VARCHAR(255) DEFAULT NULL, thu4 VARCHAR(255) DEFAULT NULL, fri1 VARCHAR(255) DEFAULT NULL, fri2 VARCHAR(255) DEFAULT NULL, fri3 VARCHAR(255) DEFAULT NULL, fri4 VARCHAR(255) DEFAULT NULL, PRIMARY KEY(class_name)) | |
SQL); | |
$this->addSql(<<<'SQL' | |
CREATE TABLE "user" (id INT NOT NULL, email VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, is_verified BOOLEAN NOT NULL, magic_token VARCHAR(255) DEFAULT NULL, login_status VARCHAR(255) DEFAULT NULL, login_token VARCHAR(255) DEFAULT NULL, login_token_created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, active_devices JSON DEFAULT NULL, is_login_verified BOOLEAN DEFAULT false NOT NULL, last_name VARCHAR(255) DEFAULT NULL, first_name VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id)) | |
SQL); | |
$this->addSql(<<<'SQL' | |
CREATE UNIQUE INDEX UNIQ_8D93D649E7927C74 ON "user" (email) | |
SQL); | |
$this->addSql(<<<'SQL' | |
CREATE TABLE messenger_messages (id BIGSERIAL NOT NULL, body TEXT NOT NULL, headers TEXT NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, available_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, delivered_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id)) | |
SQL); | |
$this->addSql(<<<'SQL' | |
CREATE INDEX IDX_75EA56E0FB7336F0 ON messenger_messages (queue_name) | |
SQL); | |
$this->addSql(<<<'SQL' | |
CREATE INDEX IDX_75EA56E0E3BD61CE ON messenger_messages (available_at) | |
SQL); | |
$this->addSql(<<<'SQL' | |
CREATE INDEX IDX_75EA56E016BA31DB ON messenger_messages (delivered_at) | |
SQL); | |
$this->addSql(<<<'SQL' | |
CREATE OR REPLACE FUNCTION notify_messenger_messages() RETURNS TRIGGER AS $$ | |
BEGIN | |
PERFORM pg_notify('messenger_messages', NEW.queue_name::text); | |
RETURN NEW; | |
END; | |
$$ LANGUAGE plpgsql; | |
SQL); | |
$this->addSql(<<<'SQL' | |
DROP TRIGGER IF EXISTS notify_trigger ON messenger_messages; | |
SQL); | |
$this->addSql(<<<'SQL' | |
CREATE TRIGGER notify_trigger AFTER INSERT OR UPDATE ON messenger_messages FOR EACH ROW EXECUTE PROCEDURE notify_messenger_messages(); | |
SQL); | |
} | |
public function down(Schema $schema): void | |
{ | |
// this down() migration is auto-generated, please modify it to your needs | |
$this->addSql(<<<'SQL' | |
CREATE SCHEMA public | |
SQL); | |
$this->addSql(<<<'SQL' | |
DROP SEQUENCE complete_time_table_id_seq CASCADE | |
SQL); | |
$this->addSql(<<<'SQL' | |
DROP SEQUENCE lesson_data_id_seq CASCADE | |
SQL); | |
$this->addSql(<<<'SQL' | |
DROP SEQUENCE request_data_id_seq CASCADE | |
SQL); | |
$this->addSql(<<<'SQL' | |
DROP SEQUENCE "user_id_seq" CASCADE | |
SQL); | |
$this->addSql(<<<'SQL' | |
DROP TABLE complete_time_table | |
SQL); | |
$this->addSql(<<<'SQL' | |
DROP TABLE lesson_data | |
SQL); | |
$this->addSql(<<<'SQL' | |
DROP TABLE request_data | |
SQL); | |
$this->addSql(<<<'SQL' | |
DROP TABLE system_setting | |
SQL); | |
$this->addSql(<<<'SQL' | |
DROP TABLE time_table | |
SQL); | |
$this->addSql(<<<'SQL' | |
DROP TABLE "user" | |
SQL); | |
$this->addSql(<<<'SQL' | |
DROP TABLE messenger_messages | |
SQL); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace DoctrineMigrations; | |
use Doctrine\DBAL\Schema\Schema; | |
use Doctrine\Migrations\AbstractMigration; | |
/** | |
* Auto-generated Migration: Please modify to your needs! | |
*/ | |
final class Version20250619145532 extends AbstractMigration | |
{ | |
public function getDescription(): string | |
{ | |
return ''; | |
} | |
public function up(Schema $schema): void | |
{ | |
// this up() migration is auto-generated, please modify it to your needs | |
$this->addSql(<<<'SQL' | |
ALTER TABLE complete_time_table ALTER class_name TYPE VARCHAR(255) | |
SQL); | |
$this->addSql(<<<'SQL' | |
ALTER TABLE complete_time_table ALTER monday_date DROP NOT NULL | |
SQL); | |
$this->addSql(<<<'SQL' | |
ALTER TABLE complete_time_table ALTER tuesday_date DROP NOT NULL | |
SQL); | |
$this->addSql(<<<'SQL' | |
ALTER TABLE complete_time_table ALTER thursday_date DROP NOT NULL | |
SQL); | |
$this->addSql(<<<'SQL' | |
ALTER TABLE complete_time_table ALTER friday_date DROP NOT NULL | |
SQL); | |
} | |
public function down(Schema $schema): void | |
{ | |
// this down() migration is auto-generated, please modify it to your needs | |
$this->addSql(<<<'SQL' | |
CREATE SCHEMA public | |
SQL); | |
$this->addSql(<<<'SQL' | |
ALTER TABLE complete_time_table ALTER class_name TYPE VARCHAR(10) | |
SQL); | |
$this->addSql(<<<'SQL' | |
ALTER TABLE complete_time_table ALTER monday_date SET NOT NULL | |
SQL); | |
$this->addSql(<<<'SQL' | |
ALTER TABLE complete_time_table ALTER tuesday_date SET NOT NULL | |
SQL); | |
$this->addSql(<<<'SQL' | |
ALTER TABLE complete_time_table ALTER thursday_date SET NOT NULL | |
SQL); | |
$this->addSql(<<<'SQL' | |
ALTER TABLE complete_time_table ALTER friday_date SET NOT NULL | |
SQL); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>{% block title %}認証待機中 - 時間割管理システム{% endblock %}</title> | |
<!-- Bootstrap CSS --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"> | |
<!-- Font Awesome --> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<style> | |
body { | |
background-color: #f4c2a1; | |
min-height: 100vh; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
margin: 0; | |
padding: 0; | |
} | |
.login-container { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
width: 600px; | |
max-width: 90vw; | |
} | |
.login-box { | |
background-color: #2e86c1; | |
padding: 40px; | |
border-radius: 0; | |
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); | |
} | |
.waiting-container { | |
text-align: center; | |
color: white; | |
} | |
.waiting-container h2 { | |
color: white; | |
font-size: 2.5rem; | |
font-weight: bold; | |
margin-bottom: 30px; | |
} | |
.waiting-container p { | |
color: white; | |
font-size: 1.1rem; | |
margin-bottom: 15px; | |
line-height: 1.6; | |
} | |
.email-address { | |
color: #f1c40f; | |
font-weight: bold; | |
} | |
.status-container { | |
margin-top: 30px; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
gap: 15px; | |
} | |
#status { | |
color: #f1c40f; | |
font-size: 1.2rem; | |
font-weight: bold; | |
} | |
.spinner { | |
width: 24px; | |
height: 24px; | |
border: 3px solid rgba(241, 196, 15, 0.3); | |
border-top: 3px solid #f1c40f; | |
border-radius: 50%; | |
animation: spin 1s linear infinite; | |
} | |
@keyframes spin { | |
0% { transform: rotate(0deg); } | |
100% { transform: rotate(360deg); } | |
} | |
.warning-notice { | |
background: rgba(255, 255, 255, 0.1); | |
padding: 15px; | |
margin: 20px 0; | |
border-radius: 4px; | |
color: white; | |
font-size: 0.9rem; | |
text-align: left; | |
} | |
.warning-notice strong { | |
color: #f1c40f; | |
} | |
.troubleshooting { | |
background: rgba(255, 255, 255, 0.1); | |
padding: 15px; | |
margin: 20px 0; | |
border-radius: 4px; | |
color: white; | |
font-size: 0.9rem; | |
text-align: left; | |
display: none; | |
} | |
.troubleshooting h4 { | |
color: #f1c40f; | |
margin-bottom: 10px; | |
} | |
.troubleshooting ul { | |
margin: 0; | |
padding-left: 20px; | |
} | |
.troubleshooting li { | |
margin-bottom: 5px; | |
} | |
#debug { | |
background: rgba(255, 255, 255, 0.1); | |
padding: 15px; | |
margin: 20px 0; | |
border-radius: 4px; | |
color: white; | |
font-family: monospace; | |
font-size: 0.9rem; | |
text-align: left; | |
max-height: 200px; | |
overflow-y: auto; | |
display: none; /* デバッグ情報は非表示 */ | |
} | |
@media (max-width: 768px) { | |
.login-container { | |
position: relative; | |
top: auto; | |
left: auto; | |
transform: none; | |
margin: 20px auto; | |
width: 95%; | |
} | |
.login-box { | |
padding: 30px 20px; | |
} | |
.waiting-container h2 { | |
font-size: 2rem; | |
} | |
.status-container { | |
flex-direction: column; | |
gap: 10px; | |
} | |
} | |
@media (max-width: 480px) { | |
.waiting-container h2 { | |
font-size: 1.8rem; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="login-container"> | |
<div class="login-box"> | |
<div class="waiting-container"> | |
<h2>認証メールを送信しました</h2> | |
<p><span class="email-address">{{ email }}</span>宛に確認メールを送信しました。</p> | |
<p>メール内のリンクから認証を行ってください。</p> | |
<p>認証が完了すると、自動的にホーム画面に遷移します。</p> | |
<div class="warning-notice"> | |
<strong><i class="fas fa-exclamation-triangle"></i> 重要なお知らせ</strong><br> | |
• 学校のWi-Fiをご利用の場合、メール配信に数分かかる場合があります<br> | |
• メールが迷惑メールフォルダに入る可能性があります<br> | |
• リンクをクリック時に「安全ではない」と表示される場合は「詳細設定」→「サイトにアクセスする」で進んでください | |
</div> | |
<div class="status-container"> | |
<div id="status">認証待機中...</div> | |
<div class="spinner"></div> | |
</div> | |
<div class="troubleshooting" id="troubleshooting"> | |
<h4><i class="fas fa-tools"></i> メールが届かない場合</h4> | |
<ul> | |
<li>迷惑メールフォルダを確認してください</li> | |
<li>学校のWi-Fiの場合、最大5分程度お待ちください</li> | |
<li>メールアドレスが正しいか確認してください</li> | |
<li>それでも届かない場合は、ページを更新して再度お試しください</li> | |
</ul> | |
</div> | |
<div id="debug"></div> | |
</div> | |
</div> | |
</div> | |
<!-- Bootstrap JS --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script> | |
<script> | |
let checkCount = 0; | |
const maxChecks = 100; // 5分間(3秒間隔 × 100回) | |
function checkAuthStatus() { | |
const debugDiv = document.getElementById('debug'); | |
const statusDiv = document.getElementById('status'); | |
const troubleshootingDiv = document.getElementById('troubleshooting'); | |
checkCount++; | |
debugDiv.innerHTML += '<br>チェック開始 (' + checkCount + '/' + maxChecks + '): ' + new Date().toLocaleTimeString(); | |
// 30回チェック後(1分30秒後)にトラブルシューティングを表示 | |
if (checkCount === 30) { | |
troubleshootingDiv.style.display = 'block'; | |
statusDiv.innerHTML = '認証待機中... (メールが届かない場合は下記をご確認ください)'; | |
} | |
// 最大チェック回数に達した場合 | |
if (checkCount >= maxChecks) { | |
statusDiv.innerHTML = 'タイムアウトしました。ページを更新して再度お試しください。'; | |
return; | |
} | |
fetch('/auth-check?email={{ email|url_encode }}') | |
.then(response => { | |
debugDiv.innerHTML += '<br>レスポンス受信: ' + response.status; | |
return response.json(); | |
}) | |
.then(data => { | |
debugDiv.innerHTML += '<br>認証状態: ' + JSON.stringify(data); | |
if (data.authenticated) { | |
statusDiv.innerHTML = '✅ 認証成功!ホーム画面に移動中...'; | |
debugDiv.innerHTML += '<br>リダイレクト先: ' + data.redirectUrl; | |
// ロールに応じたホーム画面にリダイレクト | |
setTimeout(() => { | |
window.location.href = data.redirectUrl; | |
}, 1000); | |
} else { | |
// 学校のWi-Fi環境を考慮して間隔を調整 | |
const interval = checkCount < 20 ? 3000 : 5000; // 最初は3秒、後半は5秒間隔 | |
setTimeout(checkAuthStatus, interval); | |
} | |
}) | |
.catch(error => { | |
debugDiv.innerHTML += '<br>エラー: ' + error.message; | |
console.error('認証チェックエラー:', error); | |
// エラー時も継続してチェック | |
const interval = checkCount < 20 ? 3000 : 5000; | |
setTimeout(checkAuthStatus, interval); | |
}); | |
} | |
// ページ読み込み後に認証チェック開始 | |
checkAuthStatus(); | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
when@dev: | |
web_profiler_wdt: | |
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml' | |
prefix: /_wdt | |
web_profiler_profiler: | |
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml' | |
prefix: /_profiler |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
display_errors=Off | |
display_startup_errors=Off | |
log_errors=On | |
error_reporting=22527 | |
#error_reporting = E_ALL & ~E_DEPRECATED & ~E_WARNING | |
assert.warning=0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
ここには、プログラムを作成するにあたって、 | |
「新たに作成したファイル」や「編集したファイル」 | |
について、そのファイル名を必ず、もれなく記入してください。 | |
(どんな些細な編集でも、記入漏れがあると、 | |
プログラムを統合したときに発見困難な | |
重大なバグが発生してしまうので、 | |
必ず記入してください。) | |
----------------------------- | |
↓↓↓【変更したファイル】↓↓↓ | |
(ex. Symfony/Controller/MagicLoginController.php 追加) | |
(ex. Symfony/Controller/MagicLoginController.php 修正) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment