Skip to content

Commit 0e189fa

Browse files
committed
feat: implement bulk assignment status update functionality
1 parent 16822f5 commit 0e189fa

3 files changed

Lines changed: 251 additions & 6 deletions

File tree

Ice/Areas/Admin/Controllers/StudentGroupController.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,46 @@ await studentGroupService.UpdateAssignmentProgressAsync(new UpdateAssignmentProg
142142
flashMessage.Info("課題のステータスを更新しました。");
143143
return RedirectToAction("Detail", new { id = studentGroupId });
144144
}
145+
146+
[HttpPost("{studentGroupId:long}/bulk-update-assignment-status")]
147+
public async Task<IActionResult> BulkUpdateAssignmentStatus(
148+
long studentGroupId,
149+
[FromForm] List<long> assignmentIds,
150+
[FromForm] string status,
151+
CancellationToken cancellationToken
152+
)
153+
{
154+
if (assignmentIds.Count == 0)
155+
{
156+
flashMessage.Warning("課題を選択してください。");
157+
return RedirectToAction("Detail", new { id = studentGroupId });
158+
}
159+
160+
if (!Enum.TryParse<AssignmentProgress>(status, out var assignmentStatus))
161+
{
162+
flashMessage.Danger("無効なステータスです。");
163+
return RedirectToAction("Detail", new { id = studentGroupId });
164+
}
165+
166+
foreach (var assignmentId in assignmentIds)
167+
{
168+
await studentGroupService.UpdateAssignmentProgressAsync(new UpdateAssignmentProgressDto
169+
{
170+
StudentGroupId = studentGroupId,
171+
AssignmentId = assignmentId,
172+
Status = assignmentStatus
173+
}, cancellationToken);
174+
}
175+
176+
var statusText = assignmentStatus switch
177+
{
178+
AssignmentProgress.NotStarted => "未着手",
179+
AssignmentProgress.InProgress => "進行中",
180+
AssignmentProgress.Completed => "完了",
181+
_ => "不明"
182+
};
183+
184+
flashMessage.Info($"{assignmentIds.Count}件の課題のステータスを「{statusText}」に更新しました。");
185+
return RedirectToAction("Detail", new { id = studentGroupId });
186+
}
145187
}

Ice/Areas/Admin/Views/StudentGroup/Detail.cshtml

