window.componentes = window.componentes || {}; const EvaluationForm = { template: `

mdi-clipboard-edit-outline Bulk Grades Entry

Enter grades for all students and activities at once

Save All ({{ modifiedCount }})

Loading grades matrix...

mdi-table Grades Matrix {{ students.length }} students {{ activities.length }} activities
Reload
Student Average
{{ student.last_name }}, {{ student.first_name }}
{{ student.student_code }} {{ calculateStudentAverage(student.id) }}
{{ students.length }}
Total Students
{{ activities.length }}
Total Activities
{{ modifiedCount }}
Modified Grades
{{ filledCount }}
Filled Grades
{{ snackbarIcon }} {{ snackbarMessage }} mdi-alert You have {{ modifiedCount }} unsaved changes
`, data() { return { data:{ ...{ loading: false, saving: false, // Filters filters: { school_year_id: null, grade_id: null, section_id: null, period_id: null }, // Filter options schoolYears: [], grades: [], sections: [], periods: [], // Data students: [], activities: [], grades: {}, modifiedGrades: new Set(), originalGrades: {}, // Snackbar showSnackbar: false, snackbarMessage: '', snackbarColor: 'success', snackbarIcon: 'mdi-check-circle', showUnsavedWarning: false }}, brick:{ id:"EvaluationForm", name:"Evaluar", description:"", } }; }, computed:{allActivities() { return this.activities; }, groupedActivities() { const grouped = {}; this.activities.forEach(activity => { if (!grouped[activity.period_id]) { grouped[activity.period_id] = { period_id: activity.period_id, period_name: activity.period_code, activities: [] }; } grouped[activity.period_id].activities.push(activity); }); return Object.values(grouped); }, hasChanges() { return this.modifiedGrades.size > 0; }, modifiedCount() { return this.modifiedGrades.size; }, filledCount() { return Object.values(this.grades).filter(g => g !== null && g !== undefined && g !== '').length; }}, props: { admin:{ type: Boolean, default: true }, binds: { type: Object, default: () => {} }, received_values: { type: Object, default: () => {} }, ...{} }, watch: {}, methods: {async loadFilters() { try { const years = await this.$you.find('school_years'); if (years.data?.data.length > 0) { this.schoolYears = years.data?.data ?? [] this.filters.school_year_id = this.schoolYears[0].id; } const grades = await this.$you.find('grades'); if (grades.data?.data.length > 0) { this.grades = grades.data?.data ?? [] this.filters.grade_id = this.grades[0].id; } const sections = await this.$you.find('sections'); if (sections.data?.data.length > 0) { this.sections = sections.data?.data ?? [] this.filters.section_id = this.sections[0].id; } if (this.filters.school_year_id && this.filters.grade_id && this.filters.section_id) { await this.loadMatrix(); } } catch (error) { this.showNotification('Error loading filters', 'error'); console.error(error); } }, /** * Load the grades matrix from the server */ async loadMatrix() { if (!this.filters.school_year_id || !this.filters.grade_id || !this.filters.section_id) { return; } this.loading = true; this.modifiedGrades.clear(); this.showUnsavedWarning = false; try { const params = { school_year_id: this.filters.school_year_id, grade_id: this.filters.grade_id, section_id: this.filters.section_id } if (this.filters.period_id) { params['period_id'] = this.filters.period_id; } const students = await this.$you.find('students',params); if (students.data?.data.length > 0) { this.students = students.data?.data ?? [] } const periods = await this.$you.find('periods',{school_year_id:params.school_year_id}); if (periods.data?.data.length > 0) { this.periods = periods.data?.data ?? [] } const activities = await this.$you.find('periods',{school_year_id:params.school_year_id}); if (activities.data?.data.length > 0) { this.activities = activities.data?.data ?? [] } if (result.success) { // Build grades object this.grades = {}; this.originalGrades = {}; result.data.students.forEach(student => { result.data.activities.forEach(activity => { const key = `${student.id}__${activity.id}`; const existingGrade = result.data.grades[key]; const score = existingGrade ? existingGrade.score : ''; this.grades[key] = score; this.originalGrades[key] = score; }); }); } else { this.showNotification(result.message, 'error'); } } catch (error) { this.showNotification('Error loading grades matrix', 'error'); console.error(error); } finally { this.loading = false; } }, /** * Mark a grade cell as modified */ markAsModified(studentId, activityId) { const key = `${studentId}__${activityId}`; this.modifiedGrades.add(key); if (this.modifiedGrades.size > 0) { this.showUnsavedWarning = true; } }, /** * Get CSS class for a cell */ getCellClass(studentId, activityId) { const key = `${studentId}__${activityId}`; if (this.modifiedGrades.has(key)) { return 'modified-cell'; } return ''; }, /** * Calculate average for a student */ calculateStudentAverage(studentId) { let total = 0; let count = 0; this.activities.forEach(activity => { const key = `${studentId}__${activity.id}`; const score = this.grades[key]; if (score !== null && score !== undefined && score !== '') { const numScore = parseFloat(score); if (!isNaN(numScore)) { total += numScore; count++; } } }); return count > 0 ? (total / count).toFixed(2) : '-'; }, /** * Focus handler - select all text */ onFocus(event) { event.target.select(); }, /** * Blur handler - validate score */ onBlur(studentId, activityId, event) { const key = `${studentId}__${activityId}`; const activity = this.activities.find(a => a.id === activityId); if (!activity) return; let value = event.target.value; if (value === '' || value === null) { this.grades[key] = ''; return; } let numValue = parseFloat(value); if (isNaN(numValue)) { this.showNotification('Invalid number', 'error'); this.grades[key] = this.originalGrades[key] || ''; return; } if (numValue < 0) { this.showNotification('Score cannot be negative', 'error'); this.grades[key] = this.originalGrades[key] || ''; return; } if (numValue > activity.max_score) { this.showNotification(`Score cannot exceed ${activity.max_score}`, 'error'); this.grades[key] = this.originalGrades[key] || ''; return; } this.grades[key] = numValue.toFixed(2); }, /** * Save all modified grades */ async saveAllGrades() { if (this.modifiedGrades.size === 0) { this.showNotification('No changes to save', 'info'); return; } this.saving = true; try { const gradesToSave = []; this.modifiedGrades.forEach(key => { const [studentId, activityId] = key.split('__'); const score = this.grades[key]; gradesToSave.push({ student_id: studentId, activity_id: activityId, score: score === '' ? null : parseFloat(score), comments: null }); }); const response = await fetch('/api/grades/bulk-save', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ grades: gradesToSave }) }); const result = await response.json(); if (result.success) { this.modifiedGrades.clear(); this.showUnsavedWarning = false; // Update original grades Object.keys(this.grades).forEach(key => { this.originalGrades[key] = this.grades[key]; }); this.showNotification( `Successfully saved ${result.saved_count} grades`, 'success' ); if (result.errors && result.errors.length > 0) { console.warn('Save errors:', result.errors); } } else { this.showNotification(result.message, 'error'); } } catch (error) { this.showNotification('Error saving grades', 'error'); console.error(error); } finally { this.saving = false; } }, /** * Discard all changes */ discardChanges() { this.grades = { ...this.originalGrades }; this.modifiedGrades.clear(); this.showUnsavedWarning = false; this.showNotification('Changes discarded', 'info'); }, /** * Show notification */ showNotification(message, type = 'success') { this.snackbarMessage = message; this.snackbarColor = type; this.snackbarIcon = type === 'error' ? 'mdi-alert-circle' : 'mdi-check-circle'; this.showSnackbar = true; }}, mounted(){this.loadFilters();}, created(){}, }; window.componentes.EvaluationForm = EvaluationForm; const ProductCard = { template: `
{{info.name ?? 'Product Name'}}
`, data() { return { data:{ ...{}}, brick:{ id:"ProductCard", name:"Product Card", description:"", } }; }, computed:{}, props: { admin:{ type: Boolean, default: true }, binds: { type: Object, default: () => {} }, received_values: { type: Object, default: () => {} }, ...{ info:Object, required:false, default:()=>{} } }, watch: {}, methods: {Move(id){ this.$hash('view',id) if(this.$root.url.hash=='view'){ window.location.reload() } }}, mounted(){}, created(){}, }; window.componentes.ProductCard = ProductCard; const RechargeTotal = { template: `

{{received_values.amount ?? 0}} {{$root.settings.APP_DATA.config.currency}}

{{(received_values.amount??0)*$root.settings.APP_DATA.config.currency_tax}} {{$root.settings.APP_DATA.config.currency_alt}}

Total Amount Payable

{{received_values}}
`, data() { return { data:{ ...{}}, brick:{ id:"RechargeTotal", name:"Total Amount", description:"Display the total amount", } }; }, computed:{}, props: { admin:{ type: Boolean, default: true }, binds: { type: Object, default: () => {} }, received_values: { type: Object, default: () => {} }, ...{} }, watch: {received_values: { handler(newValue, oldValue) { this.$emit('send-value','tax',parseFloat(this.$root.settings.APP_DATA.config.currency_tax).toFixed(2)??0) }, immediate: true, // Ejecutar inmediatamente deep: false }}, methods: {}, mounted(){}, created(){}, }; window.componentes.RechargeTotal = RechargeTotal; const SimilarProducts = { template: `
`, data() { return { data:{ ...{ data:[] }}, brick:{ id:"SimilarProducts", name:"Productos Similares", description:"", } }; }, computed:{}, props: { admin:{ type: Boolean, default: true }, binds: { type: Object, default: () => {} }, received_values: { type: Object, default: () => {} }, ...{ filter:{ type:Object, required:false, default:()=>{} } } }, watch: {}, methods: {async getSimilar(){ const promise = await apiAppProducts.getAll(this.binds) if(promise.data.error==false){ this.data = promise.data.data } else { this.$toast_validation("No se pudo obtener los productos similares",promise.data.data) } }}, mounted(){this.getSimilar()}, created(){}, }; window.componentes.SimilarProducts = SimilarProducts; const TestData = { template: `
Filtrar por Marca {{item.name}} Limpiar
`, data() { return { data:{ ...{ data:[] }}, brick:{ id:"TestData", name:"Test", description:"", } }; }, computed:{}, props: { admin:{ type: Boolean, default: true }, binds: { type: Object, default: () => {} }, received_values: { type: Object, default: () => {} }, ...{} }, watch: {}, methods: {async getData(){ const promise = await apiAppProductBrands.getAll() this.data = promise.data }, pushParams(k,v){ const params = new URLSearchParams(window.location.search) params.set(k,v) const new_params = (params?'?':'')+params.toString() const base_url = window.location.hash+(this.$root.url.param??'') const newUrl = window.location.pathname+new_params+base_url history.replaceState({},"",newUrl) this.$emit('send-value',k,v) }}, mounted(){this.getData()}, created(){}, }; window.componentes.TestData = TestData;