修复场地课表遗漏借用的问题
This commit is contained in:
1 parent
5872c06042
commit
54e1986ad0
3 files changed
+102
-4
No files matched your search
@@ -30,9 +30,33 @@ public sealed class VenueDisplaysController(
|
||||
term.Id, null, false, null, null, cancellationToken);
|
||||
var today = DateOnly.FromDateTime(DateTime.Now);
|
||||
var (week, day) = ClassroomReservationAvailabilityService.ResolveTeachingWeek(term, today);
|
||||
var entries = sheet?.Entries.Where(x => x.DayOfWeek == day && x.StartWeek <= week && x.EndWeek >= week &&
|
||||
var entries = sheet?.Entries
|
||||
.Where(x => x.DayOfWeek == day && x.StartWeek <= week && x.EndWeek >= week &&
|
||||
(x.WeekPattern == WeekPattern.All || x.WeekPattern == WeekPattern.Odd && week % 2 == 1 || x.WeekPattern == WeekPattern.Even && week % 2 == 0))
|
||||
.Select(x => new { x.CourseName, x.TeacherNames, x.ClassNames, x.StartPeriod, x.PeriodCount }) ?? [];
|
||||
.Select(x => new VenueDisplayClassroomEntry(
|
||||
x.CourseName,
|
||||
x.TeacherNames,
|
||||
x.ClassNames,
|
||||
x.StartPeriod,
|
||||
x.PeriodCount,
|
||||
false))
|
||||
.ToList() ?? [];
|
||||
var reservations = await db.ClassroomReservations.AsNoTracking()
|
||||
.Where(x => x.AcademicTermId == term.Id &&
|
||||
x.ClassroomId == classroomId &&
|
||||
x.Status == ClassroomReservationStatus.Approved &&
|
||||
x.ReservationDate == today)
|
||||
.OrderBy(x => x.StartPeriod)
|
||||
.Select(x => new VenueDisplayClassroomEntry(
|
||||
"教室借用",
|
||||
new[] { x.ApplicantName },
|
||||
new[] { x.Purpose },
|
||||
x.StartPeriod,
|
||||
x.PeriodCount,
|
||||
true))
|
||||
.ToListAsync(cancellationToken);
|
||||
entries.AddRange(reservations);
|
||||
entries.Sort((left, right) => left.StartPeriod.CompareTo(right.StartPeriod));
|
||||
return Ok(new { Classroom = classroom, Term = term.Name, Today = today, Week = week, Slots = sheet?.Slots ?? [], Entries = entries });
|
||||
}
|
||||
|
||||
@@ -59,3 +83,11 @@ public sealed class VenueDisplaysController(
|
||||
private Task<AcademicTerm?> CurrentTermAsync(CancellationToken cancellationToken) => db.AcademicTerms.AsNoTracking()
|
||||
.Where(x => x.IsEnabled && x.IsCurrent).FirstOrDefaultAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public sealed record VenueDisplayClassroomEntry(
|
||||
string CourseName,
|
||||
IEnumerable<string> TeacherNames,
|
||||
IEnumerable<string> ClassNames,
|
||||
int StartPeriod,
|
||||
int PeriodCount,
|
||||
bool IsReservation);
|
||||
@@ -0,0 +1,66 @@
|
||||
using System.Text.Json;
|
||||
using Jiaowu.Api.Controllers;
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Domain.Identity;
|
||||
using Jiaowu.Api.Infrastructure.Persistence;
|
||||
using Jiaowu.Api.Infrastructure.Timetables;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Jiaowu.Api.Tests;
|
||||
|
||||
public sealed class VenueDisplaysControllerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Classroom_IncludesApprovedReservationForToday()
|
||||
{
|
||||
await using var connection = new SqliteConnection("Data Source=:memory:");
|
||||
await connection.OpenAsync();
|
||||
var db = new AppDbContext(new DbContextOptionsBuilder<AppDbContext>()
|
||||
.UseSqlite(connection)
|
||||
.Options);
|
||||
await db.Database.EnsureCreatedAsync();
|
||||
|
||||
var today = DateOnly.FromDateTime(DateTime.Now);
|
||||
var campus = new Campus { Code = "MAIN", Name = "主校区" };
|
||||
var building = new Building { Code = "A", Name = "明德楼", CampusId = campus.Id };
|
||||
var classroom = new Classroom { Code = "A101", Name = "A101", BuildingId = building.Id, Capacity = 60 };
|
||||
var college = new College { Code = "CS", Name = "计算机学院" };
|
||||
var applicant = new ApplicationUser
|
||||
{
|
||||
Id = Guid.NewGuid(), UserName = "applicant", NormalizedUserName = "APPLICANT",
|
||||
DisplayName = "张老师", CollegeId = college.Id
|
||||
};
|
||||
var term = new AcademicTerm
|
||||
{
|
||||
Code = "CURRENT", Name = "当前学期", AcademicYear = "2026-2027", Season = TermSeason.Autumn,
|
||||
StartDate = today.AddDays(-7), EndDate = today.AddDays(120), IsCurrent = true
|
||||
};
|
||||
db.AddRange(campus, building, classroom, college, applicant, term);
|
||||
db.ClassroomReservations.Add(new ClassroomReservation
|
||||
{
|
||||
ApplicantUserId = applicant.Id, ApplicantName = applicant.DisplayName,
|
||||
ApplicantCollegeId = college.Id, AcademicTermId = term.Id, ClassroomId = classroom.Id,
|
||||
ReservationDate = today, StartPeriod = 3, PeriodCount = 2, AttendeeCount = 30,
|
||||
Purpose = "学院学术讲座", ContactPhone = "13800000000",
|
||||
Status = ClassroomReservationStatus.Approved
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var controller = new VenueDisplaysController(
|
||||
db,
|
||||
new TimetableDataService(db),
|
||||
new ClassroomReservationAvailabilityService(db));
|
||||
|
||||
var result = await controller.Classroom(classroom.Id, CancellationToken.None);
|
||||
|
||||
var response = Assert.IsType<OkObjectResult>(result).Value;
|
||||
using var document = JsonDocument.Parse(JsonSerializer.Serialize(response));
|
||||
var entry = Assert.Single(document.RootElement.GetProperty("Entries").EnumerateArray());
|
||||
Assert.Equal("教室借用", entry.GetProperty("CourseName").GetString());
|
||||
Assert.Equal("学院学术讲座", entry.GetProperty("ClassNames")[0].GetString());
|
||||
Assert.Equal("张老师", entry.GetProperty("TeacherNames")[0].GetString());
|
||||
Assert.True(entry.GetProperty("IsReservation").GetBoolean());
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ onBeforeUnmount(() => { window.clearInterval(timer); window.clearInterval(pageTi
|
||||
<main class="venue-display" :class="`venue-${type}`">
|
||||
<header><div><span>明序大学 · 智慧教学空间</span><h1>{{ title?.name ?? '正在加载' }}</h1><p>{{ title?.campusName }} · {{ title?.buildingName ?? title?.code }} · {{ data?.term ?? '当前教学日' }}</p></div><time>{{ timeText }}<small>{{ data?.today }}</small></time></header>
|
||||
<section v-if="type === 'classroom'" class="course-board">
|
||||
<article v-for="entry in visibleItems" :key="`${entry.startPeriod}-${entry.courseName}`"><b>第 {{ entry.startPeriod }}-{{ entry.startPeriod + entry.periodCount - 1 }} 节</b><h2>{{ entry.courseName }}</h2><p>{{ entry.classNames?.join('、') }} · {{ entry.teacherNames?.join('、') }}</p></article>
|
||||
<article v-for="entry in visibleItems" :key="`${entry.startPeriod}-${entry.courseName}`" :class="{ reservation: entry.isReservation }"><b>第 {{ entry.startPeriod }}-{{ entry.startPeriod + entry.periodCount - 1 }} 节 <em v-if="entry.isReservation">已借用</em></b><h2>{{ entry.courseName }}</h2><p>{{ entry.classNames?.join('、') }} · {{ entry.teacherNames?.join('、') }}</p></article>
|
||||
<div v-if="data && !data.entries?.length" class="empty">今日暂无排课<br><small>此教室可供安排使用</small></div>
|
||||
</section>
|
||||
<section v-else class="room-board"><div class="periods"><span v-for="slot in data?.slots" :key="slot.periodNumber">{{ slot.periodNumber }}<small>{{ slot.startsAt?.slice(0,5) }}</small></span></div><article v-for="room in visibleItems" :key="room.code"><b>{{ room.name }}</b><span v-for="slot in data?.slots" :key="slot.periodNumber" :class="{ free: room.freePeriods?.includes(slot.periodNumber) }">{{ room.freePeriods?.includes(slot.periodNumber) ? '空闲' : '占用' }}</span></article></section>
|
||||
@@ -45,6 +45,6 @@ onBeforeUnmount(() => { window.clearInterval(timer); window.clearInterval(pageTi
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.venue-display{min-height:100vh;padding:clamp(22px,4vw,62px);background:#0b1e2b;color:#eef7f5;font-family:"Microsoft YaHei",sans-serif}.venue-display header{display:flex;justify-content:space-between;gap:28px;padding-bottom:28px;border-bottom:1px solid #315464}.venue-display header span{color:#63d6bc;letter-spacing:.16em;font-size:12px}.venue-display h1{font-size:clamp(32px,5vw,68px);margin:10px 0}.venue-display p{margin:0;color:#abc5cf}.venue-display time{font:clamp(26px,4vw,52px) ui-monospace,monospace;text-align:right;color:#63d6bc}.venue-display time small{display:block;font:13px "Microsoft YaHei";color:#abc5cf;margin-top:8px}.course-board{display:grid;grid-template-columns:repeat(auto-fit,minmax(270px,1fr));gap:18px;margin-top:32px}.course-board article{padding:26px;background:#123243;border-left:5px solid #63d6bc}.course-board b{color:#63d6bc}.course-board h2{font-size:clamp(24px,3vw,40px);margin:24px 0 12px}.empty{grid-column:1/-1;padding:80px 20px;text-align:center;font-size:30px;background:#102a38;color:#63d6bc}.room-board{margin-top:30px;overflow:auto}.periods,.room-board article{display:grid;grid-template-columns:minmax(150px,2fr) repeat(12,minmax(65px,1fr));gap:4px;min-width:950px}.periods{margin-left:0}.periods span{grid-column:span 1;text-align:center;color:#abc5cf}.periods span:first-child{grid-column:1}.periods small{display:block}.room-board article{margin-top:5px}.room-board article b,.room-board article span{padding:16px 8px;background:#213c49;text-align:center}.room-board article span.free{background:#1d6b5d;color:white;font-weight:bold}@media(max-width:620px){.venue-display header{display:block}.venue-display time{text-align:left;margin-top:18px}.venue-display{padding:22px}.course-board article{padding:20px}}
|
||||
.venue-display{min-height:100vh;padding:clamp(22px,4vw,62px);background:#0b1e2b;color:#eef7f5;font-family:"Microsoft YaHei",sans-serif}.venue-display header{display:flex;justify-content:space-between;gap:28px;padding-bottom:28px;border-bottom:1px solid #315464}.venue-display header span{color:#63d6bc;letter-spacing:.16em;font-size:12px}.venue-display h1{font-size:clamp(32px,5vw,68px);margin:10px 0}.venue-display p{margin:0;color:#abc5cf}.venue-display time{font:clamp(26px,4vw,52px) ui-monospace,monospace;text-align:right;color:#63d6bc}.venue-display time small{display:block;font:13px "Microsoft YaHei";color:#abc5cf;margin-top:8px}.course-board{display:grid;grid-template-columns:repeat(auto-fit,minmax(270px,1fr));gap:18px;margin-top:32px}.course-board article{padding:26px;background:#123243;border-left:5px solid #63d6bc}.course-board article.reservation{border-left-color:#f5a524;background:#3b3020}.course-board b{color:#63d6bc}.course-board article.reservation b{color:#ffd07b}.course-board em{margin-left:8px;padding:3px 8px;border-radius:999px;background:#7a4b00;color:#fff;font-size:13px;font-style:normal}.course-board h2{font-size:clamp(24px,3vw,40px);margin:24px 0 12px}.empty{grid-column:1/-1;padding:80px 20px;text-align:center;font-size:30px;background:#102a38;color:#63d6bc}.room-board{margin-top:30px;overflow:auto}.periods,.room-board article{display:grid;grid-template-columns:minmax(150px,2fr) repeat(12,minmax(65px,1fr));gap:4px;min-width:950px}.periods{margin-left:0}.periods span{grid-column:span 1;text-align:center;color:#abc5cf}.periods span:first-child{grid-column:1}.periods small{display:block}.room-board article{margin-top:5px}.room-board article b,.room-board article span{padding:16px 8px;background:#213c49;text-align:center}.room-board article span.free{background:#1d6b5d;color:white;font-weight:bold}@media(max-width:620px){.venue-display header{display:block}.venue-display time{text-align:left;margin-top:18px}.venue-display{padding:22px}.course-board article{padding:20px}}
|
||||
.venue-display footer{display:flex;align-items:center;justify-content:center;gap:7px;margin-top:20px;color:#abc5cf}.venue-display footer span{width:7px;height:7px;border-radius:50%;background:#42616d}.venue-display footer span.active{width:22px;border-radius:5px;background:#63d6bc}.venue-display footer b{margin-left:8px;font:12px ui-monospace,monospace}
|
||||
</style>
|
||||
Reference in new issue
Block a user