Lines changed: 194 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,70 @@
2222
<div class="card-body">
2323
@if (Model.AssignmentProgress.Any())
2424
{
25+
<div class="mb-3">
26+
<div class="d-flex flex-wrap gap-2">
27+
<button type="button" class="btn btn-sm btn-secondary" onclick="selectAllAssignments()">全選択</button>
28+
<button type="button" class="btn btn-sm btn-secondary" onclick="deselectAllAssignments()">選択解除</button>
29+
</div>
30+
<div class="d-flex flex-wrap gap-2 mt-2">
31+
<button type="button" class="btn btn-sm btn-secondary flex-fill" onclick="bulkUpdateAssignmentStatus('NotStarted')">
32+
<span class="d-none d-md-inline">選択した課題を</span>未着手<span class="d-none d-md-inline">にする</span>
33+
</button>
34+
<button type="button" class="btn btn-sm btn-primary flex-fill" onclick="bulkUpdateAssignmentStatus('InProgress')">
35+
<span class="d-none d-md-inline">選択した課題を</span>進行中<span class="d-none d-md-inline">にする</span>
36+
</button>
37+
<button type="button" class="btn btn-sm btn-success flex-fill" onclick="bulkUpdateAssignmentStatus('Completed')">
38+
<span class="d-none d-md-inline">選択した課題を</span>完了<span class="d-none d-md-inline">にする</span>
39+
</button>
40+
</div>
41+
</div>
2542
<div class="table-responsive">
26-
<table class="table table-striped table-hover">
43+
<table class="table table-striped table-hover assignment-table">
2744
<thead class="table-dark">
2845
<tr>
46+
<th style="width: 60px;">
47+
<label class="checkbox-label">
48+
<input type="checkbox" id="selectAllCheckbox" onchange="toggleAllAssignments(this.checked)">
49+
<span class="d-none d-md-inline ms-1">全選択</span>
50+
</label>
51+
</th>
2952
<th>課題名</th>
30-
<th>ステータス</th>
31-
<th>操作</th>
53+
<th class="d-none d-md-table-cell">ステータス</th>
54+
<th class="d-none d-md-table-cell">操作</th>
3255
</tr>
3356
</thead>
3457
<tbody>
3558
@foreach (var assignment in Model.AssignmentProgress)
3659
{
3760
<tr>
3861
<td>
39-
<a asp-controller="Assignment" asp-action="Detail" asp-route-id="@assignment.AssignmentId">@assignment.AssignmentName</a>
62+
<label class="checkbox-label">
63+
<input type="checkbox" class="assignment-checkbox" value="@assignment.AssignmentId" id="assignment-@assignment.AssignmentId" onchange="updateSelectAllCheckbox()">
64+
</label>
4065
</td>
4166
<td>
67+
<div class="d-flex flex-column">
68+
<a asp-controller="Assignment" asp-action="Detail" asp-route-id="@assignment.AssignmentId">@assignment.AssignmentName</a>
69+
<div class="d-md-none mt-1">
70+
@switch (assignment.Status)
71+
{
72+
case AssignmentProgress.NotStarted:
73+
<span class="badge bg-secondary">未着手</span>
74+
break;
75+
case AssignmentProgress.InProgress:
76+
<span class="badge bg-primary">進行中</span>
77+
break;
78+
case AssignmentProgress.Completed:
79+
<span class="badge bg-success">完了</span>
80+
break;
81+
default:
82+
<span class="badge bg-warning">不明</span>
83+
break;
84+
}
85+
</div>
86+
</div>
87+
</td>
88+
<td class="d-none d-md-table-cell">
4289
@switch (assignment.Status)
4390
{
4491
case AssignmentProgress.NotStarted:
@@ -55,10 +102,10 @@
55102
break;
56103
}
57104
</td>
58-
<td>
105+
<td class="d-none d-md-table-cell">
59106
<form asp-action="UpdateAssignmentStatus" asp-route-studentGroupId="@Model.Id" asp-route-assignmentId="@assignment.AssignmentId" method="post" class="d-inline">
60107
<select name="status" class="form-select form-select-sm d-inline-block w-auto" onchange="this.form.submit()">
61-
<option value="">ステータスを選択してください</option>
108+
<option value="">ステータスを選択</option>
62109
<option value="NotStarted" selected="@(assignment.Status == AssignmentProgress.NotStarted)">未着手</option>
63110
<option value="InProgress" selected="@(assignment.Status == AssignmentProgress.InProgress)">進行中</option>
64111
<option value="Completed" selected="@(assignment.Status == AssignmentProgress.Completed)">完了</option>
@@ -147,3 +194,144 @@
147194
</div>
148195
</div>
149196
</div>
197+
198+
<style>
199+
/* チェックボックスのタップエリアを大きくする */
200+
.checkbox-label {
201+
display: inline-flex;
202+
align-items: center;
203+
cursor: pointer;
204+
min-width: 44px;
205+
min-height: 44px;
206+
justify-content: center;
207+
margin: 0;
208+
}
209+
210+
.checkbox-label input[type="checkbox"] {
211+
cursor: pointer;
212+
width: 20px;
213+
height: 20px;
214+
}
215+
216+
/* モバイルでの最適化 */
217+
@@media (max-width: 767.98px) {
218+
.assignment-table td {
219+
padding: 0.75rem 0.5rem;
220+
}
221+
222+
.assignment-table a {
223+
font-size: 0.95rem;
224+
}
225+
226+
.btn-sm {
227+
padding: 0.5rem 0.75rem;
228+
font-size: 0.9rem;
229+
}
230+
231+
/* モバイルではボタンを100%幅にする */
232+
.flex-fill {
233+
min-width: 0;
234+
}
235+
}
236+
237+
/* タッチデバイスでのホバー効果を無効化 */
238+
@@media (hover: none) {
239+
.table-hover tbody tr:hover {
240+
--bs-table-accent-bg: transparent;
241+
}
242+
}
243+
</style>
244+
245+
@section Scripts {
246+
<script>
247+
// 全選択
248+
function selectAllAssignments() {
249+
const checkboxes = document.querySelectorAll('.assignment-checkbox');
250+
checkboxes.forEach(cb => cb.checked = true);
251+
updateSelectAllCheckbox();
252+
}
253+
254+
// 選択解除
255+
function deselectAllAssignments() {
256+
const checkboxes = document.querySelectorAll('.assignment-checkbox');
257+
checkboxes.forEach(cb => cb.checked = false);
258+
updateSelectAllCheckbox();
259+
}
260+
261+
// 全選択チェックボックスの状態を切り替え
262+
function toggleAllAssignments(checked) {
263+
const checkboxes = document.querySelectorAll('.assignment-checkbox');
264+
checkboxes.forEach(cb => cb.checked = checked);
265+
}
266+
267+
// 全選択チェックボックスの状態を更新
268+
function updateSelectAllCheckbox() {
269+
const checkboxes = document.querySelectorAll('.assignment-checkbox');
270+
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
271+
272+
if (selectAllCheckbox) {
273+
const allChecked = Array.from(checkboxes).every(cb => cb.checked);
274+
const someChecked = Array.from(checkboxes).some(cb => cb.checked);
275+
276+
selectAllCheckbox.checked = allChecked;
277+
selectAllCheckbox.indeterminate = someChecked && !allChecked;
278+
}
279+
}
280+
281+
// 一括ステータス更新
282+
function bulkUpdateAssignmentStatus(status) {
283+
const checkboxes = document.querySelectorAll('.assignment-checkbox:checked');
284+
const selectedIds = Array.from(checkboxes).map(cb => cb.value);
285+
286+
if (selectedIds.length === 0) {
287+
alert('課題を選択してください。');
288+
return;
289+
}
290+
291+
const statusNames = {
292+
'NotStarted': '未着手',
293+
'InProgress': '進行中',
294+
'Completed': '完了'
295+
};
296+
297+
const confirmMessage = `選択した${selectedIds.length}件の課題を「${statusNames[status]}」に変更しますか?`;
298+
if (!confirm(confirmMessage)) {
299+
return;
300+
}
301+
302+
// フォームを作成して送信
303+
const form = document.createElement('form');
304+
form.method = 'POST';
305+
form.action = '@Url.Action("BulkUpdateAssignmentStatus", "StudentGroup", new { studentGroupId = Model.Id })';
306+
307+
// CSRF トークンを追加
308+
const csrfToken = document.querySelector('input[name="__RequestVerificationToken"]');
309+
if (csrfToken) {
310+
const csrfInput = document.createElement('input');
311+
csrfInput.type = 'hidden';
312+
csrfInput.name = '__RequestVerificationToken';
313+
csrfInput.value = csrfToken.value;
314+
form.appendChild(csrfInput);
315+
}
316+
317+
// 選択された課題IDを追加
318+
selectedIds.forEach(id => {
319+
const input = document.createElement('input');
320+
input.type = 'hidden';
321+
input.name = 'assignmentIds';
322+
input.value = id;
323+
form.appendChild(input);
324+
});
325+
326+
// 新しいステータスを追加
327+
const statusInput = document.createElement('input');
328+
statusInput.type = 'hidden';
329+
statusInput.name = 'status';
330+
statusInput.value = status;
331+
form.appendChild(statusInput);
332+
333+
document.body.appendChild(form);
334+
form.submit();
335+
}
336+
</script>
337+
}

Ice/Services/AssignmentStudentGroupService/AssignmentStudentGroupService.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,19 @@ public async Task<IReadOnlyList<StudentGroups>> GetCompletedStudentGroupsAsync(l
3939

4040
return completedGroups;
4141
}
42+
43+
public async Task UpdateBulkStatusAsync(long assignmentId, List<long> studentGroupIds, AssignmentProgress newStatus, CancellationToken cancellationToken)
44+
{
45+
var progressRecords = await iceDbContext.StudentGroupAssignmentsProgress
46+
.Where(p => p.AssignmentId == assignmentId && studentGroupIds.Contains(p.StudentGroupId))
47+
.ToListAsync(cancellationToken);
48+
49+
foreach (var record in progressRecords)
50+
{
51+
record.Status = newStatus;
52+
record.UpdatedAt = DateTime.UtcNow;
53+
}
54+
55+
await iceDbContext.SaveChangesAsync(cancellationToken);
56+
}
4257
}

0 commit comments

Comments
 (0)