|
|
|
|
@ -186,7 +186,7 @@ const CORSI_DISPONIBILI: Corso[] = [
|
|
|
|
|
{ nome: 'Ultrafiltri e metodi non-standard', anno: 'M', cfu: 6 },
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
function MediaPesataApp() {
|
|
|
|
|
export function MediaPesataApp() {
|
|
|
|
|
// Funzioni per localStorage
|
|
|
|
|
const loadFromStorage = () => {
|
|
|
|
|
try {
|
|
|
|
|
@ -220,13 +220,22 @@ function MediaPesataApp() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Inizializzazione con dati salvati
|
|
|
|
|
const initialData = loadFromStorage()
|
|
|
|
|
const [tipoStudente, setTipoStudente] = useState<TipoStudente>(initialData.tipoStudente)
|
|
|
|
|
const [corsiSelezionati, setCorsiSelezionati] = useState<CorsoSelezionato[]>(initialData.corsiSelezionati)
|
|
|
|
|
// const initialData = loadFromStorage()
|
|
|
|
|
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>>(initialData.sezioniAperte)
|
|
|
|
|
const [mostraRisultati, setMostraRisultati] = useState(initialData.mostraRisultati)
|
|
|
|
|
const [sezioniAperte, setSezioniAperte] = useState<Record<string, boolean>>({})
|
|
|
|
|
const [mostraRisultati, setMostraRisultati] = useState(false)
|
|
|
|
|
|
|
|
|
|
// Load data from localStorage on mount
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const initialData = loadFromStorage()
|
|
|
|
|
setTipoStudente(initialData.tipoStudente)
|
|
|
|
|
setCorsiSelezionati(initialData.corsiSelezionati)
|
|
|
|
|
setSezioniAperte(initialData.sezioniAperte)
|
|
|
|
|
setMostraRisultati(initialData.mostraRisultati)
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
// Salva automaticamente quando cambiano i dati importanti
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
@ -482,15 +491,6 @@ function MediaPesataApp() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -501,217 +501,207 @@ function MediaPesataApp() {
|
|
|
|
|
const cfuError = totaleCfu > maxCfu
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="media-pesata-app">
|
|
|
|
|
<div class="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 class="card student-type-switcher wide">
|
|
|
|
|
<div class="grid-center">
|
|
|
|
|
<h2>Corso di Laurea</h2>
|
|
|
|
|
<div class="compound-button">
|
|
|
|
|
<button
|
|
|
|
|
class={tipoStudente === 'triennale' ? 'active' : ''}
|
|
|
|
|
onClick={() => cambiaTipoStudente('triennale')}
|
|
|
|
|
>
|
|
|
|
|
Triennale
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
class={tipoStudente === 'magistrale' ? 'active' : ''}
|
|
|
|
|
onClick={() => cambiaTipoStudente('magistrale')}
|
|
|
|
|
>
|
|
|
|
|
Magistrale
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Counter CFU */}
|
|
|
|
|
<div className={`cfu-counter ${cfuError ? 'error' : ''}`}>
|
|
|
|
|
<div class={`cfu-counter wide ${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>}
|
|
|
|
|
{cfuError && <p class="error-text">⚠️ Hai superato il limite di CFU consentiti!</p>}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="main-content">
|
|
|
|
|
{/* Sezione selezione corsi */}
|
|
|
|
|
<div className="course-selection">
|
|
|
|
|
{/* Sezione selezione corsi */}
|
|
|
|
|
<div class="card">
|
|
|
|
|
<div class="title">
|
|
|
|
|
<h2>Seleziona Materie</h2>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{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>
|
|
|
|
|
{Object.entries(gruppiCorsi).map(([categoria, corsi]) => (
|
|
|
|
|
<div key={categoria} class="course-category">
|
|
|
|
|
<button onClick={() => toggleSezione(categoria)}>
|
|
|
|
|
<div class="h-flex">
|
|
|
|
|
{categoria}
|
|
|
|
|
<div class="spacer"></div>
|
|
|
|
|
<span class={`toggle-icon ${sezioniAperte[categoria] ? 'expanded' : ''}`}>▶</span>
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{sezioniAperte[categoria] && (
|
|
|
|
|
<div class="course-grid">
|
|
|
|
|
{corsi.map((corso, index) => (
|
|
|
|
|
<button
|
|
|
|
|
key={index}
|
|
|
|
|
class="course-button"
|
|
|
|
|
onClick={() => aggiungiCorso(corso)}
|
|
|
|
|
disabled={corsiSelezionati.some(c => c.nome === corso.nome)}
|
|
|
|
|
>
|
|
|
|
|
<span class="course-name">{corso.nome}</span>
|
|
|
|
|
<span class="course-cfu">{corso.cfu} CFU</span>
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
|
|
|
|
|
{/* Form per materia custom */}
|
|
|
|
|
<div class="custom-course">
|
|
|
|
|
<h3>Materia Personalizzata</h3>
|
|
|
|
|
{!showCustomForm ? (
|
|
|
|
|
<button onClick={() => setShowCustomForm(true)}>+ Aggiungi Materia Personalizzata</button>
|
|
|
|
|
) : (
|
|
|
|
|
<div class="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}>Aggiungi</button>
|
|
|
|
|
<button onClick={() => setShowCustomForm(false)}>Annulla</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Sezione lista corsi selezionati */}
|
|
|
|
|
<div className="selected-courses">
|
|
|
|
|
<div className="section-header">
|
|
|
|
|
{/* Sezione lista corsi selezionati */}
|
|
|
|
|
<div class="card">
|
|
|
|
|
<div class="h-flex">
|
|
|
|
|
<div class="title">
|
|
|
|
|
<h2>Materie Selezionate</h2>
|
|
|
|
|
{corsiSelezionati.length > 0 && (
|
|
|
|
|
<button onClick={resetTutto} className="reset-btn">
|
|
|
|
|
🗑️ Cancella Tutto
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="spacer"></div>
|
|
|
|
|
{corsiSelezionati.length > 0 && <button onClick={resetTutto}>🗑️ 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">
|
|
|
|
|
{corsiSelezionati.length === 0 ? (
|
|
|
|
|
<p>Nessuna materia selezionata</p>
|
|
|
|
|
) : (
|
|
|
|
|
<div class="courses-list">
|
|
|
|
|
{corsiSelezionati.map(corso => (
|
|
|
|
|
<div key={corso.id} class="course-item">
|
|
|
|
|
<span class="course-name">{corso.nome}</span>
|
|
|
|
|
<span class="course-cfu">{corso.cfu} CFU</span>
|
|
|
|
|
|
|
|
|
|
{!corso.passFailOnly && (
|
|
|
|
|
<div class="course-grade tall">
|
|
|
|
|
<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 class={`lode-checkbox ${corso.voto !== 30 ? 'disabled' : ''}`}>
|
|
|
|
|
<input
|
|
|
|
|
type="number"
|
|
|
|
|
placeholder="Voto"
|
|
|
|
|
min="18"
|
|
|
|
|
max="30"
|
|
|
|
|
step="1"
|
|
|
|
|
value={corso.voto || ''}
|
|
|
|
|
class="star"
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={corso.lode}
|
|
|
|
|
disabled={corso.voto !== 30}
|
|
|
|
|
onChange={e =>
|
|
|
|
|
aggiornaVoto(
|
|
|
|
|
corso.id,
|
|
|
|
|
parseInt((e.target as HTMLInputElement).value) || null,
|
|
|
|
|
)
|
|
|
|
|
aggiornaLode(corso.id, (e.target as HTMLInputElement).checked)
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
Lode
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<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>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div class="actions tall">
|
|
|
|
|
<button
|
|
|
|
|
class="icon remove-btn"
|
|
|
|
|
onClick={() => rimuoviCorso(corso.id)}
|
|
|
|
|
className="remove-btn"
|
|
|
|
|
title="Rimuovi materia"
|
|
|
|
|
>
|
|
|
|
|
×
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</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 class="calculate-section wide">
|
|
|
|
|
<button onClick={calcolaMedia}>🧮 Calcola Media e Voto di Laurea</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Risultati */}
|
|
|
|
|
{risultati && mostraRisultati && (
|
|
|
|
|
<div className="results">
|
|
|
|
|
<div class="results wide">
|
|
|
|
|
<h2>Risultati</h2>
|
|
|
|
|
{risultati.errore ? (
|
|
|
|
|
<div className="error-message">
|
|
|
|
|
<div class="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 class="results-grid">
|
|
|
|
|
<div class="result-item">
|
|
|
|
|
<span class="label">Media Pesata:</span>
|
|
|
|
|
<span class="value">{risultati.mediaPesata}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="result-item">
|
|
|
|
|
<span className="label">Bonus Lodi:</span>
|
|
|
|
|
<span className="value">+{risultati.bonusLodi}</span>
|
|
|
|
|
<div class="result-item">
|
|
|
|
|
<span class="label">Bonus Lodi:</span>
|
|
|
|
|
<span class="value">+{risultati.bonusLodi}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="result-item highlight">
|
|
|
|
|
<span className="label">Voto di Ammissione:</span>
|
|
|
|
|
<span className="value">{risultati.votoAmmissione}</span>
|
|
|
|
|
<div class="result-item highlight">
|
|
|
|
|
<span class="label">Voto di Ammissione:</span>
|
|
|
|
|
<span class="value">{risultati.votoAmmissione}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="result-item highlight">
|
|
|
|
|
<span className="label">Massimo Voto Di Laurea Possibile:</span>
|
|
|
|
|
<span className="value">
|
|
|
|
|
<div class="result-item highlight">
|
|
|
|
|
<span class="label">Massimo Voto Di Laurea Possibile:</span>
|
|
|
|
|
<span class="value">
|
|
|
|
|
{risultati.massimoVotoLaurea}
|
|
|
|
|
{risultati.conLode && <span className="lode-badge">(+lode)</span>}
|
|
|
|
|
{risultati.conLode && <span class="lode-badge">(+lode)</span>}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@ -720,9 +710,9 @@ function MediaPesataApp() {
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Nota informativa */}
|
|
|
|
|
<div className="info-section">
|
|
|
|
|
<div class="card wide">
|
|
|
|
|
<h3>📋 Come viene calcolata la media</h3>
|
|
|
|
|
<div className="info-content">
|
|
|
|
|
<div class="info-content">
|
|
|
|
|
<h4>Regole di esclusione CFU:</h4>
|
|
|
|
|
<ul>
|
|
|
|
|
<li>
|
|
|
|
|
@ -763,11 +753,11 @@ function MediaPesataApp() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Funzione per inizializzare l'app
|
|
|
|
|
export function initMediaPesataApp() {
|
|
|
|
|
const container = document.getElementById('media-pesata-app')
|
|
|
|
|
if (container) {
|
|
|
|
|
render(<MediaPesataApp />, container)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default MediaPesataApp
|
|
|
|
|
// export function initMediaPesataApp() {
|
|
|
|
|
// const container = document.getElementById('media-pesata-app')
|
|
|
|
|
// if (container) {
|
|
|
|
|
// render(<MediaPesataApp />, container)
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// export default MediaPesataApp
|
|
|
|
|
|