Files
CalendarReminder/CalendarReminder.Api/wwwroot/index.html
T
2026-06-11 22:01:31 +02:00

1459 lines
63 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="sk">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Calendar Reminder</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
height: 100%; width: 100%;
font-family: 'Segoe UI', sans-serif;
background: #0d1117; color: #e1e4e8;
overflow: hidden;
}
/* ══════════════════════════════
AUTH
══════════════════════════════ */
#auth-screen {
display: flex; align-items: center; justify-content: center;
height: 100%; background: linear-gradient(135deg, #1a1d2e, #0d1117);
}
.auth-card {
background: #161b22; border: 1px solid #30363d; border-radius: 14px;
padding: 2.25rem; width: 100%; max-width: 420px;
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
}
.auth-logo { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.3rem; }
.auth-logo .logo-icon { font-size: 1.8rem; }
.auth-logo h1 { font-size: 1.45rem; color: #58a6ff; font-weight: 700; letter-spacing: -0.3px; }
.auth-card > p { color: #8b949e; font-size: 0.88rem; margin-bottom: 1.75rem; padding-left: 2.4rem; }
.forgot-link { font-size: 0.8rem; color: #58a6ff; cursor: pointer; text-align: right; margin-top: -0.5rem; margin-bottom: 1rem; display: block; background: none; border: none; padding: 0; }
.forgot-link:hover { text-decoration: underline; }
.back-link { font-size: 0.8rem; color: #8b949e; cursor: pointer; background: none; border: none; padding: 0; margin-bottom: 1.25rem; display: flex; align-items: center; gap: 0.3rem; }
.back-link:hover { color: #c9d1d9; }
.reset-title { font-size: 0.95rem; font-weight: 600; color: #c9d1d9; margin-bottom: 0.25rem; }
.reset-subtitle { font-size: 0.82rem; color: #8b949e; margin-bottom: 1.25rem; }
.send-code-row { display: flex; gap: 0.5rem; margin-bottom: 0.9rem; }
.send-code-row input { flex: 1; }
.btn-send-code { padding: 0.6rem 0.85rem; background: #1f6feb22; border: 1px solid #1f6feb55; border-radius: 7px; color: #58a6ff; cursor: pointer; font-size: 0.82rem; white-space: nowrap; transition: all 0.15s; }
.btn-send-code:hover { background: #1f6feb44; }
.btn-send-code:disabled { opacity: 0.4; cursor: not-allowed; }
.code-section { display: none; }
.fg { margin-bottom: 0.9rem; }
.fg label { display: block; font-size: 0.8rem; color: #8b949e; margin-bottom: 0.35rem; text-transform: uppercase; letter-spacing: 0.4px; }
.fg input { width: 100%; padding: 0.6rem 0.85rem; background: #0d1117; border: 1px solid #30363d; border-radius: 7px; color: #e1e4e8; font-size: 0.95rem; outline: none; transition: border-color 0.15s; }
.fg input:focus { border-color: #58a6ff; }
.btn-primary { width: 100%; padding: 0.72rem; background: #238636; border: none; border-radius: 7px; color: #fff; font-size: 0.95rem; font-weight: 600; cursor: pointer; margin-top: 0.25rem; transition: background 0.15s; }
.btn-primary:hover { background: #2ea043; }
.err { color: #f85149; font-size: 0.82rem; margin-top: 0.65rem; text-align: center; min-height: 1.1rem; }
/* ══════════════════════════════
APP SHELL — 3-row grid
row1: topbar (48px)
row2: app body (fill)
══════════════════════════════ */
#app-screen {
display: none;
flex-direction: column;
height: 100%;
}
/* ── TOP BAR ── */
.topbar {
height: 48px; min-height: 48px;
background: #161b22;
border-bottom: 1px solid #30363d;
display: flex; align-items: center;
padding: 0 1.5rem;
gap: 1rem;
width: 100%;
flex-shrink: 0;
z-index: 100;
}
.topbar-brand {
display: flex; align-items: center; gap: 0.55rem;
flex: 0 0 auto;
}
.topbar-brand .brand-icon { font-size: 1.2rem; }
.topbar-brand h1 {
font-size: 1rem; font-weight: 700; color: #58a6ff;
letter-spacing: -0.2px; white-space: nowrap;
}
.topbar-spacer { flex: 1; }
.topbar-user {
display: flex; align-items: center; gap: 0.9rem;
flex: 0 0 auto;
}
.topbar-avatar {
width: 28px; height: 28px; border-radius: 50%;
background: linear-gradient(135deg, #1f6feb, #388bfd);
display: flex; align-items: center; justify-content: center;
font-size: 0.75rem; font-weight: 700; color: #fff; flex-shrink: 0;
}
.topbar-username { font-size: 0.88rem; color: #c9d1d9; font-weight: 500; }
.btn-logout {
padding: 0.28rem 0.85rem; background: transparent;
border: 1px solid #30363d; border-radius: 6px;
color: #8b949e; cursor: pointer; font-size: 0.8rem;
transition: all 0.15s;
}
.btn-logout:hover { border-color: #f85149; color: #f85149; }
/* ── AVATAR DROPDOWN ── */
.avatar-wrap { position: relative; }
.topbar-avatar { cursor: pointer; transition: box-shadow 0.15s; }
.topbar-avatar:hover { box-shadow: 0 0 0 2px #388bfd; }
.avatar-menu {
display: none; position: absolute; top: calc(100% + 8px); right: 0;
background: #1c1f2e; border: 1px solid #30363d; border-radius: 9px;
min-width: 180px; z-index: 500;
box-shadow: 0 8px 24px rgba(0,0,0,0.55);
overflow: hidden;
}
.avatar-menu.open { display: block; }
.avatar-menu-item {
display: flex; align-items: center; gap: 0.6rem;
padding: 0.62rem 1rem; font-size: 0.85rem; color: #c9d1d9;
cursor: pointer; transition: background 0.12s; border: none;
background: none; width: 100%; text-align: left;
}
.avatar-menu-item:hover { background: #30363d; }
.avatar-menu-item .mi-icon { font-size: 1rem; }
.avatar-menu-sep { border: none; border-top: 1px solid #30363d; margin: 3px 0; }
/* ── ADMIN PANEL ── */
.admin-overlay {
display: none; position: fixed; inset: 0;
background: rgba(0,0,0,0.7); z-index: 600;
align-items: flex-start; justify-content: center;
overflow-y: auto; padding: 2rem 1rem;
}
.admin-overlay.open { display: flex; }
.admin-panel {
background: #161b22; border: 1px solid #30363d; border-radius: 14px;
width: 100%; max-width: 820px;
box-shadow: 0 12px 48px rgba(0,0,0,0.7);
display: flex; flex-direction: column;
min-height: 0;
}
.admin-head {
display: flex; align-items: center; gap: 1rem;
padding: 1rem 1.5rem; border-bottom: 1px solid #30363d; flex-shrink: 0;
}
.admin-head h2 { font-size: 1rem; color: #c9d1d9; margin: 0; flex: 1; }
.btn-admin-close {
background: none; border: none; color: #6e7681; cursor: pointer;
font-size: 1.2rem; line-height: 1; padding: 2px;
transition: color 0.12s;
}
.btn-admin-close:hover { color: #f85149; }
.admin-tabs {
display: flex; gap: 0; border-bottom: 1px solid #30363d; flex-shrink: 0;
}
.admin-tab {
padding: 0.62rem 1.25rem; font-size: 0.85rem; color: #8b949e;
cursor: pointer; background: none; border: none;
border-bottom: 2px solid transparent; transition: all 0.12s;
font-weight: 500;
}
.admin-tab:hover { color: #c9d1d9; }
.admin-tab.active { color: #58a6ff; border-bottom-color: #58a6ff; }
.admin-body { padding: 1.25rem 1.5rem; flex: 1; }
.admin-section { display: none; }
.admin-section.active { display: block; }
.admin-toolbar {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 1rem;
}
.admin-toolbar h3 { font-size: 0.9rem; color: #8b949e; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; margin: 0; }
.btn-admin-add {
padding: 0.38rem 0.9rem; background: #238636; border: none; border-radius: 6px;
color: #fff; font-size: 0.82rem; font-weight: 600; cursor: pointer; transition: background 0.15s;
}
.btn-admin-add:hover { background: #2ea043; }
.admin-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
.admin-table th {
text-align: left; color: #6e7681; font-size: 0.74rem; text-transform: uppercase;
letter-spacing: 0.4px; padding: 0.4rem 0.75rem; border-bottom: 1px solid #21262d;
}
.admin-table td { padding: 0.55rem 0.75rem; border-bottom: 1px solid #21262d; color: #c9d1d9; vertical-align: middle; }
.admin-table tr:last-child td { border-bottom: none; }
.admin-table tr:hover td { background: #1c1f2e; }
.td-actions { display: flex; gap: 0.4rem; }
.btn-tbl { padding: 0.22rem 0.6rem; border-radius: 5px; font-size: 0.76rem; cursor: pointer; border: 1px solid; transition: all 0.12s; }
.btn-tbl-edit { border-color: #1f6feb55; color: #58a6ff; background: #1f6feb10; }
.btn-tbl-edit:hover { background: #1f6feb33; }
.btn-tbl-del { border-color: #f8514955; color: #f85149; background: #f8514910; }
.btn-tbl-del:hover { background: #f8514930; }
.badge-admin { padding: 1px 7px; border-radius: 8px; font-size: 0.72rem; font-weight: 700; background: #1f6feb22; color: #388bfd; }
.badge-mustchange { padding: 1px 7px; border-radius: 8px; font-size: 0.72rem; font-weight: 700; background: #e3b34122; color: #e3b341; }
/* Mini form in admin */
.admin-form { background: #0d1117; border: 1px solid #30363d; border-radius: 9px; padding: 1rem; margin-bottom: 1.25rem; }
.admin-form h4 { font-size: 0.8rem; color: #8b949e; text-transform: uppercase; letter-spacing: 0.4px; margin-bottom: 0.75rem; }
.admin-form .row { display: flex; gap: 0.6rem; flex-wrap: wrap; }
.admin-form .row > * { flex: 1 1 140px; }
.admin-form input, .admin-form select {
width: 100%; padding: 0.48rem 0.65rem;
background: #161b22; border: 1px solid #30363d; border-radius: 6px;
color: #e1e4e8; font-size: 0.85rem; outline: none; font-family: inherit;
transition: border-color 0.15s;
}
.admin-form input:focus, .admin-form select:focus { border-color: #58a6ff; }
.admin-form select option { background: #161b22; }
.admin-form label { display: block; font-size: 0.75rem; color: #8b949e; margin-bottom: 0.28rem; text-transform: uppercase; letter-spacing: 0.3px; }
.admin-form-actions { display: flex; gap: 0.6rem; margin-top: 0.75rem; justify-content: flex-end; }
.btn-admin-save { padding: 0.42rem 1rem; background: #1f6feb; border: none; border-radius: 6px; color: #fff; cursor: pointer; font-weight: 600; font-size: 0.84rem; }
.btn-admin-cancel { padding: 0.42rem 0.9rem; background: transparent; border: 1px solid #30363d; border-radius: 6px; color: #8b949e; cursor: pointer; font-size: 0.84rem; }
.admin-err { color: #f85149; font-size: 0.8rem; min-height: 1.1rem; margin-top: 0.35rem; }
/* ── APP BODY ── */
.app-body {
flex: 1;
display: flex;
min-height: 0; /* critical for flex child overflow */
}
/* ══════════════════════════════
CALENDAR PANEL — fills remaining space
══════════════════════════════ */
.cal-panel {
flex: 1 1 0%;
min-width: 0;
border-right: 1px solid #30363d;
display: flex; flex-direction: column;
overflow: hidden;
}
.cal-header {
height: 48px; min-height: 48px;
display: flex; align-items: center; justify-content: space-between;
padding: 0 1.25rem;
border-bottom: 1px solid #30363d;
flex-shrink: 0;
}
.cal-title { font-size: 1rem; font-weight: 700; color: #c9d1d9; min-width: 200px; }
.cal-nav { display: flex; gap: 0.4rem; align-items: center; }
.btn-nav {
padding: 0.28rem 0.75rem; background: transparent; border: 1px solid #30363d;
border-radius: 6px; color: #8b949e; cursor: pointer; font-size: 0.9rem;
transition: all 0.15s; line-height: 1;
}
.btn-nav:hover { border-color: #58a6ff; color: #58a6ff; }
.btn-today {
padding: 0.28rem 0.85rem; background: #1f6feb18; border: 1px solid #1f6feb55;
border-radius: 6px; color: #58a6ff; cursor: pointer; font-size: 0.8rem; font-weight: 600;
transition: all 0.15s;
}
.btn-today:hover { background: #1f6feb33; }
/* Calendar grid */
.cal-grid-wrap {
flex: 1;
min-height: 0;
display: flex; flex-direction: column;
padding: 0.6rem;
overflow: hidden;
}
.cal-weekdays {
display: grid; grid-template-columns: repeat(7, 1fr);
margin-bottom: 0.4rem; flex-shrink: 0;
}
.cal-weekday {
text-align: center; font-size: 0.72rem; color: #6e7681;
font-weight: 700; padding: 0.25rem 0; text-transform: uppercase; letter-spacing: 0.5px;
}
.cal-days {
flex: 1;
display: grid; grid-template-columns: repeat(7, 1fr);
grid-auto-rows: 1fr;
gap: 4px;
min-height: 0;
}
.cal-day {
background: #161b22; border: 1px solid #21262d;
border-radius: 6px; padding: 0.3rem 0.4rem;
cursor: pointer; transition: border-color 0.12s;
overflow: hidden; display: flex; flex-direction: column;
min-height: 0;
}
.cal-day:hover { border-color: #388bfd55; }
.cal-day.other-month { opacity: 0.28; }
.cal-day.today { border-color: #388bfd; background: #0d1f3c; }
.cal-day.selected { border-color: #58a6ff; background: #0d1f3c; }
.cal-day.has-reminder { background: #12192a; }
.day-num {
font-size: 0.72rem; font-weight: 700; color: #6e7681;
text-align: right; flex-shrink: 0; margin-bottom: 2px;
}
.cal-day.today .day-num { color: #58a6ff; }
.cal-day-events { flex: 1; overflow: hidden; display: flex; flex-direction: column; gap: 2px; min-height: 0; }
.cal-reminder-dot {
font-size: 0.67rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
padding: 1px 5px; border-radius: 3px; flex-shrink: 0; cursor: pointer; line-height: 1.5;
}
.dot-mine { background: #0f2a0f; color: #3fb950; border-left: 2px solid #3fb950; }
.dot-shared { background: #0d1f3c; color: #79c0ff; border-left: 2px solid #388bfd; }
.dot-done { background: #161b22; color: #484f58; text-decoration: line-through; }
.dot-overdue { background: #2d1515; color: #f85149; border-left: 2px solid #f85149; }
.dot-more { font-size: 0.63rem; color: #484f58; padding: 0 4px; flex-shrink: 0; }
/* ══════════════════════════════
SIDEBAR — fixed 350px
══════════════════════════════ */
.sidebar {
flex: 0 0 350px; width: 350px; min-width: 350px;
display: flex; flex-direction: column;
overflow: hidden;
}
/* Add form */
.add-section {
border-bottom: 1px solid #30363d;
padding: 0.9rem 1rem;
flex-shrink: 0;
background: #0d1117;
}
.add-section h3 { font-size: 0.8rem; color: #8b949e; margin-bottom: 0.6rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; }
.add-section input, .add-section textarea {
width: 100%; padding: 0.48rem 0.65rem;
background: #161b22; border: 1px solid #30363d; border-radius: 6px;
color: #e1e4e8; font-size: 0.85rem; outline: none;
font-family: inherit; margin-bottom: 0.45rem;
transition: border-color 0.15s;
}
.add-section input:focus, .add-section textarea:focus { border-color: #58a6ff; }
.add-section textarea { resize: none; height: 44px; }
.date-row { display: flex; gap: 0.45rem; }
.date-row input { margin-bottom: 0; }
.date-row input[type="date"] { flex: 1 1 55%; }
.date-row input[type="time"] { flex: 1 1 45%; }
.add-section input[type="date"]::-webkit-calendar-picker-indicator,
.add-section input[type="time"]::-webkit-calendar-picker-indicator {
filter: invert(0.65) brightness(1.4);
cursor: pointer; opacity: 0.7;
}
.add-section input[type="date"]::-webkit-calendar-picker-indicator:hover,
.add-section input[type="time"]::-webkit-calendar-picker-indicator:hover { opacity: 1; }
.user-picker { position: relative; margin-bottom: 0.45rem; }
.user-picker-tags { display: flex; flex-wrap: wrap; gap: 3px; margin-bottom: 0.3rem; }
.user-tag {
display: flex; align-items: center; gap: 3px;
background: #0d1f3c; border: 1px solid #1f6feb55; border-radius: 10px;
padding: 1px 8px; font-size: 0.73rem; color: #79c0ff;
}
.user-tag button { background: none; border: none; color: #79c0ff; cursor: pointer; font-size: 0.8rem; padding: 0; line-height: 1; }
.user-picker-input {
width: 100%; padding: 0.48rem 0.65rem;
background: #161b22; border: 1px solid #30363d; border-radius: 6px;
color: #e1e4e8; font-size: 0.85rem; outline: none; cursor: pointer;
transition: border-color 0.15s;
}
.user-picker-input:focus { border-color: #58a6ff; }
.user-dropdown {
display: none; position: absolute; top: 100%; left: 0; right: 0; z-index: 200;
background: #1c1f2e; border: 1px solid #30363d; border-radius: 6px;
max-height: 150px; overflow-y: auto; margin-top: 2px;
box-shadow: 0 4px 16px rgba(0,0,0,0.4);
}
.user-dropdown.open { display: block; }
.user-option { padding: 0.42rem 0.7rem; font-size: 0.82rem; cursor: pointer; display: flex; align-items: center; justify-content: space-between; }
.user-option:hover { background: #30363d; }
.user-option.selected { color: #3fb950; }
.user-option .uname { font-weight: 600; }
.user-option .uemail { font-size: 0.73rem; color: #6e7681; }
.btn-add-reminder {
width: 100%; padding: 0.48rem; background: #238636; border: none;
border-radius: 6px; color: #fff; font-size: 0.85rem; font-weight: 600; cursor: pointer;
transition: background 0.15s;
}
.btn-add-reminder:hover { background: #2ea043; }
/* List */
.list-section { flex: 1; overflow-y: auto; padding: 0.9rem 1rem; min-height: 0; }
.list-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.55rem; }
.list-header h3 { font-size: 0.8rem; color: #8b949e; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; }
.filters { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 0.6rem; }
.filter-btn {
padding: 0.22rem 0.6rem; border: 1px solid #30363d; border-radius: 10px;
background: transparent; color: #8b949e; cursor: pointer; font-size: 0.73rem;
transition: all 0.12s;
}
.filter-btn.active { background: #1f6feb; border-color: #1f6feb; color: #fff; }
.filter-btn:hover:not(.active) { border-color: #484f58; color: #c9d1d9; }
.reminder-item {
background: #161b22; border: 1px solid #21262d; border-radius: 7px;
padding: 0.55rem 0.7rem; margin-bottom: 0.4rem; cursor: default;
transition: border-color 0.12s;
}
.reminder-item:hover { border-color: #30363d; }
.reminder-item.overdue { border-left: 3px solid #f85149; }
.reminder-item.soon { border-left: 3px solid #e3b341; }
.reminder-item.completed { opacity: 0.4; }
.reminder-item.shared-with-me { border-left: 3px solid #388bfd; }
.ri-top { display: flex; align-items: flex-start; gap: 0.45rem; }
.check-btn {
width: 17px; height: 17px; border-radius: 50%; border: 2px solid #30363d;
background: transparent; cursor: pointer; flex-shrink: 0; margin-top: 2px;
display: flex; align-items: center; justify-content: center; font-size: 9px; color: #fff;
transition: all 0.12s;
}
.check-btn.done { background: #238636; border-color: #238636; }
.ri-title { font-size: 0.85rem; font-weight: 600; flex: 1; line-height: 1.3; }
.ri-title.done { text-decoration: line-through; color: #484f58; }
.ri-actions { display: flex; gap: 3px; opacity: 0; transition: opacity 0.12s; }
.reminder-item:hover .ri-actions { opacity: 1; }
.btn-icon { padding: 2px 5px; background: transparent; border: 1px solid #30363d; border-radius: 4px; color: #8b949e; cursor: pointer; font-size: 0.72rem; }
.btn-icon:hover { border-color: #f85149; color: #f85149; }
.ri-meta { font-size: 0.73rem; color: #6e7681; margin-top: 0.22rem; display: flex; justify-content: space-between; }
.ri-badge { display: inline-block; padding: 1px 5px; border-radius: 6px; font-size: 0.68rem; font-weight: 700; margin-left: 4px; }
.badge-red { background: #2d1515; color: #f85149; }
.badge-yellow { background: #2d2700; color: #e3b341; }
.empty-state { text-align: center; padding: 2.5rem 1rem; color: #484f58; font-size: 0.85rem; }
/* Scrollbar styling */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #30363d; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #484f58; }
/* ══════════════════════════════
EDIT MODAL
══════════════════════════════ */
.modal-overlay {
display: none; position: fixed; inset: 0;
background: rgba(0,0,0,0.75); align-items: center; justify-content: center; z-index: 300;
}
.modal-overlay.open { display: flex; }
.modal {
background: #1c1f2e; border: 1px solid #30363d; border-radius: 12px;
padding: 1.5rem; width: 100%; max-width: 440px;
box-shadow: 0 8px 32px rgba(0,0,0,0.6);
}
.modal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem; }
.modal-head h2 { font-size: 0.95rem; color: #c9d1d9; margin: 0; }
.btn-send-email {
padding: 0.3rem 0.65rem; background: transparent;
border: 1px solid #30363d; border-radius: 6px;
color: #8b949e; cursor: pointer; font-size: 1rem;
transition: all 0.15s; line-height: 1;
}
.btn-send-email:hover { border-color: #58a6ff; color: #58a6ff; }
.btn-send-email:disabled { opacity: 0.4; cursor: not-allowed; }
.send-status { font-size: 0.78rem; margin-top: 0.4rem; min-height: 1rem; text-align: right; }
.modal .fg { margin-bottom: 0.75rem; }
.modal .fg label { display: block; font-size: 0.78rem; color: #8b949e; margin-bottom: 0.3rem; text-transform: uppercase; letter-spacing: 0.4px; }
.modal .fg input { width: 100%; padding: 0.5rem 0.7rem; background: #0d1117; border: 1px solid #30363d; border-radius: 6px; color: #e1e4e8; font-size: 0.9rem; outline: none; }
.modal .fg input:focus { border-color: #58a6ff; }
.modal-actions { display: flex; gap: 0.75rem; justify-content: flex-end; margin-top: 1.1rem; }
.btn-cancel { padding: 0.45rem 1rem; background: transparent; border: 1px solid #30363d; border-radius: 6px; color: #8b949e; cursor: pointer; }
.btn-save { padding: 0.45rem 1.1rem; background: #1f6feb; border: none; border-radius: 6px; color: #fff; cursor: pointer; font-weight: 600; }
</style>
</head>
<body>
<!-- ══════════ AUTH ══════════ -->
<div id="auth-screen">
<div class="auth-card">
<div class="auth-logo">
<span class="logo-icon">📅</span>
<h1>Calendar Reminder</h1>
</div>
<p>Správa udalostí s emailovými notifikáciami</p>
<!-- LOGIN -->
<div id="login-form">
<div class="fg"><label>Email alebo meno</label><input type="text" id="l-email" placeholder="admin" /></div>
<div class="fg"><label>Heslo</label><input type="password" id="l-pass" placeholder="••••••••" onkeydown="if(event.key==='Enter')doLogin()" /></div>
<button class="forgot-link" onclick="showReset()">Zabudol som heslo</button>
<button class="btn-primary" onclick="doLogin()">Prihlásiť sa</button>
<div class="err" id="l-err"></div>
</div>
<!-- RESET HESLA -->
<div id="reset-form" style="display:none">
<button class="back-link" onclick="showLogin()">← Späť na prihlásenie</button>
<div class="reset-title">Obnova hesla</div>
<div class="reset-subtitle" id="reset-subtitle">Zadaj email a nové heslo.</div>
<div class="send-code-row">
<input type="email" id="r-email" placeholder="Email *" />
<button class="btn-send-code" id="btn-send-code" onclick="sendResetCode()">Zaslať kód</button>
</div>
<!-- Kód — skrytý, zobrazí sa po aktivácii mailovej služby -->
<div class="code-section" id="code-section">
<div class="fg"><label>Kód z emailu (8 znakov)</label><input type="text" id="r-code" placeholder="XXXXXXXX" maxlength="8" style="text-transform:uppercase;letter-spacing:0.3rem;font-weight:700" /></div>
</div>
<div class="fg"><label>Nové heslo</label><input type="password" id="r-newpass" placeholder="min. 6 znakov" /></div>
<div class="fg"><label>Potvrď heslo</label><input type="password" id="r-newpass2" placeholder="zopakuj heslo" onkeydown="if(event.key==='Enter')doResetPassword()" /></div>
<button class="btn-primary" onclick="doResetPassword()">Zmeniť heslo</button>
<div class="err" id="r-err"></div>
</div>
</div>
</div>
<!-- ══════════ APP ══════════ -->
<div id="app-screen">
<!-- TOP BAR -->
<div class="topbar">
<div class="topbar-brand">
<span class="brand-icon">📅</span>
<h1>Calendar Reminder</h1>
</div>
<div class="topbar-spacer"></div>
<div class="topbar-user">
<span class="topbar-username" id="hdr-user"></span>
<div class="avatar-wrap" id="avatar-wrap">
<div class="topbar-avatar" id="hdr-avatar" onclick="toggleAvatarMenu()">?</div>
<div class="avatar-menu" id="avatar-menu">
<button class="avatar-menu-item" id="mi-users" onclick="openAdmin('users')" style="display:none"><span class="mi-icon">👥</span> Správca užívateľov</button>
<button class="avatar-menu-item" id="mi-families" onclick="openAdmin('families')" style="display:none"><span class="mi-icon">🏠</span> Správca rodín</button>
<button class="avatar-menu-item" id="mi-settings" onclick="openAdmin('settings')" style="display:none"><span class="mi-icon">⚙️</span> Nastavenia</button>
<hr class="avatar-menu-sep" id="mi-sep" style="display:none" />
<button class="avatar-menu-item" onclick="doLogout()"><span class="mi-icon">🚪</span> Odhlásiť sa</button>
</div>
</div>
</div>
</div>
<!-- APP BODY -->
<div class="app-body">
<!-- CALENDAR (flex: 1) -->
<div class="cal-panel">
<div class="cal-header">
<div class="cal-nav">
<button class="btn-nav" onclick="changeMonth(-1)">&#8249;</button>
<button class="btn-nav" onclick="changeMonth(1)">&#8250;</button>
<button class="btn-today" onclick="goToday()">Dnes</button>
</div>
<div class="cal-title" id="cal-title"></div>
<div style="min-width:120px"></div>
</div>
<div class="cal-grid-wrap">
<div class="cal-weekdays">
<div class="cal-weekday">Pon</div>
<div class="cal-weekday">Uto</div>
<div class="cal-weekday">Str</div>
<div class="cal-weekday">Štv</div>
<div class="cal-weekday">Pia</div>
<div class="cal-weekday">Sob</div>
<div class="cal-weekday">Ned</div>
</div>
<div class="cal-days" id="cal-days"></div>
</div>
</div>
<!-- SIDEBAR (fixed 350px) -->
<div class="sidebar">
<div class="add-section">
<h3>Nová pripomienka</h3>
<input type="text" id="n-title" placeholder="Názov *" />
<div class="date-row">
<input type="date" id="n-date-day"
onfocus="document.getElementById('picker-dropdown').classList.remove('open')"
onchange="document.getElementById('picker-dropdown').classList.remove('open')" />
<input type="time" id="n-date-time" value="08:00"
onfocus="document.getElementById('picker-dropdown').classList.remove('open')"
onchange="this.blur()" />
</div>
<textarea id="n-desc" placeholder="Popis (voliteľné)"></textarea>
<div class="user-picker" id="user-picker">
<div class="user-picker-tags" id="picker-tags"></div>
<input class="user-picker-input" id="picker-input"
placeholder="👥 Zdieľať s používateľom..."
autocomplete="off"
onfocus="openPicker()"
oninput="searchUsers(this.value)" />
<div class="user-dropdown" id="picker-dropdown"></div>
</div>
<button class="btn-add-reminder" onclick="createReminder()">Pridať pripomienku</button>
</div>
<div class="list-section">
<div class="list-header">
<h3>Zoznam</h3>
</div>
<div class="filters">
<button class="filter-btn active" onclick="setFilter('all',this)">Všetky</button>
<button class="filter-btn" onclick="setFilter('week',this)">Tento týždeň</button>
<button class="filter-btn" onclick="setFilter('today',this)">Dnes</button>
<button class="filter-btn" onclick="setFilter('upcoming',this)">Nadchádzajúce</button>
<button class="filter-btn" onclick="setFilter('completed',this)">Dokončené</button>
</div>
<div id="reminder-list"></div>
</div>
</div>
</div>
</div>
<!-- EDIT MODAL -->
<div class="modal-overlay" id="edit-modal">
<div class="modal">
<div class="modal-head">
<h2>✏️ Upraviť pripomienku</h2>
<button class="btn-send-email" id="btn-send-email" onclick="sendReminderEmail()" title="Odoslať email zdieľaným používateľom">✉️</button>
</div>
<input type="hidden" id="e-id" />
<div class="fg"><label>Názov</label><input type="text" id="e-title" /></div>
<div class="fg"><label>Dátum a čas</label><input type="datetime-local" id="e-date" /></div>
<div class="fg"><label>Popis</label><input type="text" id="e-desc" /></div>
<div class="send-status" id="send-status"></div>
<div class="modal-actions">
<button class="btn-cancel" onclick="closeModal()">Zrušiť</button>
<button class="btn-save" onclick="saveEdit()">Uložiť</button>
</div>
</div>
</div>
<!-- ══════════ ADMIN PANEL ══════════ -->
<div class="admin-overlay" id="admin-overlay" onclick="onAdminOverlayClick(event)">
<div class="admin-panel">
<div class="admin-head">
<h2>⚙️ Administrácia</h2>
<button class="btn-admin-close" onclick="closeAdmin()"></button>
</div>
<div class="admin-tabs">
<button class="admin-tab" data-tab="users" onclick="switchAdminTab('users')">👥 Správca užívateľov</button>
<button class="admin-tab" data-tab="families" onclick="switchAdminTab('families')">🏠 Správca rodín</button>
<button class="admin-tab" data-tab="settings" onclick="switchAdminTab('settings')">⚙️ Nastavenia</button>
</div>
<div class="admin-body">
<!-- USERS TAB -->
<div class="admin-section" id="tab-users">
<div id="user-form-wrap" style="display:none">
<div class="admin-form">
<h4 id="user-form-title">Nový používateľ</h4>
<input type="hidden" id="uf-id" value="" />
<div class="row">
<div><label>Meno</label><input id="uf-username" placeholder="johndoe" /></div>
<div><label>Email</label><input id="uf-email" type="email" placeholder="john@example.com" /></div>
<div><label>Rodina</label>
<select id="uf-family"><option value="">— bez rodiny —</option></select>
</div>
</div>
<div class="admin-err" id="uf-err"></div>
<div class="admin-form-actions">
<button class="btn-admin-cancel" onclick="hideUserForm()">Zrušiť</button>
<button class="btn-admin-save" onclick="saveUser()">Uložiť</button>
</div>
</div>
</div>
<div class="admin-toolbar">
<h3>Používatelia</h3>
<button class="btn-admin-add" onclick="showUserForm()">+ Pridať</button>
</div>
<table class="admin-table">
<thead><tr><th>Meno</th><th>Email</th><th>Rodina</th><th>Roly</th><th></th></tr></thead>
<tbody id="admin-users-tbody"></tbody>
</table>
</div>
<!-- FAMILIES TAB -->
<div class="admin-section" id="tab-families">
<div id="family-form-wrap" style="display:none">
<div class="admin-form">
<h4 id="fam-form-title">Nová rodina</h4>
<input type="hidden" id="ff-id" value="" />
<div class="row" style="max-width:340px">
<div><label>Názov rodiny</label><input id="ff-name" placeholder="Moja rodina" /></div>
</div>
<div class="admin-err" id="ff-err"></div>
<div class="admin-form-actions">
<button class="btn-admin-cancel" onclick="hideFamilyForm()">Zrušiť</button>
<button class="btn-admin-save" onclick="saveFamily()">Uložiť</button>
</div>
</div>
</div>
<div class="admin-toolbar">
<h3>Rodiny</h3>
<button class="btn-admin-add" onclick="showFamilyForm()">+ Pridať</button>
</div>
<table class="admin-table">
<thead><tr><th>Názov</th><th>Počet členov</th><th></th></tr></thead>
<tbody id="admin-families-tbody"></tbody>
</table>
</div>
<!-- SETTINGS TAB -->
<div class="admin-section" id="tab-settings">
<p style="color:#8b949e;font-size:0.88rem;margin-top:0.5rem">Nastavenia budú dostupné čoskoro.</p>
</div>
</div>
</div>
</div>
<!-- MUST-CHANGE-PASSWORD MODAL -->
<div class="modal-overlay" id="mcp-modal">
<div class="modal" style="max-width:380px">
<div class="modal-head">
<h2>🔐 Zmeň si heslo</h2>
</div>
<p style="font-size:0.84rem;color:#8b949e;margin-bottom:1rem">
Tvoj účet bol vytvorený administrátorom.<br>Prosím zmeň si heslo pred pokračovaním.
</p>
<div class="fg"><label>Nové heslo</label><input type="password" id="mcp-pass" placeholder="min. 6 znakov" /></div>
<div class="fg"><label>Potvrď heslo</label><input type="password" id="mcp-pass2" placeholder="zopakuj heslo" onkeydown="if(event.key==='Enter')doMcpChange()" /></div>
<div class="err" id="mcp-err"></div>
<div class="modal-actions">
<button class="btn-save" onclick="doMcpChange()">Zmeniť heslo</button>
</div>
</div>
</div>
<script>
const API = '/api';
const v = id => document.getElementById(id).value;
const auth = () => ({ 'Authorization': `Bearer ${token}` });
const MONTHS_SK = ['Január','Február','Marec','Apríl','Máj','Jún','Júl','August','September','Október','November','December'];
let token = localStorage.getItem('token');
let myId = parseInt(localStorage.getItem('myId') || '0');
let reminders = [];
let currentFilter = 'all';
let calYear, calMonth;
let selectedUsers = [];
let allUsers = [];
let pickerDebounce = null;
const now0 = new Date();
calYear = now0.getFullYear();
calMonth = now0.getMonth();
if (token) showApp();
// ═══ AUTH ═══
function showLogin() {
document.getElementById('login-form').style.display = 'block';
document.getElementById('reset-form').style.display = 'none';
document.getElementById('l-err').textContent = '';
}
function showReset() {
document.getElementById('login-form').style.display = 'none';
document.getElementById('reset-form').style.display = 'block';
document.getElementById('r-err').textContent = '';
document.getElementById('reset-subtitle').textContent = 'Zadaj email a nové heslo.';
}
async function doLogin() {
const err = document.getElementById('l-err');
err.textContent = '';
const res = await fetch(`${API}/auth/login`, {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ email: v('l-email'), password: v('l-pass') })
});
if (res.ok) {
const d = await res.json();
saveAuth(d);
// Reset všetkých auth inputov
['l-email','l-pass','r-email','r-newpass','r-newpass2','r-code'].forEach(id => {
const el = document.getElementById(id);
if (el) el.value = '';
});
document.getElementById('l-err').textContent = '';
document.getElementById('r-err').textContent = '';
showLogin();
showApp();
} else err.textContent = await res.text();
}
// Zaslanie kódu — aktivuje sa keď bude mailová služba funkčná
const RESET_WITH_CODE = true;
async function sendResetCode() {
const email = v('r-email').trim();
if (!email) { document.getElementById('r-err').textContent = 'Zadaj email.'; return; }
const btn = document.getElementById('btn-send-code');
btn.disabled = true;
const res = await fetch(`${API}/auth/send-reset-code`, {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ email })
});
btn.disabled = false;
if (res.ok) {
document.getElementById('code-section').style.display = 'block';
document.getElementById('reset-subtitle').textContent = 'Kód bol odoslaný na email. Zadaj ho nižšie.';
document.getElementById('r-err').style.color = '#3fb950';
document.getElementById('r-err').textContent = '✓ Kód odoslaný';
} else {
document.getElementById('r-err').style.color = '#f85149';
document.getElementById('r-err').textContent = await res.text();
}
}
async function doResetPassword() {
const err = document.getElementById('r-err');
err.style.color = '#f85149';
err.textContent = '';
const email = v('r-email').trim();
const pass = v('r-newpass');
const pass2 = v('r-newpass2');
const code = v('r-code').trim().toUpperCase();
if (!email) { err.textContent = 'Zadaj email.'; return; }
if (pass.length < 6) { err.textContent = 'Heslo musí mať aspoň 6 znakov.'; return; }
if (pass !== pass2) { err.textContent = 'Heslá sa nezhodujú.'; return; }
if (RESET_WITH_CODE && !code) { err.textContent = 'Zadaj kód z emailu.'; return; }
const res = await fetch(`${API}/auth/reset-password`, {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ email, newPassword: pass, code: RESET_WITH_CODE ? code : null })
});
if (res.ok) {
err.style.color = '#3fb950';
err.textContent = '✓ Heslo bolo zmenené. Môžeš sa prihlásiť.';
setTimeout(showLogin, 2000);
} else {
err.textContent = await res.text();
}
}
function saveAuth(d) {
localStorage.setItem('token', d.token);
localStorage.setItem('user', JSON.stringify(d));
try {
const payload = JSON.parse(atob(d.token.split('.')[1]));
const uid = payload['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier'] || payload.sub;
myId = parseInt(uid);
localStorage.setItem('myId', String(myId));
} catch {}
token = d.token;
}
let isAdmin = false;
function doLogout() {
localStorage.clear(); token = null; myId = 0; isAdmin = false;
document.getElementById('auth-screen').style.display = 'flex';
document.getElementById('app-screen').style.display = 'none';
document.getElementById('avatar-menu').classList.remove('open');
}
function showApp() {
document.getElementById('auth-screen').style.display = 'none';
document.getElementById('app-screen').style.display = 'flex';
const u = JSON.parse(localStorage.getItem('user') || '{}');
const name = u.username || '';
isAdmin = !!u.isAdmin;
document.getElementById('hdr-user').textContent = name;
document.getElementById('hdr-avatar').textContent = name ? name[0].toUpperCase() : '?';
// Admin menu items
['mi-users','mi-families','mi-settings','mi-sep'].forEach(id => {
const el = document.getElementById(id);
if (el) el.style.display = isAdmin ? '' : 'none';
});
// MustChangePassword check
if (u.mustChangePassword) {
setTimeout(() => {
document.getElementById('mcp-modal').classList.add('open');
}, 400);
}
loadReminders();
}
// ═══ DATA ═══
async function loadReminders() {
try {
const res = await fetch(`${API}/reminders`, { headers: auth() });
if (res.status === 401) { doLogout(); return; }
if (!res.ok) {
console.error('API error:', res.status, await res.text());
renderCalendar();
renderList();
return;
}
reminders = await res.json();
} catch (e) {
console.error('Fetch failed:', e);
reminders = [];
}
renderCalendar();
renderList();
}
// ═══ CALENDAR ═══
function renderCalendar() {
document.getElementById('cal-title').textContent = `${MONTHS_SK[calMonth]} ${calYear}`;
const today = new Date();
const firstDay = new Date(calYear, calMonth, 1);
let startDow = firstDay.getDay();
startDow = startDow === 0 ? 6 : startDow - 1;
const daysInMonth = new Date(calYear, calMonth + 1, 0).getDate();
const daysInPrev = new Date(calYear, calMonth, 0).getDate();
const grid = document.getElementById('cal-days');
grid.innerHTML = '';
const totalCells = Math.ceil((startDow + daysInMonth) / 7) * 7;
const rowCount = totalCells / 7;
grid.style.gridTemplateRows = `repeat(${rowCount}, 1fr)`;
for (let i = 0; i < totalCells; i++) {
let dayNum, month = calMonth, year = calYear, otherMonth = false;
if (i < startDow) {
dayNum = daysInPrev - startDow + i + 1;
month = calMonth - 1; if (month < 0) { month = 11; year = calYear - 1; }
otherMonth = true;
} else if (i >= startDow + daysInMonth) {
dayNum = i - startDow - daysInMonth + 1;
month = calMonth + 1; if (month > 11) { month = 0; year = calYear + 1; }
otherMonth = true;
} else {
dayNum = i - startDow + 1;
}
const isToday = dayNum === today.getDate() && month === today.getMonth() && year === today.getFullYear();
const dateStr = `${year}-${String(month+1).padStart(2,'0')}-${String(dayNum).padStart(2,'0')}`;
const dayRems = reminders.filter(r => r.reminderDate.startsWith(dateStr));
const cell = document.createElement('div');
cell.className = `cal-day${otherMonth?' other-month':''}${isToday?' today':''}${dayRems.length?' has-reminder':''}`;
cell.dataset.date = dateStr;
const numEl = document.createElement('div');
numEl.className = 'day-num';
numEl.textContent = dayNum;
cell.appendChild(numEl);
const eventsEl = document.createElement('div');
eventsEl.className = 'cal-day-events';
dayRems.forEach(r => {
const rd = new Date(r.reminderDate);
const now2 = new Date();
let cls = 'dot-mine';
if (r.isCompleted) cls = 'dot-done';
else if (!r.isOwner) cls = 'dot-shared';
else if (rd < now2) cls = 'dot-overdue';
const time = rd.toLocaleTimeString('sk-SK', {hour:'2-digit',minute:'2-digit'});
const dot = document.createElement('div');
dot.className = `cal-reminder-dot ${cls}`;
dot.title = `${r.title}${time}`;
dot.textContent = `${time} ${r.title}`;
dot.onclick = e => { e.stopPropagation(); openEdit(r.id); };
eventsEl.appendChild(dot);
});
// placeholder pre "+N ďalší" — naplní sa po meraní
const morePlaceholder = document.createElement('div');
morePlaceholder.className = 'dot-more';
morePlaceholder.style.display = 'none';
eventsEl.appendChild(morePlaceholder);
cell.appendChild(eventsEl);
cell.onclick = () => filterByDay(dateStr, cell);
grid.appendChild(cell);
}
// Po vykreslení zmeraj a orizni každé políčko
requestAnimationFrame(() => trimCalendarCells());
}
function trimCalendarCells() {
document.querySelectorAll('.cal-day').forEach(cell => {
const eventsEl = cell.querySelector('.cal-day-events');
if (!eventsEl) return;
const dots = [...eventsEl.querySelectorAll('.cal-reminder-dot')];
const morePlaceholder = eventsEl.querySelector('.dot-more');
if (!dots.length) return;
// Dostupná výška pre udalosti
const numEl = cell.querySelector('.day-num');
const available = cell.clientHeight - (numEl ? numEl.offsetHeight : 0) - 6;
// Zobraz všetky, zmer koľko sa zmestí
dots.forEach(d => d.style.display = '');
if (morePlaceholder) morePlaceholder.style.display = 'none';
let used = 0;
let lastFit = dots.length - 1;
const GAP = 2;
for (let i = 0; i < dots.length; i++) {
used += dots[i].offsetHeight + GAP;
if (used > available) {
lastFit = i - 1;
break;
}
}
const hidden = dots.length - lastFit - 1;
if (hidden <= 0) return;
// Ak sa zmestí aspoň riadok "+N ďalší", schováme posledný viditeľný a nahradíme ho
// Inak schováme od lastFit
for (let i = lastFit + 1; i < dots.length; i++) {
dots[i].style.display = 'none';
}
if (morePlaceholder) {
morePlaceholder.textContent = `+${hidden} ďalší`;
morePlaceholder.style.display = '';
}
});
}
function filterByDay(dateStr, cell) {
const alreadySelected = cell.classList.contains('selected');
document.querySelectorAll('.cal-day.selected').forEach(c => c.classList.remove('selected'));
if (alreadySelected) {
// Druhé kliknutie na rovnaký deň → reset na všetky
resetToAll();
return;
}
cell.classList.add('selected');
// Filtruj list podľa dňa — vlastné aj zdieľané
renderListData(reminders.filter(r => r.reminderDate.startsWith(dateStr)));
}
function changeMonth(dir) {
calMonth += dir;
if (calMonth > 11) { calMonth = 0; calYear++; }
if (calMonth < 0) { calMonth = 11; calYear--; }
renderCalendar();
}
function goToday() {
const t = new Date();
calYear = t.getFullYear(); calMonth = t.getMonth();
renderCalendar();
}
// ═══ LIST ═══
function setFilter(f, btn) {
currentFilter = f;
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
// Zruš výber dňa v kalendári — list teraz zobrazuje podľa filtra, nie dňa
document.querySelectorAll('.cal-day.selected').forEach(c => c.classList.remove('selected'));
renderList();
}
function resetToAll() {
currentFilter = 'all';
document.querySelectorAll('.filter-btn').forEach((b, i) => b.classList.toggle('active', i === 0));
document.querySelectorAll('.cal-day.selected').forEach(c => c.classList.remove('selected'));
renderList();
}
function renderList() {
const now = new Date();
const weekEnd = new Date(now); weekEnd.setDate(now.getDate() + 7);
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const todayEnd = new Date(todayStart); todayEnd.setDate(todayStart.getDate() + 1);
let filtered = reminders.filter(r => {
const d = new Date(r.reminderDate);
switch (currentFilter) {
case 'week': return d >= todayStart && d <= weekEnd;
case 'today': return d >= todayStart && d < todayEnd;
case 'upcoming': return !r.isCompleted && d >= now;
case 'completed': return r.isCompleted;
default: return true; // 'all' — vlastné aj zdieľané, všetky
}
});
renderListData(filtered);
}
function renderListData(list) {
const el = document.getElementById('reminder-list');
if (!list.length) {
el.innerHTML = '<div class="empty-state">📭 Žiadne pripomienky</div>';
return;
}
const now = new Date();
el.innerHTML = list.map(r => {
const d = new Date(r.reminderDate);
const diffMin = (d - now) / 60000;
let cls = '', badge = '';
if (!r.isCompleted) {
if (!r.isOwner) { cls = 'shared-with-me'; }
else if (diffMin < 0) { cls = 'overdue'; badge = '<span class="ri-badge badge-red">Po termíne</span>'; }
else if (diffMin < 60) { cls = 'soon'; badge = '<span class="ri-badge badge-yellow">Čoskoro</span>'; }
}
const dateStr = d.toLocaleDateString('sk-SK') + ' ' + d.toLocaleTimeString('sk-SK', {hour:'2-digit',minute:'2-digit'});
const shared = (r.sharedWith||[]).map(u=>`@${u.username}`).join(', ');
return `
<div class="reminder-item ${cls} ${r.isCompleted?'completed':''}">
<div class="ri-top">
<button class="check-btn ${r.isCompleted?'done':''}"
onclick="toggleComplete(${r.id},${r.isCompleted})">${r.isCompleted?'✓':''}</button>
<div class="ri-title ${r.isCompleted?'done':''}">${r.title}${badge}</div>
<div class="ri-actions">
${r.isOwner?`<button class="btn-icon" onclick="openEdit(${r.id})">✏️</button>`:''}
${r.isOwner?`<button class="btn-icon" onclick="deleteReminder(${r.id})">🗑</button>`:''}
</div>
</div>
<div class="ri-meta">
<span>🗓 ${dateStr}</span>
<span>${!r.isOwner ? `od @${r.owner}` : shared ? `👥 ${shared}` : ''}</span>
</div>
</div>`;
}).join('');
}
// ═══ CRUD ═══
async function createReminder() {
const title = v('n-title').trim();
const dateDay = v('n-date-day');
const dateTime = v('n-date-time') || '00:00';
const desc = v('n-desc').trim();
if (!title || !dateDay) { alert('Vyplňte názov a dátum'); return; }
const reminderDate = new Date(`${dateDay}T${dateTime}`).toISOString();
await fetch(`${API}/reminders`, {
method:'POST', headers:{...auth(),'Content-Type':'application/json'},
body: JSON.stringify({ title, description: desc||null, reminderDate, shareWithUserIds: selectedUsers.map(u=>u.id) })
});
document.getElementById('n-title').value = '';
document.getElementById('n-date-day').value = '';
document.getElementById('n-date-time').value = '08:00';
document.getElementById('n-desc').value = '';
selectedUsers = []; renderPickerTags();
loadReminders();
}
async function toggleComplete(id, cur) {
const r = reminders.find(x=>x.id===id); if (!r) return;
await fetch(`${API}/reminders/${id}`, {
method:'PUT', headers:{...auth(),'Content-Type':'application/json'},
body: JSON.stringify({ title:r.title, description:r.description, reminderDate:r.reminderDate, isCompleted:!cur, shareWithUserIds:(r.sharedWith||[]).map(u=>u.id) })
});
loadReminders();
}
async function deleteReminder(id) {
if (!confirm('Zmazať túto pripomienku?')) return;
await fetch(`${API}/reminders/${id}`, { method:'DELETE', headers:auth() });
loadReminders();
}
// ═══ EDIT MODAL ═══
function openEdit(id) {
const r = reminders.find(x=>x.id===id); if (!r) return;
document.getElementById('e-id').value = r.id;
document.getElementById('e-title').value = r.title;
document.getElementById('e-desc').value = r.description||'';
const d = new Date(r.reminderDate);
document.getElementById('e-date').value = new Date(d - d.getTimezoneOffset()*60000).toISOString().slice(0,16);
document.getElementById('send-status').textContent = '';
// Zobraz tlačidlo obálky iba ak má zdieľaných používateľov
const hasShared = (r.sharedWith||[]).length > 0;
const btnEmail = document.getElementById('btn-send-email');
btnEmail.style.display = hasShared ? '' : 'none';
btnEmail.disabled = false;
document.getElementById('edit-modal').classList.add('open');
}
async function sendReminderEmail() {
const id = document.getElementById('e-id').value;
const btn = document.getElementById('btn-send-email');
const status = document.getElementById('send-status');
btn.disabled = true;
status.style.color = '#8b949e';
status.textContent = 'Odosielam...';
try {
const res = await fetch(`${API}/reminders/${id}/send-email`, {
method: 'POST', headers: auth()
});
if (res.ok) {
const data = await res.json();
status.style.color = '#3fb950';
status.textContent = `✓ Email odoslaný ${data.sent} používateľ${data.sent === 1 ? 'ovi' : 'om'}`;
} else {
const txt = await res.text();
status.style.color = '#f85149';
status.textContent = `✗ ${txt}`;
btn.disabled = false;
}
} catch {
status.style.color = '#f85149';
status.textContent = '✗ Chyba pripojenia';
btn.disabled = false;
}
}
function closeModal() { document.getElementById('edit-modal').classList.remove('open'); }
async function saveEdit() {
const id = document.getElementById('e-id').value;
const r = reminders.find(x=>x.id==id);
await fetch(`${API}/reminders/${id}`, {
method:'PUT', headers:{...auth(),'Content-Type':'application/json'},
body: JSON.stringify({ title:v('e-title'), description:v('e-desc')||null, reminderDate:new Date(v('e-date')).toISOString(), isCompleted:r.isCompleted, shareWithUserIds:(r.sharedWith||[]).map(u=>u.id) })
});
closeModal(); loadReminders();
}
// ═══ USER PICKER ═══
function openPicker() { searchUsers(''); }
async function searchUsers(query) {
clearTimeout(pickerDebounce);
pickerDebounce = setTimeout(async () => {
const res = await fetch(`${API}/users?search=${encodeURIComponent(query)}`, { headers: auth() });
if (!res.ok) return;
allUsers = await res.json();
renderDropdown();
}, 200);
}
function renderDropdown() {
const dd = document.getElementById('picker-dropdown');
if (!allUsers.length) { dd.classList.remove('open'); return; }
dd.innerHTML = allUsers.map(u => {
const sel = selectedUsers.some(s=>s.id===u.id);
return `<div class="user-option ${sel?'selected':''}" onclick="toggleUser(${u.id},'${u.username}')">
<div><span class="uname">${u.username}</span>&nbsp;<span class="uemail">${u.email}</span></div>
${sel ? '<span>✓</span>' : ''}
</div>`;
}).join('');
dd.classList.add('open');
}
function toggleUser(id, username) {
const idx = selectedUsers.findIndex(u=>u.id===id);
if (idx >= 0) selectedUsers.splice(idx, 1); else selectedUsers.push({id, username});
renderPickerTags(); renderDropdown();
}
function renderPickerTags() {
document.getElementById('picker-tags').innerHTML = selectedUsers.map(u =>
`<span class="user-tag">@${u.username}<button onclick="removeUser(${u.id})">×</button></span>`
).join('');
}
function removeUser(id) { selectedUsers = selectedUsers.filter(u=>u.id!==id); renderPickerTags(); }
document.addEventListener('click', e => {
if (!document.getElementById('user-picker').contains(e.target))
document.getElementById('picker-dropdown').classList.remove('open');
if (!document.getElementById('avatar-wrap').contains(e.target))
document.getElementById('avatar-menu').classList.remove('open');
});
window.addEventListener('resize', () => {
requestAnimationFrame(() => trimCalendarCells());
});
// ═══ AVATAR MENU ═══
function toggleAvatarMenu() {
document.getElementById('avatar-menu').classList.toggle('open');
}
// ═══ MUST-CHANGE-PASSWORD ═══
async function doMcpChange() {
const err = document.getElementById('mcp-err');
err.textContent = '';
const pass = document.getElementById('mcp-pass').value;
const pass2 = document.getElementById('mcp-pass2').value;
const u = JSON.parse(localStorage.getItem('user') || '{}');
if (pass.length < 6) { err.textContent = 'Heslo musí mať aspoň 6 znakov.'; return; }
if (pass !== pass2) { err.textContent = 'Heslá sa nezhodujú.'; return; }
// 1. Zmeniť heslo — server zároveň nastaví MustChangePassword = false
const res = await fetch(`${API}/auth/reset-password`, {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ email: u.email, newPassword: pass, code: null })
});
if (!res.ok) { err.textContent = await res.text(); return; }
// 2. Znovu prihlásiť — dostaneme nový token s mustChangePassword: false
const loginRes = await fetch(`${API}/auth/login`, {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ email: u.email, password: pass })
});
if (loginRes.ok) {
const d = await loginRes.json();
saveAuth(d);
// Aktualizuj meno/avatar v topbare (zostáva rovnaké, ale token je nový)
document.getElementById('hdr-user').textContent = d.username;
document.getElementById('hdr-avatar').textContent = d.username ? d.username[0].toUpperCase() : '?';
}
document.getElementById('mcp-modal').classList.remove('open');
}
// ═══ ADMIN PANEL ═══
let adminFamilies = [];
function openAdmin(tab) {
document.getElementById('avatar-menu').classList.remove('open');
document.getElementById('admin-overlay').classList.add('open');
switchAdminTab(tab || 'users');
}
function closeAdmin() {
document.getElementById('admin-overlay').classList.remove('open');
}
function onAdminOverlayClick(e) {
if (e.target === document.getElementById('admin-overlay')) closeAdmin();
}
function switchAdminTab(tab) {
document.querySelectorAll('.admin-tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tab));
document.querySelectorAll('.admin-section').forEach(s => s.classList.toggle('active', s.id === `tab-${tab}`));
if (tab === 'users') loadAdminData();
if (tab === 'families') loadAdminFamilies();
}
async function loadAdminData() {
const [uRes, fRes] = await Promise.all([
fetch(`${API}/admin/users`, { headers: auth() }),
fetch(`${API}/admin/families`, { headers: auth() })
]);
const users = uRes.ok ? await uRes.json() : [];
adminFamilies = fRes.ok ? await fRes.json() : [];
renderAdminUsers(users);
populateFamilySelect('uf-family', null);
}
async function loadAdminFamilies() {
const res = await fetch(`${API}/admin/families`, { headers: auth() });
adminFamilies = res.ok ? await res.json() : [];
renderAdminFamilies(adminFamilies);
}
function renderAdminUsers(users) {
const tbody = document.getElementById('admin-users-tbody');
if (!users.length) { tbody.innerHTML = '<tr><td colspan="5" style="color:#484f58;text-align:center;padding:1.5rem">Žiadni používatelia</td></tr>'; return; }
tbody.innerHTML = users.map(u => `
<tr>
<td>${u.username}</td>
<td style="color:#8b949e">${u.email}</td>
<td>${u.familyName || '<span style="color:#484f58">—</span>'}</td>
<td>
${u.isAdmin ? '<span class="badge-admin">Admin</span>' : ''}
${u.mustChangePassword ? '<span class="badge-mustchange">Zmeniť heslo</span>' : ''}
</td>
<td><div class="td-actions">
<button class="btn-tbl btn-tbl-edit" onclick="editUser(${u.id})">✏️ Upraviť</button>
${u.isAdmin ? '' : `<button class="btn-tbl btn-tbl-del" onclick="deleteUser(${u.id},'${u.username}')">🗑 Zmazať</button>`}
</div></td>
</tr>`).join('');
}
function renderAdminFamilies(families) {
const tbody = document.getElementById('admin-families-tbody');
if (!families.length) { tbody.innerHTML = '<tr><td colspan="3" style="color:#484f58;text-align:center;padding:1.5rem">Žiadne rodiny</td></tr>'; return; }
tbody.innerHTML = families.map(f => `
<tr>
<td>${f.name}</td>
<td style="color:#8b949e">${f.memberCount}</td>
<td><div class="td-actions">
<button class="btn-tbl btn-tbl-edit" onclick="editFamily(${f.id},'${f.name.replace(/'/g,"\\'")}')">✏️ Upraviť</button>
<button class="btn-tbl btn-tbl-del" onclick="deleteFamily(${f.id},'${f.name.replace(/'/g,"\\'")}')">🗑 Zmazať</button>
</div></td>
</tr>`).join('');
}
function populateFamilySelect(selectId, selectedId) {
const sel = document.getElementById(selectId);
sel.innerHTML = '<option value="">— bez rodiny —</option>' +
adminFamilies.map(f => `<option value="${f.id}" ${f.id == selectedId ? 'selected' : ''}>${f.name}</option>`).join('');
}
// USER FORM
function showUserForm(clearId) {
document.getElementById('uf-id').value = '';
document.getElementById('uf-username').value = '';
document.getElementById('uf-email').value = '';
document.getElementById('uf-err').textContent = '';
document.getElementById('user-form-title').textContent = 'Nový používateľ';
populateFamilySelect('uf-family', null);
document.getElementById('user-form-wrap').style.display = '';
document.getElementById('uf-username').focus();
}
function hideUserForm() { document.getElementById('user-form-wrap').style.display = 'none'; }
function editUser(id) {
// Find from table — reload users first
fetch(`${API}/admin/users`, { headers: auth() }).then(r => r.json()).then(users => {
const u = users.find(x => x.id === id); if (!u) return;
document.getElementById('uf-id').value = u.id;
document.getElementById('uf-username').value = u.username;
document.getElementById('uf-email').value = u.email;
document.getElementById('uf-err').textContent = '';
document.getElementById('user-form-title').textContent = 'Upraviť používateľa';
populateFamilySelect('uf-family', u.familyId);
document.getElementById('user-form-wrap').style.display = '';
document.getElementById('uf-username').focus();
});
}
async function saveUser() {
const id = document.getElementById('uf-id').value;
const body = {
username: document.getElementById('uf-username').value.trim(),
email: document.getElementById('uf-email').value.trim(),
familyId: document.getElementById('uf-family').value ? parseInt(document.getElementById('uf-family').value) : null
};
const err = document.getElementById('uf-err');
err.textContent = '';
if (!body.username || !body.email) { err.textContent = 'Meno a email sú povinné.'; return; }
const url = id ? `${API}/admin/users/${id}` : `${API}/admin/users`;
const method = id ? 'PUT' : 'POST';
const res = await fetch(url, { method, headers: { ...auth(), 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
if (res.ok) {
hideUserForm();
loadAdminData();
} else {
err.textContent = await res.text();
}
}
async function deleteUser(id, name) {
if (!confirm(`Zmazať používateľa „${name}"?`)) return;
const res = await fetch(`${API}/admin/users/${id}`, { method: 'DELETE', headers: auth() });
if (res.ok) loadAdminData();
else alert(await res.text());
}
// FAMILY FORM
function showFamilyForm() {
document.getElementById('ff-id').value = '';
document.getElementById('ff-name').value = '';
document.getElementById('ff-err').textContent = '';
document.getElementById('fam-form-title').textContent = 'Nová rodina';
document.getElementById('family-form-wrap').style.display = '';
document.getElementById('ff-name').focus();
}
function hideFamilyForm() { document.getElementById('family-form-wrap').style.display = 'none'; }
function editFamily(id, name) {
document.getElementById('ff-id').value = id;
document.getElementById('ff-name').value = name;
document.getElementById('ff-err').textContent = '';
document.getElementById('fam-form-title').textContent = 'Upraviť rodinu';
document.getElementById('family-form-wrap').style.display = '';
document.getElementById('ff-name').focus();
}
async function saveFamily() {
const id = document.getElementById('ff-id').value;
const name = document.getElementById('ff-name').value.trim();
const err = document.getElementById('ff-err');
err.textContent = '';
if (!name) { err.textContent = 'Názov je povinný.'; return; }
const url = id ? `${API}/admin/families/${id}` : `${API}/admin/families`;
const method = id ? 'PUT' : 'POST';
const res = await fetch(url, { method, headers: { ...auth(), 'Content-Type': 'application/json' }, body: JSON.stringify({ name }) });
if (res.ok) {
hideFamilyForm();
loadAdminFamilies();
} else {
err.textContent = await res.text();
}
}
async function deleteFamily(id, name) {
if (!confirm(`Zmazať rodinu „${name}"?`)) return;
const res = await fetch(`${API}/admin/families/${id}`, { method: 'DELETE', headers: auth() });
if (res.ok) loadAdminFamilies();
else alert(await res.text());
}
</script>
</body>
</html>