Add media pesata page and corresponding styles
- Created a new page for calculating weighted averages and graduation votes (`media-pesata.astro`) with a layout and introductory text. - Implemented a script to initialize the media pesata application. - Added CSS styles for the media pesata page and its components. - Updated existing media page (`media.astro`) to include similar structure and functionality. - Introduced error handling for the application initialization script. - Established a comprehensive CSS file for the media pesata application, including styles for various UI elements and responsive design.
This commit is contained in:
@@ -0,0 +1,654 @@
|
||||
import { useState, useEffect } from 'preact/hooks'
|
||||
import { render } from 'preact'
|
||||
|
||||
// Tipi per la gestione dei dati
|
||||
type TipoStudente = 'triennale' | 'magistrale'
|
||||
|
||||
interface Corso {
|
||||
nome: string
|
||||
anno: '1' | '2' | '3' | 'M' | 'istituzioni'
|
||||
cfu: number
|
||||
passFailOnly?: boolean // Per materie senza voto (pass/fail)
|
||||
}
|
||||
|
||||
interface CorsoSelezionato {
|
||||
id: string
|
||||
nome: string
|
||||
cfu: number
|
||||
voto: number | null
|
||||
lode: boolean
|
||||
passFailOnly?: boolean // Per materie senza voto (pass/fail)
|
||||
}
|
||||
|
||||
interface CorsoCustom {
|
||||
nome: string
|
||||
cfu: number
|
||||
}
|
||||
|
||||
// Dati dei corsi dal XML fornito
|
||||
const CORSI_DISPONIBILI: Corso[] = [
|
||||
// Primo Anno
|
||||
{ nome: 'Analisi matematica 1', anno: '1', cfu: 15 },
|
||||
{ nome: 'Aritmetica', anno: '1', cfu: 9 },
|
||||
{ nome: 'Fisica I con laboratorio', anno: '1', cfu: 9 },
|
||||
{ nome: 'Fondamenti di programmazione con laboratorio', anno: '1', cfu: 9 },
|
||||
{ nome: 'Geometria 1', anno: '1', cfu: 15 },
|
||||
{ nome: 'Laboratorio di introduzione alla matematica computazionale', anno: '1', cfu: 6, passFailOnly: true },
|
||||
{ nome: 'Algebra 1', anno: '1', cfu: 6 },
|
||||
|
||||
// Secondo Anno
|
||||
{ nome: 'Algoritmi e strutture dati', anno: '2', cfu: 6 },
|
||||
{ nome: 'Analisi matematica 2', anno: '2', cfu: 12 },
|
||||
{ nome: 'Analisi numerica con laboratorio', anno: '2', cfu: 9 },
|
||||
{ nome: 'Elementi di probabilità e statistica', anno: '2', cfu: 6 },
|
||||
{ nome: 'Geometria 2', anno: '2', cfu: 12 },
|
||||
{ nome: 'Algebra 2', anno: '2', cfu: 6 },
|
||||
|
||||
// Terzo Anno
|
||||
{ nome: 'Analisi matematica 3', anno: '3', cfu: 6 },
|
||||
{ nome: 'Calcolo scientifico', anno: '3', cfu: 6 },
|
||||
{ nome: 'Elementi di analisi complessa', anno: '3', cfu: 6 },
|
||||
{ nome: 'Elementi di calcolo delle variazioni', anno: '3', cfu: 6 },
|
||||
{ nome: 'Elementi di geometria algebrica', anno: '3', cfu: 6 },
|
||||
{ nome: 'Elementi di meccanica celeste', anno: '3', cfu: 6 },
|
||||
{ nome: 'Elementi di teoria degli insiemi', anno: '3', cfu: 6 },
|
||||
{ nome: 'Elementi di teoria delle rappresentazioni', anno: '3', cfu: 6 },
|
||||
{ nome: 'Elementi di topologia algebrica', anno: '3', cfu: 6 },
|
||||
{ nome: 'Equazioni alle derivate parziali', anno: '3', cfu: 6 },
|
||||
{ nome: 'Fisica II', anno: '3', cfu: 9 },
|
||||
{ nome: 'Fisica III', anno: '3', cfu: 6 },
|
||||
{ nome: 'Geometria e topologia differenziale', anno: '3', cfu: 6 },
|
||||
{ nome: 'Laboratorio computazionale', anno: '3', cfu: 6 },
|
||||
{ nome: 'Laboratorio sperimentale di matematica computazionale', anno: '3', cfu: 6 },
|
||||
{ nome: 'Linguaggi di programmazione con laboratorio', anno: '3', cfu: 9 },
|
||||
{ nome: 'Logica matematica', anno: '3', cfu: 6 },
|
||||
{ nome: 'Matematiche elementari da un punto di vista superiore: geometria', anno: '3', cfu: 6 },
|
||||
{ nome: 'Meccanica razionale', anno: '3', cfu: 6 },
|
||||
{ nome: 'Metodi numerici per equazioni differenziali ordinarie', anno: '3', cfu: 6 },
|
||||
{ nome: 'Ottimizzazione non lineare', anno: '3', cfu: 6 },
|
||||
{ nome: 'Probabilità', anno: '3', cfu: 6 },
|
||||
{ nome: 'Ricerca operativa', anno: '3', cfu: 6 },
|
||||
{ nome: 'Sistemi dinamici', anno: '3', cfu: 6 },
|
||||
{ nome: 'Statistica matematica', anno: '3', cfu: 6 },
|
||||
{ nome: 'Teoria algebrica dei numeri 1', anno: '3', cfu: 6 },
|
||||
{ nome: 'Teoria della misura', anno: '3', cfu: 6 },
|
||||
|
||||
// Istituzioni (Magistrale)
|
||||
{ nome: 'Istituzioni di algebra', anno: 'istituzioni', cfu: 11 },
|
||||
{ nome: 'Istituzioni di analisi matematica', anno: 'istituzioni', cfu: 11 },
|
||||
{ nome: 'Istituzioni di analisi numerica', anno: 'istituzioni', cfu: 11 },
|
||||
{ nome: 'Istituzioni di didattica della matematica', anno: 'istituzioni', cfu: 11 },
|
||||
{ nome: 'Istituzioni di fisica matematica', anno: 'istituzioni', cfu: 11 },
|
||||
{ nome: 'Istituzioni di geometria', anno: 'istituzioni', cfu: 11 },
|
||||
{ nome: 'Istituzioni di probabilità', anno: 'istituzioni', cfu: 11 },
|
||||
|
||||
// Materie a scelta (Magistrale con "M" ma non istituzioni)
|
||||
{ nome: 'Algebra superiore A', anno: 'M', cfu: 6 },
|
||||
{ nome: 'Analisi armonica', anno: 'M', cfu: 6 },
|
||||
{ nome: 'Analisi dei dati', anno: 'M', cfu: 6 },
|
||||
{ nome: 'Analisi superiore A', anno: 'M', cfu: 6 },
|
||||
{ nome: 'Analisi superiore B', anno: 'M', cfu: 6 },
|
||||
{ nome: 'Aspetti matematici nella computazione quantistica', anno: 'M', cfu: 6 },
|
||||
{ nome: 'Combinatoria algebrica', anno: 'M', cfu: 6 },
|
||||
{ nome: 'Complementi di analisi funzionale', anno: 'M', cfu: 6 },
|
||||
{ nome: 'Complementi di meccanica razionale', anno: 'M', cfu: 6 },
|
||||
{ nome: 'Didattica della matematica e nuove tecnologie', anno: 'M', cfu: 6 },
|
||||
{ nome: 'Dinamica del sistema solare', anno: 'M', cfu: 6 },
|
||||
{ nome: 'Dinamica iperbolica', anno: 'M', cfu: 6 },
|
||||
{ nome: 'Dinamica olomorfa', anno: 'M', cfu: 6 },
|
||||
{ nome: 'Elementi di calcolo in gruppi omogenei', anno: 'M', cfu: 6 },
|
||||
{ nome: 'Fisica matematica', anno: 'M', cfu: 6 },
|
||||
{ nome: 'Geometria algebrica complessa', anno: 'M', cfu: 6 },
|
||||
{ nome: 'Geometria differenziale complessa', anno: 'M', cfu: 6 },
|
||||
{ nome: 'Geometria iperbolica', anno: 'M', cfu: 6 },
|
||||
{ nome: 'Geometria riemanniana', anno: 'M', cfu: 6 },
|
||||
{ nome: 'Gruppi di Galois e gruppi fondamentali', anno: 'M', cfu: 6 },
|
||||
{ nome: 'Meccanica superiore', anno: 'M', cfu: 6 },
|
||||
{ nome: 'Metodi matematici della meccanica quantistica', anno: 'M', cfu: 6 },
|
||||
{ nome: 'Metodi numerici per il calcolo tensoriale', anno: 'M', cfu: 6 },
|
||||
{ nome: 'Metodi numerici per il controllo ottimo', anno: 'M', cfu: 6 },
|
||||
{ nome: 'Metodi numerici per problemi inversi', anno: 'M', cfu: 6 },
|
||||
{ nome: "Metodi probabilistici per l'algebra lineare numerica", anno: 'M', cfu: 6 },
|
||||
{ nome: 'Modelli matematici in biomedicina e fisica matematica', anno: 'M', cfu: 6 },
|
||||
{ nome: 'Sistemi dinamici aleatori', anno: 'M', cfu: 6 },
|
||||
{ nome: 'Superfici di Riemann e curve algebriche', anno: 'M', cfu: 6 },
|
||||
{ nome: 'Teoria dei giochi', anno: 'M', cfu: 6 },
|
||||
{ nome: 'Teoria delle categorie', anno: 'M', cfu: 6 },
|
||||
{ nome: 'Teoria delle rappresentazioni A', anno: 'M', cfu: 6 },
|
||||
{ nome: 'Topologia algebrica A', anno: 'M', cfu: 6 },
|
||||
{ nome: 'Topologia e geometria in bassa dimensione', anno: 'M', cfu: 6 },
|
||||
]
|
||||
|
||||
function MediaPesataApp() {
|
||||
const [tipoStudente, setTipoStudente] = useState<TipoStudente>('triennale')
|
||||
const [corsiSelezionati, setCorsiSelezionati] = useState<CorsoSelezionato[]>([])
|
||||
const [showCustomForm, setShowCustomForm] = useState(false)
|
||||
const [customCorso, setCustomCorso] = useState<CorsoCustom>({ nome: '', cfu: 0 })
|
||||
const [sezioniAperte, setSezioniAperte] = useState<Record<string, boolean>>({})
|
||||
const [mostraRisultati, setMostraRisultati] = useState(false)
|
||||
|
||||
const toggleSezione = (nomeSezione: string) => {
|
||||
setSezioniAperte(prev => ({
|
||||
...prev,
|
||||
[nomeSezione]: !prev[nomeSezione],
|
||||
}))
|
||||
}
|
||||
|
||||
const calcolaMedia = () => {
|
||||
const corsiConVoto = corsiSelezionati.filter(corso => corso.voto !== null && !corso.passFailOnly)
|
||||
if (corsiConVoto.length === 0) {
|
||||
alert('Inserisci almeno un voto per calcolare la media!')
|
||||
return
|
||||
}
|
||||
setMostraRisultati(true)
|
||||
}
|
||||
|
||||
// Funzioni per la gestione dei corsi
|
||||
const aggiungiCorso = (corso: Corso) => {
|
||||
// Controlla se la materia esiste già
|
||||
const materiaEsiste = corsiSelezionati.some(c => c.nome.toLowerCase() === corso.nome.toLowerCase())
|
||||
if (materiaEsiste) {
|
||||
alert('Questa materia è già stata aggiunta')
|
||||
return
|
||||
}
|
||||
|
||||
// Controllo per magistrali: massimo 3 istituzioni
|
||||
if (tipoStudente === 'magistrale' && corso.anno === 'istituzioni') {
|
||||
const istituzioniAttuali = corsiSelezionati.filter(c => c.nome.toLowerCase().includes('istituzioni')).length
|
||||
if (istituzioniAttuali >= 3) {
|
||||
alert('Puoi selezionare al massimo 3 istituzioni per la magistrale')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const id = Date.now().toString()
|
||||
const nuovoCorso: CorsoSelezionato = {
|
||||
id,
|
||||
nome: corso.nome,
|
||||
cfu: corso.cfu,
|
||||
voto: null,
|
||||
lode: false,
|
||||
passFailOnly: corso.passFailOnly,
|
||||
}
|
||||
setCorsiSelezionati([...corsiSelezionati, nuovoCorso])
|
||||
}
|
||||
|
||||
const aggiungiCorsoCustom = () => {
|
||||
// Validazioni
|
||||
if (!customCorso.nome.trim()) {
|
||||
alert('Il nome della materia non può essere vuoto')
|
||||
return
|
||||
}
|
||||
if (customCorso.cfu <= 0 || customCorso.cfu > 30) {
|
||||
alert('I CFU devono essere tra 1 e 30')
|
||||
return
|
||||
}
|
||||
if (!Number.isInteger(customCorso.cfu)) {
|
||||
alert('I CFU devono essere un numero intero')
|
||||
return
|
||||
}
|
||||
|
||||
// Controlla se la materia esiste già
|
||||
const materiaEsiste = corsiSelezionati.some(
|
||||
corso => corso.nome.toLowerCase().trim() === customCorso.nome.toLowerCase().trim(),
|
||||
)
|
||||
if (materiaEsiste) {
|
||||
alert('Questa materia è già stata aggiunta')
|
||||
return
|
||||
}
|
||||
|
||||
const id = Date.now().toString()
|
||||
const nuovoCorso: CorsoSelezionato = {
|
||||
id,
|
||||
nome: customCorso.nome.trim(),
|
||||
cfu: customCorso.cfu,
|
||||
voto: null,
|
||||
lode: false,
|
||||
}
|
||||
setCorsiSelezionati([...corsiSelezionati, nuovoCorso])
|
||||
setCustomCorso({ nome: '', cfu: 0 })
|
||||
setShowCustomForm(false)
|
||||
}
|
||||
|
||||
const rimuoviCorso = (id: string) => {
|
||||
setCorsiSelezionati(corsiSelezionati.filter(corso => corso.id !== id))
|
||||
}
|
||||
|
||||
const aggiornaVoto = (id: string, voto: number | null) => {
|
||||
// Validazione voto: deve essere tra 18 e 30 e intero
|
||||
if (voto !== null && (voto < 18 || voto > 30 || !Number.isInteger(voto))) {
|
||||
return // Ignora valori non validi
|
||||
}
|
||||
|
||||
setCorsiSelezionati(
|
||||
corsiSelezionati.map(corso =>
|
||||
corso.id === id ? { ...corso, voto, lode: voto !== 30 ? false : corso.lode } : corso,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const aggiornaLode = (id: string, lode: boolean) => {
|
||||
setCorsiSelezionati(corsiSelezionati.map(corso => (corso.id === id ? { ...corso, lode } : corso)))
|
||||
}
|
||||
|
||||
const resetTutto = () => {
|
||||
if (corsiSelezionati.length > 0) {
|
||||
if (confirm('Sei sicuro di voler cancellare tutte le materie selezionate?')) {
|
||||
setCorsiSelezionati([])
|
||||
setCustomCorso({ nome: '', cfu: 0 })
|
||||
setShowCustomForm(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calcoli della media pesata
|
||||
const calcolaMediaPesata = () => {
|
||||
// Escludi le materie pass/fail dal calcolo
|
||||
const corsiConVoto = corsiSelezionati.filter(corso => corso.voto !== null && !corso.passFailOnly)
|
||||
if (corsiConVoto.length === 0) {
|
||||
return {
|
||||
mediaPesata: 0,
|
||||
votoAmmissione: 0,
|
||||
massimoVotoLaurea: 0,
|
||||
conLode: false,
|
||||
bonusLodi: 0,
|
||||
errore: 'Nessun voto inserito per il calcolo della media',
|
||||
}
|
||||
}
|
||||
|
||||
// Calcola CFU totali con voto
|
||||
const cfuTotaliConVoto = corsiConVoto.reduce((sum, corso) => sum + corso.cfu, 0)
|
||||
const cfuDaEscludere = tipoStudente === 'triennale' ? 15 : 9
|
||||
|
||||
// Se i CFU sono insufficienti per l'esclusione, avvisa l'utente
|
||||
if (cfuTotaliConVoto < cfuDaEscludere) {
|
||||
return {
|
||||
mediaPesata: 0,
|
||||
votoAmmissione: 0,
|
||||
massimoVotoLaurea: 0,
|
||||
conLode: false,
|
||||
bonusLodi: 0,
|
||||
errore: `Hai inserito solo ${cfuTotaliConVoto} CFU con voto. Servono almeno ${cfuDaEscludere} CFU per applicare le regole di esclusione.`,
|
||||
}
|
||||
}
|
||||
|
||||
// Ordina per voto crescente
|
||||
const corsiOrdinati = [...corsiConVoto].sort((a, b) => a.voto! - b.voto!)
|
||||
|
||||
let cfuEsclusi = 0
|
||||
const corsiValidi: CorsoSelezionato[] = []
|
||||
|
||||
for (const corso of corsiOrdinati) {
|
||||
if (cfuEsclusi < cfuDaEscludere) {
|
||||
const cfuRimanentiDaEscludere = cfuDaEscludere - cfuEsclusi
|
||||
if (corso.cfu <= cfuRimanentiDaEscludere) {
|
||||
// Escludi tutto il corso
|
||||
cfuEsclusi += corso.cfu
|
||||
} else {
|
||||
// Escludi solo una parte del corso
|
||||
const cfuValidi = corso.cfu - cfuRimanentiDaEscludere
|
||||
corsiValidi.push({ ...corso, cfu: cfuValidi })
|
||||
cfuEsclusi = cfuDaEscludere
|
||||
}
|
||||
} else {
|
||||
corsiValidi.push(corso)
|
||||
}
|
||||
}
|
||||
|
||||
// Calcola media pesata
|
||||
const sommaPesata = corsiValidi.reduce((sum, corso) => sum + corso.voto! * corso.cfu, 0)
|
||||
const sommaCfu = corsiValidi.reduce((sum, corso) => sum + corso.cfu, 0)
|
||||
|
||||
const mediaPesata = sommaCfu > 0 ? sommaPesata / sommaCfu : 0
|
||||
|
||||
// Calcola bonus lodi
|
||||
const bonusLodi = corsiConVoto.reduce((bonus, corso) => {
|
||||
if (corso.lode) {
|
||||
return bonus + (corso.cfu > 6 ? 0.5 : 0.25)
|
||||
}
|
||||
return bonus
|
||||
}, 0)
|
||||
|
||||
const bonusLodiFinal = Math.min(bonusLodi, 2)
|
||||
|
||||
// Voto di ammissione alla laurea (media pesata * 11/3)
|
||||
const votoAmmissione = (mediaPesata * 11) / 3
|
||||
|
||||
// Voto di ammissione finale = voto ammissione + bonus lodi
|
||||
const votoAmmissioneFinale = votoAmmissione + bonusLodiFinal
|
||||
|
||||
// Massimo voto di laurea possibile = voto ammissione + 10 (cappato a 110)
|
||||
const massimoVotoLaurea = Math.min(votoAmmissioneFinale + 10, 110)
|
||||
const conLode = massimoVotoLaurea === 110
|
||||
|
||||
return {
|
||||
mediaPesata: Math.round(mediaPesata * 100) / 100,
|
||||
votoAmmissione: Math.round(votoAmmissioneFinale * 100) / 100,
|
||||
massimoVotoLaurea: Math.round(massimoVotoLaurea * 100) / 100,
|
||||
conLode,
|
||||
bonusLodi: Math.round(bonusLodiFinal * 100) / 100,
|
||||
errore: null,
|
||||
}
|
||||
}
|
||||
|
||||
// Filtra corsi disponibili in base al tipo di studente
|
||||
const getCorsiDisponibili = () => {
|
||||
if (tipoStudente === 'triennale') {
|
||||
return CORSI_DISPONIBILI
|
||||
} else {
|
||||
return CORSI_DISPONIBILI.filter(
|
||||
corso => corso.anno === 'istituzioni' || corso.anno === '3' || corso.anno === 'M',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Raggruppa corsi per categoria
|
||||
const raggruppaCorsi = () => {
|
||||
const corsi = getCorsiDisponibili()
|
||||
const gruppi: Record<string, Corso[]> = {}
|
||||
|
||||
if (tipoStudente === 'triennale') {
|
||||
gruppi['Primo Anno'] = corsi.filter(c => c.anno === '1')
|
||||
gruppi['Secondo Anno'] = corsi.filter(c => c.anno === '2')
|
||||
gruppi['Terzo Anno'] = corsi.filter(c => c.anno === '3')
|
||||
gruppi['Istituzioni'] = corsi.filter(c => c.anno === 'istituzioni')
|
||||
gruppi['Materie a Scelta'] = corsi.filter(c => c.anno === 'M')
|
||||
} else {
|
||||
// Per magistrali: prima le istituzioni, poi tutto il resto come "Materie a Scelta"
|
||||
gruppi['Istituzioni'] = corsi.filter(c => c.anno === 'istituzioni')
|
||||
gruppi['Materie a Scelta'] = corsi.filter(c => c.anno === '3' || c.anno === 'M')
|
||||
}
|
||||
|
||||
return gruppi
|
||||
}
|
||||
|
||||
const cambiaTipoStudente = (nuovoTipo: TipoStudente) => {
|
||||
if (corsiSelezionati.length > 0 && nuovoTipo !== tipoStudente) {
|
||||
if (
|
||||
!confirm(
|
||||
'Cambiando il tipo di studente, alcune materie potrebbero non essere più disponibili. Continuare?',
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
}
|
||||
setTipoStudente(nuovoTipo)
|
||||
}
|
||||
|
||||
const gruppiCorsi = raggruppaCorsi()
|
||||
const risultati = calcolaMediaPesata()
|
||||
const totaleCfu = corsiSelezionati.reduce((sum, corso) => sum + corso.cfu, 0)
|
||||
const maxCfu = tipoStudente === 'triennale' ? 171 : 93 // 180 − 9 e 120 − 27 per la tesi
|
||||
const cfuError = totaleCfu > maxCfu
|
||||
|
||||
return (
|
||||
<div className="media-pesata-app">
|
||||
{/* Selezione tipo studente */}
|
||||
<div className="student-type-selector">
|
||||
<h2>Corso di Laurea</h2>
|
||||
<div className="radio-group">
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
value="triennale"
|
||||
checked={tipoStudente === 'triennale'}
|
||||
onChange={e => cambiaTipoStudente((e.target as HTMLInputElement).value as TipoStudente)}
|
||||
/>
|
||||
Triennale
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
value="magistrale"
|
||||
checked={tipoStudente === 'magistrale'}
|
||||
onChange={e => cambiaTipoStudente((e.target as HTMLInputElement).value as TipoStudente)}
|
||||
/>
|
||||
Magistrale
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Counter CFU */}
|
||||
<div className={`cfu-counter ${cfuError ? 'error' : ''}`}>
|
||||
<h3>
|
||||
CFU Totali: {totaleCfu}/{maxCfu}
|
||||
{tipoStudente === 'triennale' ? ' (+9 tesi)' : ' (+27 tesi)'}
|
||||
</h3>
|
||||
{cfuError && <p className="error-text">⚠️ Hai superato il limite di CFU consentiti!</p>}
|
||||
</div>
|
||||
|
||||
<div className="main-content">
|
||||
{/* Sezione selezione corsi */}
|
||||
<div className="course-selection">
|
||||
<h2>Seleziona Materie</h2>
|
||||
|
||||
{Object.entries(gruppiCorsi).map(([categoria, corsi]) => (
|
||||
<div key={categoria} className="course-category">
|
||||
<button className="category-header" onClick={() => toggleSezione(categoria)}>
|
||||
<h3>{categoria}</h3>
|
||||
<span className={`toggle-icon ${sezioniAperte[categoria] ? 'expanded' : ''}`}>▶</span>
|
||||
</button>
|
||||
|
||||
{sezioniAperte[categoria] && (
|
||||
<div className="course-grid">
|
||||
{corsi.map((corso, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className="course-button"
|
||||
onClick={() => aggiungiCorso(corso)}
|
||||
disabled={corsiSelezionati.some(c => c.nome === corso.nome)}
|
||||
>
|
||||
<span className="course-name">{corso.nome}</span>
|
||||
<span className="course-cfu">{corso.cfu} CFU</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Form per materia custom */}
|
||||
<div className="custom-course">
|
||||
<h3>Materia Personalizzata</h3>
|
||||
{!showCustomForm ? (
|
||||
<button onClick={() => setShowCustomForm(true)} className="add-custom-btn">
|
||||
+ Aggiungi Materia Personalizzata
|
||||
</button>
|
||||
) : (
|
||||
<div className="custom-form">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Nome materia"
|
||||
value={customCorso.nome}
|
||||
onChange={e =>
|
||||
setCustomCorso({ ...customCorso, nome: (e.target as HTMLInputElement).value })
|
||||
}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="CFU"
|
||||
min="1"
|
||||
max="30"
|
||||
step="1"
|
||||
value={customCorso.cfu || ''}
|
||||
onChange={e =>
|
||||
setCustomCorso({
|
||||
...customCorso,
|
||||
cfu: parseInt((e.target as HTMLInputElement).value) || 0,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<button onClick={aggiungiCorsoCustom} className="confirm-btn">
|
||||
Aggiungi
|
||||
</button>
|
||||
<button onClick={() => setShowCustomForm(false)} className="cancel-btn">
|
||||
Annulla
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sezione lista corsi selezionati */}
|
||||
<div className="selected-courses">
|
||||
<div className="section-header">
|
||||
<h2>Materie Selezionate</h2>
|
||||
{corsiSelezionati.length > 0 && (
|
||||
<button onClick={resetTutto} className="reset-btn">
|
||||
🗑️ Cancella Tutto
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{corsiSelezionati.length === 0 ? (
|
||||
<p className="no-courses">Nessuna materia selezionata</p>
|
||||
) : (
|
||||
<div className="courses-list">
|
||||
{corsiSelezionati.map(corso => (
|
||||
<div key={corso.id} className="course-item">
|
||||
<div className="course-info">
|
||||
<span className="course-name">{corso.nome}</span>
|
||||
<span className="course-cfu">{corso.cfu} CFU</span>
|
||||
{corso.passFailOnly && <span className="pass-fail-badge">Pass/Fail</span>}
|
||||
</div>
|
||||
|
||||
{!corso.passFailOnly && (
|
||||
<div className="course-grade">
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Voto"
|
||||
min="18"
|
||||
max="30"
|
||||
step="1"
|
||||
value={corso.voto || ''}
|
||||
onChange={e =>
|
||||
aggiornaVoto(
|
||||
corso.id,
|
||||
parseInt((e.target as HTMLInputElement).value) || null,
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<label className={`lode-checkbox ${corso.voto !== 30 ? 'disabled' : ''}`}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={corso.lode}
|
||||
disabled={corso.voto !== 30}
|
||||
onChange={e =>
|
||||
aggiornaLode(corso.id, (e.target as HTMLInputElement).checked)
|
||||
}
|
||||
/>
|
||||
Lode
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => rimuoviCorso(corso.id)}
|
||||
className="remove-btn"
|
||||
title="Rimuovi materia"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pulsante Calcola */}
|
||||
{corsiSelezionati.length > 0 && (
|
||||
<div className="calculate-section">
|
||||
<button onClick={calcolaMedia} className="calculate-btn">
|
||||
🧮 Calcola Media e Voto di Laurea
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Risultati */}
|
||||
{risultati && mostraRisultati && (
|
||||
<div className="results">
|
||||
<h2>Risultati</h2>
|
||||
{risultati.errore ? (
|
||||
<div className="error-message">
|
||||
<p>⚠️ {risultati.errore}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="results-grid">
|
||||
<div className="result-item">
|
||||
<span className="label">Media Pesata:</span>
|
||||
<span className="value">{risultati.mediaPesata}</span>
|
||||
</div>
|
||||
<div className="result-item">
|
||||
<span className="label">Bonus Lodi:</span>
|
||||
<span className="value">+{risultati.bonusLodi}</span>
|
||||
</div>
|
||||
<div className="result-item highlight">
|
||||
<span className="label">Voto di Ammissione:</span>
|
||||
<span className="value">{risultati.votoAmmissione}</span>
|
||||
</div>
|
||||
<div className="result-item highlight">
|
||||
<span className="label">Massimo Voto Di Laurea Possibile:</span>
|
||||
<span className="value">
|
||||
{risultati.massimoVotoLaurea}
|
||||
{risultati.conLode && <span className="lode-badge">(+lode)</span>}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Nota informativa */}
|
||||
<div className="info-section">
|
||||
<h3>📋 Come viene calcolata la media</h3>
|
||||
<div className="info-content">
|
||||
<h4>Regole di esclusione CFU:</h4>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Triennale:</strong> I 15 CFU con i voti più bassi vengono esclusi dalla media
|
||||
</li>
|
||||
<li>
|
||||
<strong>Magistrale:</strong> I 9 CFU con i voti più bassi vengono esclusi dalla media
|
||||
</li>
|
||||
<li>Se un corso ha più CFU di quelli da escludere, viene diviso proporzionalmente</li>
|
||||
</ul>
|
||||
|
||||
<h4>Calcolo del voto finale:</h4>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Media pesata:</strong> Somma dei (voto × CFU) diviso per i CFU totali
|
||||
</li>
|
||||
<li>
|
||||
<strong>Bonus lodi:</strong> +0.5 per lodi in materie > 6 CFU, +0.25 per lodi in materie
|
||||
≤ 6 CFU (max +2)
|
||||
</li>
|
||||
<li>
|
||||
<strong>Voto di laurea:</strong> (Voto finale × 11) ÷ 3
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h4>Note:</h4>
|
||||
<ul>
|
||||
<li>
|
||||
Le materie <strong>Pass/Fail</strong> non contribuiscono al calcolo della media
|
||||
</li>
|
||||
<li>Il voto finale è limitato a 30</li>
|
||||
<li>Per i magistrali: massimo 3 istituzioni selezionabili</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Funzione per inizializzare l'app
|
||||
export function initMediaPesataApp() {
|
||||
const container = document.getElementById('media-pesata-app')
|
||||
if (container) {
|
||||
render(<MediaPesataApp />, container)
|
||||
}
|
||||
}
|
||||
|
||||
export default MediaPesataApp
|
||||
Reference in New Issue
Block a user