CSSの最適化や内部挙動の改良
This commit is contained in:
@@ -1,35 +1,38 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "content"}}
|
||||
<h1 class="mb-4"><i class="bi bi-key me-2"></i>APIキー管理</h1>
|
||||
<h1 class="mb-4"><i class="bi bi-key me-2" aria-hidden="true"></i>APIキー管理</h1>
|
||||
|
||||
{{if .error}}<div class="alert alert-danger">{{.error}}</div>{{end}}
|
||||
{{if .error}}<div class="alert alert-danger" role="alert">{{.error}}</div>{{end}}
|
||||
|
||||
{{if .newKey}}
|
||||
<div class="alert alert-success">
|
||||
<h5 class="alert-heading"><i class="bi bi-check-circle me-2"></i>APIキーが作成されました</h5>
|
||||
<div class="alert alert-success" role="status">
|
||||
<h5 class="alert-heading"><i class="bi bi-check-circle me-2" aria-hidden="true"></i>APIキーが作成されました</h5>
|
||||
<p class="mb-2">キー名: <strong>{{.newKeyName}}</strong></p>
|
||||
<p class="mb-0">以下のキーを安全な場所に保存してください。このキーは二度と表示されません。</p>
|
||||
<hr>
|
||||
<div class="d-flex align-items-center">
|
||||
<code class="flex-grow-1 bg-dark text-light p-2 rounded me-2" id="newApiKey">{{.newKey}}</code>
|
||||
<button class="btn btn-outline-secondary" onclick="copyKey()"><i class="bi bi-clipboard"></i></button>
|
||||
<code class="flex-grow-1 bg-dark text-light p-2 rounded me-2" id="newApiKey" aria-label="APIキー">{{.newKey}}</code>
|
||||
<button class="btn btn-outline-secondary" id="copyKeyBtn" aria-label="APIキーをコピー">
|
||||
<i class="bi bi-clipboard" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-plus-circle me-2"></i>新規APIキー作成
|
||||
<i class="bi bi-plus-circle me-2" aria-hidden="true"></i>新規APIキー作成
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="/admin/api-keys" method="POST" class="row g-3">
|
||||
{{.csrfField}}
|
||||
<div class="col-md-8">
|
||||
<input type="text" class="form-control" name="name" placeholder="キー名(例: 外部連携用)" required>
|
||||
<label for="keyName" class="visually-hidden">キー名</label>
|
||||
<input type="text" class="form-control" id="keyName" name="name" placeholder="キー名(例: 外部連携用)" required>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<button type="submit" class="btn btn-primary w-100"><i class="bi bi-plus me-1"></i>作成</button>
|
||||
<button type="submit" class="btn btn-primary w-100"><i class="bi bi-plus me-1" aria-hidden="true"></i>作成</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -37,31 +40,32 @@
|
||||
|
||||
{{if .apiKeys}}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<table class="table table-hover" aria-label="APIキー一覧">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>キー名</th>
|
||||
<th>作成者</th>
|
||||
<th>最終使用</th>
|
||||
<th>作成日</th>
|
||||
<th style="width: 100px">操作</th>
|
||||
<th scope="col">ID</th>
|
||||
<th scope="col">キー名</th>
|
||||
<th scope="col">作成者</th>
|
||||
<th scope="col">最終使用</th>
|
||||
<th scope="col">作成日</th>
|
||||
<th scope="col" style="width: 100px">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .apiKeys}}
|
||||
<tr>
|
||||
<td>{{.ID}}</td>
|
||||
<td><i class="bi bi-key me-1"></i>{{.Name}}</td>
|
||||
<td><i class="bi bi-key me-1" aria-hidden="true"></i>{{.Name}}</td>
|
||||
<td>{{if .User}}{{.User.Name}}{{else}}-{{end}}</td>
|
||||
<td>{{if .LastUsed}}{{formatDateTime .LastUsed}}{{else}}<span class="text-muted">未使用</span>{{end}}</td>
|
||||
<td>{{formatDate .CreatedAt}}</td>
|
||||
<td>
|
||||
<form action="/admin/api-keys/{{.ID}}/delete" method="POST" class="d-inline"
|
||||
onsubmit="return confirm('このAPIキーを削除しますか?')">
|
||||
data-confirm="このAPIキーを削除しますか?">
|
||||
<input type="hidden" name="_csrf" value="{{$.csrfToken}}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="削除"><i
|
||||
class="bi bi-trash"></i></button>
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" aria-label="{{.Name}}を削除">
|
||||
<i class="bi bi-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -71,7 +75,7 @@
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-key display-1 text-muted"></i>
|
||||
<i class="bi bi-key display-1 text-muted" aria-hidden="true"></i>
|
||||
<h3 class="mt-3">APIキーがありません</h3>
|
||||
<p class="text-muted">上のフォームから新しいAPIキーを作成してください。</p>
|
||||
</div>
|
||||
@@ -79,12 +83,11 @@
|
||||
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-info-circle me-2"></i>API使用方法
|
||||
<i class="bi bi-info-circle me-2" aria-hidden="true"></i>API使用方法
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-2">APIにアクセスするには、<code>Authorization</code>ヘッダーにAPIキーを設定してください:</p>
|
||||
<pre
|
||||
class="bg-dark text-light p-3 rounded"><code>curl -H "Authorization: Bearer YOUR_API_KEY" http://localhost:8080/api/v1/assignments</code></pre>
|
||||
<pre class="bg-dark text-light p-3 rounded"><code>curl -H "Authorization: Bearer YOUR_API_KEY" http://localhost:8080/api/v1/assignments</code></pre>
|
||||
<h6 class="mt-3">利用可能なエンドポイント:</h6>
|
||||
<ul class="mb-0">
|
||||
<li><code>GET /api/v1/assignments</code> - 課題一覧取得</li>
|
||||
@@ -110,11 +113,20 @@
|
||||
|
||||
{{define "scripts"}}
|
||||
<script>
|
||||
function copyKey() {
|
||||
const key = document.getElementById('newApiKey').innerText;
|
||||
navigator.clipboard.writeText(key).then(() => {
|
||||
alert('APIキーをコピーしました');
|
||||
var copyKeyBtn = document.getElementById('copyKeyBtn');
|
||||
if (copyKeyBtn) {
|
||||
copyKeyBtn.addEventListener('click', function() {
|
||||
var key = document.getElementById('newApiKey').textContent.trim();
|
||||
if (!navigator.clipboard) {
|
||||
showCopyFeedback('クリップボードがサポートされていません。手動でコピーしてください。');
|
||||
return;
|
||||
}
|
||||
navigator.clipboard.writeText(key).then(function() {
|
||||
showCopyFeedback('APIキーをコピーしました');
|
||||
}).catch(function(err) {
|
||||
showCopyFeedback('コピーに失敗しました: ' + (err.message || '不明なエラー'));
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -1,55 +1,68 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "content"}}
|
||||
<h1 class="mb-4"><i class="bi bi-people me-2"></i>ユーザー管理</h1>
|
||||
<h1 class="mb-4"><i class="bi bi-people me-2" aria-hidden="true"></i>ユーザー管理</h1>
|
||||
|
||||
{{if .error}}<div class="alert alert-danger">{{.error}}</div>{{end}}
|
||||
{{if .error}}<div class="alert alert-danger" role="alert">{{.error}}</div>{{end}}
|
||||
|
||||
{{if .users}}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<table class="table table-hover" aria-label="ユーザー一覧">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>名前</th>
|
||||
<th>メールアドレス</th>
|
||||
<th>ロール</th>
|
||||
<th>登録日</th>
|
||||
<th style="width: 200px">操作</th>
|
||||
<th scope="col">ID</th>
|
||||
<th scope="col">名前</th>
|
||||
<th scope="col">メールアドレス</th>
|
||||
<th scope="col">ロール</th>
|
||||
<th scope="col">登録日</th>
|
||||
<th scope="col" style="width: 200px">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .users}}
|
||||
<tr {{if eq .ID $.currentUserID}}class="table-primary" {{end}}>
|
||||
<tr {{if eq .ID $.currentUserID}}class="table-primary"{{end}}>
|
||||
<td>{{.ID}}</td>
|
||||
<td>{{.Name}}{{if eq .ID $.currentUserID}}<span class="badge bg-info ms-2">自分</span>{{end}}</td>
|
||||
<td>{{.Email}}</td>
|
||||
<td>{{if eq .Role "admin"}}<span class="badge bg-danger">管理者</span>{{else}}<span
|
||||
class="badge bg-secondary">ユーザー</span>{{end}}</td>
|
||||
<td>
|
||||
{{if eq .Role "admin"}}
|
||||
<span class="badge bg-danger">管理者</span>
|
||||
{{else}}
|
||||
<span class="badge bg-secondary">ユーザー</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td>{{formatDate .CreatedAt}}</td>
|
||||
<td>
|
||||
{{if ne .ID $.currentUserID}}
|
||||
<form action="/admin/users/{{.ID}}/role" method="POST" class="d-inline" {{if eq .Role "admin"
|
||||
}}onsubmit="return confirm('このユーザーを一般ユーザーに降格しますか?')"
|
||||
{{else}}onsubmit="return confirm('このユーザーを管理者に昇格しますか?')" {{end}}>
|
||||
{{if eq .Role "admin"}}
|
||||
<form action="/admin/users/{{.ID}}/role" method="POST" class="d-inline"
|
||||
data-confirm="このユーザーを一般ユーザーに降格しますか?">
|
||||
<input type="hidden" name="_csrf" value="{{$.csrfToken}}">
|
||||
{{if eq .Role "admin"}}
|
||||
<input type="hidden" name="role" value="user">
|
||||
<button type="submit" class="btn btn-sm btn-outline-secondary" title="ユーザーに降格"><i
|
||||
class="bi bi-arrow-down"></i></button>
|
||||
{{else}}
|
||||
<input type="hidden" name="role" value="admin">
|
||||
<button type="submit" class="btn btn-sm btn-outline-warning" title="管理者に昇格"><i
|
||||
class="bi bi-arrow-up"></i></button>
|
||||
{{end}}
|
||||
<button type="submit" class="btn btn-sm btn-outline-secondary" aria-label="{{.Name}}をユーザーに降格">
|
||||
<i class="bi bi-arrow-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
</form>
|
||||
<form action="/admin/users/{{.ID}}/delete" method="POST" class="d-inline"
|
||||
onsubmit="return confirm('このユーザーを削除しますか?')">
|
||||
{{else}}
|
||||
<form action="/admin/users/{{.ID}}/role" method="POST" class="d-inline"
|
||||
data-confirm="このユーザーを管理者に昇格しますか?">
|
||||
<input type="hidden" name="_csrf" value="{{$.csrfToken}}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="削除"><i
|
||||
class="bi bi-trash"></i></button>
|
||||
<input type="hidden" name="role" value="admin">
|
||||
<button type="submit" class="btn btn-sm btn-outline-warning" aria-label="{{.Name}}を管理者に昇格">
|
||||
<i class="bi bi-arrow-up" aria-hidden="true"></i>
|
||||
</button>
|
||||
</form>
|
||||
{{else}}<span class="text-muted">-</span>{{end}}
|
||||
{{end}}
|
||||
<form action="/admin/users/{{.ID}}/delete" method="POST" class="d-inline"
|
||||
data-confirm="このユーザーを削除しますか?">
|
||||
<input type="hidden" name="_csrf" value="{{$.csrfToken}}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" aria-label="{{.Name}}を削除">
|
||||
<i class="bi bi-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</form>
|
||||
{{else}}
|
||||
<span class="text-muted">-</span>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
@@ -58,8 +71,8 @@
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-people display-1 text-muted"></i>
|
||||
<i class="bi bi-people display-1 text-muted" aria-hidden="true"></i>
|
||||
<h3 class="mt-3">ユーザーがいません</h3>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -5,85 +5,71 @@
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-pencil me-2"></i>課題編集</h5>
|
||||
<h5 class="mb-0"><i class="bi bi-pencil me-2" aria-hidden="true"></i>課題編集</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{if .error}}<div class="alert alert-danger">{{.error}}</div>{{end}}
|
||||
{{if .error}}<div class="alert alert-danger" role="alert">{{.error}}</div>{{end}}
|
||||
<form method="POST" action="/assignments/{{.assignment.ID}}">
|
||||
{{.csrfField}}
|
||||
<div class="mb-3">
|
||||
<label for="title" class="form-label">タイトル <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="title" name="title" value="{{.assignment.Title}}"
|
||||
required>
|
||||
<label for="title" class="form-label">タイトル <span class="text-danger" aria-hidden="true">*</span><span class="visually-hidden">(必須)</span></label>
|
||||
<input type="text" class="form-control" id="title" name="title" value="{{.assignment.Title}}" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="subject" class="form-label">科目</label>
|
||||
<input type="text" class="form-control" id="subject" name="subject"
|
||||
value="{{.assignment.Subject}}">
|
||||
<input type="text" class="form-control" id="subject" name="subject" value="{{.assignment.Subject}}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="priority" class="form-label">重要度</label>
|
||||
<select class="form-select" id="priority" name="priority">
|
||||
<option value="low" {{if eq .assignment.Priority "low" }}selected{{end}}>小</option>
|
||||
<option value="medium" {{if eq .assignment.Priority "medium" }}selected{{end}}>中</option>
|
||||
<option value="high" {{if eq .assignment.Priority "high" }}selected{{end}}>大</option>
|
||||
<option value="low" {{if eq .assignment.Priority "low" }}selected{{end}}>低</option>
|
||||
<option value="medium" {{if eq .assignment.Priority "medium"}}selected{{end}}>中</option>
|
||||
<option value="high" {{if eq .assignment.Priority "high" }}selected{{end}}>高</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="due_date" class="form-label">提出期限 <span class="text-danger">*</span></label>
|
||||
<input type="datetime-local" class="form-control" id="due_date" name="due_date"
|
||||
value="{{formatDateInput .assignment.DueDate}}" required>
|
||||
<label for="due_date" class="form-label">提出期限 <span class="text-danger" aria-hidden="true">*</span><span class="visually-hidden">(必須)</span></label>
|
||||
<input type="datetime-local" class="form-control" id="due_date" name="due_date" value="{{formatDateInput .assignment.DueDate}}" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">説明</label>
|
||||
<textarea class="form-control" id="description" name="description"
|
||||
rows="3">{{.assignment.Description}}</textarea>
|
||||
<textarea class="form-control" id="description" name="description" rows="3">{{.assignment.Description}}</textarea>
|
||||
</div>
|
||||
<!-- 通知設定 -->
|
||||
<div class="card bg-light mb-3">
|
||||
<div class="card-body py-2">
|
||||
<h6 class="mb-2"><i class="bi bi-bell me-1"></i>通知設定</h6>
|
||||
<!-- 督促通知 -->
|
||||
<h6 class="mb-2"><i class="bi bi-bell me-1" aria-hidden="true"></i>通知設定</h6>
|
||||
<div class="form-check form-switch mb-2">
|
||||
<input class="form-check-input" type="checkbox" id="urgent_reminder_enabled"
|
||||
name="urgent_reminder_enabled" {{if
|
||||
.assignment.UrgentReminderEnabled}}checked{{end}}>
|
||||
<input class="form-check-input" type="checkbox" id="urgent_reminder_enabled" name="urgent_reminder_enabled" {{if .assignment.UrgentReminderEnabled}}checked{{end}}>
|
||||
<label class="form-check-label" for="urgent_reminder_enabled">
|
||||
督促通知(期限3時間前から繰り返し通知)
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-text small mb-2">
|
||||
重要度により間隔が変わります:大=10分、中=30分、小=1時間
|
||||
重要度により通知間隔が変わります:高=10分ごと、中=30分ごと、低=1時間ごと
|
||||
</div>
|
||||
<hr class="my-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="reminder_enabled"
|
||||
name="reminder_enabled" {{if .assignment.ReminderEnabled}}checked{{end}}
|
||||
onchange="toggleReminderDate(this)">
|
||||
<input class="form-check-input" type="checkbox" id="reminder_enabled" name="reminder_enabled" {{if .assignment.ReminderEnabled}}checked{{end}} onchange="toggleReminderDate(this)">
|
||||
<label class="form-check-label" for="reminder_enabled">
|
||||
リマインダー(指定日時に通知)
|
||||
</label>
|
||||
</div>
|
||||
<div class="mt-2" id="reminder_at_group"
|
||||
style="display: {{if .assignment.ReminderEnabled}}block{{else}}none{{end}};">
|
||||
<div class="mt-2" id="reminder_at_group" style="display: {{if .assignment.ReminderEnabled}}block{{else}}none{{end}};">
|
||||
<label for="reminder_at" class="form-label small">通知日時</label>
|
||||
<input type="datetime-local" class="form-control form-control-sm" id="reminder_at"
|
||||
name="reminder_at"
|
||||
value="{{if .assignment.ReminderAt}}{{formatDateInput .assignment.ReminderAt}}{{end}}">
|
||||
<input type="datetime-local" class="form-control form-control-sm" id="reminder_at" name="reminder_at" value="{{if .assignment.ReminderAt}}{{formatDateInput .assignment.ReminderAt}}{{end}}">
|
||||
{{if .assignment.ReminderSent}}
|
||||
<div class="text-success small mt-1"><i class="bi bi-check-circle me-1"></i>通知送信済み</div>
|
||||
<div class="text-success small mt-1"><i class="bi bi-check-circle me-1" aria-hidden="true"></i>通知送信済み</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{if .recurring}}
|
||||
<!-- 繰り返し設定 -->
|
||||
<div class="card bg-light mb-3">
|
||||
<div class="card-body py-2">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="mb-0"><i class="bi bi-arrow-repeat me-1"></i>繰り返し設定</h6>
|
||||
<h6 class="mb-0"><i class="bi bi-arrow-repeat me-1" aria-hidden="true"></i>繰り返し設定</h6>
|
||||
<a href="/recurring/{{.recurring.ID}}/edit" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-pencil me-1"></i>編集
|
||||
<i class="bi bi-pencil me-1" aria-hidden="true"></i>編集
|
||||
</a>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -110,7 +96,7 @@
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg me-1"></i>更新</button>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg me-1" aria-hidden="true"></i>更新</button>
|
||||
<a href="/assignments" class="btn btn-outline-secondary">キャンセル</a>
|
||||
</div>
|
||||
</form>
|
||||
@@ -123,4 +109,4 @@
|
||||
document.getElementById('reminder_at_group').style.display = checkbox.checked ? 'block' : 'none';
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -2,132 +2,100 @@
|
||||
|
||||
{{define "content"}}
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<h4 class="mb-0 fw-bold"><i class="bi bi-list-task me-2"></i>課題一覧</h4>
|
||||
</div>
|
||||
<h4 class="mb-0 fw-bold"><i class="bi bi-list-task me-2" aria-hidden="true"></i>課題一覧</h4>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-sm btn-secondary text-white" onclick="toggleCountdown()" id="toggleCountdownBtn">
|
||||
<i class="bi bi-clock me-1"></i><span id="countdownBtnText">カウントダウン表示中</span>
|
||||
<button class="btn btn-sm btn-secondary text-white" onclick="toggleCountdown()" id="toggleCountdownBtn" aria-pressed="false">
|
||||
<i class="bi bi-clock me-1" aria-hidden="true"></i><span id="countdownBtnText">カウントダウンを非表示</span>
|
||||
</button>
|
||||
<a href="/assignments/new" class="btn btn-sm btn-primary">
|
||||
<i class="bi bi-plus-lg me-1"></i>新規登録
|
||||
<i class="bi bi-plus-lg me-1" aria-hidden="true"></i>新規登録
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<ul class="nav nav-tabs border-0 mb-2" id="assignmentTabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link py-2 rounded-0 {{if eq .filter "pending"}}fw-bold border-bottom border-dark border-3
|
||||
text-dark{{else}}border-0 text-muted{{end}}"
|
||||
href="/assignments?filter=pending&q={{.query}}&priority={{.priority}}">
|
||||
未完了
|
||||
</a>
|
||||
<ul class="nav nav-tabs border-0 mb-2" id="assignmentTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link py-2 rounded-0 {{if eq .filter "pending"}}fw-bold border-bottom border-dark border-3 text-dark{{else}}border-0 text-muted{{end}}"
|
||||
href="/assignments?filter=pending&q={{.query}}&priority={{.priority}}">未完了</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link py-2 rounded-0 {{if eq .filter "due_today"}}fw-bold border-bottom border-dark border-3
|
||||
text-dark{{else}}border-0 text-muted{{end}}"
|
||||
href="/assignments?filter=due_today&q={{.query}}&priority={{.priority}}">
|
||||
今日が期限
|
||||
</a>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link py-2 rounded-0 {{if eq .filter "due_today"}}fw-bold border-bottom border-dark border-3 text-dark{{else}}border-0 text-muted{{end}}"
|
||||
href="/assignments?filter=due_today&q={{.query}}&priority={{.priority}}">今日が期限</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link py-2 rounded-0 {{if eq .filter "due_this_week"}}fw-bold border-bottom border-dark border-3
|
||||
text-dark{{else}}border-0 text-muted{{end}}"
|
||||
href="/assignments?filter=due_this_week&q={{.query}}&priority={{.priority}}">
|
||||
今週が期限
|
||||
</a>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link py-2 rounded-0 {{if eq .filter "due_this_week"}}fw-bold border-bottom border-dark border-3 text-dark{{else}}border-0 text-muted{{end}}"
|
||||
href="/assignments?filter=due_this_week&q={{.query}}&priority={{.priority}}">今週が期限</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link py-2 rounded-0 {{if eq .filter "completed"}}fw-bold border-bottom border-dark border-3
|
||||
text-dark{{else}}border-0 text-muted{{end}}"
|
||||
href="/assignments?filter=completed&q={{.query}}&priority={{.priority}}">
|
||||
完了済み
|
||||
</a>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link py-2 rounded-0 {{if eq .filter "completed"}}fw-bold border-bottom border-dark border-3 text-dark{{else}}border-0 text-muted{{end}}"
|
||||
href="/assignments?filter=completed&q={{.query}}&priority={{.priority}}">完了済み</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link py-2 rounded-0 {{if eq .filter "overdue"}}fw-bold border-bottom border-dark border-3
|
||||
text-dark{{else}}border-0 text-muted{{end}}"
|
||||
href="/assignments?filter=overdue&q={{.query}}&priority={{.priority}}">
|
||||
期限切れ
|
||||
</a>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link py-2 rounded-0 {{if eq .filter "overdue"}}fw-bold border-bottom border-dark border-3 text-dark{{else}}border-0 text-muted{{end}}"
|
||||
href="/assignments?filter=overdue&q={{.query}}&priority={{.priority}}">期限切れ</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link py-2 rounded-0 border-0 text-muted"
|
||||
href="/recurring">
|
||||
繰り返し
|
||||
<li class="nav-item ms-auto" role="presentation">
|
||||
<a class="nav-link py-2 rounded-0 border-0 text-muted" href="/recurring">
|
||||
<i class="bi bi-arrow-repeat me-1" aria-hidden="true"></i>繰り返し管理
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<hr class="mt-0 mb-3 text-muted" style="opacity: 0.1;">
|
||||
|
||||
<!-- Filter Section -->
|
||||
<form action="/assignments" method="GET" class="row g-2 mb-3 align-items-center">
|
||||
<form action="/assignments" method="GET" class="row g-2 mb-3 align-items-center" role="search">
|
||||
<input type="hidden" name="filter" value="{{.filter}}">
|
||||
<div class="col-md-5">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text bg-white border-end-0 text-muted"><i class="bi bi-search"></i></span>
|
||||
<input type="text" class="form-control border-start-0 ps-0 bg-white" name="q" placeholder="検索..."
|
||||
value="{{.query}}">
|
||||
<span class="input-group-text bg-white border-end-0 text-muted"><i class="bi bi-search" aria-hidden="true"></i></span>
|
||||
<label for="searchInput" class="visually-hidden">課題を検索</label>
|
||||
<input type="text" class="form-control border-start-0 ps-0 bg-white" id="searchInput" name="q" placeholder="検索..." value="{{.query}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<select class="form-select form-select-sm bg-white" name="priority" onchange="this.form.submit()">
|
||||
<label for="priorityFilter" class="visually-hidden">重要度で絞り込み</label>
|
||||
<select class="form-select form-select-sm bg-white" id="priorityFilter" name="priority" onchange="this.form.submit()">
|
||||
<option value="">全ての重要度</option>
|
||||
<option value="high" {{if eq .priority "high" }}selected{{end}}>高</option>
|
||||
<option value="medium" {{if eq .priority "medium" }}selected{{end}}>中</option>
|
||||
<option value="low" {{if eq .priority "low" }}selected{{end}}>低</option>
|
||||
<option value="high" {{if eq .priority "high" }}selected{{end}}>高</option>
|
||||
<option value="medium" {{if eq .priority "medium"}}selected{{end}}>中</option>
|
||||
<option value="low" {{if eq .priority "low" }}selected{{end}}>低</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<a href="/assignments?filter={{.filter}}" class="btn btn-sm btn-outline-secondary w-100 bg-white">
|
||||
クリア
|
||||
</a>
|
||||
<a href="/assignments?filter={{.filter}}" class="btn btn-sm btn-outline-secondary w-100 bg-white">クリア</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="card shadow-sm border-0 rounded-0">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0 custom-table">
|
||||
<table class="table table-hover align-middle mb-0 custom-table" aria-label="課題一覧">
|
||||
<thead class="bg-secondary-subtle">
|
||||
<tr>
|
||||
<th style="width: 50px;" class="ps-3 text-center text-dark fw-bold">状態</th>
|
||||
<th style="width: 120px;" class="text-dark fw-bold">科目</th>
|
||||
<th style="width: 80px;" class="text-dark fw-bold">重要度</th>
|
||||
<th class="text-dark fw-bold">タイトル</th>
|
||||
<th style="width: 140px;" class="text-dark fw-bold">期限</th>
|
||||
<th style="width: 120px;" class="countdown-col text-dark fw-bold">残り</th>
|
||||
<th style="width: 80px;" class="text-end pe-3 text-dark fw-bold">操作</th>
|
||||
<th scope="col" style="width: 50px;" class="ps-3 text-center text-dark fw-bold">状態</th>
|
||||
<th scope="col" style="width: 120px;" class="text-dark fw-bold">科目</th>
|
||||
<th scope="col" style="width: 80px;" class="text-dark fw-bold">重要度</th>
|
||||
<th scope="col" class="text-dark fw-bold">タイトル</th>
|
||||
<th scope="col" style="width: 140px;" class="text-dark fw-bold">期限</th>
|
||||
<th scope="col" style="width: 120px;" class="countdown-col text-dark fw-bold">残り</th>
|
||||
<th scope="col" style="width: 80px;" class="text-end pe-3 text-dark fw-bold">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .assignments}}
|
||||
<tr class="assignment-row border-bottom" data-due-ts="{{.DueDate.Unix}}"
|
||||
data-completed="{{.IsCompleted}}">
|
||||
<tr class="assignment-row border-bottom" data-due-ts="{{.DueDate.Unix}}" data-completed="{{.IsCompleted}}">
|
||||
<td class="ps-3 text-center">
|
||||
{{if .IsCompleted}}
|
||||
<form action="/assignments/{{.ID}}/toggle" method="POST" class="d-inline">
|
||||
<input type="hidden" name="_csrf" value="{{$.csrfToken}}">
|
||||
<button type="submit"
|
||||
class="btn btn-link p-0 text-success text-decoration-none hover-dark"
|
||||
title="未完了に戻す">
|
||||
<i class="bi bi-check-circle-fill"></i>
|
||||
<button type="submit" class="btn btn-link p-0 text-success text-decoration-none btn-touch" aria-label="未完了に戻す">
|
||||
<i class="bi bi-check-circle-fill" aria-hidden="true"></i>
|
||||
</button>
|
||||
</form>
|
||||
{{else}}
|
||||
<form action="/assignments/{{.ID}}/toggle" method="POST" class="d-inline">
|
||||
<input type="hidden" name="_csrf" value="{{$.csrfToken}}">
|
||||
<button type="submit"
|
||||
class="btn btn-link p-0 text-secondary text-decoration-none hover-dark"
|
||||
title="完了にする">
|
||||
<i class="bi bi-circle"></i>
|
||||
<button type="submit" class="btn btn-link p-0 text-secondary text-decoration-none btn-touch" aria-label="完了にする">
|
||||
<i class="bi bi-circle" aria-hidden="true"></i>
|
||||
</button>
|
||||
</form>
|
||||
{{end}}
|
||||
@@ -135,57 +103,54 @@
|
||||
<td><span class="badge bg-secondary text-white border-0 fw-bold">{{.Subject}}</span></td>
|
||||
<td>
|
||||
{{if eq .Priority "high"}}
|
||||
<span class="badge bg-danger text-white border-0 fw-bold small">高</span>
|
||||
<span class="badge bg-danger text-white border-0 fw-bold small">高<span class="visually-hidden">(重要度:高)</span></span>
|
||||
{{else if eq .Priority "medium"}}
|
||||
<span class="badge bg-warning text-dark border-0 fw-bold small">中</span>
|
||||
<span class="badge bg-warning text-dark border-0 fw-bold small">中<span class="visually-hidden">(重要度:中)</span></span>
|
||||
{{else}}
|
||||
<span class="badge bg-dark text-white border-0 fw-bold small">低</span>
|
||||
<span class="badge bg-dark text-white border-0 fw-bold small">低<span class="visually-hidden">(重要度:低)</span></span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="fw-bold text-dark text-truncate" style="max-width: 280px;">{{.Title}}</div>
|
||||
{{if .RecurringAssignmentID}}
|
||||
<button type="button" class="btn btn-link p-0 ms-2 text-info" data-bs-toggle="modal"
|
||||
data-bs-target="#recurringModal" data-recurring-id="{{.RecurringAssignmentID}}"
|
||||
<button type="button" class="btn btn-link p-0 ms-2 text-info btn-touch" data-bs-toggle="modal"
|
||||
data-bs-target="#recurringModal"
|
||||
data-recurring-id="{{.RecurringAssignmentID}}"
|
||||
data-assignment-id="{{.ID}}"
|
||||
data-recurring-title="{{.Title}}"
|
||||
data-recurring-type="{{if .RecurringAssignment}}{{.RecurringAssignment.RecurrenceType}}{{else}}unknown{{end}}"
|
||||
data-recurring-active="{{if .RecurringAssignment}}{{.RecurringAssignment.IsActive}}{{else}}true{{end}}"
|
||||
title="繰り返し課題">
|
||||
<i class="fa-solid fa-repeat"></i>
|
||||
aria-label="繰り返し設定を表示">
|
||||
<i class="bi bi-repeat" aria-hidden="true"></i>
|
||||
</button>
|
||||
{{end}}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="small fw-bold text-dark user-select-all">{{.DueDate.Format "2006/01/02 15:04"}}
|
||||
</div>
|
||||
<div class="small fw-bold text-dark user-select-all">{{.DueDate.Format "2006/01/02 15:04"}}</div>
|
||||
</td>
|
||||
<td class="countdown-col">
|
||||
{{if not .IsCompleted}}
|
||||
<span class="countdown small fw-bold font-monospace text-dark">...</span>
|
||||
<span class="countdown small fw-bold font-monospace text-dark" aria-live="off">...</span>
|
||||
{{else}}
|
||||
<span class="text-secondary small fw-bold">-</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td class="text-end pe-3">
|
||||
<div class="btn-group">
|
||||
<a href="/assignments/{{.ID}}/edit" class="text-primary me-3 text-decoration-none">
|
||||
<i class="bi bi-pencil-fill"></i>
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<a href="/assignments/{{.ID}}/edit" class="text-primary text-decoration-none btn-touch d-inline-flex align-items-center" aria-label="編集">
|
||||
<i class="bi bi-pencil-fill" aria-hidden="true"></i>
|
||||
</a>
|
||||
{{if .RecurringAssignmentID}}
|
||||
<button type="button" class="btn btn-link p-0 text-danger text-decoration-none border-0 bg-transparent"
|
||||
onclick="showDeleteRecurringModal({{.ID}}, {{.RecurringAssignmentID}})">
|
||||
<i class="bi bi-trash-fill"></i>
|
||||
<button type="button" class="btn btn-link p-0 text-danger text-decoration-none border-0 bg-transparent btn-touch" onclick="showDeleteRecurringModal({{.ID}}, {{.RecurringAssignmentID}})" aria-label="削除">
|
||||
<i class="bi bi-trash-fill" aria-hidden="true"></i>
|
||||
</button>
|
||||
{{else}}
|
||||
<form action="/assignments/{{.ID}}/delete" method="POST" class="d-inline"
|
||||
onsubmit="return confirm('削除しますか?');">
|
||||
<form action="/assignments/{{.ID}}/delete" method="POST" class="d-inline" data-confirm="削除しますか?">
|
||||
<input type="hidden" name="_csrf" value="{{$.csrfToken}}">
|
||||
<button type="submit"
|
||||
class="btn btn-link p-0 text-danger text-decoration-none border-0 bg-transparent">
|
||||
<i class="bi bi-trash-fill"></i>
|
||||
<button type="submit" class="btn btn-link p-0 text-danger text-decoration-none border-0 bg-transparent btn-touch" aria-label="削除">
|
||||
<i class="bi bi-trash-fill" aria-hidden="true"></i>
|
||||
</button>
|
||||
</form>
|
||||
{{end}}
|
||||
@@ -194,8 +159,12 @@
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr>
|
||||
<td colspan="7" class="text-center py-4 text-secondary fw-bold small">
|
||||
課題なし
|
||||
<td colspan="7" class="text-center py-5">
|
||||
<i class="bi bi-inbox display-4 text-muted" aria-hidden="true"></i>
|
||||
<p class="mt-2 mb-1 text-muted fw-bold">課題がありません</p>
|
||||
<a href="/assignments/new" class="btn btn-sm btn-primary mt-1">
|
||||
<i class="bi bi-plus-lg me-1" aria-hidden="true"></i>課題を登録する
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
@@ -205,21 +174,19 @@
|
||||
</div>
|
||||
{{if gt .totalPages 1}}
|
||||
<div class="card-footer bg-white border-top-0 py-2">
|
||||
<nav>
|
||||
<nav aria-label="ページナビゲーション">
|
||||
<ul class="pagination pagination-sm justify-content-center mb-0">
|
||||
<li class="page-item {{if not .hasPrev}}disabled{{end}}">
|
||||
<a class="page-link border-0 text-secondary"
|
||||
href="/assignments?page={{.prevPage}}&filter={{.filter}}&q={{.query}}&priority={{.priority}}">
|
||||
<i class="bi bi-chevron-left"></i>
|
||||
<a class="page-link border-0 text-secondary" href="/assignments?page={{.prevPage}}&filter={{.filter}}&q={{.query}}&priority={{.priority}}" aria-label="前のページ">
|
||||
<i class="bi bi-chevron-left" aria-hidden="true"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link border-0 text-dark fw-bold">{{.currentPage}} / {{.totalPages}}</span>
|
||||
<span class="page-link border-0 text-dark fw-bold" aria-current="page">{{.currentPage}} / {{.totalPages}}</span>
|
||||
</li>
|
||||
<li class="page-item {{if not .hasNext}}disabled{{end}}">
|
||||
<a class="page-link border-0 text-secondary"
|
||||
href="/assignments?page={{.nextPage}}&filter={{.filter}}&q={{.query}}&priority={{.priority}}">
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
<a class="page-link border-0 text-secondary" href="/assignments?page={{.nextPage}}&filter={{.filter}}&q={{.query}}&priority={{.priority}}" aria-label="次のページ">
|
||||
<i class="bi bi-chevron-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -229,121 +196,119 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var _countdownInterval = null;
|
||||
|
||||
function updateCountdowns() {
|
||||
const now = new Date();
|
||||
document.querySelectorAll('.assignment-row').forEach(row => {
|
||||
var now = new Date();
|
||||
var hasUnder24h = false;
|
||||
|
||||
document.querySelectorAll('.assignment-row').forEach(function(row) {
|
||||
if (row.getAttribute('data-completed') === 'true') return;
|
||||
|
||||
const dueTs = row.getAttribute('data-due-ts');
|
||||
var dueTs = row.getAttribute('data-due-ts');
|
||||
if (!dueTs) return;
|
||||
|
||||
// Fix: Use timestamp directly to avoid parsing issues
|
||||
const due = new Date(parseInt(dueTs) * 1000);
|
||||
var due = new Date(parseInt(dueTs) * 1000);
|
||||
if (isNaN(due.getTime())) return;
|
||||
|
||||
const diff = due - now;
|
||||
const countdownEl = row.querySelector('.countdown');
|
||||
var diff = due - now;
|
||||
var countdownEl = row.querySelector('.countdown');
|
||||
|
||||
// Reset classes
|
||||
row.classList.remove('anxiety-danger', 'anxiety-warning', 'bg-danger-subtle');
|
||||
if (countdownEl) countdownEl.className = 'countdown small fw-bold font-monospace';
|
||||
|
||||
if (diff < 0) {
|
||||
if (countdownEl) {
|
||||
countdownEl.textContent = "期限切れ";
|
||||
countdownEl.textContent = '期限切れ';
|
||||
countdownEl.classList.add('text-danger');
|
||||
}
|
||||
row.classList.add('bg-danger-subtle');
|
||||
return;
|
||||
}
|
||||
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
||||
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
|
||||
var days = Math.floor(diff / 86400000);
|
||||
var hours = Math.floor((diff % 86400000) / 3600000);
|
||||
var minutes = Math.floor((diff % 3600000) / 60000);
|
||||
var seconds = Math.floor((diff % 60000) / 1000);
|
||||
var remainingHours = days * 24 + hours;
|
||||
|
||||
let text = "";
|
||||
let remainingHours = (days * 24) + hours;
|
||||
|
||||
if (days > 0) {
|
||||
text += `${days}日 `;
|
||||
}
|
||||
text += `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
||||
var text = (days > 0 ? days + '日 ' : '') +
|
||||
String(hours).padStart(2, '0') + ':' +
|
||||
String(minutes).padStart(2, '0') + ':' +
|
||||
String(seconds).padStart(2, '0');
|
||||
|
||||
if (countdownEl) countdownEl.textContent = text;
|
||||
|
||||
// Anxiety Logic
|
||||
if (remainingHours < 24) {
|
||||
hasUnder24h = true;
|
||||
row.classList.add('anxiety-danger');
|
||||
if (countdownEl) {
|
||||
countdownEl.classList.add('text-danger', 'countdown-urgent');
|
||||
countdownEl.innerHTML = '<i class="bi bi-exclamation-triangle-fill me-1"></i>' + text;
|
||||
countdownEl.innerHTML = '<i class="bi bi-exclamation-triangle-fill me-1" aria-hidden="true"></i>' + text;
|
||||
}
|
||||
} else if (days < 7) {
|
||||
row.classList.add('anxiety-warning');
|
||||
if (countdownEl) {
|
||||
countdownEl.classList.add('text-dark');
|
||||
countdownEl.innerHTML = '<i class="bi bi-exclamation-triangle-fill text-warning me-1"></i>' + text;
|
||||
countdownEl.innerHTML = '<i class="bi bi-exclamation-triangle-fill text-warning me-1" aria-hidden="true"></i>' + text;
|
||||
}
|
||||
} else {
|
||||
if (countdownEl) countdownEl.classList.add('text-secondary');
|
||||
}
|
||||
});
|
||||
|
||||
scheduleNextUpdate(hasUnder24h);
|
||||
}
|
||||
|
||||
function scheduleNextUpdate(hasUnder24h) {
|
||||
if (_countdownInterval !== null) return;
|
||||
var interval = hasUnder24h ? 1000 : 60000;
|
||||
_countdownInterval = setTimeout(function() {
|
||||
_countdownInterval = null;
|
||||
updateCountdowns();
|
||||
}, interval);
|
||||
}
|
||||
|
||||
function toggleCountdown() {
|
||||
const cols = document.querySelectorAll('.countdown-col');
|
||||
const btnText = document.getElementById('countdownBtnText');
|
||||
const isHidden = cols[0] && cols[0].style.display === 'none';
|
||||
var cols = document.querySelectorAll('.countdown-col');
|
||||
var btn = document.getElementById('toggleCountdownBtn');
|
||||
var btnText = document.getElementById('countdownBtnText');
|
||||
var isHidden = cols[0] && cols[0].style.display === 'none';
|
||||
|
||||
cols.forEach(col => {
|
||||
col.style.display = isHidden ? '' : 'none';
|
||||
});
|
||||
cols.forEach(function(col) { col.style.display = isHidden ? '' : 'none'; });
|
||||
|
||||
btnText.textContent = isHidden ? 'カウントダウン表示中' : 'カウントダウン非表示中';
|
||||
localStorage.setItem('countdownHidden', !isHidden);
|
||||
var nowHidden = !isHidden;
|
||||
btnText.textContent = nowHidden ? 'カウントダウンを表示' : 'カウントダウンを非表示';
|
||||
btn.setAttribute('aria-pressed', nowHidden ? 'true' : 'false');
|
||||
localStorage.setItem('countdownHidden', nowHidden);
|
||||
}
|
||||
|
||||
// Init with higher frequency for smooth panic
|
||||
setInterval(updateCountdowns, 1000);
|
||||
updateCountdowns();
|
||||
|
||||
// Check preference
|
||||
const isHidden = localStorage.getItem('countdownHidden') === 'true';
|
||||
if (isHidden) {
|
||||
document.querySelectorAll('.countdown-col').forEach(col => {
|
||||
col.style.display = 'none';
|
||||
});
|
||||
const btnText = document.getElementById('countdownBtnText');
|
||||
if (btnText) btnText.textContent = 'カウントダウン非表示中';
|
||||
if (localStorage.getItem('countdownHidden') === 'true') {
|
||||
document.querySelectorAll('.countdown-col').forEach(function(col) { col.style.display = 'none'; });
|
||||
var btn = document.getElementById('toggleCountdownBtn');
|
||||
var btnText = document.getElementById('countdownBtnText');
|
||||
if (btnText) btnText.textContent = 'カウントダウンを表示';
|
||||
if (btn) btn.setAttribute('aria-pressed', 'true');
|
||||
}
|
||||
|
||||
// Recurring modal handler - wait for DOM to be ready
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const recurringModal = document.getElementById('recurringModal');
|
||||
var recurringModal = document.getElementById('recurringModal');
|
||||
if (recurringModal) {
|
||||
recurringModal.addEventListener('show.bs.modal', function (event) {
|
||||
const button = event.relatedTarget;
|
||||
const id = button.getAttribute('data-recurring-id');
|
||||
const assignmentId = button.getAttribute('data-assignment-id');
|
||||
const title = button.getAttribute('data-recurring-title');
|
||||
const type = button.getAttribute('data-recurring-type');
|
||||
const isActive = button.getAttribute('data-recurring-active') === 'true';
|
||||
recurringModal.addEventListener('show.bs.modal', function(event) {
|
||||
var button = event.relatedTarget;
|
||||
var id = button.getAttribute('data-recurring-id');
|
||||
var title = button.getAttribute('data-recurring-title');
|
||||
var type = button.getAttribute('data-recurring-type');
|
||||
var isActive = button.getAttribute('data-recurring-active') === 'true';
|
||||
|
||||
document.getElementById('recurringModalTitle').textContent = title;
|
||||
document.getElementById('recurringStopForm').action = '/recurring/' + id + '/stop';
|
||||
document.getElementById('recurringEditBtn').href = '/recurring/' + id + '/edit';
|
||||
|
||||
const typeLabels = {
|
||||
'daily': '毎日',
|
||||
'weekly': '毎週',
|
||||
'monthly': '毎月',
|
||||
'unknown': '(読み込み中...)'
|
||||
};
|
||||
var typeLabels = { daily: '毎日', weekly: '毎週', monthly: '毎月', unknown: '(不明)' };
|
||||
document.getElementById('recurringTypeLabel').textContent = typeLabels[type] || type || '不明';
|
||||
|
||||
|
||||
const statusEl = document.getElementById('recurringStatus');
|
||||
var statusEl = document.getElementById('recurringStatus');
|
||||
if (isActive) {
|
||||
statusEl.innerHTML = '<span class="badge bg-success">有効</span>';
|
||||
document.getElementById('recurringStopBtn').style.display = 'inline-block';
|
||||
@@ -356,43 +321,41 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Recurring Modal -->
|
||||
<div class="modal fade" id="recurringModal" tabindex="-1">
|
||||
<div class="modal fade" id="recurringModal" tabindex="-1" aria-labelledby="recurringModalHeading" aria-modal="true" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="fa-solid fa-repeat me-2"></i>繰り返し課題</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
<h5 class="modal-title" id="recurringModalHeading"><i class="bi bi-repeat me-2" aria-hidden="true"></i>繰り返し課題</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="閉じる"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h6 id="recurringModalTitle" class="mb-3 fw-bold"></h6>
|
||||
<table class="table table-sm table-borderless">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th class="text-muted" style="width: 100px;">繰り返し</th>
|
||||
<th class="text-muted" style="width: 100px;" scope="row">繰り返し</th>
|
||||
<td id="recurringTypeLabel">読み込み中...</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-muted">状態</th>
|
||||
<th class="text-muted" scope="row">状態</th>
|
||||
<td id="recurringStatus">読み込み中...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="alert alert-info small mb-0">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
<div class="alert alert-info small mb-0" role="note">
|
||||
<i class="bi bi-info-circle me-1" aria-hidden="true"></i>
|
||||
繰り返しを停止すると、今後新しい課題は自動作成されなくなります。既存の課題はそのまま残ります。
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">閉じる</button>
|
||||
<a id="recurringEditBtn" href="#" class="btn btn-primary">
|
||||
<i class="bi bi-pencil me-1"></i>編集
|
||||
<i class="bi bi-pencil me-1" aria-hidden="true"></i>編集
|
||||
</a>
|
||||
<form id="recurringStopForm" method="POST" class="d-inline">
|
||||
<form id="recurringStopForm" method="POST" class="d-inline" data-confirm="繰り返しを停止しますか?">
|
||||
<input type="hidden" name="_csrf" value="{{.csrfToken}}">
|
||||
<button type="submit" id="recurringStopBtn" class="btn btn-danger"
|
||||
onclick="return confirm('繰り返しを停止しますか?');">
|
||||
<i class="bi bi-stop-fill me-1"></i>停止
|
||||
<button type="submit" id="recurringStopBtn" class="btn btn-danger">
|
||||
<i class="bi bi-stop-fill me-1" aria-hidden="true"></i>停止
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -400,13 +363,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Recurring Confirmation Modal -->
|
||||
<div class="modal fade" id="deleteRecurringModal" tabindex="-1">
|
||||
<div class="modal fade" id="deleteRecurringModal" tabindex="-1" aria-labelledby="deleteRecurringModalHeading" aria-modal="true" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-trash me-2"></i>繰り返し課題の削除</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
<h5 class="modal-title" id="deleteRecurringModalHeading"><i class="bi bi-trash me-2" aria-hidden="true"></i>繰り返し課題の削除</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="閉じる"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>この課題は繰り返し設定に関連付けられています。</p>
|
||||
@@ -416,15 +378,11 @@
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">キャンセル</button>
|
||||
<form id="deleteOnlyForm" method="POST" class="d-inline">
|
||||
<input type="hidden" name="_csrf" value="{{.csrfToken}}">
|
||||
<button type="submit" class="btn btn-outline-danger">
|
||||
課題のみ削除
|
||||
</button>
|
||||
<button type="submit" class="btn btn-outline-danger">課題のみ削除</button>
|
||||
</form>
|
||||
<form id="deleteAndStopForm" method="POST" class="d-inline">
|
||||
<input type="hidden" name="_csrf" value="{{.csrfToken}}">
|
||||
<button type="submit" class="btn btn-danger">
|
||||
削除して繰り返しも削除
|
||||
</button>
|
||||
<button type="submit" class="btn btn-danger">削除して繰り返しも削除</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -432,11 +390,11 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showDeleteRecurringModal(assignmentId, recurringId) {
|
||||
var modal = new bootstrap.Modal(document.getElementById('deleteRecurringModal'));
|
||||
document.getElementById('deleteOnlyForm').action = '/assignments/' + assignmentId + '/delete';
|
||||
document.getElementById('deleteAndStopForm').action = '/assignments/' + assignmentId + '/delete?stop_recurring=' + recurringId;
|
||||
modal.show();
|
||||
}
|
||||
function showDeleteRecurringModal(assignmentId, recurringId) {
|
||||
var modal = new bootstrap.Modal(document.getElementById('deleteRecurringModal'));
|
||||
document.getElementById('deleteOnlyForm').action = '/assignments/' + assignmentId + '/delete';
|
||||
document.getElementById('deleteAndStopForm').action = '/assignments/' + assignmentId + '/delete?stop_recurring=' + recurringId;
|
||||
modal.show();
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -5,83 +5,71 @@
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-plus-circle me-2"></i>課題登録</h5>
|
||||
<h5 class="mb-0"><i class="bi bi-plus-circle me-2" aria-hidden="true"></i>課題登録</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{if .error}}<div class="alert alert-danger">{{.error}}</div>{{end}}
|
||||
{{if .error}}<div class="alert alert-danger" role="alert">{{.error}}</div>{{end}}
|
||||
<form method="POST" action="/assignments">
|
||||
{{.csrfField}}
|
||||
<div class="mb-3">
|
||||
<label for="title" class="form-label">タイトル <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="title" name="title" value="{{.formTitle}}" required
|
||||
autofocus>
|
||||
<label for="title" class="form-label">タイトル <span class="text-danger" aria-hidden="true">*</span><span class="visually-hidden">(必須)</span></label>
|
||||
<input type="text" class="form-control" id="title" name="title" value="{{.formTitle}}" required autofocus>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="subject" class="form-label">科目</label>
|
||||
<input type="text" class="form-control" id="subject" name="subject" value="{{.subject}}"
|
||||
placeholder="例: 数学、英語、情報">
|
||||
<input type="text" class="form-control" id="subject" name="subject" value="{{.subject}}" placeholder="例: 数学、英語、情報">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="priority" class="form-label">重要度</label>
|
||||
<select class="form-select" id="priority" name="priority">
|
||||
<option value="low" {{if eq .priority "low" }}selected{{end}}>小</option>
|
||||
<option value="medium" {{if not (or (eq .priority "low" ) (eq .priority "high"
|
||||
))}}selected{{end}}>中</option>
|
||||
<option value="high" {{if eq .priority "high" }}selected{{end}}>大</option>
|
||||
<option value="low" {{if eq .priority "low" }}selected{{end}}>低</option>
|
||||
<option value="medium" {{if not (or (eq .priority "low") (eq .priority "high"))}}selected{{end}}>中</option>
|
||||
<option value="high" {{if eq .priority "high" }}selected{{end}}>高</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="due_date" class="form-label">提出期限 <span class="text-danger">*</span></label>
|
||||
<input type="datetime-local" class="form-control" id="due_date" name="due_date" required>
|
||||
<label for="due_date" class="form-label">提出期限 <span class="text-danger" aria-hidden="true">*</span><span class="visually-hidden">(必須)</span></label>
|
||||
<input type="datetime-local" class="form-control" id="due_date" name="due_date" value="{{.defaultDueDate}}" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">説明</label>
|
||||
<textarea class="form-control" id="description" name="description"
|
||||
rows="3">{{.description}}</textarea>
|
||||
<textarea class="form-control" id="description" name="description" rows="3">{{.description}}</textarea>
|
||||
</div>
|
||||
<!-- 通知設定 -->
|
||||
<div class="card bg-light mb-3">
|
||||
<div class="card-body py-2">
|
||||
<h6 class="mb-2"><i class="bi bi-bell me-1"></i>通知設定</h6>
|
||||
<!-- 督促通知 -->
|
||||
<h6 class="mb-2"><i class="bi bi-bell me-1" aria-hidden="true"></i>通知設定</h6>
|
||||
<div class="form-check form-switch mb-2">
|
||||
<input class="form-check-input" type="checkbox" id="urgent_reminder_enabled"
|
||||
name="urgent_reminder_enabled" checked>
|
||||
<input class="form-check-input" type="checkbox" id="urgent_reminder_enabled" name="urgent_reminder_enabled" checked>
|
||||
<label class="form-check-label" for="urgent_reminder_enabled">
|
||||
督促通知(期限3時間前から繰り返し通知)
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-text small mb-2">
|
||||
重要度により間隔が変わります:大=10分、中=30分、小=1時間
|
||||
重要度により通知間隔が変わります:高=10分ごと、中=30分ごと、低=1時間ごと
|
||||
</div>
|
||||
<hr class="my-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="reminder_enabled"
|
||||
name="reminder_enabled" onchange="toggleReminderDate(this)">
|
||||
<input class="form-check-input" type="checkbox" id="reminder_enabled" name="reminder_enabled" onchange="toggleReminderDate(this)">
|
||||
<label class="form-check-label" for="reminder_enabled">
|
||||
リマインダー(指定日時に通知)
|
||||
</label>
|
||||
</div>
|
||||
<div class="mt-2" id="reminder_at_group" style="display: none;">
|
||||
<label for="reminder_at" class="form-label small">通知日時</label>
|
||||
<input type="datetime-local" class="form-control form-control-sm" id="reminder_at"
|
||||
name="reminder_at">
|
||||
<input type="datetime-local" class="form-control form-control-sm" id="reminder_at" name="reminder_at">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card bg-light mb-3">
|
||||
<div class="card-header py-2" style="cursor: pointer;" data-bs-toggle="collapse"
|
||||
data-bs-target="#recurringSettings">
|
||||
<h6 class="mb-0"><i class="bi bi-arrow-repeat me-1"></i>繰り返し設定 <i
|
||||
class="bi bi-chevron-down float-end"></i></h6>
|
||||
<div class="card-header py-2" style="cursor: pointer;" data-bs-toggle="collapse" data-bs-target="#recurringSettings" aria-expanded="false" aria-controls="recurringSettings">
|
||||
<h6 class="mb-0"><i class="bi bi-arrow-repeat me-1" aria-hidden="true"></i>繰り返し設定 <i class="bi bi-chevron-down float-end" aria-hidden="true"></i></h6>
|
||||
</div>
|
||||
<div class="collapse" id="recurringSettings">
|
||||
<div class="card-body py-2">
|
||||
<div class="row mb-2">
|
||||
<div class="col-6">
|
||||
<label for="recurrence_type" class="form-label small">繰り返しタイプ</label>
|
||||
<select class="form-select form-select-sm" id="recurrence_type"
|
||||
name="recurrence_type" onchange="updateRecurrenceOptions()">
|
||||
<select class="form-select form-select-sm" id="recurrence_type" name="recurrence_type" onchange="updateRecurrenceOptions()">
|
||||
<option value="none" selected>なし</option>
|
||||
<option value="daily">毎日</option>
|
||||
<option value="weekly">毎週</option>
|
||||
@@ -91,107 +79,130 @@
|
||||
<div class="col-6" id="interval_group" style="display: none;">
|
||||
<label for="recurrence_interval" class="form-label small">間隔</label>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="number" class="form-control" id="recurrence_interval"
|
||||
name="recurrence_interval" value="1" min="1" max="12">
|
||||
<input type="number" class="form-control" id="recurrence_interval" name="recurrence_interval" value="1" min="1" max="12" onchange="updateLeadDaysMax()">
|
||||
<span class="input-group-text" id="interval_label">週</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="weekday_group" style="display: none;" class="mb-2">
|
||||
<div id="weekday_group" class="mb-2">
|
||||
<label class="form-label small">曜日</label>
|
||||
<div class="btn-group btn-group-sm w-100" role="group">
|
||||
<input type="radio" class="btn-check" name="recurrence_weekday" id="wd0"
|
||||
value="0" {{if eq .currentWeekday 0}}checked{{end}}>
|
||||
<div class="btn-group btn-group-sm w-100" role="group" aria-label="曜日選択">
|
||||
<input type="radio" class="btn-check" name="recurrence_weekday" id="wd0" value="0" {{if eq .currentWeekday 0}}checked{{end}}>
|
||||
<label class="btn btn-outline-primary" for="wd0">日</label>
|
||||
<input type="radio" class="btn-check" name="recurrence_weekday" id="wd1"
|
||||
value="1" {{if eq .currentWeekday 1}}checked{{end}}>
|
||||
<input type="radio" class="btn-check" name="recurrence_weekday" id="wd1" value="1" {{if eq .currentWeekday 1}}checked{{end}}>
|
||||
<label class="btn btn-outline-primary" for="wd1">月</label>
|
||||
<input type="radio" class="btn-check" name="recurrence_weekday" id="wd2"
|
||||
value="2" {{if eq .currentWeekday 2}}checked{{end}}>
|
||||
<input type="radio" class="btn-check" name="recurrence_weekday" id="wd2" value="2" {{if eq .currentWeekday 2}}checked{{end}}>
|
||||
<label class="btn btn-outline-primary" for="wd2">火</label>
|
||||
<input type="radio" class="btn-check" name="recurrence_weekday" id="wd3"
|
||||
value="3" {{if eq .currentWeekday 3}}checked{{end}}>
|
||||
<input type="radio" class="btn-check" name="recurrence_weekday" id="wd3" value="3" {{if eq .currentWeekday 3}}checked{{end}}>
|
||||
<label class="btn btn-outline-primary" for="wd3">水</label>
|
||||
<input type="radio" class="btn-check" name="recurrence_weekday" id="wd4"
|
||||
value="4" {{if eq .currentWeekday 4}}checked{{end}}>
|
||||
<input type="radio" class="btn-check" name="recurrence_weekday" id="wd4" value="4" {{if eq .currentWeekday 4}}checked{{end}}>
|
||||
<label class="btn btn-outline-primary" for="wd4">木</label>
|
||||
<input type="radio" class="btn-check" name="recurrence_weekday" id="wd5"
|
||||
value="5" {{if eq .currentWeekday 5}}checked{{end}}>
|
||||
<input type="radio" class="btn-check" name="recurrence_weekday" id="wd5" value="5" {{if eq .currentWeekday 5}}checked{{end}}>
|
||||
<label class="btn btn-outline-primary" for="wd5">金</label>
|
||||
<input type="radio" class="btn-check" name="recurrence_weekday" id="wd6"
|
||||
value="6" {{if eq .currentWeekday 6}}checked{{end}}>
|
||||
<input type="radio" class="btn-check" name="recurrence_weekday" id="wd6" value="6" {{if eq .currentWeekday 6}}checked{{end}}>
|
||||
<label class="btn btn-outline-primary" for="wd6">土</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="day_group" style="display: none;" class="mb-2">
|
||||
<div id="day_group" class="mb-2">
|
||||
<label for="recurrence_day" class="form-label small">日</label>
|
||||
<select class="form-select form-select-sm" id="recurrence_day"
|
||||
name="recurrence_day">
|
||||
<select class="form-select form-select-sm" id="recurrence_day" name="recurrence_day">
|
||||
{{range $i := seq 1 31}}
|
||||
<option value="{{$i}}" {{if eq $.currentDay $i}}selected{{end}}>{{$i}}日</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div id="lead_days_group" style="display: none;" class="mb-2">
|
||||
<label class="form-label small">リストに追加するタイミング</label>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="input-group input-group-sm" style="width: 120px;">
|
||||
<input type="number" class="form-control" id="generation_lead_days" name="generation_lead_days" value="0" min="0" max="0">
|
||||
<span class="input-group-text">日前</span>
|
||||
</div>
|
||||
<input type="time" class="form-control form-control-sm" id="generation_lead_time" name="generation_lead_time" style="width: 110px;">
|
||||
</div>
|
||||
<div class="form-text small text-muted" id="lead_days_hint"></div>
|
||||
</div>
|
||||
<div id="end_group" style="display: none;">
|
||||
<label class="form-label small">終了条件</label>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="end_type" id="end_never"
|
||||
value="never" checked>
|
||||
<input class="form-check-input" type="radio" name="end_type" id="end_never" value="never" checked>
|
||||
<label class="form-check-label small" for="end_never">無期限</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="end_type" id="end_count"
|
||||
value="count">
|
||||
<input class="form-check-input" type="radio" name="end_type" id="end_count" value="count">
|
||||
<label class="form-check-label small" for="end_count">回数</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="end_type" id="end_date"
|
||||
value="date">
|
||||
<input class="form-check-input" type="radio" name="end_type" id="end_date" value="date">
|
||||
<label class="form-check-label small" for="end_date">終了日</label>
|
||||
</div>
|
||||
<div class="mt-1" id="end_count_group" style="display: none;">
|
||||
<input type="number" class="form-control form-control-sm" id="end_count_value"
|
||||
name="end_count" value="10" min="1" style="width: 100px;">
|
||||
<input type="number" class="form-control form-control-sm" id="end_count_value" name="end_count" value="10" min="1" style="width: 100px;">
|
||||
</div>
|
||||
<div class="mt-1" id="end_date_group" style="display: none;">
|
||||
<input type="date" class="form-control form-control-sm" id="end_date_value"
|
||||
name="end_date" style="width: 150px;">
|
||||
<input type="date" class="form-control form-control-sm" id="end_date_value" name="end_date" style="width: 150px;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg me-1" aria-hidden="true"></i>登録</button>
|
||||
<a href="/assignments" class="btn btn-outline-secondary">キャンセル</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg me-1"></i>登録</button>
|
||||
<a href="/assignments" class="btn btn-outline-secondary">キャンセル</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function toggleReminderDate(checkbox) {
|
||||
document.getElementById('reminder_at_group').style.display = checkbox.checked ? 'block' : 'none';
|
||||
}
|
||||
function getLeadDaysMax() {
|
||||
const type = document.getElementById('recurrence_type').value;
|
||||
const interval = parseInt(document.getElementById('recurrence_interval').value) || 1;
|
||||
if (type === 'daily') return interval;
|
||||
if (type === 'weekly') return interval * 7;
|
||||
if (type === 'monthly') return interval * 28;
|
||||
return 0;
|
||||
}
|
||||
function updateLeadDaysMax() {
|
||||
const max = getLeadDaysMax();
|
||||
const input = document.getElementById('generation_lead_days');
|
||||
input.max = max;
|
||||
if (parseInt(input.value) > max) input.value = max;
|
||||
const type = document.getElementById('recurrence_type').value;
|
||||
const labels = { daily: '日', weekly: '週', monthly: 'ヶ月' };
|
||||
const interval = document.getElementById('recurrence_interval').value;
|
||||
document.getElementById('lead_days_hint').textContent =
|
||||
max === 0 ? '間隔が1日のため、事前追加は設定できません' :
|
||||
`最大${max}日前まで指定可能(繰り返し間隔${interval}${labels[type]}以内)`;
|
||||
}
|
||||
function updateRecurrenceOptions() {
|
||||
const type = document.getElementById('recurrence_type').value;
|
||||
const isRecurring = type !== 'none';
|
||||
document.getElementById('interval_group').style.display = isRecurring ? 'block' : 'none';
|
||||
document.getElementById('weekday_group').style.display = type === 'weekly' ? 'block' : 'none';
|
||||
document.getElementById('day_group').style.display = type === 'monthly' ? 'block' : 'none';
|
||||
document.getElementById('lead_days_group').style.display = isRecurring ? 'block' : 'none';
|
||||
document.getElementById('end_group').style.display = isRecurring ? 'block' : 'none';
|
||||
const label = document.getElementById('interval_label');
|
||||
if (type === 'daily') label.textContent = '日';
|
||||
else if (type === 'weekly') label.textContent = '週';
|
||||
else if (type === 'monthly') label.textContent = '月';
|
||||
if (label) {
|
||||
if (type === 'daily') label.textContent = '日';
|
||||
else if (type === 'weekly') label.textContent = '週';
|
||||
else if (type === 'monthly') label.textContent = '月';
|
||||
}
|
||||
updateLeadDaysMax();
|
||||
}
|
||||
document.querySelectorAll('input[name="end_type"]').forEach(radio => {
|
||||
document.querySelectorAll('input[name="end_type"]').forEach(function(radio) {
|
||||
radio.addEventListener('change', function () {
|
||||
document.getElementById('end_count_group').style.display = this.value === 'count' ? 'block' : 'none';
|
||||
document.getElementById('end_date_group').style.display = this.value === 'date' ? 'block' : 'none';
|
||||
});
|
||||
});
|
||||
document.getElementById('recurringSettings').addEventListener('show.bs.collapse', function () {
|
||||
updateLeadDaysMax();
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -2,111 +2,43 @@
|
||||
|
||||
{{define "head"}}
|
||||
<style>
|
||||
.stat-card {
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.progress {
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.subject-row:hover {
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.table-progress {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.stats-table {
|
||||
min-width: 700px;
|
||||
}
|
||||
|
||||
.stats-table th,
|
||||
.stats-table td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.stats-table th:first-child,
|
||||
.stats-table td:first-child {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.stat-card { transition: transform 0.2s ease-in-out; }
|
||||
.stat-card:hover { transform: translateY(-5px); }
|
||||
.progress { height: 25px; }
|
||||
.progress-bar { font-size: 0.9rem; font-weight: 500; }
|
||||
.subject-row:hover { background-color: rgba(0, 0, 0, 0.02); }
|
||||
.table-progress { height: 20px; }
|
||||
.pagination-info { font-size: 0.875rem; }
|
||||
.stats-table { min-width: 700px; }
|
||||
.stats-table th, .stats-table td { white-space: nowrap; }
|
||||
.stats-table th:first-child, .stats-table td:first-child { min-width: 120px; }
|
||||
#shareCard {
|
||||
width: 600px;
|
||||
height: 315px;
|
||||
position: fixed;
|
||||
left: -9999px;
|
||||
top: 0;
|
||||
width: 600px; height: 315px;
|
||||
position: fixed; left: -9999px; top: 0;
|
||||
background: linear-gradient(135deg, #005bea 0%, #00c6fb 100%);
|
||||
padding: 2rem;
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 2rem; color: white; border-radius: 12px;
|
||||
display: flex; flex-direction: column; justify-content: space-between;
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
#shareCard .card-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
#shareCard .rate-display {
|
||||
font-size: 5rem;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
#shareCard .card-title { font-size: 1.5rem; font-weight: bold; opacity: 0.9; }
|
||||
#shareCard .rate-display { font-size: 5rem; font-weight: bold; line-height: 1; margin: 1rem 0; }
|
||||
#shareCard .stats-row {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
display: flex; justify-content: space-around;
|
||||
background: rgba(255,255,255,0.1); border-radius: 8px; padding: 1rem;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
#shareCard .stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#shareCard .stat-label {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.8;
|
||||
display: block;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
#shareCard .stat-value {
|
||||
font-size: 1.4rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
#shareCard .stat-item { text-align: center; }
|
||||
#shareCard .stat-label { font-size: 0.8rem; opacity: 0.8; display: block; margin-bottom: 0.2rem; }
|
||||
#shareCard .stat-value { font-size: 1.4rem; font-weight: bold; }
|
||||
</style>
|
||||
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1><i class="bi bi-bar-chart me-2"></i>統計</h1>
|
||||
<h1><i class="bi bi-bar-chart me-2" aria-hidden="true"></i>統計</h1>
|
||||
<a href="/assignments" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i>課題一覧に戻る
|
||||
<i class="bi bi-arrow-left me-1" aria-hidden="true"></i>課題一覧に戻る
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -114,35 +46,67 @@
|
||||
<div class="card-body">
|
||||
<form method="GET" action="/statistics" class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">科目</label>
|
||||
<select name="subject" class="form-select">
|
||||
<label for="subjectFilter" class="form-label">科目</label>
|
||||
<select id="subjectFilter" name="subject" class="form-select">
|
||||
<option value="">すべての科目</option>
|
||||
{{range .subjects}}
|
||||
<option value="{{.}}" {{if eq . $.selectedSubject}}selected{{end}}>{{.}}{{if index
|
||||
$.archivedSubjects .}} (アーカイブ済){{end}}</option>
|
||||
<option value="{{.}}" {{if eq . $.selectedSubject}}selected{{end}}>{{.}}{{if index $.archivedSubjects .}} (アーカイブ済){{end}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">登録日(開始)</label>
|
||||
<input type="date" name="from" class="form-control" value="{{.fromDate}}">
|
||||
<label for="fromDate" class="form-label">登録日(開始)</label>
|
||||
<input type="date" id="fromDate" name="from" class="form-control" value="{{.fromDate}}">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">登録日(終了)</label>
|
||||
<input type="date" name="to" class="form-control" value="{{.toDate}}">
|
||||
<label for="toDate" class="form-label">登録日(終了)</label>
|
||||
<input type="date" id="toDate" name="to" class="form-control" value="{{.toDate}}">
|
||||
</div>
|
||||
<div class="col-md-4 d-flex align-items-end">
|
||||
<button type="submit" class="btn btn-primary me-2">
|
||||
<i class="bi bi-filter me-1"></i>絞り込み
|
||||
<i class="bi bi-filter me-1" aria-hidden="true"></i>絞り込み
|
||||
</button>
|
||||
<a href="/statistics" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-x-lg me-1"></i>リセット
|
||||
<i class="bi bi-x-lg me-1" aria-hidden="true"></i>リセット
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-download me-2" aria-hidden="true"></i>CSVエクスポート
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="GET" action="/assignments/export" class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label for="csvSubject" class="form-label">科目</label>
|
||||
<select id="csvSubject" name="subject" class="form-select">
|
||||
<option value="">すべての科目</option>
|
||||
{{range .subjects}}
|
||||
<option value="{{.}}">{{.}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label for="csvFrom" class="form-label">提出期限(開始)</label>
|
||||
<input type="date" id="csvFrom" name="from" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label for="csvTo" class="form-label">提出期限(終了)</label>
|
||||
<input type="date" id="csvTo" name="to" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-3 d-flex align-items-end">
|
||||
<button type="submit" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-file-earmark-spreadsheet me-1" aria-hidden="true"></i>CSVダウンロード
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<small class="text-muted mt-2 d-block">期間・科目を指定しない場合は全件をエクスポートします。提出期限を基準に絞り込みます。</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-6 col-lg-3">
|
||||
<div class="card stat-card bg-primary text-white h-100">
|
||||
@@ -152,7 +116,7 @@
|
||||
<h6 class="text-white-50 mb-1">総課題数</h6>
|
||||
<h2 class="mb-0">{{.stats.TotalAssignments}}</h2>
|
||||
</div>
|
||||
<i class="bi bi-list-task display-4 opacity-50"></i>
|
||||
<i class="bi bi-list-task display-4 opacity-50" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -165,7 +129,7 @@
|
||||
<h6 class="text-white-50 mb-1">完了</h6>
|
||||
<h2 class="mb-0">{{.stats.CompletedAssignments}}</h2>
|
||||
</div>
|
||||
<i class="bi bi-check-circle display-4 opacity-50"></i>
|
||||
<i class="bi bi-check-circle display-4 opacity-50" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -178,7 +142,7 @@
|
||||
<h6 class="text-dark-50 mb-1">未完了</h6>
|
||||
<h2 class="mb-0">{{.stats.PendingAssignments}}</h2>
|
||||
</div>
|
||||
<i class="bi bi-hourglass-split display-4 opacity-50"></i>
|
||||
<i class="bi bi-hourglass-split display-4 opacity-50" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -191,7 +155,7 @@
|
||||
<h6 class="text-white-50 mb-1">期限切れ</h6>
|
||||
<h2 class="mb-0">{{.stats.OverdueAssignments}}</h2>
|
||||
</div>
|
||||
<i class="bi bi-exclamation-triangle display-4 opacity-50"></i>
|
||||
<i class="bi bi-exclamation-triangle display-4 opacity-50" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -200,38 +164,24 @@
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span><i class="bi bi-clock-history me-2"></i>期限内完了率</span>
|
||||
<span><i class="bi bi-clock-history me-2" aria-hidden="true"></i>期限内完了率</span>
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="generateShareImage()">
|
||||
<i class="bi bi-share me-1"></i>シェア
|
||||
<i class="bi bi-share me-1" aria-hidden="true"></i>シェア
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-3 text-center mb-3 mb-md-0">
|
||||
<h1
|
||||
class="display-3 mb-0 {{if ge .stats.OnTimeCompletionRate 80.0}}text-success{{else if ge .stats.OnTimeCompletionRate 50.0}}text-warning{{else}}text-danger{{end}}">
|
||||
<p class="display-3 mb-0 {{if ge .stats.OnTimeCompletionRate 80.0}}text-success{{else if ge .stats.OnTimeCompletionRate 50.0}}text-warning{{else}}text-danger{{end}}" aria-label="期限内完了率 {{printf "%.1f" .stats.OnTimeCompletionRate}}パーセント">
|
||||
{{printf "%.1f" .stats.OnTimeCompletionRate}}%
|
||||
</h1>
|
||||
</p>
|
||||
<small class="text-muted">期限内完了率</small>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<div class="progress">
|
||||
{{if ge .stats.OnTimeCompletionRate 80.0}}
|
||||
<div class="progress-bar bg-success" role="progressbar"
|
||||
style="width: {{.stats.OnTimeCompletionRate}}%">
|
||||
<div class="progress" role="progressbar" aria-label="期限内完了率" aria-valuenow="{{printf "%.0f" .stats.OnTimeCompletionRate}}" aria-valuemin="0" aria-valuemax="100">
|
||||
<div class="progress-bar {{if ge .stats.OnTimeCompletionRate 80.0}}bg-success{{else if ge .stats.OnTimeCompletionRate 50.0}}bg-warning{{else}}bg-danger{{end}}" style="width: {{.stats.OnTimeCompletionRate}}%">
|
||||
{{printf "%.1f" .stats.OnTimeCompletionRate}}% 期限内に完了
|
||||
</div>
|
||||
{{else if ge .stats.OnTimeCompletionRate 50.0}}
|
||||
<div class="progress-bar bg-warning" role="progressbar"
|
||||
style="width: {{.stats.OnTimeCompletionRate}}%">
|
||||
{{printf "%.1f" .stats.OnTimeCompletionRate}}% 期限内に完了
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="progress-bar bg-danger" role="progressbar"
|
||||
style="width: {{.stats.OnTimeCompletionRate}}%">
|
||||
{{printf "%.1f" .stats.OnTimeCompletionRate}}% 期限内に完了
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<small class="text-muted mt-2 d-block">完了した課題のうち、期限内に完了した割合を表示しています。</small>
|
||||
</div>
|
||||
@@ -241,22 +191,22 @@
|
||||
|
||||
<div class="card mb-4" id="activeSubjectsCard">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span><i class="bi bi-collection me-2"></i>アクティブ科目</span>
|
||||
<span><i class="bi bi-collection me-2" aria-hidden="true"></i>アクティブ科目</span>
|
||||
<span class="badge bg-primary" id="activeCount">0</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0 stats-table">
|
||||
<table class="table table-hover mb-0 stats-table" aria-label="アクティブ科目統計">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>科目</th>
|
||||
<th class="text-center">総数</th>
|
||||
<th class="text-center">完了</th>
|
||||
<th class="text-center">未完了</th>
|
||||
<th class="text-center">期限切れ</th>
|
||||
<th class="text-center">完了率</th>
|
||||
<th style="width: 150px;">進捗</th>
|
||||
<th class="text-center">操作</th>
|
||||
<th scope="col">科目</th>
|
||||
<th scope="col" class="text-center">総数</th>
|
||||
<th scope="col" class="text-center">完了</th>
|
||||
<th scope="col" class="text-center">未完了</th>
|
||||
<th scope="col" class="text-center">期限切れ</th>
|
||||
<th scope="col" class="text-center">完了率</th>
|
||||
<th scope="col" style="width: 150px;">進捗</th>
|
||||
<th scope="col" class="text-center">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="activeSubjectsBody"></tbody>
|
||||
@@ -265,7 +215,7 @@
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-between align-items-center">
|
||||
<span class="pagination-info" id="activePageInfo"></span>
|
||||
<nav>
|
||||
<nav aria-label="アクティブ科目ページナビゲーション">
|
||||
<ul class="pagination pagination-sm mb-0" id="activePagination"></ul>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -273,22 +223,22 @@
|
||||
|
||||
<div class="card" id="archivedSubjectsCard">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span><i class="bi bi-archive me-2"></i>アーカイブ済み科目</span>
|
||||
<span><i class="bi bi-archive me-2" aria-hidden="true"></i>アーカイブ済み科目</span>
|
||||
<span class="badge bg-secondary" id="archivedCount">0</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0 stats-table">
|
||||
<table class="table table-hover mb-0 stats-table" aria-label="アーカイブ済み科目統計">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>科目</th>
|
||||
<th class="text-center">総数</th>
|
||||
<th class="text-center">完了</th>
|
||||
<th class="text-center">未完了</th>
|
||||
<th class="text-center">期限切れ</th>
|
||||
<th class="text-center">完了率</th>
|
||||
<th style="width: 150px;">進捗</th>
|
||||
<th class="text-center">操作</th>
|
||||
<th scope="col">科目</th>
|
||||
<th scope="col" class="text-center">総数</th>
|
||||
<th scope="col" class="text-center">完了</th>
|
||||
<th scope="col" class="text-center">未完了</th>
|
||||
<th scope="col" class="text-center">期限切れ</th>
|
||||
<th scope="col" class="text-center">完了率</th>
|
||||
<th scope="col" style="width: 150px;">進捗</th>
|
||||
<th scope="col" class="text-center">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="archivedSubjectsBody"></tbody>
|
||||
@@ -297,7 +247,7 @@
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-between align-items-center">
|
||||
<span class="pagination-info" id="archivedPageInfo"></span>
|
||||
<nav>
|
||||
<nav aria-label="アーカイブ済み科目ページナビゲーション">
|
||||
<ul class="pagination pagination-sm mb-0" id="archivedPagination"></ul>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -305,27 +255,25 @@
|
||||
|
||||
<div class="card d-none" id="noSubjectsCard">
|
||||
<div class="card-body text-center py-5">
|
||||
<i class="bi bi-inbox display-1 text-muted"></i>
|
||||
<i class="bi bi-inbox display-1 text-muted" aria-hidden="true"></i>
|
||||
<h4 class="mt-3">科目別の統計データがありません</h4>
|
||||
<p class="text-muted">課題を登録して科目を設定すると、ここに統計が表示されます。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="shareCard">
|
||||
<div id="shareCard" aria-hidden="true">
|
||||
<div>
|
||||
<div class="d-flex align-items-center mb-4">
|
||||
<i class="bi bi-journal-check me-2" style="font-size: 1.5rem;"></i>
|
||||
<span class="card-title">Super Homework Manager</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<div style="font-size: 1.5rem; font-weight: bold; margin-bottom: 0.5rem; opacity: 0.9;">期限内完了率</div>
|
||||
<div class="rate-display" style="margin-top: 0;">
|
||||
{{printf "%.1f" .stats.OnTimeCompletionRate}}<span style="font-size: 2.5rem;">%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-row">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">完了</span>
|
||||
@@ -342,31 +290,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Share Modal -->
|
||||
<div class="modal fade" id="shareModal" tabindex="-1">
|
||||
<div class="modal fade" id="shareModal" tabindex="-1" aria-labelledby="shareModalHeading" aria-modal="true" role="dialog">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-share me-2"></i>シェア</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
<h5 class="modal-title" id="shareModalHeading"><i class="bi bi-share me-2" aria-hidden="true"></i>シェア</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="閉じる"></button>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<div id="sharePreviewContainer" class="mb-3" style="max-width: 100%; overflow: hidden;">
|
||||
</div>
|
||||
<div id="sharePreviewContainer" class="mb-3" style="max-width: 100%; overflow: hidden;"></div>
|
||||
<p class="text-muted small mb-3">
|
||||
画像を保存またはコピーして、SNSに貼り付けてください。<br>
|
||||
<span class="text-danger"><i class="bi bi-info-circle me-1"></i>ブラウザの制限により、自動で画像は添付されません。</span>
|
||||
<span class="text-danger"><i class="bi bi-info-circle me-1" aria-hidden="true"></i>ブラウザの制限により、自動で画像は添付されません。</span>
|
||||
</p>
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-outline-primary" onclick="copyImageToClipboard(this)">
|
||||
<i class="bi bi-clipboard me-2"></i>画像をコピー
|
||||
<button class="btn btn-outline-primary" id="copyImageBtn">
|
||||
<i class="bi bi-clipboard me-2" aria-hidden="true"></i>画像をコピー
|
||||
</button>
|
||||
<a id="downloadLink" class="btn btn-outline-secondary" download="stats.png">
|
||||
<i class="bi bi-download me-2"></i>画像を保存
|
||||
<i class="bi bi-download me-2" aria-hidden="true"></i>画像を保存
|
||||
</a>
|
||||
<a id="twitterShareBtn" href="#" target="_blank" class="btn btn-dark"
|
||||
style="background-color: #000;">
|
||||
<i class="bi bi-twitter-x me-2"></i>Xでポストする
|
||||
<a id="twitterShareBtn" href="#" target="_blank" rel="noopener noreferrer" class="btn btn-dark" style="background-color: #000;">
|
||||
<i class="bi bi-twitter-x me-2" aria-hidden="true"></i>Xでポストする
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -381,28 +326,23 @@
|
||||
</script>
|
||||
<script>
|
||||
(function () {
|
||||
var data = JSON.parse(document.getElementById('subjectsData').textContent);
|
||||
var csrfToken = data.csrfToken;
|
||||
var subjects = data.subjects;
|
||||
var PAGE_SIZE = 10;
|
||||
var activeSubjects = subjects.filter(function (s) { return !s.isArchived; });
|
||||
var data = JSON.parse(document.getElementById('subjectsData').textContent);
|
||||
var csrfToken = data.csrfToken;
|
||||
var subjects = data.subjects;
|
||||
var PAGE_SIZE = 10;
|
||||
var activeSubjects = subjects.filter(function (s) { return !s.isArchived; });
|
||||
var archivedSubjects = subjects.filter(function (s) { return s.isArchived; });
|
||||
var activePage = 1;
|
||||
var archivedPage = 1;
|
||||
var activePage = 1;
|
||||
var archivedPage = 1;
|
||||
var _capturedCanvas = null;
|
||||
|
||||
// Share Functionality
|
||||
window.generateShareImage = function () {
|
||||
var card = document.getElementById('shareCard');
|
||||
// Ensure card is visible for rendering but off-screen
|
||||
card.style.display = 'flex';
|
||||
|
||||
html2canvas(card, {
|
||||
backgroundColor: null,
|
||||
scale: 2 // High resolution
|
||||
}).then(canvas => {
|
||||
html2canvas(card, { backgroundColor: null, scale: 2 }).then(function(canvas) {
|
||||
_capturedCanvas = canvas;
|
||||
var imgData = canvas.toDataURL('image/png');
|
||||
|
||||
// Set up preview
|
||||
var previewContainer = document.getElementById('sharePreviewContainer');
|
||||
previewContainer.innerHTML = '';
|
||||
var img = document.createElement('img');
|
||||
@@ -410,67 +350,51 @@
|
||||
img.style.maxWidth = '100%';
|
||||
img.style.borderRadius = '8px';
|
||||
img.style.boxShadow = '0 4px 12px rgba(0,0,0,0.1)';
|
||||
img.alt = '統計シェア画像';
|
||||
previewContainer.appendChild(img);
|
||||
|
||||
// Set up download link
|
||||
var downloadLink = document.getElementById('downloadLink');
|
||||
downloadLink.href = imgData;
|
||||
document.getElementById('downloadLink').href = imgData;
|
||||
|
||||
// Set up Twitter button
|
||||
var text = '私の課題完了状況\n期限内完了率: {{printf "%.1f" .stats.OnTimeCompletionRate}}%\n#SuperHomeworkManager';
|
||||
var twitterBtn = document.getElementById('twitterShareBtn');
|
||||
twitterBtn.href = "https://twitter.com/intent/tweet?text=" + encodeURIComponent(text);
|
||||
document.getElementById('twitterShareBtn').href = 'https://twitter.com/intent/tweet?text=' + encodeURIComponent(text);
|
||||
|
||||
// Show modal
|
||||
var modal = new bootstrap.Modal(document.getElementById('shareModal'));
|
||||
modal.show();
|
||||
new bootstrap.Modal(document.getElementById('shareModal')).show();
|
||||
});
|
||||
};
|
||||
|
||||
// Helper to convert Data URL to Blob
|
||||
function dataURLtoBlob(dataurl) {
|
||||
var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],
|
||||
bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
|
||||
while (n--) {
|
||||
u8arr[n] = bstr.charCodeAt(n);
|
||||
}
|
||||
return new Blob([u8arr], { type: mime });
|
||||
var arr = dataurl.split(',');
|
||||
var mime = arr[0].match(/:(.*?);/)[1];
|
||||
var bstr = atob(arr[1]);
|
||||
var n = bstr.length;
|
||||
var u8 = new Uint8Array(n);
|
||||
while (n--) u8[n] = bstr.charCodeAt(n);
|
||||
return new Blob([u8], { type: mime });
|
||||
}
|
||||
|
||||
window.copyImageToClipboard = function (btn) {
|
||||
var canvas = document.querySelector('#sharePreviewContainer img');
|
||||
if (!canvas) return;
|
||||
document.getElementById('copyImageBtn').addEventListener('click', function() {
|
||||
var btn = this;
|
||||
var imgEl = document.querySelector('#sharePreviewContainer img');
|
||||
if (!imgEl) return;
|
||||
|
||||
if (!navigator.clipboard) {
|
||||
alert('このブラウザまたは環境(非HTTPS/非localhost)では、クリップボードへのコピー機能がサポートされていません。\n「画像を保存」を使用してください。');
|
||||
if (!navigator.clipboard || !window.ClipboardItem) {
|
||||
showCopyFeedback('このブラウザではクリップボードへの画像コピーがサポートされていません。「画像を保存」をご利用ください。');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var blob = dataURLtoBlob(canvas.src);
|
||||
navigator.clipboard.write([
|
||||
new ClipboardItem({
|
||||
'image/png': blob
|
||||
})
|
||||
]).then(function () {
|
||||
var originalText = btn.innerHTML;
|
||||
btn.innerHTML = '<i class="bi bi-check me-2"></i>コピーしました';
|
||||
btn.classList.remove('btn-outline-primary');
|
||||
btn.classList.add('btn-success');
|
||||
setTimeout(function () {
|
||||
btn.innerHTML = originalText;
|
||||
btn.classList.add('btn-outline-primary');
|
||||
btn.classList.remove('btn-success');
|
||||
}, 2000);
|
||||
}).catch(function (err) {
|
||||
console.error('Failed to copy: ', err);
|
||||
alert('画像のコピーに失敗しました。\nエラー: ' + err.message + '\n「画像を保存」を使用してください。');
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to create blob: ', err);
|
||||
alert('画像データの生成に失敗しました: ' + err.message);
|
||||
}
|
||||
};
|
||||
var blob = dataURLtoBlob(imgEl.src);
|
||||
navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]).then(function() {
|
||||
var orig = btn.innerHTML;
|
||||
btn.innerHTML = '<i class="bi bi-check me-2" aria-hidden="true"></i>コピーしました';
|
||||
btn.classList.replace('btn-outline-primary', 'btn-success');
|
||||
setTimeout(function() {
|
||||
btn.innerHTML = orig;
|
||||
btn.classList.replace('btn-success', 'btn-outline-primary');
|
||||
}, 2000);
|
||||
}).catch(function(err) {
|
||||
showCopyFeedback('画像のコピーに失敗しました。「画像を保存」をご利用ください。(' + (err.message || err) + ')');
|
||||
});
|
||||
});
|
||||
|
||||
function getRateClass(rate) {
|
||||
if (rate >= 80) return 'text-success';
|
||||
@@ -479,22 +403,22 @@
|
||||
}
|
||||
|
||||
function renderProgress(completed, pending, overdue, total) {
|
||||
if (total === 0) return '<div class="progress table-progress"></div>';
|
||||
if (total === 0) return '<div class="progress table-progress" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" aria-label="進捗なし"></div>';
|
||||
var cP = (completed / total * 100).toFixed(1);
|
||||
var pP = (pending / total * 100).toFixed(1);
|
||||
var oP = (overdue / total * 100).toFixed(1);
|
||||
return '<div class="progress table-progress">' +
|
||||
var pP = (pending / total * 100).toFixed(1);
|
||||
var oP = (overdue / total * 100).toFixed(1);
|
||||
return '<div class="progress table-progress" role="progressbar" aria-valuenow="' + cP + '" aria-valuemin="0" aria-valuemax="100" aria-label="完了' + cP + '%、未完了' + pP + '%、期限切れ' + oP + '%">' +
|
||||
'<div class="progress-bar bg-success" style="width:' + cP + '%" title="完了: ' + completed + '"></div>' +
|
||||
'<div class="progress-bar bg-warning" style="width:' + pP + '%" title="未完了: ' + pending + '"></div>' +
|
||||
'<div class="progress-bar bg-danger" style="width:' + oP + '%" title="期限切れ: ' + overdue + '"></div></div>';
|
||||
'<div class="progress-bar bg-danger" style="width:' + oP + '%" title="期限切れ: ' + overdue + '"></div></div>';
|
||||
}
|
||||
|
||||
function renderRow(s, isArchived) {
|
||||
var action = isArchived ?
|
||||
'<form action="/statistics/unarchive-subject" method="POST" class="d-inline"><input type="hidden" name="_csrf" value="' + csrfToken + '"><input type="hidden" name="subject" value="' + s.subject + '"><button type="submit" class="btn btn-sm btn-outline-success" title="復元"><i class="bi bi-arrow-counterclockwise"></i></button></form>' :
|
||||
'<form action="/statistics/archive-subject" method="POST" class="d-inline"><input type="hidden" name="_csrf" value="' + csrfToken + '"><input type="hidden" name="subject" value="' + s.subject + '"><button type="submit" class="btn btn-sm btn-outline-secondary" title="アーカイブ"><i class="bi bi-archive"></i></button></form>';
|
||||
var action = isArchived
|
||||
? '<form action="/statistics/unarchive-subject" method="POST" class="d-inline"><input type="hidden" name="_csrf" value="' + csrfToken + '"><input type="hidden" name="subject" value="' + XSS.escapeHtml(s.subject) + '"><button type="submit" class="btn btn-sm btn-outline-success" aria-label="' + XSS.escapeHtml(s.subject) + 'を復元"><i class="bi bi-arrow-counterclockwise" aria-hidden="true"></i></button></form>'
|
||||
: '<form action="/statistics/archive-subject" method="POST" class="d-inline"><input type="hidden" name="_csrf" value="' + csrfToken + '"><input type="hidden" name="subject" value="' + XSS.escapeHtml(s.subject) + '"><button type="submit" class="btn btn-sm btn-outline-secondary" aria-label="' + XSS.escapeHtml(s.subject) + 'をアーカイブ"><i class="bi bi-archive" aria-hidden="true"></i></button></form>';
|
||||
return '<tr class="subject-row">' +
|
||||
'<td><a href="/statistics?subject=' + encodeURIComponent(s.subject) + '" class="text-decoration-none"><i class="bi bi-folder me-1"></i>' + s.subject + '</a></td>' +
|
||||
'<td><a href="/statistics?subject=' + encodeURIComponent(s.subject) + '" class="text-decoration-none"><i class="bi bi-folder me-1" aria-hidden="true"></i>' + XSS.escapeHtml(s.subject) + '</a></td>' +
|
||||
'<td class="text-center">' + s.total + '</td>' +
|
||||
'<td class="text-center text-success">' + s.completed + '</td>' +
|
||||
'<td class="text-center text-warning">' + s.pending + '</td>' +
|
||||
@@ -510,7 +434,7 @@
|
||||
if (total <= 1) return;
|
||||
var prev = document.createElement('li');
|
||||
prev.className = 'page-item' + (page === 1 ? ' disabled' : '');
|
||||
prev.innerHTML = '<a class="page-link" href="#">«</a>';
|
||||
prev.innerHTML = '<a class="page-link" href="#" aria-label="前のページ">«</a>';
|
||||
if (page > 1) prev.onclick = function (e) { e.preventDefault(); cb(page - 1); };
|
||||
el.appendChild(prev);
|
||||
for (var i = 1; i <= total; i++) {
|
||||
@@ -522,28 +446,28 @@
|
||||
}
|
||||
var next = document.createElement('li');
|
||||
next.className = 'page-item' + (page === total ? ' disabled' : '');
|
||||
next.innerHTML = '<a class="page-link" href="#">»</a>';
|
||||
next.innerHTML = '<a class="page-link" href="#" aria-label="次のページ">»</a>';
|
||||
if (page < total) next.onclick = function (e) { e.preventDefault(); cb(page + 1); };
|
||||
el.appendChild(next);
|
||||
}
|
||||
|
||||
function renderActiveSubjects() {
|
||||
var start = (activePage - 1) * PAGE_SIZE;
|
||||
var end = Math.min(start + PAGE_SIZE, activeSubjects.length);
|
||||
var start = (activePage - 1) * PAGE_SIZE;
|
||||
var end = Math.min(start + PAGE_SIZE, activeSubjects.length);
|
||||
var totalPages = Math.ceil(activeSubjects.length / PAGE_SIZE);
|
||||
document.getElementById('activeSubjectsBody').innerHTML = activeSubjects.slice(start, end).map(function (s) { return renderRow(s, false); }).join('');
|
||||
document.getElementById('activeCount').textContent = activeSubjects.length;
|
||||
document.getElementById('activeCount').textContent = activeSubjects.length;
|
||||
document.getElementById('activePageInfo').textContent = activeSubjects.length > 0 ? (start + 1) + '-' + end + ' / ' + activeSubjects.length + ' 件' : '0 件';
|
||||
renderPagination('activePagination', activePage, totalPages, function (p) { activePage = p; renderActiveSubjects(); });
|
||||
document.getElementById('activeSubjectsCard').style.display = activeSubjects.length > 0 ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function renderArchivedSubjects() {
|
||||
var start = (archivedPage - 1) * PAGE_SIZE;
|
||||
var end = Math.min(start + PAGE_SIZE, archivedSubjects.length);
|
||||
var start = (archivedPage - 1) * PAGE_SIZE;
|
||||
var end = Math.min(start + PAGE_SIZE, archivedSubjects.length);
|
||||
var totalPages = Math.ceil(archivedSubjects.length / PAGE_SIZE);
|
||||
document.getElementById('archivedSubjectsBody').innerHTML = archivedSubjects.slice(start, end).map(function (s) { return renderRow(s, true); }).join('');
|
||||
document.getElementById('archivedCount').textContent = archivedSubjects.length;
|
||||
document.getElementById('archivedCount').textContent = archivedSubjects.length;
|
||||
document.getElementById('archivedPageInfo').textContent = archivedSubjects.length > 0 ? (start + 1) + '-' + end + ' / ' + archivedSubjects.length + ' 件' : '0 件';
|
||||
renderPagination('archivedPagination', archivedPage, totalPages, function (p) { archivedPage = p; renderArchivedSubjects(); });
|
||||
document.getElementById('archivedSubjectsCard').style.display = archivedSubjects.length > 0 ? 'block' : 'none';
|
||||
@@ -553,9 +477,9 @@
|
||||
renderArchivedSubjects();
|
||||
if (activeSubjects.length === 0 && archivedSubjects.length === 0) {
|
||||
document.getElementById('noSubjectsCard').classList.remove('d-none');
|
||||
document.getElementById('activeSubjectsCard').style.display = 'none';
|
||||
document.getElementById('activeSubjectsCard').style.display = 'none';
|
||||
document.getElementById('archivedSubjectsCard').style.display = 'none';
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -8,18 +8,16 @@
|
||||
<title>{{.title}} - Super Homework Manager</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.5.1/css/all.min.css" rel="stylesheet">
|
||||
<link href="/static/css/style.css" rel="stylesheet">
|
||||
<style>
|
||||
.navbar-dark .navbar-nav .nav-link,
|
||||
.navbar-brand {
|
||||
color: #fff !important;
|
||||
color: #fff;
|
||||
transition: color 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.navbar-dark .navbar-nav .nav-link:hover,
|
||||
.navbar-brand:hover {
|
||||
color: rgba(255, 255, 255, 0.75) !important;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
</style>
|
||||
{{template "head" .}}
|
||||
@@ -27,53 +25,51 @@
|
||||
|
||||
<body>
|
||||
{{if .userName}}
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary" role="navigation" aria-label="メインナビゲーション">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/">
|
||||
<i class="bi bi-journal-check me-2"></i>Super Homework Manager
|
||||
<i class="bi bi-journal-check me-2" aria-hidden="true"></i>Super Homework Manager
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<button class="navbar-toggler" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#navbarNav"
|
||||
aria-controls="navbarNav" aria-expanded="false" aria-label="ナビゲーションを開く">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/"><i class="bi bi-house-door me-1"></i>ダッシュボード</a>
|
||||
<a class="nav-link" href="/"><i class="bi bi-house-door me-1" aria-hidden="true"></i>ダッシュボード</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/assignments"><i class="bi bi-list-task me-1"></i>課題一覧</a>
|
||||
<a class="nav-link" href="/assignments"><i class="bi bi-list-task me-1" aria-hidden="true"></i>課題一覧</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/assignments/new"><i class="bi bi-plus-circle me-1"></i>課題登録</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/statistics"><i class="bi bi-bar-chart me-1"></i>統計</a>
|
||||
<a class="nav-link" href="/statistics"><i class="bi bi-bar-chart me-1" aria-hidden="true"></i>統計</a>
|
||||
</li>
|
||||
{{if .isAdmin}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/users"><i class="bi bi-people me-1"></i>ユーザー管理</a>
|
||||
<a class="nav-link" href="/admin/users"><i class="bi bi-people me-1" aria-hidden="true"></i>ユーザー管理</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/api-keys"><i class="bi bi-key me-1"></i>APIキー管理</a>
|
||||
<a class="nav-link" href="/admin/api-keys"><i class="bi bi-key me-1" aria-hidden="true"></i>APIキー管理</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-person-circle me-1"></i>{{.userName}}
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button"
|
||||
data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="bi bi-person-circle me-1" aria-hidden="true"></i>{{.userName}}
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li><a class="dropdown-item" href="/profile"><i class="bi bi-person me-2"></i>プロフィール</a>
|
||||
</li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li><a class="dropdown-item" href="/profile"><i class="bi bi-person me-2" aria-hidden="true"></i>プロフィール</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<form action="/logout" method="POST" class="d-inline">
|
||||
<input type="hidden" name="_csrf" value="{{.csrfToken}}">
|
||||
<button type="submit" class="dropdown-item"><i
|
||||
class="bi bi-box-arrow-right me-2"></i>ログアウト</button>
|
||||
<button type="submit" class="dropdown-item">
|
||||
<i class="bi bi-box-arrow-right me-2" aria-hidden="true"></i>ログアウト
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -97,6 +93,18 @@
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<div class="modal fade" id="confirmModal" tabindex="-1" aria-labelledby="confirmModalLabel" aria-modal="true" role="dialog">
|
||||
<div class="modal-dialog modal-sm modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body" id="confirmModalBody"></div>
|
||||
<div class="modal-footer py-2 border-0">
|
||||
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">キャンセル</button>
|
||||
<button type="button" class="btn btn-danger btn-sm" id="confirmModalOk">確認</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/static/js/app.js"></script>
|
||||
{{template "scripts" .}}
|
||||
@@ -106,4 +114,4 @@
|
||||
{{end}}
|
||||
|
||||
{{define "head"}}{{end}}
|
||||
{{define "scripts"}}{{end}}
|
||||
{{define "scripts"}}{{end}}
|
||||
|
||||
@@ -2,94 +2,42 @@
|
||||
|
||||
{{define "head"}}
|
||||
<style>
|
||||
@keyframes pulse-bg {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
background-color: #fff3cd;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-color: #ffe69c;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-bg-danger {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
background-color: #f8d7da;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-color: #f5c2c7;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink-banner {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.anxiety-warning {
|
||||
animation: pulse-bg 2s infinite;
|
||||
}
|
||||
|
||||
.anxiety-danger {
|
||||
animation: pulse-bg-danger 1s infinite;
|
||||
}
|
||||
|
||||
.urgent-banner {
|
||||
z-index: 1030;
|
||||
animation: blink-banner 1s infinite;
|
||||
}
|
||||
|
||||
.urgent-banner-danger {
|
||||
background: linear-gradient(90deg, #dc3545, #c82333);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.urgent-banner-warning {
|
||||
background: linear-gradient(90deg, #fd7e14, #e06c00);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.urgent-countdown {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.dashboard-stat-card {
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dashboard-stat-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.urgent-banner-danger {
|
||||
background: linear-gradient(90deg, #dc3545, #c82333);
|
||||
color: white;
|
||||
}
|
||||
.urgent-banner-warning {
|
||||
background: linear-gradient(90deg, #fd7e14, #e06c00);
|
||||
color: white;
|
||||
}
|
||||
.urgent-countdown {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<div id="urgentBanner" class="urgent-banner py-3 text-center d-none">
|
||||
<div class="container">
|
||||
<i class="bi bi-exclamation-octagon-fill me-2"></i>
|
||||
<div id="urgentBanner" class="urgent-banner py-3 text-center d-none" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="container position-relative">
|
||||
<i class="bi bi-exclamation-octagon-fill me-2" aria-hidden="true"></i>
|
||||
<span id="urgentMessage"></span>
|
||||
<div class="urgent-countdown mt-1">
|
||||
<i class="bi bi-stopwatch"></i> <span id="urgentCountdown"></span>
|
||||
<i class="bi bi-stopwatch" aria-hidden="true"></i> <span id="urgentCountdown"></span>
|
||||
</div>
|
||||
<button type="button" id="closeBanner" class="btn-close btn-close-white position-absolute top-0 end-0" aria-label="バナーを閉じる" onclick="document.getElementById('urgentBanner').classList.add('d-none')"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 class="mb-4"><i class="bi bi-house-door me-2"></i>ダッシュボード</h1>
|
||||
<h1 class="mb-4"><i class="bi bi-house-door me-2" aria-hidden="true"></i>ダッシュボード</h1>
|
||||
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-6 col-md-3">
|
||||
@@ -101,7 +49,7 @@
|
||||
<h6 class="text-white-50">未完了の課題</h6>
|
||||
<h2 class="mb-0">{{.stats.TotalPending}}</h2>
|
||||
</div>
|
||||
<i class="bi bi-list-task display-4 opacity-50"></i>
|
||||
<i class="bi bi-list-task display-4 opacity-50" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,7 +64,7 @@
|
||||
<h6 class="text-dark-50">今日が期限</h6>
|
||||
<h2 class="mb-0">{{.stats.DueToday}}</h2>
|
||||
</div>
|
||||
<i class="bi bi-calendar-event display-4 opacity-50"></i>
|
||||
<i class="bi bi-calendar-event display-4 opacity-50" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -131,7 +79,7 @@
|
||||
<h6 class="text-white-50">今週が期限</h6>
|
||||
<h2 class="mb-0">{{.stats.DueThisWeek}}</h2>
|
||||
</div>
|
||||
<i class="bi bi-calendar-week display-4 opacity-50"></i>
|
||||
<i class="bi bi-calendar-week display-4 opacity-50" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -146,7 +94,7 @@
|
||||
<h6 class="text-white-50">期限切れ</h6>
|
||||
<h2 class="mb-0">{{.stats.Overdue}}</h2>
|
||||
</div>
|
||||
<i class="bi bi-exclamation-triangle display-4 opacity-50"></i>
|
||||
<i class="bi bi-exclamation-triangle display-4 opacity-50" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -158,20 +106,22 @@
|
||||
{{if .overdue}}
|
||||
<div class="col-lg-6">
|
||||
<div class="card border-danger">
|
||||
<div class="card-header bg-danger text-white"><i class="bi bi-exclamation-triangle me-2"></i>期限切れの課題</div>
|
||||
<div class="card-header bg-danger text-white"><i class="bi bi-exclamation-triangle me-2" aria-hidden="true"></i>期限切れの課題</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
{{range .overdue}}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center"
|
||||
data-priority="{{.Priority}}" data-due="{{.DueDate.UnixMilli}}">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center" data-priority="{{.Priority}}" data-due="{{.DueDate.UnixMilli}}">
|
||||
<div>
|
||||
{{if .Subject}}<span class="badge bg-secondary me-1">{{.Subject}}</span>{{end}}
|
||||
{{if eq .Priority "high"}}<span class="badge bg-danger me-1">重要</span>{{end}}
|
||||
{{if eq .Priority "high"}}<span class="badge bg-danger me-1">高<span class="visually-hidden">(重要度:高)</span></span>{{end}}
|
||||
<strong>{{.Title}}</strong>
|
||||
<br><small class="text-danger">{{formatDateTime .DueDate}}</small>
|
||||
</div>
|
||||
<form action="/assignments/{{.ID}}/toggle" method="POST"><input type="hidden" name="_csrf"
|
||||
value="{{$.csrfToken}}"><button type="submit" class="btn btn-sm btn-success"
|
||||
title="完了にする"><i class="bi bi-check-lg"></i></button></form>
|
||||
<form action="/assignments/{{.ID}}/toggle" method="POST">
|
||||
<input type="hidden" name="_csrf" value="{{$.csrfToken}}">
|
||||
<button type="submit" class="btn btn-sm btn-success" aria-label="{{.Title}}を完了にする">
|
||||
<i class="bi bi-check-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
@@ -181,20 +131,22 @@
|
||||
{{if .dueToday}}
|
||||
<div class="col-lg-6">
|
||||
<div class="card border-warning">
|
||||
<div class="card-header bg-warning text-dark"><i class="bi bi-calendar-event me-2"></i>今日が期限</div>
|
||||
<div class="card-header bg-warning text-dark"><i class="bi bi-calendar-event me-2" aria-hidden="true"></i>今日が期限</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
{{range .dueToday}}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center"
|
||||
data-priority="{{.Priority}}" data-due="{{.DueDate.UnixMilli}}">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center" data-priority="{{.Priority}}" data-due="{{.DueDate.UnixMilli}}">
|
||||
<div>
|
||||
{{if .Subject}}<span class="badge bg-secondary me-1">{{.Subject}}</span>{{end}}
|
||||
{{if eq .Priority "high"}}<span class="badge bg-danger me-1">重要</span>{{end}}
|
||||
{{if eq .Priority "high"}}<span class="badge bg-danger me-1">高<span class="visually-hidden">(重要度:高)</span></span>{{end}}
|
||||
<strong>{{.Title}}</strong>
|
||||
<br><small class="text-muted">{{formatDateTime .DueDate}}</small>
|
||||
</div>
|
||||
<form action="/assignments/{{.ID}}/toggle" method="POST"><input type="hidden" name="_csrf"
|
||||
value="{{$.csrfToken}}"><button type="submit" class="btn btn-sm btn-success"
|
||||
title="完了にする"><i class="bi bi-check-lg"></i></button></form>
|
||||
<form action="/assignments/{{.ID}}/toggle" method="POST">
|
||||
<input type="hidden" name="_csrf" value="{{$.csrfToken}}">
|
||||
<button type="submit" class="btn btn-sm btn-success" aria-label="{{.Title}}を完了にする">
|
||||
<i class="bi bi-check-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
@@ -204,20 +156,22 @@
|
||||
{{if .upcoming}}
|
||||
<div class="col-lg-6">
|
||||
<div class="card border-info">
|
||||
<div class="card-header bg-info text-white"><i class="bi bi-calendar-week me-2"></i>今週の課題</div>
|
||||
<div class="card-header bg-info text-white"><i class="bi bi-calendar-week me-2" aria-hidden="true"></i>今週の課題</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
{{range .upcoming}}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center"
|
||||
data-priority="{{.Priority}}" data-due="{{.DueDate.UnixMilli}}">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center" data-priority="{{.Priority}}" data-due="{{.DueDate.UnixMilli}}">
|
||||
<div>
|
||||
{{if .Subject}}<span class="badge bg-secondary me-1">{{.Subject}}</span>{{end}}
|
||||
{{if eq .Priority "high"}}<span class="badge bg-danger me-1">重要</span>{{end}}
|
||||
{{if eq .Priority "high"}}<span class="badge bg-danger me-1">高<span class="visually-hidden">(重要度:高)</span></span>{{end}}
|
||||
<strong>{{.Title}}</strong>
|
||||
<br><small class="text-muted">{{formatDateTime .DueDate}}</small>
|
||||
</div>
|
||||
<form action="/assignments/{{.ID}}/toggle" method="POST"><input type="hidden" name="_csrf"
|
||||
value="{{$.csrfToken}}"><button type="submit" class="btn btn-sm btn-success"
|
||||
title="完了にする"><i class="bi bi-check-lg"></i></button></form>
|
||||
<form action="/assignments/{{.ID}}/toggle" method="POST">
|
||||
<input type="hidden" name="_csrf" value="{{$.csrfToken}}">
|
||||
<button type="submit" class="btn btn-sm btn-success" aria-label="{{.Title}}を完了にする">
|
||||
<i class="bi bi-check-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
@@ -228,10 +182,10 @@
|
||||
|
||||
{{if and (not .overdue) (not .dueToday) (not .upcoming)}}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-emoji-smile display-1 text-success"></i>
|
||||
<i class="bi bi-emoji-smile display-1 text-success" aria-hidden="true"></i>
|
||||
<h3 class="mt-3">今週の課題はありません!</h3>
|
||||
<p class="text-muted">新しい課題を登録しましょう</p>
|
||||
<a href="/assignments/new" class="btn btn-primary"><i class="bi bi-plus-circle me-1"></i>課題を登録</a>
|
||||
<a href="/assignments/new" class="btn btn-primary"><i class="bi bi-plus-circle me-1" aria-hidden="true"></i>課題を登録</a>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -239,20 +193,20 @@
|
||||
{{define "scripts"}}
|
||||
<script>
|
||||
(function () {
|
||||
var banner = document.getElementById('urgentBanner');
|
||||
var message = document.getElementById('urgentMessage');
|
||||
var banner = document.getElementById('urgentBanner');
|
||||
var message = document.getElementById('urgentMessage');
|
||||
var countdown = document.getElementById('urgentCountdown');
|
||||
var body = document.body;
|
||||
|
||||
var reduced = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
|
||||
var items = document.querySelectorAll('[data-priority="high"][data-due]');
|
||||
var mostUrgent = null;
|
||||
var mostUrgentDue = Infinity;
|
||||
|
||||
items.forEach(function (item) {
|
||||
var due = parseInt(item.dataset.due);
|
||||
var now = Date.now();
|
||||
var due = parseInt(item.dataset.due);
|
||||
var now = Date.now();
|
||||
var diff = due - now;
|
||||
|
||||
if (diff > 0 && diff < mostUrgentDue) {
|
||||
mostUrgentDue = diff;
|
||||
var titleEl = item.querySelector('strong');
|
||||
@@ -260,59 +214,47 @@
|
||||
}
|
||||
});
|
||||
|
||||
var hasOverdueHigh = false;
|
||||
var overdueItems = document.querySelectorAll('[data-priority="high"]');
|
||||
overdueItems.forEach(function (item) {
|
||||
var hasOverdueHigh = Array.from(document.querySelectorAll('[data-priority="high"]')).some(function(item) {
|
||||
var due = parseInt(item.dataset.due);
|
||||
if (due && due < Date.now()) {
|
||||
hasOverdueHigh = true;
|
||||
}
|
||||
return due && due < Date.now();
|
||||
});
|
||||
|
||||
if (hasOverdueHigh) {
|
||||
banner.classList.remove('d-none');
|
||||
banner.classList.add('urgent-banner-danger');
|
||||
message.innerHTML = '🚨 <strong>期限切れの重要課題があります!</strong>';
|
||||
if (!reduced) banner.classList.add('anxiety-danger');
|
||||
message.textContent = '期限切れの重要課題があります!';
|
||||
countdown.textContent = '今すぐ対応してください!';
|
||||
body.classList.add('anxiety-danger');
|
||||
} else if (mostUrgent && mostUrgentDue < 24 * 60 * 60 * 1000) {
|
||||
banner.classList.remove('d-none');
|
||||
banner.classList.add('urgent-banner-danger');
|
||||
message.innerHTML = '🚨 <strong>「' + mostUrgent.title + '」の期限が迫っています!</strong>';
|
||||
body.classList.add('anxiety-danger');
|
||||
if (!reduced) banner.classList.add('anxiety-danger');
|
||||
message.textContent = '「' + mostUrgent.title + '」の期限が迫っています!';
|
||||
updateCountdown();
|
||||
setInterval(updateCountdown, 1000);
|
||||
if (!reduced) setInterval(updateCountdown, 1000);
|
||||
} else if (mostUrgent && mostUrgentDue < 3 * 24 * 60 * 60 * 1000) {
|
||||
banner.classList.remove('d-none');
|
||||
banner.classList.add('urgent-banner-warning');
|
||||
message.innerHTML = '⚠️ <strong>「' + mostUrgent.title + '」の期限が近づいています</strong>';
|
||||
body.classList.add('anxiety-warning');
|
||||
if (!reduced) banner.classList.add('anxiety-warning');
|
||||
message.textContent = '「' + mostUrgent.title + '」の期限が近づいています';
|
||||
updateCountdown();
|
||||
setInterval(updateCountdown, 1000);
|
||||
if (!reduced) setInterval(updateCountdown, 60000);
|
||||
}
|
||||
|
||||
function updateCountdown() {
|
||||
if (!mostUrgent) return;
|
||||
var now = Date.now();
|
||||
var diff = mostUrgent.due - now;
|
||||
|
||||
if (diff <= 0) {
|
||||
countdown.textContent = '期限切れ!';
|
||||
return;
|
||||
}
|
||||
|
||||
var days = Math.floor(diff / 86400000);
|
||||
var diff = mostUrgent.due - Date.now();
|
||||
if (diff <= 0) { countdown.textContent = '期限切れ!'; return; }
|
||||
var days = Math.floor(diff / 86400000);
|
||||
var hours = Math.floor((diff % 86400000) / 3600000);
|
||||
var mins = Math.floor((diff % 3600000) / 60000);
|
||||
var secs = Math.floor((diff % 60000) / 1000);
|
||||
|
||||
var text = 'あと ';
|
||||
var mins = Math.floor((diff % 3600000) / 60000);
|
||||
var secs = Math.floor((diff % 60000) / 1000);
|
||||
var text = 'あと ';
|
||||
if (days > 0) text += days + '日 ' + hours + '時間 ' + mins + '分 ' + secs + '秒';
|
||||
else if (hours > 0) text += hours + '時間 ' + mins + '分 ' + secs + '秒';
|
||||
else text += mins + '分 ' + secs + '秒';
|
||||
|
||||
countdown.textContent = text;
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "content"}}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-exclamation-triangle display-1 text-danger"></i>
|
||||
<div class="text-center py-5" role="main">
|
||||
<i class="bi bi-exclamation-triangle display-1 text-danger" aria-hidden="true"></i>
|
||||
<h1 class="mt-4">{{.title}}</h1>
|
||||
<p class="lead text-muted">{{.message}}</p>
|
||||
<a href="/" class="btn btn-primary mt-3"><i class="bi bi-house-door me-1"></i>ダッシュボードに戻る</a>
|
||||
<p class="text-muted small">問題が続く場合はページを再読み込みするか、最初からやり直してください。</p>
|
||||
<div class="d-flex justify-content-center gap-2 mt-3">
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="location.reload()">
|
||||
<i class="bi bi-arrow-clockwise me-1" aria-hidden="true"></i>再読み込み
|
||||
</button>
|
||||
<a href="/" class="btn btn-primary">
|
||||
<i class="bi bi-house-door me-1" aria-hidden="true"></i>ダッシュボードに戻る
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{{define "content"}}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-10">
|
||||
<h1 class="mb-4"><i class="bi bi-person me-2"></i>プロフィール</h1>
|
||||
<h1 class="mb-4"><i class="bi bi-person me-2" aria-hidden="true"></i>プロフィール</h1>
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
@@ -11,25 +11,23 @@
|
||||
<h5 class="mb-0">アカウント情報</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{if .error}}<div class="alert alert-danger">{{.error}}</div>{{end}}
|
||||
{{if .success}}<div class="alert alert-success">{{.success}}</div>{{end}}
|
||||
{{if .error}}<div class="alert alert-danger" role="alert">{{.error}}</div>{{end}}
|
||||
{{if .success}}<div class="alert alert-success" role="status">{{.success}}</div>{{end}}
|
||||
<form method="POST" action="/profile">
|
||||
{{.csrfField}}
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">メールアドレス</label>
|
||||
<input type="email" class="form-control" id="email" value="{{.user.Email}}" disabled>
|
||||
<p class="form-label mb-1 text-muted small">メールアドレス</p>
|
||||
<p class="mb-0 fw-bold">{{.user.Email}}</p>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">名前</label>
|
||||
<input type="text" class="form-control" id="name" name="name" value="{{.user.Name}}"
|
||||
required>
|
||||
<input type="text" class="form-control" id="name" name="name" value="{{.user.Name}}" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">ロール</label>
|
||||
<input type="text" class="form-control"
|
||||
value="{{if eq .user.Role `admin`}}管理者{{else}}ユーザー{{end}}" disabled>
|
||||
<p class="form-label mb-1 text-muted small">ロール</p>
|
||||
<p class="mb-0 fw-bold">{{if eq .user.Role "admin"}}管理者{{else}}ユーザー{{end}}</p>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg me-1"></i>更新</button>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg me-1" aria-hidden="true"></i>更新</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,111 +38,101 @@
|
||||
<h5 class="mb-0">パスワード変更</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{if .passwordError}}<div class="alert alert-danger">{{.passwordError}}</div>{{end}}
|
||||
{{if .passwordSuccess}}<div class="alert alert-success">{{.passwordSuccess}}</div>{{end}}
|
||||
{{if .passwordError}}<div class="alert alert-danger" role="alert">{{.passwordError}}</div>{{end}}
|
||||
{{if .passwordSuccess}}<div class="alert alert-success" role="status">{{.passwordSuccess}}</div>{{end}}
|
||||
<form method="POST" action="/profile/password">
|
||||
{{.csrfField}}
|
||||
<div class="mb-3">
|
||||
<label for="old_password" class="form-label">現在のパスワード</label>
|
||||
<input type="password" class="form-control" id="old_password" name="old_password"
|
||||
required>
|
||||
<input type="password" class="form-control" id="old_password" name="old_password" required autocomplete="current-password">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="new_password" class="form-label">新しいパスワード</label>
|
||||
<input type="password" class="form-control" id="new_password" name="new_password"
|
||||
required minlength="6">
|
||||
<div class="form-text">6文字以上</div>
|
||||
<input type="password" class="form-control" id="new_password" name="new_password" required minlength="8" autocomplete="new-password">
|
||||
<div class="form-text">8文字以上</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="confirm_password" class="form-label">新しいパスワード(確認)</label>
|
||||
<input type="password" class="form-control" id="confirm_password"
|
||||
name="confirm_password" required>
|
||||
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required minlength="8" autocomplete="new-password">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-warning"><i class="bi bi-key me-1"></i>パスワード変更</button>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-key me-1" aria-hidden="true"></i>パスワード変更</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2段階認証設定 -->
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-shield-lock me-2"></i>2段階認証(2FA)</h5>
|
||||
<h5 class="mb-0"><i class="bi bi-shield-lock me-2" aria-hidden="true"></i>2段階認証(2FA)</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{if .totpError}}<div class="alert alert-danger">{{.totpError}}</div>{{end}}
|
||||
{{if .totpSuccess}}<div class="alert alert-success">{{.totpSuccess}}</div>{{end}}
|
||||
{{if .totpError}}<div class="alert alert-danger" role="alert">{{.totpError}}</div>{{end}}
|
||||
{{if .totpSuccess}}<div class="alert alert-success" role="status">{{.totpSuccess}}</div>{{end}}
|
||||
{{if .user.TOTPEnabled}}
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<span class="badge bg-success me-2"><i class="bi bi-check-lg me-1"></i>有効</span>
|
||||
<span class="badge bg-success me-2"><i class="bi bi-check-lg me-1" aria-hidden="true"></i>有効</span>
|
||||
<span class="text-muted">2段階認証が有効になっています</span>
|
||||
</div>
|
||||
<form method="POST" action="/profile/totp/disable">
|
||||
{{.csrfField}}
|
||||
<div class="mb-3">
|
||||
<label for="totp_disable_password" class="form-label">現在のパスワードを入力して無効化</label>
|
||||
<input type="password" class="form-control" id="totp_disable_password" name="password"
|
||||
placeholder="パスワード" required style="max-width:320px">
|
||||
<input type="password" class="form-control" id="totp_disable_password" name="password" placeholder="パスワード" required style="max-width:320px" autocomplete="current-password">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="bi bi-shield-x me-1"></i>2段階認証を無効化
|
||||
<i class="bi bi-shield-x me-1" aria-hidden="true"></i>2段階認証を無効化
|
||||
</button>
|
||||
</form>
|
||||
{{else}}
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<span class="badge bg-secondary me-2"><i class="bi bi-x-lg me-1"></i>無効</span>
|
||||
<span class="badge bg-secondary me-2"><i class="bi bi-x-lg me-1" aria-hidden="true"></i>無効</span>
|
||||
<span class="text-muted">2段階認証が設定されていません</span>
|
||||
</div>
|
||||
<p class="text-muted small">2段階認証を有効にするとセキュリティが向上します。Google Authenticator などのアプリが必要です。</p>
|
||||
<a href="/profile/totp/setup" class="btn btn-primary">
|
||||
<i class="bi bi-shield-plus me-1"></i>2段階認証を設定する
|
||||
<i class="bi bi-shield-plus me-1" aria-hidden="true"></i>2段階認証を設定する
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 通知設定 -->
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-bell me-2"></i>通知設定</h5>
|
||||
<h5 class="mb-0"><i class="bi bi-bell me-2" aria-hidden="true"></i>通知設定</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{if .notifyError}}<div class="alert alert-danger">{{.notifyError}}</div>{{end}}
|
||||
{{if .notifySuccess}}<div class="alert alert-success">{{.notifySuccess}}</div>{{end}}
|
||||
{{if .notifyError}}<div class="alert alert-danger" role="alert">{{.notifyError}}</div>{{end}}
|
||||
{{if .notifySuccess}}<div class="alert alert-success" role="status">{{.notifySuccess}}</div>{{end}}
|
||||
<form method="POST" action="/profile/notifications">
|
||||
{{.csrfField}}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6 class="mb-3"><i class="bi bi-telegram me-1"></i>Telegram</h6>
|
||||
<h6 class="mb-3"><i class="bi bi-telegram me-1" aria-hidden="true"></i>Telegram</h6>
|
||||
<div class="form-check form-switch mb-2">
|
||||
<input class="form-check-input" type="checkbox" id="telegram_enabled"
|
||||
name="telegram_enabled" {{if .notifySettings.TelegramEnabled}}checked{{end}}>
|
||||
<input class="form-check-input" type="checkbox" id="telegram_enabled" name="telegram_enabled" {{if .notifySettings.TelegramEnabled}}checked{{end}}>
|
||||
<label class="form-check-label" for="telegram_enabled">Telegram通知を有効化</label>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="telegram_chat_id" class="form-label">Chat ID</label>
|
||||
<input type="text" class="form-control" id="telegram_chat_id" name="telegram_chat_id"
|
||||
value="{{.notifySettings.TelegramChatID}}" placeholder="例: 123456789">
|
||||
<input type="text" class="form-control" id="telegram_chat_id" name="telegram_chat_id" value="{{.notifySettings.TelegramChatID}}" placeholder="例: 123456789">
|
||||
<div class="form-text">
|
||||
Botに<code>/start</code>を送信後、<a href="https://t.me/userinfobot"
|
||||
target="_blank">@userinfobot</a>でIDを確認
|
||||
Botに<code>/start</code>を送信後、<a href="https://t.me/userinfobot" target="_blank" rel="noopener noreferrer">@userinfobot</a>でIDを確認
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="my-3">
|
||||
<div class="form-check form-switch mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="notify_on_create" name="notify_on_create"
|
||||
{{if .notifySettings.NotifyOnCreate}}checked{{end}}>
|
||||
<input class="form-check-input" type="checkbox" id="notify_on_create" name="notify_on_create" {{if .notifySettings.NotifyOnCreate}}checked{{end}}>
|
||||
<label class="form-check-label" for="notify_on_create">
|
||||
<i class="bi bi-plus-circle me-1"></i>課題追加時に通知する
|
||||
<i class="bi bi-plus-circle me-1" aria-hidden="true"></i>課題追加時に通知する
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg me-1"></i>通知設定を保存</button>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg me-1" aria-hidden="true"></i>通知設定を保存</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -5,15 +5,15 @@
|
||||
<div class="col-md-7 col-lg-6">
|
||||
<div class="card shadow">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-shield-lock me-2"></i>2段階認証の設定</h5>
|
||||
<h5 class="mb-0"><i class="bi bi-shield-lock me-2" aria-hidden="true"></i>2段階認証の設定</h5>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
{{if .error}}
|
||||
<div class="alert alert-danger">{{.error}}</div>
|
||||
<div class="alert alert-danger" role="alert">{{.error}}</div>
|
||||
{{end}}
|
||||
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
<div class="alert alert-info" role="note">
|
||||
<i class="bi bi-info-circle me-1" aria-hidden="true"></i>
|
||||
Google Authenticator、Authy などの認証アプリを使用してください。
|
||||
</div>
|
||||
|
||||
@@ -25,17 +25,16 @@
|
||||
</ol>
|
||||
|
||||
<div class="text-center mb-4">
|
||||
<img src="data:image/png;base64,{{.qrCode}}" alt="QRコード" class="border rounded"
|
||||
style="max-width:200px">
|
||||
<img src="data:image/png;base64,{{.qrCode}}" alt="2段階認証設定用QRコード" class="border rounded" style="max-width:200px">
|
||||
<p class="text-muted small mt-2">QRコードを再スキャンしたい場合はページを再読み込みしてください。</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-bold">シークレットキー(手動入力の場合)</label>
|
||||
<label class="form-label fw-bold" for="secretKey">シークレットキー(手動入力の場合)</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control font-monospace" id="secretKey" value="{{.secret}}"
|
||||
readonly>
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="copySecret()" title="コピー">
|
||||
<i class="bi bi-clipboard"></i>
|
||||
<input type="text" class="form-control font-monospace" id="secretKey" value="{{.secret}}" readonly aria-label="シークレットキー">
|
||||
<button class="btn btn-outline-secondary" type="button" id="copySecretBtn" aria-label="シークレットキーをコピー">
|
||||
<i class="bi bi-clipboard" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-text">認証アプリで「手動入力」を選択し、このキーを入力してください。</div>
|
||||
@@ -45,19 +44,16 @@
|
||||
{{.csrfField}}
|
||||
<div class="mb-3">
|
||||
<label for="totp_password" class="form-label fw-bold">現在のパスワード</label>
|
||||
<input type="password" class="form-control" id="totp_password" name="password"
|
||||
placeholder="パスワードを入力" required>
|
||||
<input type="password" class="form-control" id="totp_password" name="password" placeholder="パスワードを入力" required autocomplete="current-password">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="totp_code" class="form-label fw-bold">認証コードで確認</label>
|
||||
<input type="text" class="form-control form-control-lg text-center" id="totp_code"
|
||||
name="totp_code" placeholder="000000" maxlength="6" pattern="[0-9]{6}" inputmode="numeric"
|
||||
autocomplete="off" autofocus required>
|
||||
<input type="text" class="form-control form-control-lg text-center" id="totp_code" name="totp_code" placeholder="000000" maxlength="6" pattern="[0-9]{6}" inputmode="numeric" autocomplete="off" autofocus required>
|
||||
<div class="form-text">認証アプリに表示された6桁のコードを入力してください。</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-shield-check me-1"></i>有効化
|
||||
<i class="bi bi-shield-check me-1" aria-hidden="true"></i>有効化
|
||||
</button>
|
||||
<a href="/profile" class="btn btn-outline-secondary">キャンセル</a>
|
||||
</div>
|
||||
@@ -70,14 +66,23 @@
|
||||
|
||||
{{define "scripts"}}
|
||||
<script>
|
||||
function copySecret() {
|
||||
const el = document.getElementById('secretKey');
|
||||
el.select();
|
||||
navigator.clipboard.writeText(el.value).then(() => {
|
||||
const btn = el.nextElementSibling;
|
||||
btn.innerHTML = '<i class="bi bi-check-lg"></i>';
|
||||
setTimeout(() => { btn.innerHTML = '<i class="bi bi-clipboard"></i>'; }, 2000);
|
||||
document.getElementById('copySecretBtn').addEventListener('click', function() {
|
||||
var btn = this;
|
||||
var val = document.getElementById('secretKey').value;
|
||||
if (!navigator.clipboard) {
|
||||
showCopyFeedback('クリップボードがサポートされていません。手動でコピーしてください。');
|
||||
return;
|
||||
}
|
||||
navigator.clipboard.writeText(val).then(function() {
|
||||
btn.innerHTML = '<i class="bi bi-check-lg" aria-hidden="true"></i>';
|
||||
btn.setAttribute('aria-label', 'コピーしました');
|
||||
setTimeout(function() {
|
||||
btn.innerHTML = '<i class="bi bi-clipboard" aria-hidden="true"></i>';
|
||||
btn.setAttribute('aria-label', 'シークレットキーをコピー');
|
||||
}, 2000);
|
||||
}).catch(function(err) {
|
||||
showCopyFeedback('コピーに失敗しました: ' + (err.message || err));
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-arrow-repeat me-2"></i>繰り返し課題の編集</h5>
|
||||
<h5 class="mb-0"><i class="bi bi-arrow-repeat me-2" aria-hidden="true"></i>繰り返し課題の編集</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{if .error}}<div class="alert alert-danger">{{.error}}</div>{{end}}
|
||||
{{if .error}}<div class="alert alert-danger" role="alert">{{.error}}</div>{{end}}
|
||||
<form method="POST" action="/recurring/{{.recurring.ID}}">
|
||||
{{.csrfField}}
|
||||
<div class="mb-3">
|
||||
<label for="title" class="form-label">タイトル <span class="text-danger">*</span></label>
|
||||
<label for="title" class="form-label">タイトル <span class="text-danger" aria-hidden="true">*</span><span class="visually-hidden">(必須)</span></label>
|
||||
<input type="text" class="form-control" id="title" name="title" value="{{.recurring.Title}}" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
@@ -22,9 +22,9 @@
|
||||
<div class="mb-3">
|
||||
<label for="priority" class="form-label">重要度</label>
|
||||
<select class="form-select" id="priority" name="priority">
|
||||
<option value="low" {{if eq .recurring.Priority "low"}}selected{{end}}>小</option>
|
||||
<option value="low" {{if eq .recurring.Priority "low" }}selected{{end}}>低</option>
|
||||
<option value="medium" {{if eq .recurring.Priority "medium"}}selected{{end}}>中</option>
|
||||
<option value="high" {{if eq .recurring.Priority "high"}}selected{{end}}>大</option>
|
||||
<option value="high" {{if eq .recurring.Priority "high" }}selected{{end}}>高</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
@@ -35,30 +35,30 @@
|
||||
<label for="due_time" class="form-label">時刻</label>
|
||||
<input type="time" class="form-control" id="due_time" name="due_time" value="{{.recurring.DueTime}}">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card bg-light mb-3">
|
||||
<div class="card-body py-3">
|
||||
<h6 class="mb-3"><i class="bi bi-arrow-repeat me-1"></i>繰り返し設定</h6>
|
||||
<h6 class="mb-3"><i class="bi bi-arrow-repeat me-1" aria-hidden="true"></i>繰り返し設定</h6>
|
||||
<div class="row mb-3">
|
||||
<div class="col-6">
|
||||
<label for="recurrence_type" class="form-label small">繰り返しタイプ</label>
|
||||
<select class="form-select form-select-sm" id="recurrence_type" name="recurrence_type" onchange="updateRecurrenceOptions()">
|
||||
<option value="daily" {{if eq .recurring.RecurrenceType "daily"}}selected{{end}}>毎日</option>
|
||||
<option value="weekly" {{if eq .recurring.RecurrenceType "weekly"}}selected{{end}}>毎週</option>
|
||||
<option value="daily" {{if eq .recurring.RecurrenceType "daily" }}selected{{end}}>毎日</option>
|
||||
<option value="weekly" {{if eq .recurring.RecurrenceType "weekly" }}selected{{end}}>毎週</option>
|
||||
<option value="monthly" {{if eq .recurring.RecurrenceType "monthly"}}selected{{end}}>毎月</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="recurrence_interval" class="form-label small">間隔</label>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="number" class="form-control" id="recurrence_interval" name="recurrence_interval" value="{{.recurring.RecurrenceInterval}}" min="1" max="12">
|
||||
<input type="number" class="form-control" id="recurrence_interval" name="recurrence_interval" value="{{.recurring.RecurrenceInterval}}" min="1" max="12" onchange="updateLeadDaysMax()">
|
||||
<span class="input-group-text" id="interval_label">{{if eq .recurring.RecurrenceType "daily"}}日{{else if eq .recurring.RecurrenceType "weekly"}}週{{else}}月{{end}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="weekday_group" class="mb-3">
|
||||
<label class="form-label small">曜日</label>
|
||||
<div class="btn-group btn-group-sm w-100" role="group">
|
||||
<div class="btn-group btn-group-sm w-100" role="group" aria-label="曜日選択">
|
||||
<input type="radio" class="btn-check" name="recurrence_weekday" id="wd0" value="0" {{if .recurring.RecurrenceWeekday}}{{if eq (derefInt .recurring.RecurrenceWeekday) 0}}checked{{end}}{{end}}>
|
||||
<label class="btn btn-outline-primary" for="wd0">日</label>
|
||||
<input type="radio" class="btn-check" name="recurrence_weekday" id="wd1" value="1" {{if .recurring.RecurrenceWeekday}}{{if eq (derefInt .recurring.RecurrenceWeekday) 1}}checked{{end}}{{end}}>
|
||||
@@ -83,7 +83,18 @@
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label small">リストに追加するタイミング</label>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="input-group input-group-sm" style="width: 120px;">
|
||||
<input type="number" class="form-control" id="generation_lead_days" name="generation_lead_days" value="{{.recurring.GenerationLeadDays}}" min="0" max="365">
|
||||
<span class="input-group-text">日前</span>
|
||||
</div>
|
||||
<input type="time" class="form-control form-control-sm" id="generation_lead_time" name="generation_lead_time" value="{{.recurring.GenerationLeadTime}}" style="width: 110px;">
|
||||
</div>
|
||||
<div class="form-text small text-muted" id="lead_days_hint"></div>
|
||||
</div>
|
||||
<hr class="my-2">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="small text-muted">状態:</span>
|
||||
@@ -95,25 +106,25 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg me-1"></i>更新</button>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg me-1" aria-hidden="true"></i>更新</button>
|
||||
<a href="/assignments" class="btn btn-outline-secondary">キャンセル</a>
|
||||
{{if .recurring.IsActive}}
|
||||
<button type="button" class="btn btn-outline-danger" onclick="if(confirm('繰り返しを停止しますか?今後新しい課題は自動作成されなくなります。')) document.getElementById('stopForm').submit();">
|
||||
<i class="bi bi-stop-fill me-1"></i>停止
|
||||
<button type="button" class="btn btn-outline-danger" id="stopBtn">
|
||||
<i class="bi bi-stop-fill me-1" aria-hidden="true"></i>停止
|
||||
</button>
|
||||
{{else}}
|
||||
<button type="button" class="btn btn-outline-success" onclick="document.getElementById('resumeForm').submit();">
|
||||
<i class="bi bi-play-fill me-1"></i>再開
|
||||
<i class="bi bi-play-fill me-1" aria-hidden="true"></i>再開
|
||||
</button>
|
||||
{{end}}
|
||||
<button type="button" class="btn btn-outline-danger ms-auto" onclick="if(confirm('この繰り返し設定を削除しますか?この操作は取り消せません。')) document.getElementById('deleteForm').submit();">
|
||||
<i class="bi bi-trash me-1"></i>削除
|
||||
<button type="button" class="btn btn-outline-danger ms-auto" id="deleteBtn">
|
||||
<i class="bi bi-trash me-1" aria-hidden="true"></i>削除
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
{{if .recurring.IsActive}}
|
||||
<form id="stopForm" action="/recurring/{{.recurring.ID}}/stop" method="POST" class="d-none">
|
||||
{{.csrfField}}
|
||||
@@ -132,17 +143,58 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function getLeadDaysMax() {
|
||||
var type = document.getElementById('recurrence_type').value;
|
||||
var interval = parseInt(document.getElementById('recurrence_interval').value) || 1;
|
||||
if (type === 'daily') return interval;
|
||||
if (type === 'weekly') return interval * 7;
|
||||
if (type === 'monthly') return interval * 28;
|
||||
return 0;
|
||||
}
|
||||
function updateLeadDaysMax() {
|
||||
var max = getLeadDaysMax();
|
||||
var input = document.getElementById('generation_lead_days');
|
||||
input.max = max;
|
||||
if (parseInt(input.value) > max) input.value = max;
|
||||
var type = document.getElementById('recurrence_type').value;
|
||||
var labels = { daily: '日', weekly: '週', monthly: 'ヶ月' };
|
||||
var interval = document.getElementById('recurrence_interval').value;
|
||||
document.getElementById('lead_days_hint').textContent =
|
||||
max === 0 ? '間隔が1日のため、事前追加は設定できません' :
|
||||
'最大' + max + '日前まで指定可能(繰り返し間隔' + interval + labels[type] + '以内)';
|
||||
}
|
||||
function updateRecurrenceOptions() {
|
||||
var type = document.getElementById('recurrence_type').value;
|
||||
document.getElementById('weekday_group').style.display = type === 'weekly' ? 'block' : 'none';
|
||||
document.getElementById('day_group').style.display = type === 'monthly' ? 'block' : 'none';
|
||||
document.getElementById('weekday_group').style.display = type === 'weekly' ? 'block' : 'none';
|
||||
document.getElementById('day_group').style.display = type === 'monthly' ? 'block' : 'none';
|
||||
var label = document.getElementById('interval_label');
|
||||
if (type === 'daily') label.textContent = '日';
|
||||
else if (type === 'weekly') label.textContent = '週';
|
||||
else if (type === 'monthly') label.textContent = '月';
|
||||
if (label) {
|
||||
if (type === 'daily') label.textContent = '日';
|
||||
else if (type === 'weekly') label.textContent = '週';
|
||||
else if (type === 'monthly') label.textContent = '月';
|
||||
}
|
||||
updateLeadDaysMax();
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
updateRecurrenceOptions();
|
||||
|
||||
var stopBtn = document.getElementById('stopBtn');
|
||||
if (stopBtn) {
|
||||
stopBtn.addEventListener('click', function() {
|
||||
showConfirmModal('繰り返しを停止しますか?今後新しい課題は自動作成されなくなります。', function() {
|
||||
document.getElementById('stopForm').submit();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var deleteBtn = document.getElementById('deleteBtn');
|
||||
if (deleteBtn) {
|
||||
deleteBtn.addEventListener('click', function() {
|
||||
showConfirmModal('この繰り返し設定を削除しますか?この操作は取り消せません。', function() {
|
||||
document.getElementById('deleteForm').submit();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -2,24 +2,22 @@
|
||||
|
||||
{{define "content"}}
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<h4 class="mb-0 fw-bold"><i class="bi bi-arrow-repeat me-2"></i>繰り返し設定一覧</h4>
|
||||
</div>
|
||||
<h4 class="mb-0 fw-bold"><i class="bi bi-arrow-repeat me-2" aria-hidden="true"></i>繰り返し設定一覧</h4>
|
||||
<a href="/assignments" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i>課題一覧に戻る
|
||||
<i class="bi bi-arrow-left me-1" aria-hidden="true"></i>課題一覧に戻る
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<table class="table table-hover mb-0" aria-label="繰り返し設定一覧">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="ps-3">タイトル</th>
|
||||
<th>科目</th>
|
||||
<th>繰り返し</th>
|
||||
<th>状態</th>
|
||||
<th class="text-end pe-3">操作</th>
|
||||
<th scope="col" class="ps-3">タイトル</th>
|
||||
<th scope="col">科目</th>
|
||||
<th scope="col">繰り返し</th>
|
||||
<th scope="col">状態</th>
|
||||
<th scope="col" class="text-end pe-3">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -38,9 +36,7 @@
|
||||
<span class="text-muted">-</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-dark">{{recurringSummary .}}</span>
|
||||
</td>
|
||||
<td><span class="text-dark">{{recurringSummary .}}</span></td>
|
||||
<td>
|
||||
{{if .IsActive}}
|
||||
<span class="badge bg-success">有効</span>
|
||||
@@ -49,15 +45,19 @@
|
||||
{{end}}
|
||||
</td>
|
||||
<td class="text-end pe-3">
|
||||
<a href="/recurring/{{.ID}}/edit" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-pencil"></i>
|
||||
<a href="/recurring/{{.ID}}/edit" class="btn btn-sm btn-outline-primary" aria-label="{{.Title}}を編集">
|
||||
<i class="bi bi-pencil" aria-hidden="true"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-4 text-muted">
|
||||
繰り返し設定がありません
|
||||
<td colspan="5" class="text-center py-5">
|
||||
<i class="bi bi-arrow-repeat display-4 text-muted" aria-hidden="true"></i>
|
||||
<p class="mt-2 mb-1 text-muted fw-bold">繰り返し設定がありません</p>
|
||||
<a href="/assignments/new" class="btn btn-sm btn-primary mt-1">
|
||||
<i class="bi bi-plus-lg me-1" aria-hidden="true"></i>繰り返し課題を作成する
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
@@ -65,4 +65,4 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user