Skip to content

Commit 689c332

Browse files
authored
Merge pull request #31 from 6mile0/features/improve-ticket-func
TA/SAが出払っている場合はエラーにするようにした
2 parents 0c18575 + a3cac01 commit 689c332

4 files changed

Lines changed: 111 additions & 53 deletions

File tree

Ice/Areas/Student/Controllers/TicketController.cs

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Ice.Areas.Student.Dtos.Req;
22
using Ice.Areas.Student.ViewModels;
3+
using Ice.Exception;
34
using Ice.Services.AssignmentService;
45
using Ice.Services.StudentGroupService;
56
using Ice.Services.TicketService;
@@ -13,7 +14,11 @@ namespace Ice.Areas.Student.Controllers;
1314
[Authorize(Policy = "AllowedEmailDomain")]
1415
[Area("student")]
1516
[Route("tickets/add")]
16-
public class TicketController(ITicketService ticketService, IAssignmentService assignmentService, IStudentGroupService studentGroupService, IFlashMessage flashMessage) : Controller
17+
public class TicketController(
18+
ITicketService ticketService,
19+
IAssignmentService assignmentService,
20+
IStudentGroupService studentGroupService,
21+
IFlashMessage flashMessage) : Controller
1722
{
1823
[HttpGet]
1924
public async Task<IActionResult> AddTicket(
@@ -35,9 +40,9 @@ CancellationToken cancellationToken
3540
flashMessage.Danger("現在対応可能な課題が存在しません。お近くのTA/SAを呼んでください。");
3641
return RedirectToAction("Index", "Top");
3742
}
38-
43+
3944
var isAbleAddTicket = await ticketService.IsAbleAddTicketAsync(studentGroup.Id, cancellationToken);
40-
45+
4146
var enumerableAssignments = assignments
4247
.OrderBy(a => a.SortOrder)
4348
.Select(a => new SelectListItem
@@ -46,7 +51,7 @@ CancellationToken cancellationToken
4651
Text = a.Name
4752
})
4853
.ToList();
49-
54+
5055
return View("Add", new AddTicketViewModel
5156
{
5257
StudentGroupId = studentGroup.Id,
@@ -63,42 +68,59 @@ CancellationToken cancellationToken
6368
)
6469
{
6570
var isAbleAddTicket = await ticketService.IsAbleAddTicketAsync(model.StudentGroupId, cancellationToken);
66-
71+
6772
if (isAbleAddTicket != null)
6873
{
6974
flashMessage.Danger("現在対応中のチケットが存在するため、新しいチケットを追加できません。対応が完了するまでお待ちください。");
7075
return RedirectToAction("AddTicket", new { studentGroupId = model.StudentGroupId });
7176
}
7277

78+
var assignments = await assignmentService.GetAllAssignmentsAsync(cancellationToken);
79+
var enumerableAssignments = assignments
80+
.OrderBy(a => a.SortOrder)
81+
.Select(a => new SelectListItem
82+
{
83+
Value = a.Id.ToString(),
84+
Text = a.Name
85+
})
86+
.ToList();
87+
7388
if (!ModelState.IsValid)
7489
{
75-
var assignments = await assignmentService.GetAllAssignmentsAsync(cancellationToken);
76-
var enumerableAssignments = assignments
77-
.OrderBy(a => a.SortOrder)
78-
.Select(a => new SelectListItem
79-
{
80-
Value = a.Id.ToString(),
81-
Text = a.Name
82-
})
83-
.ToList();
84-
8590
return View("Add", new AddTicketViewModel
8691
{
8792
StudentGroupId = model.StudentGroupId,
8893
AssignmentId = model.AssignmentId,
8994
Title = model.Title,
90-
Assignments = enumerableAssignments
95+
Assignments = enumerableAssignments,
96+
IsAbleAddTicket = false
9197
});
9298
}
93-
99+
94100
var request = new AddTicketDto
95101
{
96102
StudentGroupId = model.StudentGroupId,
97103
AssignmentId = model.AssignmentId,
98104
Title = model.Title,
99105
};
100106

101-
await ticketService.CreateTicketAsync(request, cancellationToken);
107+
try
108+
{
109+
await ticketService.CreateTicketAsync(request, cancellationToken);
110+
}
111+
catch (AllStaffCurrentlyAssistingException)
112+
{
113+
flashMessage.Danger("現在全てのスタッフが対応中のため、新しいチケットを追加できません。しばらくしてから再度お試しください。");
114+
return View("Add", new AddTicketViewModel
115+
{
116+
StudentGroupId = model.StudentGroupId,
117+
AssignmentId = model.AssignmentId,
118+
Title = model.Title,
119+
Assignments = enumerableAssignments,
120+
IsAbleAddTicket = false
121+
});
122+
}
123+
102124
return RedirectToAction("AddTicket", "Ticket", new { studentGroupId = request.StudentGroupId });
103125
}
104126
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
namespace Ice.Exception;
2+
3+
public class AllStaffCurrentlyAssistingException(string message) : System.Exception(message);

Ice/Services/TicketService/TicketService.cs

Lines changed: 67 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
namespace Ice.Services.TicketService;
1212

13-
public class TicketService(IceDbContext iceDbContext, INotificationService notificationService): ITicketService
13+
public class TicketService(IceDbContext iceDbContext, INotificationService notificationService) : ITicketService
1414
{
1515
public async Task<IReadOnlyList<Tickets>> GetAllTicketsAsync(CancellationToken cancellationToken)
1616
{
@@ -31,7 +31,8 @@ public async Task<IReadOnlyList<Tickets>> GetAllTicketsAsync(CancellationToken c
3131
.FirstOrDefaultAsync(t => t.Id == ticketId, cancellationToken);
3232
}
3333

34-
public async Task<IReadOnlyList<Tickets>> GetTicketsByStudentGroupIdAsync(long studentGroupId, CancellationToken cancellationToken)
34+
public async Task<IReadOnlyList<Tickets>> GetTicketsByStudentGroupIdAsync(long studentGroupId,
35+
CancellationToken cancellationToken)
3536
{
3637
return await iceDbContext.Tickets
3738
.Where(t => t.StudentGroupId == studentGroupId)
@@ -40,7 +41,7 @@ public async Task<IReadOnlyList<Tickets>> GetTicketsByStudentGroupIdAsync(long s
4041
.OrderByDescending(t => t.CreatedAt)
4142
.ToListAsync(cancellationToken);
4243
}
43-
44+
4445
public async Task<AddTicketResDto> CreateTicketAsync(AddTicketDto addTicketDto, CancellationToken cancellationToken)
4546
{
4647
var transaction = await iceDbContext.Database.BeginTransactionAsync(cancellationToken);
@@ -66,15 +67,16 @@ public async Task<AddTicketResDto> CreateTicketAsync(AddTicketDto addTicketDto,
6667

6768
iceDbContext.Tickets.Add(ticket);
6869
await iceDbContext.SaveChangesAsync(cancellationToken);
69-
70+
7071
// 講師を割り当てる
7172
var targetTutor = await AssignTutorToTicketAsync(ticket.Id, cancellationToken);
72-
73+
7374
// 課題とチケットの関連付け
74-
await LinkAssignmentToTicketAsync(ticket.Id, addTicketDto.AssignmentId, addTicketDto.StudentGroupId, cancellationToken);
75+
await LinkAssignmentToTicketAsync(ticket.Id, addTicketDto.AssignmentId, addTicketDto.StudentGroupId,
76+
cancellationToken);
7577

7678
await transaction.CommitAsync(cancellationToken);
77-
79+
7880
// SSE通知を送信
7981
await notificationService.NotifyTicketCreatedAsync(
8082
ticket.Id,
@@ -131,7 +133,8 @@ public async Task DeleteTicketAsync(long ticketId, CancellationToken cancellatio
131133
return await iceDbContext.Tickets
132134
.Include(t => t.TicketAdminUser)
133135
.ThenInclude(tau => tau!.AdminUser)
134-
.FirstOrDefaultAsync(t => t.StudentGroupId == studentGroupId && t.Status == TicketStatus.InProgress, cancellationToken);
136+
.FirstOrDefaultAsync(t => t.StudentGroupId == studentGroupId && t.Status == TicketStatus.InProgress,
137+
cancellationToken);
135138
}
136139

137140
public async Task<Tickets> AssignTicketAsync(AssignTicketReqDto req, CancellationToken cancellationToken)
@@ -182,32 +185,60 @@ public async Task<Tickets> AssignTicketAsync(AssignTicketReqDto req, Cancellatio
182185

183186
private async Task<AdminUsers> AssignTutorToTicketAsync(long ticketId, CancellationToken cancellationToken)
184187
{
185-
// 講師ごとの担当チケット数を一度のクエリで取得
186-
var tutorTicketCounts = await iceDbContext.AdminUsers
187-
.GroupJoin(
188-
iceDbContext.TicketAdminUsers,
189-
admin => admin.Id,
190-
tau => tau.AdminUserId,
191-
(admin, tickets) => new
192-
{
193-
TutorId = admin.Id,
194-
TicketCount = tickets.Count()
195-
})
188+
// 対応中のチケットとその担当講師を取得
189+
var inProgressTickets = await iceDbContext.Tickets
190+
.Where(t => t.Status == TicketStatus.InProgress)
191+
.Include(t => t.TicketAdminUser)
196192
.ToListAsync(cancellationToken);
197193

198-
if (tutorTicketCounts.Count == 0)
194+
// 講師ごとの対応中チケット数をカウント
195+
var inProgressTicketsByTutor = inProgressTickets
196+
.Where(t => t.TicketAdminUser != null)
197+
.GroupBy(t => t.TicketAdminUser!.AdminUserId)
198+
.ToDictionary(g => g.Key, g => g.Count());
199+
200+
// すべての講師を取得
201+
var allTutors = await iceDbContext.AdminUsers.ToListAsync(cancellationToken);
202+
203+
if (allTutors.Count == 0)
199204
{
200205
throw new InvalidOperationException("講師が登録されていません。");
201206
}
202207

203-
// 最も少ないチケット数の講師を取得
204-
var minTicketCount = tutorTicketCounts.Min(t => t.TicketCount);
205-
var leastBusyTutorId = tutorTicketCounts
206-
.First(t => t.TicketCount == minTicketCount)
207-
.TutorId;
208+
// 対応中のチケットを持たない講師を検索
209+
var availableTutors = allTutors
210+
.Where(tutor => !inProgressTicketsByTutor.ContainsKey(tutor.Id))
211+
.ToList();
208212

209-
var targetUser = await iceDbContext.AdminUsers
210-
.FirstAsync(u => u.Id == leastBusyTutorId, cancellationToken);
213+
// 全員が対応中の場合はエラー
214+
if (availableTutors.Count == 0)
215+
{
216+
throw new AllStaffCurrentlyAssistingException("TA/SAの全員が対応中です。しばらく経ってから再度チケットを作成してください。");
217+
}
218+
219+
// 対応できる講師の中で最も総チケット数が少ない講師を選択
220+
AdminUsers targetUser;
221+
if (availableTutors.Count == allTutors.Count)
222+
{
223+
// 全員が対応中でない場合、総チケット数が最も少ない講師を選択
224+
var ticketCounts = await iceDbContext.TicketAdminUsers
225+
.GroupBy(tau => tau.AdminUserId)
226+
.Select(g => new { AdminUserId = g.Key, Count = g.Count() })
227+
.ToDictionaryAsync(x => x.AdminUserId, x => x.Count, cancellationToken
228+
);
229+
var minTicketCount = ticketCounts.Values.Min();
230+
var candidates = availableTutors
231+
.Where(tutor => ticketCounts.GetValueOrDefault(tutor.Id, 0) == minTicketCount)
232+
.ToList();
233+
targetUser = candidates.First();
234+
}
235+
else
236+
{
237+
// 対応中でない講師の中からランダムに選択
238+
var random = new Random();
239+
var randomIndex = random.Next(availableTutors.Count);
240+
targetUser = availableTutors[randomIndex];
241+
}
211242

212243
var adminUserTicket = new TicketAdminUsers
213244
{
@@ -216,23 +247,24 @@ private async Task<AdminUsers> AssignTutorToTicketAsync(long ticketId, Cancellat
216247
CreatedAt = DateTime.UtcNow,
217248
UpdatedAt = DateTime.UtcNow
218249
};
219-
250+
220251
iceDbContext.TicketAdminUsers.Add(adminUserTicket);
221252
await iceDbContext.SaveChangesAsync(cancellationToken);
222-
253+
223254
return targetUser;
224255
}
225-
226-
private async Task LinkAssignmentToTicketAsync(long ticketId, long assignmentId, long studentGroupId, CancellationToken cancellationToken)
256+
257+
private async Task LinkAssignmentToTicketAsync(long ticketId, long assignmentId, long studentGroupId,
258+
CancellationToken cancellationToken)
227259
{
228260
var assignmentExists = await iceDbContext.Assignments
229261
.AnyAsync(a => a.Id == assignmentId, cancellationToken);
230-
262+
231263
if (!assignmentExists)
232264
{
233265
throw new EntityNotFoundException($"課題ID {assignmentId} の課題が見つかりません。");
234-
}
235-
266+
}
267+
236268
var ticketAssignment = new TicketAssignments
237269
{
238270
TicketId = ticketId,
@@ -241,7 +273,7 @@ private async Task LinkAssignmentToTicketAsync(long ticketId, long assignmentId,
241273
UpdatedAt = DateTime.UtcNow,
242274
StudentGroupId = studentGroupId
243275
};
244-
276+
245277
iceDbContext.TicketAssignments.Add(ticketAssignment);
246278
await iceDbContext.SaveChangesAsync(cancellationToken);
247279
}

compose.prod.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@
1919
- POSTGRES_DB=ice
2020
- POSTGRES_USER=ice
2121
- POSTGRES_PASSWORD=icepassword
22+
restart: always
2223
volumes:
2324
ice_db:

0 commit comments

Comments
 (0)