Absences rework

This commit is contained in:
Zan
2026-01-12 19:03:17 +01:00
parent f75eff8c09
commit 430bed67c2
5 changed files with 894 additions and 782 deletions

View File

@@ -29,131 +29,50 @@ body {
}
.absences-page {
display: grid;
grid-template-columns: 300px 1fr;
grid-template-rows: 1fr;
grid-template-areas:
"sidebar content";
gap: 24px;
min-height: calc(100vh - 200px);
}
.absences-sidebar {
grid-area: sidebar;
display: flex;
flex-direction: column;
gap: 16px;
position: sticky;
top: 24px;
height: fit-content;
gap: 24px;
}
.filter-card {
.stats-section {
background: var(--card-card);
border-radius: 20px;
padding: 24px;
box-shadow: 0px 1px var(--shadow-blur) 0px var(--accent-shadow);
}
.filter-header {
.stats-title {
font-size: 20px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 12px;
}
.filter-header h2 {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
margin: 0;
background-color: #00000000 !important;
}
.filter-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.filter-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.filter-group label {
display: flex;
align-items: center;
gap: 10px;
color: var(--text-secondary);
font-size: 14px;
font-weight: 500;
}
.filter-group label img {
width: 20px;
height: 20px;
opacity: 0.8;
}
.filter-input {
padding: 12px 16px;
border: 2px solid transparent;
border-radius: 12px;
background: var(--button-secondaryFill);
color: var(--text-primary);
font-family: inherit;
font-size: 14px;
transition: all 0.2s ease;
cursor: pointer;
}
.filter-input:hover {
background: var(--accent-15);
}
.filter-input:focus {
outline: none;
border-color: var(--accent-accent);
background: var(--button-secondaryFill);
}
.stats-section {
grid-area: stats;
background-color: transparent !important;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-columns: repeat(6, 1fr);
gap: 12px;
}
.stat-card {
background: var(--card-card);
background: var(--button-secondaryFill);
border-radius: 16px;
padding: 20px;
padding: 20px 16px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
box-shadow: 0px 1px var(--shadow-blur) 0px var(--accent-shadow);
transition: transform 0.2s ease, box-shadow 0.2s ease;
min-height: 100px;
border: 2px solid transparent;
}
.stat-card:hover {
transform: translateY(-2px);
}
.stat-icon {
width: 24px;
height: 24px;
margin-bottom: 8px;
opacity: 0.8;
}
.stat-number {
font-size: 32px;
font-weight: 700;
@@ -166,18 +85,46 @@ body {
color: var(--accent-accent);
}
.stat-card.total {
border-color: var(--accent-15);
}
.stat-card.absence .stat-number {
color: var(--accent-accent);
}
.stat-card.late .stat-number {
color: var(--grades-2);
}
.stat-card.late {
border-color: var(--grades-background-2);
}
.stat-card.justified .stat-number {
color: var(--grades-4);
}
.stat-card.justified {
border-color: var(--grades-background-4);
}
.stat-card.unjustified .stat-number {
color: var(--grades-1);
}
.stat-card.unjustified {
border-color: var(--grades-background-1);
}
.stat-card.pending .stat-number {
color: var(--grades-3);
}
.stat-card.pending {
border-color: var(--grades-background-3);
}
.stat-label {
color: var(--text-secondary);
font-size: 12px;
@@ -186,163 +133,369 @@ body {
letter-spacing: 0.5px;
}
.absences-content {
grid-area: content;
.calendar-legend {
display: flex;
flex-direction: column;
gap: 24px;
flex-wrap: wrap;
gap: 16px;
padding: 16px 24px;
background: var(--card-card);
border-radius: 16px;
box-shadow: 0px 1px var(--shadow-blur) 0px var(--accent-shadow);
}
.day-group {
.legend-item {
display: flex;
align-items: center;
gap: 8px;
}
.legend-color {
width: 24px;
height: 24px;
border-radius: 8px;
}
.legend-color.status-justified {
background: var(--grades-background-4);
border: 2px solid var(--grades-4);
}
.legend-color.status-pending {
background: var(--grades-background-3);
border: 2px solid var(--grades-3);
}
.legend-color.status-unjustified {
background: var(--grades-background-1);
border: 2px solid var(--grades-1);
}
.legend-color.type-late-legend {
background: var(--grades-background-2);
border: 2px dashed var(--grades-2);
}
.legend-item span {
font-size: 13px;
color: var(--text-secondary);
font-weight: 500;
}
.calendar-section {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px;
}
.month-container {
background: var(--card-card);
border-radius: 20px;
overflow: hidden;
box-shadow: 0px 1px var(--shadow-blur) 0px var(--accent-shadow);
}
.day-header {
.month-header {
background: var(--button-secondaryFill);
color: white;
padding: 16px 20px;
font-size: 16px;
font-weight: 600;
text-align: center;
}
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
padding: 12px;
}
.calendar-day-header {
text-align: center;
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
padding: 8px 0;
text-transform: uppercase;
}
.calendar-day {
aspect-ratio: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 10px;
background: var(--button-secondaryFill);
position: relative;
min-height: 40px;
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.calendar-day.empty {
background: transparent;
}
.calendar-day.future {
background: var(--accent-15);
opacity: 0.5;
}
.calendar-day.future .day-number {
color: var(--text-teritary);
}
.calendar-day.today .day-number {
color: var(--accent-accent);
font-weight: 700;
}
.calendar-day .day-number {
font-size: 14px;
font-weight: 500;
color: var(--text-primary);
z-index: 1;
}
.calendar-day.has-absence {
cursor: pointer;
}
.calendar-day.has-absence:hover {
transform: scale(1.1);
box-shadow: 0 4px 12px var(--accent-shadow);
z-index: 2;
}
.calendar-day.status-justified {
background: var(--grades-background-4);
border: 2px solid var(--grades-4);
}
.calendar-day.status-justified .day-number {
color: var(--grades-4);
font-weight: 700;
}
.calendar-day.status-pending {
background: var(--grades-background-3);
border: 2px solid var(--grades-3);
}
.calendar-day.status-pending .day-number {
color: var(--grades-3);
font-weight: 700;
}
.calendar-day.status-unjustified {
background: var(--grades-background-1);
border: 2px solid var(--grades-1);
}
.calendar-day.status-unjustified .day-number {
color: var(--grades-1);
font-weight: 700;
}
.calendar-day.type-late {
border-style: dashed;
}
.calendar-day.type-mixed {
border-style: double;
border-width: 3px;
}
.absence-count {
position: absolute;
top: 2px;
right: 2px;
background: var(--accent-accent);
color: white;
padding: 16px 24px;
font-size: 9px;
font-weight: 700;
width: 16px;
height: 16px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.calendar-day.status-justified .absence-count {
background: var(--grades-4);
}
.calendar-day.status-pending .absence-count {
background: var(--grades-3);
}
.calendar-day.status-unjustified .absence-count {
background: var(--grades-1);
}
.absence-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
opacity: 0;
transition: opacity 0.2s ease;
padding: 20px;
}
.absence-modal-overlay.visible {
opacity: 1;
}
.absence-modal {
background: var(--card-card);
border-radius: 24px;
max-width: 500px;
width: 100%;
max-height: 80vh;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
transform: scale(0.95);
transition: transform 0.2s ease;
}
.absence-modal-overlay.visible .absence-modal {
transform: scale(1);
}
.modal-header {
background: var(--button-secondaryFill);
border-bottom: 1px solid #00000000 !important;
color: white;
padding: 14px 18px !important;
display: flex;
align-items: center;
justify-content: space-between;
font-weight: 600;
}
.day-date {
display: flex;
align-items: center;
gap: 12px;
.modal-header h2 {
font-size: 16px;
font-weight: 600;
margin: 0;
background: transparent !important;
}
.day-date img {
width: 20px;
height: 20px;
filter: brightness(0) invert(1);
.modal-content {
background-color: var(--card-card) !important;
padding: 16px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 8px;
}
.day-count {
background: rgba(255, 255, 255, 0.2);
padding: 4px 12px;
border-radius: 20px;
font-size: 13px;
}
.day-absences {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 16px;
padding: 20px;
}
.absence-card {
.modal-absence-card {
background: var(--button-secondaryFill);
border-radius: 16px;
padding: 20px;
display: grid;
grid-template-columns: auto 1fr minmax(auto, max-content);
grid-template-rows: auto auto;
grid-template-areas:
"lesson subject status"
"lesson topic status";
gap: 8px 12px;
transition: transform 0.2s ease, box-shadow 0.2s ease;
border: 1px solid var(--accent-15);
border-radius: 12px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 6px;
border-left: 3px solid transparent;
}
.absence-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px var(--accent-shadow);
}
.absence-lesson {
grid-area: lesson;
.modal-card-header {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
background: var(--accent-15);
border-radius: 12px;
font-size: 20px;
gap: 8px;
flex-wrap: wrap;
}
.modal-lesson {
font-size: 16px;
font-weight: 700;
color: var(--accent-accent);
min-width: 24px;
}
.type-indicator {
display: inline-flex;
align-items: center;
padding: 3px 8px 1px 8px;
border-radius: 12px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
}
.type-indicator.absence {
background: var(--accent-15);
color: var(--accent-accent);
}
.absence-subject {
grid-area: subject;
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
align-self: end;
.type-indicator.late {
background: var(--grades-background-2);
color: var(--grades-2);
}
.absence-topic {
grid-area: topic;
font-size: 13px;
color: var(--text-secondary);
align-self: start;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.absence-status {
grid-area: status;
display: flex;
align-items: center;
justify-content: center;
}
.status-badge {
.modal-status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
border-radius: 24px;
font-size: 11px;
padding: 3px 8px;
border-radius: 12px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.2px;
white-space: nowrap;
flex-shrink: 0;
margin-left: auto;
}
.status-badge img {
width: 16px;
height: 16px;
}
.status-badge.justified {
.modal-status-badge.justified {
background: var(--grades-background-4);
color: var(--grades-4);
}
.status-badge.justified img {
filter: brightness(0) saturate(100%) invert(76%) sepia(43%) saturate(609%) hue-rotate(53deg) brightness(101%) contrast(92%);
}
.status-badge.unjustified {
background: var(--grades-background-1);
color: var(--grades-1);
}
.status-badge.unjustified .material-icons-round {
font-size: 16px;
}
.status-badge.pending {
.modal-status-badge.pending {
background: var(--grades-background-3);
color: var(--grades-3);
}
.status-badge.pending .material-icons-round {
font-size: 16px;
.modal-status-badge.unjustified {
background: var(--grades-background-1);
color: var(--grades-1);
}
.modal-subject {
font-size: 15px;
font-weight: 600;
color: var(--text-primary);
line-height: 1.3;
}
.modal-info-row {
display: flex;
flex-wrap: wrap;
gap: 6px;
font-size: 12px;
color: var(--text-secondary);
}
.modal-info-item {
display: inline-block;
}
.modal-info-item:not(:last-child)::after {
content: '•';
margin-left: 6px;
color: var(--text-teritary);
}
.modal-minutes {
color: var(--grades-2);
font-weight: 600;
}
.empty-state {
@@ -375,14 +528,9 @@ body {
color: var(--text-secondary);
}
@media (max-width: 1024px) {
.absences-page {
grid-template-columns: 260px 1fr;
gap: 20px;
}
.day-absences {
grid-template-columns: 1fr;
@media (max-width: 1200px) {
.stats-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@@ -395,41 +543,18 @@ body {
padding: 16px;
}
.absences-page {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
grid-template-areas:
"sidebar"
"content";
gap: 16px;
}
.absences-sidebar {
position: static;
}
.filter-card {
.stats-section {
padding: 20px;
border-radius: 16px;
}
.filter-content {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.filter-group label {
font-size: 12px;
}
.filter-input {
padding: 10px 12px;
font-size: 13px;
.stats-title {
font-size: 18px;
margin-bottom: 16px;
}
.stats-grid {
grid-template-columns: repeat(4, 1fr);
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
@@ -447,33 +572,78 @@ body {
font-size: 10px;
}
.day-group {
.calendar-section {
grid-template-columns: 1fr;
}
.calendar-legend {
gap: 12px;
padding: 14px 18px;
}
.legend-item span {
font-size: 12px;
}
.month-container {
border-radius: 16px;
}
.day-header {
padding: 14px 18px;
.month-header {
padding: 14px 16px;
font-size: 15px;
}
.calendar-grid {
gap: 3px;
padding: 10px;
}
.calendar-day {
min-height: 36px;
border-radius: 8px;
}
.calendar-day .day-number {
font-size: 13px;
}
.absence-count {
width: 14px;
height: 14px;
font-size: 8px;
}
.absence-modal {
max-width: 100%;
max-height: 90vh;
border-radius: 20px;
}
.modal-header {
padding: 12px 16px;
}
.modal-header h2 {
font-size: 15px;
}
.modal-content {
padding: 12px;
gap: 6px;
}
.modal-absence-card {
padding: 10px;
gap: 5px;
}
.modal-subject {
font-size: 14px;
}
.day-absences {
padding: 14px;
gap: 12px;
}
.absence-card {
padding: 16px;
border-radius: 12px;
}
.absence-lesson {
width: 44px;
height: 44px;
font-size: 18px;
}
.absence-subject {
font-size: 15px;
.modal-info-row {
font-size: 11px;
}
}
@@ -483,26 +653,13 @@ body {
}
.absences-page {
gap: 12px;
gap: 16px;
}
.filter-card {
.stats-section {
padding: 16px;
}
.filter-header {
margin-bottom: 16px;
}
.filter-header h2 {
font-size: 16px;
}
.filter-content {
grid-template-columns: 1fr;
gap: 14px;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
gap: 8px;
@@ -523,63 +680,35 @@ body {
letter-spacing: 0.3px;
}
.day-header {
padding: 12px 16px;
flex-wrap: wrap;
gap: 8px;
}
.day-date {
font-size: 14px;
}
.day-count {
font-size: 12px;
padding: 3px 10px;
}
.day-absences {
padding: 12px;
.calendar-legend {
flex-direction: column;
gap: 10px;
}
.absence-card {
padding: 14px;
grid-template-columns: auto 1fr;
grid-template-rows: auto auto auto;
grid-template-areas:
"lesson subject"
"lesson topic"
"status status";
gap: 6px 12px;
.calendar-grid {
gap: 2px;
padding: 8px;
}
.absence-lesson {
width: 40px;
height: 40px;
font-size: 16px;
grid-row: span 2;
.calendar-day {
min-height: 32px;
}
.absence-status {
justify-content: flex-start;
margin-top: 8px;
padding-top: 10px;
border-top: 1px solid var(--accent-15);
}
.status-badge {
padding: 6px 12px;
font-size: 11px;
}
.absence-subject {
font-size: 14px;
}
.absence-topic {
.calendar-day .day-number {
font-size: 12px;
}
.absence-modal-overlay {
padding: 12px;
}
.modal-absence-card {
padding: 14px;
}
.modal-subject {
font-size: 16px;
}
}
@keyframes fadeIn {
@@ -593,17 +722,15 @@ body {
}
}
.day-group {
.month-container {
animation: fadeIn 0.3s ease forwards;
}
.day-group:nth-child(1) { animation-delay: 0.05s; }
.day-group:nth-child(2) { animation-delay: 0.1s; }
.day-group:nth-child(3) { animation-delay: 0.15s; }
.day-group:nth-child(4) { animation-delay: 0.2s; }
.day-group:nth-child(5) { animation-delay: 0.25s; }
.month-container:nth-child(1) { animation-delay: 0.05s; }
.month-container:nth-child(2) { animation-delay: 0.1s; }
.month-container:nth-child(3) { animation-delay: 0.15s; }
.month-container:nth-child(4) { animation-delay: 0.2s; }
.month-container:nth-child(5) { animation-delay: 0.25s; }
.loading-overlay {
position: fixed;

View File

@@ -15,7 +15,7 @@ async function collectAbsencesData() {
try {
const currentDomain = window.location.hostname;
const response = await fetch(
`https://${currentDomain}/api/HianyzasokApi/GetHianyzasGrid?sort=MulasztasDatum-desc&page=1&pageSize=100&group=&filter=&data=%7B%7D&_=${Date.now()}`,
`https://${currentDomain}/api/HianyzasokApi/GetHianyzasGrid?sort=MulasztasDatum-desc&page=1&pageSize=500&group=&filter=&data=%7B%7D&_=${Date.now()}`,
{
method: "GET",
credentials: "include",
@@ -45,178 +45,77 @@ async function collectAbsencesData() {
justificationStatus = "unjustified";
}
const absenceType = item.MulasztasTipus === 1500 ? 'absence' : item.MulasztasTipus === 1499 ? 'late' : 'other';
absences.push({
date: formattedDate,
rawDate: date,
dateKey: `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`,
lesson: item.Oraszam?.toString() || "",
subject: item.Targy || "",
topic: item.Tema || "",
type: item.MulasztasTipus_DNAME || "",
absenceType: absenceType,
absenceTypeId: item.MulasztasTipus,
justified: item.Igazolt_BOOL === true,
justificationStatus: justificationStatus,
purposeful: item.TanoraiCeluMulasztas_BNAME || "",
justificationType: item.IgazolasTipus_DNAME || "",
minutes: item.KesesPercben || 0,
});
});
}
const groupedAbsences = absences.reduce((groups, absence) => {
const date = absence.date;
if (!groups[date]) {
groups[date] = [];
const groupedByDate = absences.reduce((groups, absence) => {
const key = absence.dateKey;
if (!groups[key]) {
groups[key] = [];
}
groups[date].push(absence);
groups[key].push(absence);
return groups;
}, {});
Object.keys(groupedAbsences).forEach(date => {
groupedAbsences[date].sort((a, b) => parseInt(a.lesson) - parseInt(b.lesson));
Object.keys(groupedByDate).forEach(date => {
groupedByDate[date].sort((a, b) => parseInt(a.lesson) - parseInt(b.lesson));
});
return { basicData, absences, groupedAbsences };
return { basicData, absences, groupedByDate };
} catch (error) {
console.error("Hiba az API hívás során:", error);
return { basicData, absences: [], groupedAbsences: {} };
return { basicData, absences: [], groupedByDate: {} };
}
}
function createFilterCard(absences) {
const filterCard = document.createElement('div');
filterCard.className = 'filter-card';
const filterHeader = document.createElement('div');
filterHeader.className = 'filter-header';
const h2 = document.createElement('h2');
h2.textContent = LanguageManager.t('absences.filter_title');
filterHeader.appendChild(h2);
const filterContent = document.createElement('div');
filterContent.className = 'filter-content';
const dateGroup = document.createElement('div');
dateGroup.className = 'filter-group';
const dateLabel = document.createElement('label');
const dateImg = document.createElement('img');
dateImg.src = chrome.runtime.getURL('icons/Calendar.svg');
dateImg.alt = 'Dátum';
dateLabel.appendChild(dateImg);
dateLabel.appendChild(document.createTextNode(' ' + LanguageManager.t('absences.date')));
const dateInput = document.createElement('input');
dateInput.type = 'date';
dateInput.id = 'dateFilter';
dateInput.className = 'filter-input';
dateGroup.appendChild(dateLabel);
dateGroup.appendChild(dateInput);
filterContent.appendChild(dateGroup);
const subjectGroup = document.createElement('div');
subjectGroup.className = 'filter-group';
const subjectLabel = document.createElement('label');
const subjectImg = document.createElement('img');
subjectImg.src = chrome.runtime.getURL('icons/Subject.svg');
subjectImg.alt = 'Tantárgy';
subjectLabel.appendChild(subjectImg);
subjectLabel.appendChild(document.createTextNode(' ' + LanguageManager.t('absences.subject')));
const subjectSelect = document.createElement('select');
subjectSelect.id = 'subjectFilter';
subjectSelect.className = 'filter-input';
const defaultSubjectOption = document.createElement('option');
defaultSubjectOption.value = '';
defaultSubjectOption.textContent = LanguageManager.t('absences.all_subjects');
subjectSelect.appendChild(defaultSubjectOption);
const subjects = [...new Set(absences.map(a => a.subject))].sort();
subjects.forEach(subject => {
const option = document.createElement('option');
option.value = subject;
option.textContent = subject;
subjectSelect.appendChild(option);
});
subjectGroup.appendChild(subjectLabel);
subjectGroup.appendChild(subjectSelect);
filterContent.appendChild(subjectGroup);
const justificationGroup = document.createElement('div');
justificationGroup.className = 'filter-group';
const justificationLabel = document.createElement('label');
const justificationImg = document.createElement('img');
justificationImg.src = chrome.runtime.getURL('icons/BadgeCheck.svg');
justificationImg.alt = 'Igazolás';
justificationLabel.appendChild(justificationImg);
justificationLabel.appendChild(document.createTextNode(' ' + LanguageManager.t('absences.justification')));
const justificationSelect = document.createElement('select');
justificationSelect.id = 'justificationFilter';
justificationSelect.className = 'filter-input';
const justificationOptions = [
{ value: '', text: LanguageManager.t('absences.all_types') },
{ value: 'justified', text: LanguageManager.t('absences.justified') },
{ value: 'unjustified', text: LanguageManager.t('absences.unjustified') },
{ value: 'pending', text: LanguageManager.t('absences.pending') }
];
justificationOptions.forEach(optionData => {
const option = document.createElement('option');
option.value = optionData.value;
option.textContent = optionData.text;
justificationSelect.appendChild(option);
});
justificationGroup.appendChild(justificationLabel);
justificationGroup.appendChild(justificationSelect);
filterContent.appendChild(justificationGroup);
filterCard.appendChild(filterHeader);
filterCard.appendChild(filterContent);
return filterCard;
}
function createStatsSection(absences) {
const statsSection = document.createElement('div');
statsSection.className = 'stats-section';
const statsTitle = document.createElement('h2');
statsTitle.className = 'stats-title';
statsTitle.textContent = LanguageManager.t('absences.title');
statsSection.appendChild(statsTitle);
const statsGrid = document.createElement('div');
statsGrid.className = 'stats-grid';
const totalAbsences = absences.filter(a => a.absenceType === 'absence').length;
const totalLates = absences.filter(a => a.absenceType === 'late').length;
const justified = absences.filter(a => a.justificationStatus === 'justified').length;
const unjustified = absences.filter(a => a.justificationStatus === 'unjustified').length;
const pending = absences.filter(a => a.justificationStatus === 'pending').length;
const stats = [
{
type: 'total',
number: absences.length,
label: LanguageManager.t('absences.total_absences')
},
{
type: 'justified',
number: absences.filter(a => a.justificationStatus === 'justified').length,
label: LanguageManager.t('absences.justified')
},
{
type: 'unjustified',
number: absences.filter(a => a.justificationStatus === 'unjustified').length,
label: LanguageManager.t('absences.unjustified')
},
{
type: 'pending',
number: absences.filter(a => a.justificationStatus === 'pending').length,
label: LanguageManager.t('absences.pending')
}
{ type: 'total', number: absences.length, label: LanguageManager.t('absences.total_absences') },
{ type: 'absence', number: totalAbsences, label: LanguageManager.t('absences.absence_type') },
{ type: 'late', number: totalLates, label: LanguageManager.t('absences.late_type') },
{ type: 'justified', number: justified, label: LanguageManager.t('absences.justified') },
{ type: 'unjustified', number: unjustified, label: LanguageManager.t('absences.unjustified') },
{ type: 'pending', number: pending, label: LanguageManager.t('absences.pending') },
];
stats.forEach(stat => {
const statCard = document.createElement('div');
statCard.className = `stat-card ${stat.type}`;
statCard.dataset.type = stat.type;
const statNumber = document.createElement('div');
statNumber.className = 'stat-number';
@@ -235,166 +134,322 @@ function createStatsSection(absences) {
return statsSection;
}
function createDayGroup(date, dayAbsences) {
const dayGroup = document.createElement('div');
dayGroup.className = 'day-group';
dayGroup.dataset.date = date;
const dayHeader = document.createElement('div');
dayHeader.className = 'day-header';
const dayDate = document.createElement('div');
dayDate.className = 'day-date';
const calendarIcon = document.createElement('img');
calendarIcon.src = chrome.runtime.getURL('icons/Calendar.svg');
calendarIcon.alt = 'Dátum';
const dateText = document.createElement('span');
dateText.textContent = formatDateWithDay(date);
dayDate.appendChild(calendarIcon);
dayDate.appendChild(dateText);
const dayCount = document.createElement('div');
dayCount.className = 'day-count';
dayCount.textContent = `${dayAbsences.length} ${LanguageManager.t('absences.hours')}`;
dayHeader.appendChild(dayDate);
dayHeader.appendChild(dayCount);
const dayAbsencesContainer = document.createElement('div');
dayAbsencesContainer.className = 'day-absences';
dayAbsences.forEach(absence => {
const absenceCard = createAbsenceCard(absence);
dayAbsencesContainer.appendChild(absenceCard);
});
dayGroup.appendChild(dayHeader);
dayGroup.appendChild(dayAbsencesContainer);
return dayGroup;
function getSchoolYearStart() {
const now = new Date();
const year = now.getMonth() >= 8 ? now.getFullYear() : now.getFullYear() - 1;
return new Date(year, 8, 1);
}
function formatDateWithDay(dateStr) {
const parts = dateStr.split('.');
const year = parseInt(parts[0]);
const month = parseInt(parts[1]) - 1;
const day = parseInt(parts[2]);
function createCalendarSection(groupedByDate, absences) {
const calendarSection = document.createElement('div');
calendarSection.className = 'calendar-section';
const date = new Date(year, month, day);
const days = [
LanguageManager.t('common.sunday'),
LanguageManager.t('common.monday'),
LanguageManager.t('common.tuesday'),
LanguageManager.t('common.wednesday'),
LanguageManager.t('common.thursday'),
LanguageManager.t('common.friday'),
LanguageManager.t('common.saturday')
const schoolYearStart = getSchoolYearStart();
const now = new Date();
const months = [];
let currentDate = new Date(schoolYearStart);
while (currentDate <= now) {
months.push({
year: currentDate.getFullYear(),
month: currentDate.getMonth()
});
currentDate.setMonth(currentDate.getMonth() + 1);
}
months.forEach(({ year, month }) => {
const monthCalendar = createMonthCalendar(year, month, groupedByDate, absences);
calendarSection.appendChild(monthCalendar);
});
return calendarSection;
}
function createMonthCalendar(year, month, groupedByDate, absences) {
const monthContainer = document.createElement('div');
monthContainer.className = 'month-container';
const monthHeader = document.createElement('div');
monthHeader.className = 'month-header';
const monthNames = [
LanguageManager.t('common.january') || 'Január',
LanguageManager.t('common.february') || 'Február',
LanguageManager.t('common.march') || 'Március',
LanguageManager.t('common.april') || 'Április',
LanguageManager.t('common.may') || 'Május',
LanguageManager.t('common.june') || 'Június',
LanguageManager.t('common.july') || 'Július',
LanguageManager.t('common.august') || 'Augusztus',
LanguageManager.t('common.september') || 'Szeptember',
LanguageManager.t('common.october') || 'Október',
LanguageManager.t('common.november') || 'November',
LanguageManager.t('common.december') || 'December'
];
const dayName = days[date.getDay()];
return `${dateStr} - ${dayName.charAt(0).toUpperCase() + dayName.slice(1)}`;
monthHeader.textContent = `${monthNames[month]} ${year}`;
monthContainer.appendChild(monthHeader);
const calendarGrid = document.createElement('div');
calendarGrid.className = 'calendar-grid';
const dayNames = [
LanguageManager.t('common.mon') || 'H',
LanguageManager.t('common.tue') || 'K',
LanguageManager.t('common.wed') || 'Sze',
LanguageManager.t('common.thu') || 'Cs',
LanguageManager.t('common.fri') || 'P',
LanguageManager.t('common.sat') || 'Szo',
LanguageManager.t('common.sun') || 'V'
];
dayNames.forEach(day => {
const dayHeader = document.createElement('div');
dayHeader.className = 'calendar-day-header';
dayHeader.textContent = day;
calendarGrid.appendChild(dayHeader);
});
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const daysInMonth = lastDay.getDate();
let startDay = firstDay.getDay() - 1;
if (startDay < 0) startDay = 6;
for (let i = 0; i < startDay; i++) {
const emptyCell = document.createElement('div');
emptyCell.className = 'calendar-day empty';
calendarGrid.appendChild(emptyCell);
}
function createAbsenceCard(absence) {
const card = document.createElement('div');
card.className = 'absence-card';
card.dataset.subject = absence.subject;
card.dataset.status = absence.justificationStatus;
card.dataset.date = absence.date;
const today = new Date();
today.setHours(0, 0, 0, 0);
const lessonDiv = document.createElement('div');
lessonDiv.className = 'absence-lesson';
lessonDiv.textContent = absence.lesson + '.';
for (let day = 1; day <= daysInMonth; day++) {
const dateKey = `${year}-${(month + 1).toString().padStart(2, "0")}-${day.toString().padStart(2, "0")}`;
const dayAbsences = groupedByDate[dateKey] || [];
const currentDayDate = new Date(year, month, day);
currentDayDate.setHours(0, 0, 0, 0);
const subjectDiv = document.createElement('div');
subjectDiv.className = 'absence-subject';
subjectDiv.textContent = absence.subject;
const dayCell = document.createElement('div');
dayCell.className = 'calendar-day';
const topicDiv = document.createElement('div');
topicDiv.className = 'absence-topic';
topicDiv.textContent = absence.topic || '-';
topicDiv.title = absence.topic;
if (currentDayDate > today) {
dayCell.classList.add('future');
}
const statusDiv = document.createElement('div');
statusDiv.className = 'absence-status';
if (currentDayDate.getTime() === today.getTime()) {
dayCell.classList.add('today');
}
if (dayAbsences.length > 0) {
dayCell.classList.add('has-absence');
const hasUnjustified = dayAbsences.some(a => a.justificationStatus === 'unjustified');
const hasPending = dayAbsences.some(a => a.justificationStatus === 'pending');
const hasJustified = dayAbsences.some(a => a.justificationStatus === 'justified');
const hasAbsence = dayAbsences.some(a => a.absenceType === 'absence');
const hasLate = dayAbsences.some(a => a.absenceType === 'late');
if (hasUnjustified) {
dayCell.classList.add('status-unjustified');
} else if (hasPending) {
dayCell.classList.add('status-pending');
} else if (hasJustified) {
dayCell.classList.add('status-justified');
}
if (hasAbsence && hasLate) {
dayCell.classList.add('type-mixed');
} else if (hasLate) {
dayCell.classList.add('type-late');
}
dayCell.addEventListener('click', () => {
openAbsenceModal(dateKey, dayAbsences);
});
const countBadge = document.createElement('span');
countBadge.className = 'absence-count';
countBadge.textContent = dayAbsences.length;
dayCell.appendChild(countBadge);
}
const dayNumber = document.createElement('span');
dayNumber.className = 'day-number';
dayNumber.textContent = day;
dayCell.appendChild(dayNumber);
calendarGrid.appendChild(dayCell);
}
monthContainer.appendChild(calendarGrid);
return monthContainer;
}
function openAbsenceModal(dateKey, dayAbsences) {
const existingModal = document.querySelector('.absence-modal-overlay');
if (existingModal) {
existingModal.remove();
}
const overlay = document.createElement('div');
overlay.className = 'absence-modal-overlay';
const modal = document.createElement('div');
modal.className = 'absence-modal';
const modalHeader = document.createElement('div');
modalHeader.className = 'modal-header';
const dateParts = dateKey.split('-');
const displayDate = new Date(parseInt(dateParts[0]), parseInt(dateParts[1]) - 1, parseInt(dateParts[2]));
const dayNames = [
LanguageManager.t('common.sunday') || 'Vasárnap',
LanguageManager.t('common.monday') || 'Hétfő',
LanguageManager.t('common.tuesday') || 'Kedd',
LanguageManager.t('common.wednesday') || 'Szerda',
LanguageManager.t('common.thursday') || 'Csütörtök',
LanguageManager.t('common.friday') || 'Péntek',
LanguageManager.t('common.saturday') || 'Szombat'
];
const modalTitle = document.createElement('h2');
modalTitle.textContent = `${dateParts[0]}.${dateParts[1]}.${dateParts[2]}. - ${dayNames[displayDate.getDay()]}`;
modalHeader.appendChild(modalTitle);
const modalContent = document.createElement('div');
modalContent.className = 'modal-content';
dayAbsences.forEach(absence => {
const absenceCard = document.createElement('div');
absenceCard.className = `modal-absence-card ${absence.justificationStatus}`;
const headerRow = document.createElement('div');
headerRow.className = 'modal-card-header';
const lessonSpan = document.createElement('span');
lessonSpan.className = 'modal-lesson';
lessonSpan.textContent = `${absence.lesson}.`;
const typeIndicator = document.createElement('span');
typeIndicator.className = `type-indicator ${absence.absenceType}`;
typeIndicator.textContent = absence.absenceType === 'late'
? (LanguageManager.t('absences.late_type') || 'Késés')
: (LanguageManager.t('absences.absence_type') || 'Hiányzás');
const statusBadge = document.createElement('span');
statusBadge.className = `status-badge ${absence.justificationStatus}`;
statusBadge.className = `modal-status-badge ${absence.justificationStatus}`;
let statusText = '';
if (absence.justificationStatus === 'justified') {
const img = document.createElement('img');
img.src = chrome.runtime.getURL('icons/BadgeCheck.svg');
img.alt = 'Igazolt';
statusBadge.appendChild(img);
statusBadge.appendChild(document.createTextNode(' ' + LanguageManager.t('absences.justified')));
statusText = LanguageManager.t('absences.justified') || 'Igazolt';
} else if (absence.justificationStatus === 'unjustified') {
const span = document.createElement('span');
span.className = 'material-icons-round';
span.textContent = 'cancel';
statusBadge.appendChild(span);
statusBadge.appendChild(document.createTextNode(' ' + LanguageManager.t('absences.unjustified')));
statusText = LanguageManager.t('absences.unjustified') || 'Igazolatlan';
} else {
const img = document.createElement('img');
img.src = chrome.runtime.getURL('icons/pending.svg');
img.alt = 'Függőben';
statusBadge.appendChild(img);
statusBadge.appendChild(document.createTextNode(' ' + LanguageManager.t('absences.pending')));
statusText = LanguageManager.t('absences.pending') || 'Igazolásra vár';
}
statusBadge.textContent = statusText;
headerRow.appendChild(lessonSpan);
headerRow.appendChild(typeIndicator);
headerRow.appendChild(statusBadge);
const subjectDiv = document.createElement('div');
subjectDiv.className = 'modal-subject';
subjectDiv.textContent = absence.subject;
const infoRow = document.createElement('div');
infoRow.className = 'modal-info-row';
if (absence.topic) {
const topicSpan = document.createElement('span');
topicSpan.className = 'modal-info-item';
topicSpan.textContent = absence.topic;
infoRow.appendChild(topicSpan);
}
statusDiv.appendChild(statusBadge);
card.appendChild(lessonDiv);
card.appendChild(subjectDiv);
card.appendChild(topicDiv);
card.appendChild(statusDiv);
return card;
if (absence.justificationType) {
const justificationSpan = document.createElement('span');
justificationSpan.className = 'modal-info-item';
justificationSpan.textContent = absence.justificationType;
infoRow.appendChild(justificationSpan);
}
function createAbsencesContent(groupedAbsences) {
const content = document.createElement('div');
content.className = 'absences-content';
if (absence.absenceType === 'late' && absence.minutes > 0) {
const minutesSpan = document.createElement('span');
minutesSpan.className = 'modal-info-item modal-minutes';
minutesSpan.textContent = `${absence.minutes} ${LanguageManager.t('absences.minutes') || 'perc'}`;
infoRow.appendChild(minutesSpan);
}
const sortedDates = Object.keys(groupedAbsences).sort((a, b) => {
const dateA = new Date(a.replace(/\./g, '-').slice(0, -1));
const dateB = new Date(b.replace(/\./g, '-').slice(0, -1));
return dateB - dateA;
absenceCard.appendChild(headerRow);
absenceCard.appendChild(subjectDiv);
if (infoRow.children.length > 0) {
absenceCard.appendChild(infoRow);
}
modalContent.appendChild(absenceCard);
});
if (sortedDates.length === 0) {
const emptyState = document.createElement('div');
emptyState.className = 'empty-state';
modal.appendChild(modalHeader);
modal.appendChild(modalContent);
overlay.appendChild(modal);
const emptyIcon = document.createElement('img');
emptyIcon.src = chrome.runtime.getURL('icons/BadgeCheck.svg');
emptyIcon.alt = 'Nincs hiányzás';
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
overlay.remove();
}
});
const emptyTitle = document.createElement('h3');
emptyTitle.textContent = LanguageManager.t('absences.title');
const handleEscape = (e) => {
if (e.key === 'Escape') {
overlay.remove();
document.removeEventListener('keydown', handleEscape);
}
};
document.addEventListener('keydown', handleEscape);
const emptyText = document.createElement('p');
emptyText.textContent = LanguageManager.t('dashboard.not_supported');
document.body.appendChild(overlay);
emptyState.appendChild(emptyIcon);
emptyState.appendChild(emptyTitle);
emptyState.appendChild(emptyText);
content.appendChild(emptyState);
} else {
sortedDates.forEach(date => {
const dayGroup = createDayGroup(date, groupedAbsences[date]);
content.appendChild(dayGroup);
requestAnimationFrame(() => {
overlay.classList.add('visible');
});
}
return content;
function createLegend() {
const legend = document.createElement('div');
legend.className = 'calendar-legend';
const legendItems = [
{ class: 'status-justified', label: LanguageManager.t('absences.justified') || 'Igazolt' },
{ class: 'status-pending', label: LanguageManager.t('absences.pending') || 'Igazolásra vár' },
{ class: 'status-unjustified', label: LanguageManager.t('absences.unjustified') || 'Igazolatlan' },
{ class: 'type-late-legend', label: LanguageManager.t('absences.late_type') || 'Késés', isType: true },
];
legendItems.forEach(item => {
const legendItem = document.createElement('div');
legendItem.className = 'legend-item';
const legendColor = document.createElement('div');
legendColor.className = `legend-color ${item.class}`;
const legendLabel = document.createElement('span');
legendLabel.textContent = item.label;
legendItem.appendChild(legendColor);
legendItem.appendChild(legendLabel);
legend.appendChild(legendItem);
});
return legend;
}
async function transformAbsencesPage() {
const { basicData, absences, groupedAbsences } = await collectAbsencesData();
const { basicData, absences, groupedByDate } = await collectAbsencesData();
document.body.textContent = '';
@@ -413,162 +468,26 @@ async function transformAbsencesPage() {
const main = document.createElement('main');
main.className = 'kreta-main';
const pageGrid = document.createElement('div');
pageGrid.className = 'absences-page';
const sidebar = document.createElement('div');
sidebar.className = 'absences-sidebar';
const filterCard = createFilterCard(absences);
sidebar.appendChild(filterCard);
const pageContent = document.createElement('div');
pageContent.className = 'absences-page';
const statsSection = createStatsSection(absences);
sidebar.appendChild(statsSection);
pageContent.appendChild(statsSection);
const absencesContent = createAbsencesContent(groupedAbsences);
const legend = createLegend();
pageContent.appendChild(legend);
pageGrid.appendChild(sidebar);
pageGrid.appendChild(absencesContent);
const calendarSection = createCalendarSection(groupedByDate, absences);
pageContent.appendChild(calendarSection);
main.appendChild(pageGrid);
main.appendChild(pageContent);
container.appendChild(main);
document.body.appendChild(container);
setupUserDropdown();
setupFilters(groupedAbsences);
loadingScreen.hide();
}
function setupFilters(originalGroupedAbsences) {
try {
const dateFilter = document.getElementById("dateFilter");
const subjectFilter = document.getElementById("subjectFilter");
const justificationFilter = document.getElementById("justificationFilter");
if (!dateFilter || !subjectFilter || !justificationFilter) {
console.warn("Some filter elements were not found in the DOM");
return;
}
const filterAbsences = () => {
try {
const dateFilterValue = dateFilter.value;
const subject = subjectFilter.value;
const justified = justificationFilter.value;
const selectedDate = dateFilterValue ? new Date(dateFilterValue) : null;
document.querySelectorAll(".absence-card").forEach((card) => {
const dateStr = card.dataset.date;
const dateParts = dateStr.split(".");
if (dateParts.length < 3) {
return;
}
const parsedYear = parseInt(dateParts[0].trim(), 10);
const parsedMonth = parseInt(dateParts[1].trim(), 10) - 1;
const parsedDay = parseInt(dateParts[2].trim(), 10);
const cardDate = new Date(parsedYear, parsedMonth, parsedDay);
let showCard = true;
if (selectedDate) {
if (
cardDate.getFullYear() !== selectedDate.getFullYear() ||
cardDate.getMonth() !== selectedDate.getMonth() ||
cardDate.getDate() !== selectedDate.getDate()
) {
showCard = false;
}
}
if (subject && card.dataset.subject !== subject) {
showCard = false;
}
if (justified && card.dataset.status !== justified) {
showCard = false;
}
card.style.display = showCard ? "" : "none";
});
updateDayGroupsVisibility();
updateStatistics();
} catch (err) {
console.error("Error during filtering absences:", err);
}
};
[dateFilter, subjectFilter, justificationFilter].forEach((filter) => {
if (filter) {
filter.addEventListener("change", filterAbsences);
}
});
} catch (err) {
console.error("Error setting up filters:", err);
}
}
function updateDayGroupsVisibility() {
document.querySelectorAll(".day-group").forEach((group) => {
const visibleCards = group.querySelectorAll('.absence-card:not([style*="display: none"])');
const dayCount = group.querySelector('.day-count');
if (visibleCards.length > 0) {
group.style.display = "";
if (dayCount) {
dayCount.textContent = `${visibleCards.length} ${LanguageManager.t('absences.hours')}`;
}
} else {
group.style.display = "none";
}
});
}
function updateStatistics() {
try {
const visibleCards = document.querySelectorAll('.absence-card:not([style*="display: none"])');
const totalVisible = visibleCards.length;
const justifiedVisible = Array.from(visibleCards).filter(
card => card.dataset.status === 'justified'
).length;
const unjustifiedVisible = Array.from(visibleCards).filter(
card => card.dataset.status === 'unjustified'
).length;
const pendingVisible = Array.from(visibleCards).filter(
card => card.dataset.status === 'pending'
).length;
const statCards = document.querySelectorAll(".stat-card");
statCards.forEach(card => {
const type = card.dataset.type;
const numberEl = card.querySelector('.stat-number');
if (numberEl) {
switch(type) {
case 'total':
numberEl.textContent = totalVisible;
break;
case 'justified':
numberEl.textContent = justifiedVisible;
break;
case 'unjustified':
numberEl.textContent = unjustifiedVisible;
break;
case 'pending':
numberEl.textContent = pendingVisible;
break;
}
}
});
} catch (err) {
console.error("Error updating statistics:", err);
}
}
if (window.location.href.includes("/Hianyzas/Hianyzasok")) {
transformAbsencesPage().catch((error) => {
console.error(LanguageManager.t("absences.page_transform_error"), error);

View File

@@ -266,7 +266,10 @@
"last_30_days": "Letzte 30 Tage",
"total_absences": "Alle Fehlzeiten",
"topic": "Thema",
"status": "Status"
"status": "Status",
"absence_type": "Abwesenheit",
"late_type": "Verspätung",
"minutes": "Minuten"
},
"profile": {
"title": "Profil",
@@ -418,8 +421,27 @@
"friday": "Freitag",
"saturday": "Samstag",
"sunday": "Sonntag",
"mon": "M",
"tue": "D",
"wed": "M",
"thu": "D",
"fri": "F",
"sat": "S",
"sun": "S",
"today": "Heute",
"tomorrow": "Morgen"
"tomorrow": "Morgen",
"january": "Januar",
"february": "Februar",
"march": "März",
"april": "April",
"may": "Mai",
"june": "Juni",
"july": "Juli",
"august": "August",
"september": "September",
"october": "Oktober",
"november": "November",
"december": "Dezember"
},
"months": {
"january": "Januar",

View File

@@ -266,7 +266,10 @@
"last_30_days": "Last 30 days",
"total_absences": "Total absences",
"topic": "Topic",
"status": "Status"
"status": "Status",
"absence_type": "Absence",
"late_type": "Late",
"minutes": "minutes"
},
"profile": {
"title": "Profile",
@@ -418,8 +421,27 @@
"friday": "Friday",
"saturday": "Saturday",
"sunday": "Sunday",
"mon": "M",
"tue": "T",
"wed": "W",
"thu": "T",
"fri": "F",
"sat": "S",
"sun": "S",
"today": "Today",
"tomorrow": "Tomorrow"
"tomorrow": "Tomorrow",
"january": "January",
"february": "February",
"march": "March",
"april": "April",
"may": "May",
"june": "June",
"july": "July",
"august": "August",
"september": "September",
"october": "October",
"november": "November",
"december": "December"
},
"months": {
"january": "January",

View File

@@ -266,9 +266,12 @@
"last_month": "Előző hónap",
"current_semester": "Aktuális félév",
"last_30_days": "Utolsó 30 nap",
"total_absences": "Összes hiányzás",
"total_absences": "Összes mulasztás",
"topic": "Téma",
"status": "Állapot"
"status": "Állapot",
"absence_type": "Hiányzás",
"late_type": "Késés",
"minutes": "perc"
},
"profile": {
"title": "Profil",
@@ -420,8 +423,27 @@
"friday": "péntek",
"saturday": "szombat",
"sunday": "vasárnap",
"mon": "H",
"tue": "K",
"wed": "Sze",
"thu": "Cs",
"fri": "P",
"sat": "Szo",
"sun": "V",
"today": "Ma",
"tomorrow": "Holnap"
"tomorrow": "Holnap",
"january": "Január",
"february": "Február",
"march": "Március",
"april": "Április",
"may": "Május",
"june": "Június",
"july": "Július",
"august": "Augusztus",
"september": "Szeptember",
"october": "Október",
"november": "November",
"december": "December"
},
"months": {
"january": "január",