1
This commit is contained in:
parent
541a1c5311
commit
f6e969f15c
@ -11,16 +11,22 @@ from app.services.notification_service import create_notifications_for_class
|
||||
async def create_schedule(
|
||||
db: AsyncSession, class_id: int, data: ScheduleCreate
|
||||
) -> Schedule:
|
||||
payload = data.model_dump()
|
||||
if payload.get("type") == "deadline" and payload.get("end_time") is not None:
|
||||
# Deadlines are anchored on the due time only; reuse start_time as the sort/query key.
|
||||
payload["start_time"] = payload["end_time"]
|
||||
|
||||
item = Schedule(
|
||||
class_id=class_id,
|
||||
**data.model_dump(),
|
||||
**payload,
|
||||
)
|
||||
db.add(item)
|
||||
await db.commit()
|
||||
await db.refresh(item)
|
||||
|
||||
# Send notifications + email to class members
|
||||
time_str = data.start_time.strftime("%Y-%m-%d %H:%M")
|
||||
effective_time = payload["end_time"] if payload.get("type") == "deadline" and payload.get("end_time") else payload["start_time"]
|
||||
time_str = effective_time.strftime("%Y-%m-%d %H:%M")
|
||||
location_info = f" · {data.location}" if data.location else ""
|
||||
await create_notifications_for_class(
|
||||
db, class_id, "schedule", f"新排期: {data.title}",
|
||||
@ -37,7 +43,13 @@ async def create_schedule(
|
||||
async def update_schedule(
|
||||
db: AsyncSession, item: Schedule, data: ScheduleUpdate
|
||||
) -> Schedule:
|
||||
for field, value in data.model_dump(exclude_unset=True).items():
|
||||
payload = data.model_dump(exclude_unset=True)
|
||||
next_type = payload.get("type", item.type)
|
||||
next_end_time = payload.get("end_time", item.end_time)
|
||||
if next_type == "deadline" and next_end_time is not None:
|
||||
payload["start_time"] = next_end_time
|
||||
|
||||
for field, value in payload.items():
|
||||
setattr(item, field, value)
|
||||
await db.commit()
|
||||
await db.refresh(item)
|
||||
|
||||
@ -51,6 +51,10 @@ export default function SchedulePage() {
|
||||
const [formEndTime, setFormEndTime] = useState("");
|
||||
const [formLocation, setFormLocation] = useState("");
|
||||
const [formDesc, setFormDesc] = useState("");
|
||||
const isDeadlineType = formType === "deadline";
|
||||
|
||||
const getDisplayTime = (item: ScheduleItem) =>
|
||||
item.type === "deadline" && item.end_time ? item.end_time : item.start_time;
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
if (!activeClassId) {
|
||||
@ -82,10 +86,10 @@ export default function SchedulePage() {
|
||||
void loadData();
|
||||
}, [loadData]);
|
||||
|
||||
const getCountdown = (startTime: string) => {
|
||||
const diff = new Date(startTime).getTime() - Date.now();
|
||||
const getCountdown = (dateTime: string) => {
|
||||
const diff = new Date(dateTime).getTime() - Date.now();
|
||||
const days = Math.ceil(diff / (1000 * 60 * 60 * 24));
|
||||
if (days <= 0) return { text: "已开始", urgent: false };
|
||||
if (days <= 0) return { text: "已截止", urgent: false };
|
||||
if (days === 1) return { text: "明天", urgent: true };
|
||||
if (days <= 3) return { text: `${days}天后`, urgent: true };
|
||||
if (days <= 7) return { text: `${days}天后`, urgent: false };
|
||||
@ -93,28 +97,23 @@ export default function SchedulePage() {
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formTitle.trim() || !formStartTime) return;
|
||||
const primaryTime = isDeadlineType ? formEndTime : formStartTime;
|
||||
if (!formTitle.trim() || !primaryTime) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const payload = {
|
||||
type: formType,
|
||||
title: formTitle,
|
||||
start_time: isDeadlineType ? primaryTime : formStartTime,
|
||||
end_time: isDeadlineType ? primaryTime : formEndTime || null,
|
||||
location: isDeadlineType ? null : formLocation || null,
|
||||
description: formDesc || null,
|
||||
};
|
||||
if (editingId) {
|
||||
await putAPI(`/api/schedule/${editingId}`, {
|
||||
type: formType,
|
||||
title: formTitle,
|
||||
start_time: formStartTime,
|
||||
end_time: formEndTime || null,
|
||||
location: formLocation || null,
|
||||
description: formDesc || null,
|
||||
});
|
||||
await putAPI(`/api/schedule/${editingId}`, payload);
|
||||
toast.success("排期已更新");
|
||||
} else {
|
||||
await postAPI(`/api/schedule/?class_id=${activeClassId}`, {
|
||||
type: formType,
|
||||
title: formTitle,
|
||||
start_time: formStartTime,
|
||||
end_time: formEndTime || null,
|
||||
location: formLocation || null,
|
||||
description: formDesc || null,
|
||||
});
|
||||
await postAPI(`/api/schedule/?class_id=${activeClassId}`, payload);
|
||||
toast.success("排期已创建");
|
||||
}
|
||||
setDialogOpen(false);
|
||||
@ -137,9 +136,10 @@ export default function SchedulePage() {
|
||||
const dt = new Date(d);
|
||||
return `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())}T${pad(dt.getHours())}:${pad(dt.getMinutes())}`;
|
||||
};
|
||||
setFormStartTime(fmt(item.start_time));
|
||||
setFormEndTime(item.end_time ? fmt(item.end_time) : "");
|
||||
setFormLocation(item.location || "");
|
||||
const effectiveTime = item.type === "deadline" && item.end_time ? item.end_time : item.start_time;
|
||||
setFormStartTime(item.type === "deadline" ? "" : fmt(item.start_time));
|
||||
setFormEndTime(item.type === "deadline" ? fmt(effectiveTime) : item.end_time ? fmt(item.end_time) : "");
|
||||
setFormLocation(item.type === "deadline" ? "" : item.location || "");
|
||||
setFormDesc(item.description || "");
|
||||
setDialogOpen(true);
|
||||
};
|
||||
@ -220,29 +220,42 @@ export default function SchedulePage() {
|
||||
value={formTitle}
|
||||
onChange={(e) => setFormTitle(e.target.value)}
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{isDeadlineType ? (
|
||||
<div>
|
||||
<label className="text-sm text-gray-500">开始时间</label>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
value={formStartTime}
|
||||
onChange={(e) => setFormStartTime(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-gray-500">结束时间</label>
|
||||
<label className="text-sm text-gray-500">截止时间</label>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
value={formEndTime}
|
||||
onChange={(e) => setFormEndTime(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
placeholder="地点"
|
||||
value={formLocation}
|
||||
onChange={(e) => setFormLocation(e.target.value)}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="text-sm text-gray-500">开始时间</label>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
value={formStartTime}
|
||||
onChange={(e) => setFormStartTime(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-gray-500">结束时间</label>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
value={formEndTime}
|
||||
onChange={(e) => setFormEndTime(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
placeholder="地点"
|
||||
value={formLocation}
|
||||
onChange={(e) => setFormLocation(e.target.value)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Textarea
|
||||
placeholder="描述"
|
||||
value={formDesc}
|
||||
@ -270,7 +283,8 @@ export default function SchedulePage() {
|
||||
<h2 className="text-lg font-semibold mb-3">即将到来</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{upcoming.map((item) => {
|
||||
const countdown = getCountdown(item.start_time);
|
||||
const displayTime = getDisplayTime(item);
|
||||
const countdown = getCountdown(displayTime);
|
||||
const typeInfo = SCHEDULE_TYPES[item.type] || { label: item.type, color: "bg-gray-400" };
|
||||
return (
|
||||
<Card key={item.id} className={countdown.urgent ? "border-red-200 bg-red-50/30" : ""}>
|
||||
@ -291,7 +305,7 @@ export default function SchedulePage() {
|
||||
</div>
|
||||
<h3 className="font-medium mt-2">{item.title}</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
{new Date(item.start_time).toLocaleString("zh-CN", {
|
||||
{new Date(displayTime).toLocaleString("zh-CN", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
@ -358,8 +372,8 @@ export default function SchedulePage() {
|
||||
<div>
|
||||
<p className="font-medium">{item.title}</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
{new Date(item.start_time).toLocaleString("zh-CN")}
|
||||
{item.location ? ` · ${item.location}` : ""}
|
||||
{new Date(getDisplayTime(item)).toLocaleString("zh-CN")}
|
||||
{item.type !== "deadline" && item.location ? ` · ${item.location}` : ""}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -16,12 +16,14 @@ export function CalendarView({ events, onEventClick }: CalendarViewProps) {
|
||||
|
||||
const year = currentDate.getFullYear();
|
||||
const month = currentDate.getMonth();
|
||||
const getDisplayTime = (event: ScheduleItem) =>
|
||||
event.type === "deadline" && event.end_time ? event.end_time : event.start_time;
|
||||
|
||||
// Get events grouped by date
|
||||
const eventsByDate = useMemo(() => {
|
||||
const map = new Map<string, ScheduleItem[]>();
|
||||
for (const event of events) {
|
||||
const d = new Date(event.start_time);
|
||||
const d = new Date(getDisplayTime(event));
|
||||
const key = `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
|
||||
if (!map.has(key)) map.set(key, []);
|
||||
map.get(key)!.push(event);
|
||||
@ -166,11 +168,11 @@ export function CalendarView({ events, onEventClick }: CalendarViewProps) {
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[#4e1d1a]">{event.title}</p>
|
||||
<p className="text-xs text-[#7a5e4f]">
|
||||
{new Date(event.start_time).toLocaleTimeString("zh-CN", {
|
||||
{new Date(getDisplayTime(event)).toLocaleTimeString("zh-CN", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
{event.location ? ` · ${event.location}` : ""}
|
||||
{event.type !== "deadline" && event.location ? ` · ${event.location}` : ""}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user