Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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
84 changes: 72 additions & 12 deletions app/composables/useCoach.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,20 @@ import utc from 'dayjs/plugin/utc'

dayjs.extend(utc)
export const useCoach = () => {
const { classId } = useSelectedClass()
const callFormApi = async <T>(method: 'GET' | 'POST' | 'PUT' | 'DELETE', params: Record<string, unknown> = {}, body?: Record<string, unknown>): Promise<T> => {
if (!classId.value) {
throw new Error('Select a class before managing forms')
}

const scopedParams = {
...params,
...(method === 'DELETE' ? body : {}),
classId: classId.value,
}
const scopedBody = { ...body, classId: classId.value }
const queryString = method === 'GET' || method === 'DELETE'
? `?${new URLSearchParams(Object.entries(params).reduce((acc, [key, value]) => {
? `?${new URLSearchParams(Object.entries(scopedParams).reduce((acc, [key, value]) => {
if (value !== undefined && value !== null) {
acc[key] = String(value)
}
Expand All @@ -18,7 +29,7 @@ export const useCoach = () => {

return await $fetch<T>(`/api/form${queryString}`, {
method,
body: method === 'GET' ? undefined : body,
body: method === 'GET' || method === 'DELETE' ? undefined : scopedBody,
})
}

Expand Down Expand Up @@ -215,6 +226,11 @@ export const useCoach = () => {
)

const loadPublishedForms = async () => {
if (!classId.value) {
publishedForms.value = []
return
}

try {
const forms = await callFormApi<any[]>('GET', {
action: 'listForms',
Expand All @@ -227,6 +243,12 @@ export const useCoach = () => {
}

const syncGroupRangeFromWeeklyDate = async () => {
if (!classId.value) {
historyGroupStartDate.value = ''
historyGroupEndDate.value = ''
return
}

if (!historyWeekStart.value) {
historyGroupStartDate.value = ''
historyGroupEndDate.value = ''
Expand Down Expand Up @@ -262,8 +284,11 @@ export const useCoach = () => {
}

watch(
historyWeekStart,
[historyWeekStart, classId],
async () => {
publishedForms.value = []
selectedFormDetails.value = null
editingFormId.value = null
await syncGroupRangeFromWeeklyDate()
await loadPublishedForms()
},
Expand Down Expand Up @@ -433,7 +458,7 @@ export const useCoach = () => {
questions.value = JSON.parse(JSON.stringify(form.questions))
editingFormId.value = form.id
builderSubTab.value = 'creation'
navigateTo('/coach/builder')
navigateTo({ path: '/coach/builder', query: { class: classId.value } })
}

const toggleFormPublish = async (form: any) => {
Expand All @@ -442,9 +467,10 @@ export const useCoach = () => {
form.status = form.status === 'Active' ? 'Unpublished' : 'Active'
const newStatus = form.status === 'Active' ? true : false

await useFetch(`/api/form/${form.id}`, {
method: 'PUT',
body: {published: newStatus}
await callFormApi('PUT', {}, {
action: 'updateForm',
id: form.id,
published: newStatus,
})
}

Expand All @@ -453,11 +479,45 @@ export const useCoach = () => {
}

// ── Students / Progress ──
const students = useState<any[]>('coachStudents', () => [
{ id: 1, name: 'Aiden Smith', initials: 'AS', email: 'aiden@school.edu', tickets: 12, streak: 4, lastActive: '2 hours ago' },
{ id: 2, name: 'Nevin Kumar', initials: 'NK', email: 'nevin@school.edu', tickets: 14, streak: 5, lastActive: 'Just now' },
{ id: 3, name: 'Swarna Jay', initials: 'SJ', email: 'swarna@school.edu',tickets: 8, streak: 2, lastActive: 'Yesterday' },
])
const students = useState<any[]>('coachStudents', () => [])

const loadStudents = async () => {
if (!classId.value) {
students.value = []
return
}

const classStudents = await $fetch<Array<{ id: number; name: string; exp: number }>>(
Comment thread
AidanLoran marked this conversation as resolved.
`/api/admin/classes/${classId.value}/students`
Comment thread
AidanLoran marked this conversation as resolved.
Outdated
)

students.value = classStudents.map((student) => ({
...student,
initials: student.name
.split(/\s+/)
.filter(Boolean)
.slice(0, 2)
.map((part) => part[0]?.toUpperCase())
.join(''),
tickets: student.exp,
streak: 0,
lastActive: '',
}))
}

watch(
classId,
async () => {
students.value = []

try {
await loadStudents()
} catch (error) {
console.error('Failed to load class students', error)
}
},
{ immediate: true }
)

const searchStudent = useState('searchStudent', () => '')
const sortStudent = useState('sortStudent', () => 'tickets')
Expand Down
21 changes: 15 additions & 6 deletions app/composables/useCurrentFormGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,26 @@ export const useCurrentFormGroup = () => {
try {
const formGroupAPIResponse = await $fetch<FormGroup | FormGroup[]>('/api/formGroup?active=true')

const activeFg = Array.isArray(formGroupAPIResponse) ? formGroupAPIResponse[0] : formGroupAPIResponse
const activeFormGroups = Array.isArray(formGroupAPIResponse)
Comment thread
AidanLoran marked this conversation as resolved.
Outdated
? formGroupAPIResponse
: formGroupAPIResponse
? [formGroupAPIResponse]
: []
const activeFg = activeFormGroups[0]

if (activeFg) {
FormGroup.value.activeFormGroup = activeFg

try {
const formsAPIResponse = await $fetch<Form[]>('/api/form', {
query: { action: 'getOnlyActiveFormsinGroup', formGroup: activeFg.id }
})
const formResponses = await Promise.all(
activeFormGroups.map((formGroup) =>
$fetch<Form[]>('/api/form', {
query: { action: 'getOnlyActiveFormsinGroup', formGroup: formGroup.id }
})
)
)

FormGroup.value.forms = Array.isArray(formsAPIResponse) ? formsAPIResponse : []
FormGroup.value.forms = formResponses.flat()

// Load form components for each form in parallel
FormGroup.value.formComponents = {}
Expand Down Expand Up @@ -72,4 +81,4 @@ export const useCurrentFormGroup = () => {
loadFormComponents,
totalFormsInGroup
}
}
}
38 changes: 33 additions & 5 deletions app/composables/useRaffleSpin.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Student, FormGroup, Form, FormSubmission } from '~~/prisma/generated/client'
export const useRaffleSpin = () => {
const { classId } = useSelectedClass()

const raffleWeekStart = ref<Date | string>(new Date())

Expand All @@ -10,9 +11,17 @@ export const useRaffleSpin = () => {
const spinCount = ref(0)

const loadRaffleFormGroup = async () => {
if (!classId.value) {
raffleFormGroup.value = null
return
}

const val = raffleWeekStart.value
const dateStr = val instanceof Date ? val.toISOString().split('T')[0] : String(val).split('T')[0]
const data = await $fetch<FormGroup>(`/api/formGroup?date=${dateStr}`, { method: 'GET' })
const data = await $fetch<FormGroup>('/api/formGroup', {
method: 'GET',
query: { date: dateStr, classId: classId.value },
})
raffleFormGroup.value = data || null
}

Expand All @@ -21,7 +30,14 @@ export const useRaffleSpin = () => {
raffleForms.value = []
return
}
const data = await $fetch<Form[]>(`/api/form?action=listForms&formGroup=${raffleFormGroup.value.id}`, { method: 'GET' })
const data = await $fetch<Form[]>('/api/form', {
method: 'GET',
query: {
action: 'listForms',
formGroup: raffleFormGroup.value.id,
classId: classId.value,
},
})
raffleForms.value = data || []
}

Expand All @@ -30,7 +46,10 @@ export const useRaffleSpin = () => {
raffleSubmissions.value = []
return
}
const data = await $fetch<FormSubmission[]>(`/api/formSubmission?formGroup=${raffleFormGroup.value.id}`, { method: 'GET' })
const data = await $fetch<FormSubmission[]>('/api/formSubmission', {
method: 'GET',
query: { formGroup: raffleFormGroup.value.id, classId: classId.value },
})
raffleSubmissions.value = data || []
}

Expand Down Expand Up @@ -63,7 +82,8 @@ export const useRaffleSpin = () => {
method: 'PUT',
body: {
id: raffleFormGroup.value.id,
raffleWinner: studentId
raffleWinner: studentId,
classId: classId.value,
}
})
await loadRaffleData()
Expand All @@ -73,6 +93,14 @@ export const useRaffleSpin = () => {
}
}

watch(classId, async () => {
raffleFormGroup.value = null
raffleForms.value = []
raffleSubmissions.value = []
raffleWinner.value = null
await loadRaffleData()
})

return {
raffleWinner,
raffleFormGroup,
Expand All @@ -83,4 +111,4 @@ export const useRaffleSpin = () => {
loadRaffleData,
spinCount
}
}
}
18 changes: 18 additions & 0 deletions app/composables/useSelectedClass.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export const useSelectedClass = () => {
const route = useRoute()
const storedClassId = useCookie<string | null>('selected-class-token', {
sameSite: 'lax',
})

const classId = computed(() => {
const queryClassId = route.query.class
const normalizedQueryClassId = Array.isArray(queryClassId) ? queryClassId[0] : queryClassId

return normalizedQueryClassId || storedClassId.value || null
})

return {
classId,
storedClassId,
}
}
17 changes: 1 addition & 16 deletions app/layouts/coach.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
<!-- layouts/coach.vue -->
<script setup lang="ts">
import { authClient } from '~/utils/auth-client'

const route = useRoute()
const selectedClassToken = useCookie<string | null>('selected-class-token', {
sameSite: 'lax',
})

const routeClassToken = computed(() => {
const classQuery = route.query.class
return Array.isArray(classQuery) ? classQuery[0] : classQuery
Expand All @@ -26,18 +23,6 @@ function navigateWithinClass(path: string) {
query: classToken ? { class: classToken } : {},
})
}

async function logout() {
const confirmed = confirm('Are you sure you want to log out?')
if (!confirmed) return

try {
await authClient.signOut()
window.location.href = '/auth'
} catch (error) {
console.error('Logout failed:', error)
}
}
</script>

<template>
Expand All @@ -64,7 +49,7 @@ async function logout() {

<div class="rh-sidebar-footer">
<NuxtLink to="/" class="rh-back-link">← Back to Portal</NuxtLink>
<button class="rh-logout-link" @click="logout">⎋ Logout</button>
<LogoutButton class="rh-logout-link" />
</div>
</aside>

Expand Down
7 changes: 6 additions & 1 deletion app/middleware/auth.global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ export default defineNuxtRouteMiddleware(async (to) => {
}

if (to.path === '/auth') {
const requestedRole = Array.isArray(to.query.role) ? to.query.role[0] : to.query.role

if (requestedRole === 'reader') {
return navigateTo('/reader')
}

if (userRole === 'admin') {
return navigateTo('/admin')
}
Expand All @@ -37,7 +43,6 @@ export default defineNuxtRouteMiddleware(async (to) => {
if (isCoachRoute && userRole !== 'admin' && userRole !== 'coach') {
return navigateTo('/reader/profile')
}

if (isCoachRoute && to.path !== '/coach/create-class' && userRole === 'coach') {
const requestFetch = useRequestFetch()
const classes = await requestFetch<Array<{ joinToken: string }>>('/api/admin/classes')
Expand Down
Loading