Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.web.bind.annotation.*;
import team.projectpulse.common.Result;

import java.util.List;
import java.util.Map;

@RestController
Expand Down Expand Up @@ -48,4 +49,23 @@ public Result<Map<String, Object>> searchSections(
public Result<Section> findById(@PathVariable Long id) {
return Result.success(sectionService.findById(id));
}

/**
* UC-6: Get all computed weeks for a section (Monday–Sunday ranges).
* GET /api/v1/sections/{id}/weeks
*/
@GetMapping("/{id}/weeks")
public Result<List<SectionWeekInfo>> getWeeks(@PathVariable Long id) {
return Result.success(sectionService.getWeeks(id));
}

/**
* UC-6: Save the admin's chosen active weeks for a section.
* POST /api/v1/sections/{id}/weeks
*/
@PostMapping("/{id}/weeks")
public Result<Void> setUpActiveWeeks(@PathVariable Long id, @RequestBody List<String> activeWeeks) {
sectionService.setUpActiveWeeks(id, activeWeeks);
return Result.success("Active weeks updated.", null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.TemporalAdjusters;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@Service
public class SectionService {
Expand All @@ -23,4 +32,40 @@ public Section findById(Long id) {
return sectionRepository.findById(id)
.orElseThrow(() -> new SectionNotFoundException(id));
}

/**
* UC-6: Compute all Monday–Sunday weeks between section start/end dates,
* marking each as active if its week number appears in section.activeWeeks.
*/
public List<SectionWeekInfo> getWeeks(Long sectionId) {
Section section = findById(sectionId);
if (section.getStartDate() == null || section.getEndDate() == null) {
return List.of();
}
Set<String> activeSet = new HashSet<>(
section.getActiveWeeks() != null ? section.getActiveWeeks() : List.of()
);
List<SectionWeekInfo> weeks = new ArrayList<>();
LocalDate weekStart = section.getStartDate()
.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
int num = 1;
while (!weekStart.isAfter(section.getEndDate())) {
LocalDate weekEnd = weekStart.with(DayOfWeek.SUNDAY);
String n = String.valueOf(num);
weeks.add(new SectionWeekInfo(n, weekStart.toString(), weekEnd.toString(), activeSet.contains(n)));
weekStart = weekStart.plusWeeks(1);
num++;
}
return weeks;
}

/**
* UC-6: Replace the section's active weeks with the provided list.
*/
@Transactional
public void setUpActiveWeeks(Long sectionId, List<String> activeWeeks) {
Section section = findById(sectionId);
section.setActiveWeeks(activeWeeks != null ? activeWeeks : List.of());
sectionRepository.save(section);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package team.projectpulse.section;

public record SectionWeekInfo(String weekNumber, String monday, String sunday, boolean isActive) {
}
6 changes: 4 additions & 2 deletions src/frontend/src/apis/section/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import request from '@/utils/request'
import type {
PaginationParams, Section, SectionSearchCriteria,
CreateSectionResponse, FindSectionByIdResponse, SearchSectionByCriteriaResponse,
UpdateSectionResponse, AssignRubricToSectionResponse, SetUpActiveWeeksResponse,
SendEmailInvitationsResponse, InviteOrAddInstructorsResponse,
UpdateSectionResponse, AssignRubricToSectionResponse, GetSectionWeeksResponse,
SetUpActiveWeeksResponse, SendEmailInvitationsResponse, InviteOrAddInstructorsResponse,
GetInstructorsResponse, RemoveInstructorResponse
} from './types'

Expand All @@ -16,6 +16,8 @@ export const createSection = (section: Section) => request.post<any, CreateSecti
export const updateSection = (section: Section) => request.put<any, UpdateSectionResponse>(`${API.SECTIONS}/${section.sectionId}`, section)
export const assignRubricToSection = (sectionId: number, rubricId: number) =>
request.put<any, AssignRubricToSectionResponse>(`${API.SECTIONS}/${sectionId}/rubrics/${rubricId}`)
export const getSectionWeeks = (sectionId: number) =>
request.get<any, GetSectionWeeksResponse>(`${API.SECTIONS}/${sectionId}/weeks`)
export const setUpActiveWeeks = (sectionId: number, activeWeeks: string[]) =>
request.post<any, SetUpActiveWeeksResponse>(`${API.SECTIONS}/${sectionId}/weeks`, activeWeeks)
export const sendEmailInvitationsToStudents = (courseId: number, sectionId: number, emails: string[]) =>
Expand Down
1 change: 1 addition & 0 deletions src/frontend/src/apis/section/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface FindSectionByIdResponse { flag: boolean; code: number; message:
export interface CreateSectionResponse { flag: boolean; code: number; message: string; data: Section }
export interface UpdateSectionResponse { flag: boolean; code: number; message: string; data: Section }
export interface AssignRubricToSectionResponse { flag: boolean; code: number; message: string }
export interface GetSectionWeeksResponse { flag: boolean; code: number; message: string; data: WeekInfo[] }
export interface SetUpActiveWeeksResponse { flag: boolean; code: number; message: string }
export interface SendEmailInvitationsResponse { flag: boolean; code: number; message: string }
export interface InviteOrAddInstructorsResponse {
Expand Down
146 changes: 140 additions & 6 deletions src/frontend/src/pages/sections/SectionDetail.vue
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,21 @@
<!-- Active Weeks -->
<v-col cols="12" md="6">
<v-card rounded="lg">
<v-card-title class="d-flex align-center gap-2 pb-2">
<v-icon icon="mdi-calendar-check" color="success" size="20" />
Active Weeks
<v-card-title class="d-flex align-center justify-space-between pb-2">
<div class="d-flex align-center gap-2">
<v-icon icon="mdi-calendar-check" color="success" size="20" />
Active Weeks
</div>
<v-btn
v-if="userInfoStore.isAdmin"
size="small"
color="primary"
variant="tonal"
prepend-icon="mdi-pencil"
@click="openWeeksDialog"
>
Set Up
</v-btn>
</v-card-title>
<v-divider />
<v-card-text>
Expand Down Expand Up @@ -180,13 +192,78 @@
Section not found or failed to load.
</v-alert>
</div>

<!-- UC-6: Set Up Active Weeks Dialog -->
<v-dialog v-model="weeksDialog" max-width="640" persistent scrollable>
<v-card rounded="lg">
<v-card-title class="pa-4 d-flex align-center gap-2">
<v-icon icon="mdi-calendar-check" color="success" />
Set Up Active Weeks
</v-card-title>
<v-divider />

<v-card-text style="max-height: 480px;">
<div v-if="loadingWeeks" class="d-flex justify-center py-8">
<v-progress-circular indeterminate color="primary" />
</div>
<div v-else-if="!allWeeks.length" class="text-medium-emphasis text-body-2 py-4 text-center">
No weeks available. Ensure the section has start and end dates configured.
</div>
<v-table v-else density="compact">
<thead>
<tr>
<th style="width:40px">
<v-checkbox
:model-value="allSelected"
:indeterminate="someSelected && !allSelected"
hide-details
density="compact"
@update:model-value="toggleAll"
/>
</th>
<th>Week</th>
<th>Monday</th>
<th>Sunday</th>
</tr>
</thead>
<tbody>
<tr v-for="week in allWeeks" :key="week.weekNumber">
<td>
<v-checkbox
:model-value="selectedWeeks.has(week.weekNumber)"
hide-details
density="compact"
@update:model-value="toggleWeek(week.weekNumber)"
/>
</td>
<td class="font-weight-medium">Week {{ week.weekNumber }}</td>
<td class="text-medium-emphasis">{{ week.monday }}</td>
<td class="text-medium-emphasis">{{ week.sunday }}</td>
</tr>
</tbody>
</v-table>
</v-card-text>

<v-divider />
<v-card-actions class="pa-4">
<span class="text-caption text-medium-emphasis">
{{ selectedWeeks.size }} of {{ allWeeks.length }} weeks selected
</span>
<v-spacer />
<v-btn variant="text" @click="weeksDialog = false">Cancel</v-btn>
<v-btn color="primary" variant="flat" :loading="savingWeeks" @click="saveWeeks">
Save
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { findSectionById } from '@/apis/section'
import type { Section } from '@/apis/section/types'
import { findSectionById, getSectionWeeks, setUpActiveWeeks } from '@/apis/section'
import type { Section, WeekInfo } from '@/apis/section/types'
import { useUserInfoStore } from '@/stores/userInfo'
import { useNotifyStore } from '@/stores/notify'

Expand All @@ -197,6 +274,16 @@ const notifyStore = useNotifyStore()
const section = ref<Section | null>(null)
const loading = ref(false)

// UC-6: active weeks dialog state
const weeksDialog = ref(false)
const loadingWeeks = ref(false)
const savingWeeks = ref(false)
const allWeeks = ref<WeekInfo[]>([])
const selectedWeeks = ref(new Set<string>())

const allSelected = computed(() => allWeeks.value.length > 0 && selectedWeeks.value.size === allWeeks.value.length)
const someSelected = computed(() => selectedWeeks.value.size > 0)

async function loadSection() {
loading.value = true
try {
Expand All @@ -210,5 +297,52 @@ async function loadSection() {
}
}

async function openWeeksDialog() {
weeksDialog.value = true
loadingWeeks.value = true
try {
const id = Number(route.params.sectionId)
const res = await getSectionWeeks(id)
allWeeks.value = res.data
selectedWeeks.value = new Set(res.data.filter(w => w.isActive).map(w => w.weekNumber))
} catch {
// handled by request interceptor
} finally {
loadingWeeks.value = false
}
}

function toggleWeek(weekNumber: string) {
if (selectedWeeks.value.has(weekNumber)) {
selectedWeeks.value.delete(weekNumber)
} else {
selectedWeeks.value.add(weekNumber)
}
selectedWeeks.value = new Set(selectedWeeks.value)
}

function toggleAll(val: boolean | null) {
if (val) {
selectedWeeks.value = new Set(allWeeks.value.map(w => w.weekNumber))
} else {
selectedWeeks.value = new Set()
}
}

async function saveWeeks() {
savingWeeks.value = true
try {
const id = Number(route.params.sectionId)
await setUpActiveWeeks(id, Array.from(selectedWeeks.value))
notifyStore.success('Active weeks saved.')
weeksDialog.value = false
await loadSection()
} catch {
// handled by request interceptor
} finally {
savingWeeks.value = false
}
}

onMounted(loadSection)
</script>
Loading