修复场地课表遗漏借用的问题
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);
|
term.Id, null, false, null, null, cancellationToken);
|
||||||
var today = DateOnly.FromDateTime(DateTime.Now);
|
var today = DateOnly.FromDateTime(DateTime.Now);
|
||||||
var (week, day) = ClassroomReservationAvailabilityService.ResolveTeachingWeek(term, today);
|
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))
|
(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 });
|
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()
|
private Task<AcademicTerm?> CurrentTermAsync(CancellationToken cancellationToken) => db.AcademicTerms.AsNoTracking()
|
||||||
.Where(x => x.IsEnabled && x.IsCurrent).FirstOrDefaultAsync(cancellationToken);
|
.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}`">
|
<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>
|
<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">
|
<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>
|
<div v-if="data && !data.entries?.length" class="empty">今日暂无排课<br><small>此教室可供安排使用</small></div>
|
||||||
</section>
|
</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>
|
<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>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<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}
|
.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>
|
</style>
|
||||||
Reference in new issue
Block a user