1459 lines
63 KiB
HTML
1459 lines
63 KiB
HTML
<!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)">‹</button>
|
||
<button class="btn-nav" onclick="changeMonth(1)">›</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> <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>
|