refactor: extract duplicated patterns into shared helpers
Backend: create api/helpers.py with eligible_enrollment_filter, count_eligible_students, get_active_enrollment, ensure_unique_name, upsert_app_configs, and build_heatmap. Add full_name properties to Student model. Apply across all route files (-481/+184 lines). Frontend: create stores/helpers.js with withLoading composable, apply to assessments and classes Pinia stores. 96/96 tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import assessmentsService from '@/services/assessments'
|
||||
import { withLoading } from './helpers'
|
||||
|
||||
export const useAssessmentsStore = defineStore('assessments', () => {
|
||||
// State
|
||||
@@ -10,7 +11,7 @@ export const useAssessmentsStore = defineStore('assessments', () => {
|
||||
const currentGrades = ref([])
|
||||
const loading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
|
||||
// Filters
|
||||
const filters = ref({
|
||||
trimester: null,
|
||||
@@ -21,22 +22,22 @@ export const useAssessmentsStore = defineStore('assessments', () => {
|
||||
|
||||
// Getters
|
||||
const assessmentsCount = computed(() => assessments.value.length)
|
||||
|
||||
|
||||
const filteredAssessments = computed(() => {
|
||||
let result = [...assessments.value]
|
||||
|
||||
|
||||
if (filters.value.trimester) {
|
||||
result = result.filter(a => a.trimester === filters.value.trimester)
|
||||
}
|
||||
|
||||
|
||||
if (filters.value.class_id) {
|
||||
result = result.filter(a => a.class_group_id === filters.value.class_id)
|
||||
}
|
||||
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
const incompleteAssessments = computed(() =>
|
||||
const incompleteAssessments = computed(() =>
|
||||
assessments.value.filter(a => {
|
||||
const progress = a.progress || a.grading_progress
|
||||
return progress?.percentage < 100
|
||||
@@ -44,130 +45,61 @@ export const useAssessmentsStore = defineStore('assessments', () => {
|
||||
)
|
||||
|
||||
// Actions
|
||||
async function fetchAssessments(customFilters = null) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const queryFilters = customFilters || filters.value
|
||||
assessments.value = await assessmentsService.getAll(queryFilters)
|
||||
} catch (e) {
|
||||
error.value = e.message
|
||||
throw e
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
const fetchAssessments = withLoading(loading, error, async (customFilters = null) => {
|
||||
const queryFilters = customFilters || filters.value
|
||||
assessments.value = await assessmentsService.getAll(queryFilters)
|
||||
})
|
||||
|
||||
async function fetchAssessment(id) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
currentAssessment.value = await assessmentsService.getById(id)
|
||||
return currentAssessment.value
|
||||
} catch (e) {
|
||||
error.value = e.message
|
||||
throw e
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
const fetchAssessment = withLoading(loading, error, async (id) => {
|
||||
currentAssessment.value = await assessmentsService.getById(id)
|
||||
return currentAssessment.value
|
||||
})
|
||||
|
||||
async function fetchResults(id) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
currentResults.value = await assessmentsService.getResults(id)
|
||||
return currentResults.value
|
||||
} catch (e) {
|
||||
error.value = e.message
|
||||
throw e
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
const fetchResults = withLoading(loading, error, async (id) => {
|
||||
currentResults.value = await assessmentsService.getResults(id)
|
||||
return currentResults.value
|
||||
})
|
||||
|
||||
async function fetchGrades(id) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
currentGrades.value = await assessmentsService.getGrades(id)
|
||||
return currentGrades.value
|
||||
} catch (e) {
|
||||
error.value = e.message
|
||||
throw e
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
const fetchGrades = withLoading(loading, error, async (id) => {
|
||||
currentGrades.value = await assessmentsService.getGrades(id)
|
||||
return currentGrades.value
|
||||
})
|
||||
|
||||
async function createAssessment(data) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const newAssessment = await assessmentsService.create(data)
|
||||
assessments.value.unshift(newAssessment)
|
||||
return newAssessment
|
||||
} catch (e) {
|
||||
error.value = e.message
|
||||
throw e
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
const createAssessment = withLoading(loading, error, async (data) => {
|
||||
const newAssessment = await assessmentsService.create(data)
|
||||
assessments.value.unshift(newAssessment)
|
||||
return newAssessment
|
||||
})
|
||||
|
||||
async function updateAssessment(id, data) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const updated = await assessmentsService.update(id, data)
|
||||
const index = assessments.value.findIndex(a => a.id === id)
|
||||
if (index !== -1) {
|
||||
assessments.value[index] = updated
|
||||
}
|
||||
if (currentAssessment.value?.id === id) {
|
||||
currentAssessment.value = updated
|
||||
}
|
||||
return updated
|
||||
} catch (e) {
|
||||
error.value = e.message
|
||||
throw e
|
||||
} finally {
|
||||
loading.value = false
|
||||
const updateAssessment = withLoading(loading, error, async (id, data) => {
|
||||
const updated = await assessmentsService.update(id, data)
|
||||
const index = assessments.value.findIndex(a => a.id === id)
|
||||
if (index !== -1) {
|
||||
assessments.value[index] = updated
|
||||
}
|
||||
}
|
||||
if (currentAssessment.value?.id === id) {
|
||||
currentAssessment.value = updated
|
||||
}
|
||||
return updated
|
||||
})
|
||||
|
||||
async function deleteAssessment(id) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
await assessmentsService.delete(id)
|
||||
assessments.value = assessments.value.filter(a => a.id !== id)
|
||||
if (currentAssessment.value?.id === id) {
|
||||
currentAssessment.value = null
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = e.message
|
||||
throw e
|
||||
} finally {
|
||||
loading.value = false
|
||||
const deleteAssessment = withLoading(loading, error, async (id) => {
|
||||
await assessmentsService.delete(id)
|
||||
assessments.value = assessments.value.filter(a => a.id !== id)
|
||||
if (currentAssessment.value?.id === id) {
|
||||
currentAssessment.value = null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function saveGrades(id, grades) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const result = await assessmentsService.saveGrades(id, grades)
|
||||
// Refresh assessment to update progress
|
||||
await fetchAssessment(id)
|
||||
return result
|
||||
} catch (e) {
|
||||
error.value = e.message
|
||||
throw e
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
// fetchAssessment is declared above so it can be called here safely.
|
||||
// withLoading sets loading=false in its finally block, so the inner call
|
||||
// to fetchAssessment runs as a nested operation within the outer wrapper.
|
||||
const saveGrades = withLoading(loading, error, async (id, grades) => {
|
||||
const result = await assessmentsService.saveGrades(id, grades)
|
||||
// Refresh assessment to update progress
|
||||
await fetchAssessment(id)
|
||||
return result
|
||||
})
|
||||
|
||||
function setFilters(newFilters) {
|
||||
filters.value = { ...filters.value, ...newFilters }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import classesService from '@/services/classes'
|
||||
import { withLoading } from './helpers'
|
||||
|
||||
export const useClassesStore = defineStore('classes', () => {
|
||||
// State
|
||||
@@ -12,104 +13,50 @@ export const useClassesStore = defineStore('classes', () => {
|
||||
|
||||
// Getters
|
||||
const classesCount = computed(() => classes.value.length)
|
||||
const totalStudents = computed(() =>
|
||||
const totalStudents = computed(() =>
|
||||
classes.value.reduce((sum, c) => sum + (c.students_count || c.student_count || 0), 0)
|
||||
)
|
||||
|
||||
// Actions
|
||||
async function fetchClasses() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
classes.value = await classesService.getAll()
|
||||
} catch (e) {
|
||||
error.value = e.message
|
||||
throw e
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
const fetchClasses = withLoading(loading, error, async () => {
|
||||
classes.value = await classesService.getAll()
|
||||
})
|
||||
|
||||
async function fetchClass(id) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
currentClass.value = await classesService.getById(id)
|
||||
return currentClass.value
|
||||
} catch (e) {
|
||||
error.value = e.message
|
||||
throw e
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
const fetchClass = withLoading(loading, error, async (id) => {
|
||||
currentClass.value = await classesService.getById(id)
|
||||
return currentClass.value
|
||||
})
|
||||
|
||||
async function fetchClassStats(id, trimester = null) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
currentStats.value = await classesService.getStats(id, trimester)
|
||||
return currentStats.value
|
||||
} catch (e) {
|
||||
error.value = e.message
|
||||
throw e
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
const fetchClassStats = withLoading(loading, error, async (id, trimester = null) => {
|
||||
currentStats.value = await classesService.getStats(id, trimester)
|
||||
return currentStats.value
|
||||
})
|
||||
|
||||
async function createClass(data) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const newClass = await classesService.create(data)
|
||||
classes.value.push(newClass)
|
||||
return newClass
|
||||
} catch (e) {
|
||||
error.value = e.message
|
||||
throw e
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
const createClass = withLoading(loading, error, async (data) => {
|
||||
const newClass = await classesService.create(data)
|
||||
classes.value.push(newClass)
|
||||
return newClass
|
||||
})
|
||||
|
||||
async function updateClass(id, data) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const updated = await classesService.update(id, data)
|
||||
const index = classes.value.findIndex(c => c.id === id)
|
||||
if (index !== -1) {
|
||||
classes.value[index] = updated
|
||||
}
|
||||
if (currentClass.value?.id === id) {
|
||||
currentClass.value = updated
|
||||
}
|
||||
return updated
|
||||
} catch (e) {
|
||||
error.value = e.message
|
||||
throw e
|
||||
} finally {
|
||||
loading.value = false
|
||||
const updateClass = withLoading(loading, error, async (id, data) => {
|
||||
const updated = await classesService.update(id, data)
|
||||
const index = classes.value.findIndex(c => c.id === id)
|
||||
if (index !== -1) {
|
||||
classes.value[index] = updated
|
||||
}
|
||||
}
|
||||
if (currentClass.value?.id === id) {
|
||||
currentClass.value = updated
|
||||
}
|
||||
return updated
|
||||
})
|
||||
|
||||
async function deleteClass(id) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
await classesService.delete(id)
|
||||
classes.value = classes.value.filter(c => c.id !== id)
|
||||
if (currentClass.value?.id === id) {
|
||||
currentClass.value = null
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = e.message
|
||||
throw e
|
||||
} finally {
|
||||
loading.value = false
|
||||
const deleteClass = withLoading(loading, error, async (id) => {
|
||||
await classesService.delete(id)
|
||||
classes.value = classes.value.filter(c => c.id !== id)
|
||||
if (currentClass.value?.id === id) {
|
||||
currentClass.value = null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function clearCurrent() {
|
||||
currentClass.value = null
|
||||
|
||||
22
frontend/src/stores/helpers.js
Normal file
22
frontend/src/stores/helpers.js
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Wraps an async function with loading/error state management.
|
||||
*
|
||||
* @param {import('vue').Ref<boolean>} loading - shared loading ref
|
||||
* @param {import('vue').Ref<string|null>} error - shared error ref
|
||||
* @param {Function} fn - async function to wrap
|
||||
* @returns {Function} wrapped function with identical signature
|
||||
*/
|
||||
export function withLoading(loading, error, fn) {
|
||||
return async (...args) => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
return await fn(...args)
|
||||
} catch (e) {
|
||||
error.value = e.message
|
||||
throw e
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user