1963 lines
68 KiB
HTML
1963 lines
68 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="nl">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||
<title>Victorie Opkomsten Planner</title>
|
||
|
||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||
<style>
|
||
:root {
|
||
--primary-color: #e95551;
|
||
--secondary-color: #f0f3f8;
|
||
--text-color: #2d3436;
|
||
--border-color: #dfe6e9;
|
||
--delete-color: #d63031;
|
||
--add-color: #00b894;
|
||
--copy-color: #fdcb6e;
|
||
--move-color: #e95551;
|
||
--category-color: #d63031;
|
||
--info-color: #2ecc71;
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||
background-color: #e9ecef;
|
||
color: var(--text-color);
|
||
margin: 0;
|
||
padding: 10px;
|
||
display: flex;
|
||
justify-content: center;
|
||
min-height: 100vh;
|
||
-webkit-text-size-adjust: 100%;
|
||
}
|
||
|
||
.container {
|
||
width: 100%;
|
||
max-width: 850px;
|
||
background: white;
|
||
padding: 20px;
|
||
border-radius: 12px;
|
||
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
|
||
position: relative;
|
||
box-sizing: border-box;
|
||
margin-bottom: 20vh;
|
||
}
|
||
|
||
#loading-overlay {
|
||
position: absolute;
|
||
top: 0; left: 0; right: 0; bottom: 0;
|
||
background: rgba(255,255,255,0.95);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 2000;
|
||
border-radius: 12px;
|
||
}
|
||
.spinner {
|
||
border: 4px solid #f3f3f3;
|
||
border-top: 4px solid var(--primary-color);
|
||
border-radius: 50%;
|
||
width: 40px;
|
||
height: 40px;
|
||
animation: spin 1s linear infinite;
|
||
margin-bottom: 15px;
|
||
}
|
||
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
||
|
||
header {
|
||
text-align: center;
|
||
margin-bottom: 20px;
|
||
border-bottom: 3px solid var(--secondary-color);
|
||
padding-bottom: 15px;
|
||
}
|
||
|
||
h1 { margin: 0; color: var(--primary-color); font-size: 2rem; line-height: 1.2; }
|
||
p.subtitle {
|
||
color: #636e72;
|
||
margin-top: 10px;
|
||
font-size: 0.9rem;
|
||
line-height: 1.4;
|
||
cursor: pointer;
|
||
text-decoration: underline;
|
||
transition: color 0.2s;
|
||
}
|
||
p.subtitle:hover {
|
||
color: var(--primary-color);
|
||
}
|
||
|
||
.setup-section {
|
||
background: var(--primary-color);
|
||
padding: 15px;
|
||
border-radius: 10px;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 12px;
|
||
color: white;
|
||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.setup-label {
|
||
font-weight: 600;
|
||
font-size: 1rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
input[type="time"] {
|
||
padding: 10px;
|
||
border-radius: 5px;
|
||
border: none;
|
||
font-size: 1rem;
|
||
width: 140px;
|
||
cursor: pointer;
|
||
outline: none;
|
||
color: #333;
|
||
}
|
||
|
||
.extra-info-container {
|
||
background-color: var(--secondary-color);
|
||
padding: 15px;
|
||
border-radius: 10px;
|
||
margin: 20px 0;
|
||
border: 1px solid var(--border-color);
|
||
}
|
||
|
||
.extra-info-title {
|
||
font-weight: 600;
|
||
color: var(--primary-color);
|
||
margin-bottom: 10px;
|
||
font-size: 1.1rem;
|
||
}
|
||
|
||
.extra-info-list {
|
||
margin: 0;
|
||
padding-left: 20px;
|
||
}
|
||
|
||
.extra-info-item {
|
||
margin-bottom: 8px;
|
||
color: var(--text-color);
|
||
}
|
||
|
||
.extra-info-item a {
|
||
color: var(--primary-color);
|
||
text-decoration: underline;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.sort-controls {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 8px 12px;
|
||
background-color: #f8f9fa;
|
||
border-bottom: 1px solid #eee;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.sort-btn {
|
||
background: none;
|
||
border: none;
|
||
color: var(--primary-color);
|
||
font-size: 0.9rem;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.sort-btn:hover, .sort-btn.active {
|
||
background-color: rgba(233, 85, 81, 0.1);
|
||
}
|
||
|
||
.category-filter {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
padding: 8px 12px;
|
||
background-color: #f8f9fa;
|
||
border-bottom: 1px solid #eee;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.category-chip {
|
||
background-color: var(--category-color);
|
||
color: white;
|
||
padding: 4px 10px;
|
||
border-radius: 16px;
|
||
font-size: 0.8rem;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.category-chip:hover {
|
||
background-color: #b91c18;
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
.category-chip.active {
|
||
background-color: var(--primary-color);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.category-chip .close-btn {
|
||
font-size: 0.8rem;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.action-buttons {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-top: 20px;
|
||
border-top: 2px solid var(--secondary-color);
|
||
padding-top: 15px;
|
||
}
|
||
|
||
button {
|
||
padding: 10px 16px;
|
||
cursor: pointer;
|
||
border: none;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
transition: all 0.2s;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
color: white;
|
||
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
||
touch-action: manipulation;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.btn-add {
|
||
background-color: var(--add-color);
|
||
width: 100%;
|
||
justify-content: center;
|
||
font-size: 16px;
|
||
padding: 15px;
|
||
margin-top: 15px;
|
||
border: 2px solid var(--border-color);
|
||
border-radius: 12px;
|
||
box-sizing: border-box;
|
||
}
|
||
.btn-add:hover { background-color: #00a884; }
|
||
|
||
@media (max-width: 600px) {
|
||
.btn-add {
|
||
font-size: 14px;
|
||
padding: 12px;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.action-buttons {
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.btn-share, .btn-copy {
|
||
width: 100%;
|
||
justify-content: center;
|
||
}
|
||
|
||
.btn-copy[onclick="addDay()"] {
|
||
background-color: var(--add-color);
|
||
color: white;
|
||
width: 100%;
|
||
justify-content: center;
|
||
font-size: 14px;
|
||
padding: 12px;
|
||
}
|
||
}
|
||
|
||
.btn-share { background-color: var(--primary-color); justify-content: center; flex-grow: 1; }
|
||
.btn-share:hover { background-color: #d63031; }
|
||
|
||
.btn-copy { background-color: var(--copy-color); color: #333; flex-grow: 1; justify-content: center; }
|
||
.btn-copy:hover { background-color: #e1b12c; }
|
||
|
||
.activities-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 15px;
|
||
margin-top: 10px;
|
||
min-height: 200px;
|
||
}
|
||
|
||
.activity-row {
|
||
background: #fff;
|
||
padding: 15px;
|
||
border: 2px solid var(--border-color);
|
||
border-radius: 12px;
|
||
transition: transform 0.2s, box-shadow 0.2s;
|
||
position: relative;
|
||
touch-action: manipulation;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.activity-row:hover {
|
||
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.activity-row {
|
||
display: grid;
|
||
grid-template-columns: 40px 100px 1fr 40px;
|
||
grid-template-rows: auto auto;
|
||
gap: 10px;
|
||
align-items: center;
|
||
}
|
||
|
||
@media (max-width: 600px) {
|
||
.activity-row {
|
||
grid-template-columns: 35px 80px 1fr 35px;
|
||
padding: 12px;
|
||
}
|
||
h1 { font-size: 1.8rem; }
|
||
.setup-section {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
text-align: center;
|
||
}
|
||
.setup-label { width: 100%; justify-content: center; margin-bottom: 5px; }
|
||
input[type="time"] { width: 100%; box-sizing: border-box; }
|
||
}
|
||
|
||
.move-controls {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 5px;
|
||
justify-content: center;
|
||
align-items: center;
|
||
color: var(--move-color);
|
||
}
|
||
|
||
.move-btn {
|
||
background: none;
|
||
border: none;
|
||
font-size: 1.2rem;
|
||
cursor: pointer;
|
||
padding: 2px;
|
||
color: #ccc;
|
||
transition: color 0.2s;
|
||
}
|
||
.move-btn.up:hover { color: var(--add-color); }
|
||
.move-btn.down:hover { color: var(--primary-color); }
|
||
|
||
.time-column {
|
||
background-color: var(--secondary-color);
|
||
padding: 8px 5px;
|
||
border-radius: 8px;
|
||
text-align: center;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: 1px solid #dcdcdc;
|
||
}
|
||
.time-label { font-size: 8px; text-transform: uppercase; color: #777; }
|
||
.time-value { font-family: 'Courier New', Courier, monospace; font-weight: bold; font-size: 1rem; color: var(--primary-color); }
|
||
|
||
.duration-wrapper {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 2px;
|
||
}
|
||
.duration-label { font-size: 8px; color: #888; }
|
||
|
||
input[type="number"] {
|
||
width: 70px;
|
||
padding: 10px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 6px;
|
||
font-size: 16px;
|
||
text-align: center;
|
||
font-weight: bold;
|
||
color: var(--text-color);
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.activity-input-wrapper {
|
||
width: 100%;
|
||
grid-column: 1 / -1;
|
||
}
|
||
|
||
.activity-input {
|
||
width: 100%;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
border: none;
|
||
border-bottom: 2px solid var(--secondary-color);
|
||
padding: 10px 0;
|
||
color: var(--text-color);
|
||
background: transparent;
|
||
box-sizing: border-box;
|
||
}
|
||
.activity-input:focus {
|
||
border-bottom: 2px solid var(--primary-color);
|
||
outline: none;
|
||
background-color: #fafafa;
|
||
}
|
||
|
||
.btn-delete {
|
||
background-color: var(--delete-color);
|
||
padding: 8px;
|
||
font-size: 16px;
|
||
width: 35px;
|
||
height: 35px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
.btn-delete:hover { background-color: #c0392b; transform: scale(1.1); }
|
||
|
||
.suggestions-list {
|
||
position: absolute;
|
||
top: 100%;
|
||
left: 0;
|
||
right: 0;
|
||
background: #fff;
|
||
border: 1px solid #ddd;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||
z-index: 100;
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
display: none;
|
||
width: 100%;
|
||
}
|
||
.suggestions-list.show { display: block; }
|
||
|
||
.suggestion-item {
|
||
padding: 12px;
|
||
cursor: pointer;
|
||
border-bottom: 1px solid #eee;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
transition: background 0.2s;
|
||
}
|
||
.suggestion-item:hover { background-color: var(--secondary-color); }
|
||
.suggestion-item .duration-suggestion { font-size: 0.8rem; color: #666; background: #e0e0e0; padding: 2px 8px; border-radius: 10px; }
|
||
.suggestion-item .category-badge { font-size: 0.7rem; color: #fff; background: var(--category-color); padding: 2px 6px; border-radius: 4px; margin-right: 8px; }
|
||
.suggestion-item .info-badge {
|
||
font-size: 0.7rem;
|
||
color: #fff;
|
||
background: var(--info-color);
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
margin-left: 8px;
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
/* Mobile-specific suggestion item layout */
|
||
@media (max-width: 600px) {
|
||
.suggestion-item {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
}
|
||
.suggestion-item > span:first-child {
|
||
width: 100%;
|
||
}
|
||
.suggestion-item > span:last-child {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
width: 100%;
|
||
gap: 8px;
|
||
}
|
||
.suggestion-item .info-badge {
|
||
margin-left: 0;
|
||
margin-right: auto;
|
||
order: -1;
|
||
}
|
||
}
|
||
|
||
.scroll-bottom-btn {
|
||
position: fixed;
|
||
bottom: 20px;
|
||
right: 20px;
|
||
background-color: var(--primary-color);
|
||
color: white;
|
||
width: 50px;
|
||
height: 50px;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: 0 4px 10px rgba(0,0,0,0.3);
|
||
z-index: 1000;
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
transition: transform 0.2s;
|
||
}
|
||
.scroll-bottom-btn:hover { transform: scale(1.1); background-color: #d63031; }
|
||
|
||
#toast {
|
||
visibility: hidden;
|
||
min-width: 250px;
|
||
background-color: #333;
|
||
color: #fff;
|
||
text-align: center;
|
||
border-radius: 25px;
|
||
padding: 16px;
|
||
position: fixed;
|
||
z-index: 1000;
|
||
left: 50%;
|
||
bottom: 30px;
|
||
transform: translateX(-50%);
|
||
font-size: 17px;
|
||
max-width: 90%;
|
||
width: auto;
|
||
}
|
||
#toast.show { visibility: visible; animation: fadein 0.5s, fadeout 0.5s 2.5s; }
|
||
@keyframes fadein { from {bottom: 0; opacity: 0;} to {bottom: 30px; opacity: 1;} }
|
||
@keyframes fadeout { from {bottom: 30px; opacity: 1;} to {bottom: 0; opacity: 0;} }
|
||
|
||
@media print {
|
||
body { background: white; padding: 0; }
|
||
.container { box-shadow: none; max-width: 100%; padding: 0; border: none; }
|
||
.setup-section, .btn-share, .btn-copy, .move-controls, .duration-wrapper, .suggestions-list, #loading-overlay, .scroll-bottom-btn { display: none !important; }
|
||
.activity-row { break-inside: avoid; border: 1px solid #ccc; margin-bottom: 15px; padding: 15px; background: white; }
|
||
.time-column { border: 1px solid #000; background: #eee; }
|
||
.activity-input { border-bottom: 1px solid #000; }
|
||
}
|
||
|
||
.day-header {
|
||
background: var(--primary-color);
|
||
color: white;
|
||
padding: 15px;
|
||
border-radius: 10px;
|
||
margin-bottom: 15px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.day-header h3 {
|
||
margin: 0;
|
||
font-size: 1.2rem;
|
||
}
|
||
|
||
.day-setup {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.day-header .setup-label {
|
||
color: white;
|
||
font-weight: 600;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
}
|
||
|
||
.day-header input[type="time"] {
|
||
padding: 5px;
|
||
border-radius: 4px;
|
||
border: none;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.btn-delete-day:hover {
|
||
background-color: #c0392b !important;
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
.day-controls {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
margin-right: 15px;
|
||
}
|
||
|
||
.day-move-btn {
|
||
background: none;
|
||
border: none;
|
||
font-size: 0.8rem;
|
||
cursor: pointer;
|
||
padding: 2px;
|
||
color: #ccc;
|
||
transition: color 0.2s;
|
||
border-radius: 2px;
|
||
}
|
||
|
||
.day-move-btn:hover:not(:disabled) {
|
||
color: var(--add-color);
|
||
background-color: rgba(0, 184, 148, 0.1);
|
||
}
|
||
|
||
.day-move-btn:disabled {
|
||
color: #eee;
|
||
cursor: not-allowed;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.day-container {
|
||
transition: transform 0.5s ease, opacity 0.5s ease;
|
||
}
|
||
|
||
.day-container.moving-up {
|
||
transform: translateY(-100%);
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.day-container.moving-down {
|
||
transform: translateY(100%);
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.activity-row {
|
||
transition: transform 0.4s ease, opacity 0.4s ease;
|
||
}
|
||
|
||
.activity-row.moving-up {
|
||
transform: translateY(-80px);
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.activity-row.moving-down {
|
||
transform: translateY(80px);
|
||
opacity: 0.6;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="container" id="planner-content">
|
||
|
||
<div id="loading-overlay">
|
||
<div class="spinner"></div>
|
||
<p id="loading-text">Verbinden met Google Sheets...</p>
|
||
</div>
|
||
|
||
<header>
|
||
<h1><a href="/" style="color: var(--primary-color); text-decoration: none;">Victorie Opkomsten Planner</a></h1>
|
||
<p class="subtitle" onclick="window.open('https://docs.google.com/spreadsheets/d/1Bqln-FYGSEkxDi7c34HacnLAjXIDX2kB3KaUTC5Iqy4/edit?usp=sharing', '_blank')">
|
||
Live gekoppeld aan de activiteiten tabel in de drive
|
||
</p>
|
||
</header>
|
||
|
||
<div class="setup-section">
|
||
<div class="setup-label">
|
||
<i class="fa-solid fa-clock"></i> Starttijd Dag
|
||
</div>
|
||
<input type="time" id="dayStartTime" value="08:00" onchange="recalculateEntireSchedule()">
|
||
</div>
|
||
|
||
<div id="activitiesContainer" class="activities-list">
|
||
<div style="text-align: center; color: #aaa; padding: 40px; border: 2px dashed #ddd; border-radius: 10px; display: flex; flex-direction: column; align-items: center;">
|
||
<i class="fa-solid fa-list-ul" style="font-size: 3rem; margin-bottom: 15px; color: #ddd;"></i>
|
||
<p>Je planning is leeg.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Add Activity Button - only visible in single day mode -->
|
||
<button class="btn-add" onclick="addActivityRow()" id="mainAddButton" style="display: none;">
|
||
<i class="fa-solid fa-plus"></i> Activiteit Toevoegen
|
||
</button>
|
||
|
||
|
||
|
||
<!-- Extra Info Container -->
|
||
<div id="extraInfoContainer" class="extra-info-container" style="display: none;">
|
||
<h3 class="extra-info-title">Extra Informatie</h3>
|
||
<ul id="extraInfoList" class="extra-info-list"></ul>
|
||
</div>
|
||
|
||
<div class="action-buttons">
|
||
<button class="btn-share" onclick="shareSchedule()">
|
||
<i class="fa-solid fa-share-nodes"></i> Deel Planning
|
||
</button>
|
||
<button class="btn-copy" onclick="copySchedule()">
|
||
<i class="fa-solid fa-copy"></i> Kopieer Tekst
|
||
</button>
|
||
<button class="btn-copy" onclick="addDay()" style="background-color: var(--add-color); color: white; ">
|
||
<i class="fa-solid fa-plus"></i> Dag Toevoegen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="scroll-bottom-btn" onclick="window.scrollTo({top: document.body.scrollHeight, behavior: 'smooth'})">
|
||
<i class="fa-solid fa-arrow-down"></i>
|
||
</div>
|
||
|
||
<div id="toast"></div>
|
||
|
||
<script>
|
||
let activitiesDatabase = [];
|
||
let activityExtraInfo = {};
|
||
let currentSort = 'name';
|
||
let activeCategories = [];
|
||
let activityIdMap = {}; // Map to store activity ID for each row
|
||
let days = [{ startTime: '08:00', activities: [] }]; // Multi-day structure
|
||
let currentDayIndex = 0; // Track current day for adding activities
|
||
|
||
// Changed to TSV URL (tab-separated values)
|
||
const CSV_URL = "https://docs.google.com/spreadsheets/d/1Bqln-FYGSEkxDi7c34HacnLAjXIDX2kB3KaUTC5Iqy4/export?format=tsv";
|
||
|
||
// Function to capitalize each word in a string
|
||
function capitalizeWords(str) {
|
||
return str.split(' ').map(word =>
|
||
word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
|
||
).join(' ');
|
||
}
|
||
|
||
// Function to convert / to , in benodigheden
|
||
function formatBenodigheden(str) {
|
||
if (!str) return '';
|
||
return str.split('/').map(item => item.trim()).join(', ');
|
||
}
|
||
|
||
// Updated parseTSV function to handle tab-separated values
|
||
function parseTSV(text) {
|
||
const lines = text.split(/\r\n|\n/);
|
||
const result = [];
|
||
const allCategories = new Set();
|
||
|
||
for (let i = 1; i < lines.length; i++) {
|
||
if (lines[i].trim() === "") continue;
|
||
// Split by tab character instead of comma
|
||
const row = lines[i].split("\t");
|
||
if (row.length >= 3) {
|
||
const id = row[0].trim();
|
||
const name = capitalizeWords(row[1].trim());
|
||
let duration = parseInt(row[2].trim());
|
||
if (isNaN(duration) || duration < 1) duration = 60;
|
||
|
||
// Parse and capitalize categories (now using tab separation)
|
||
const category = row.length >= 4 ? row[3].trim() : "";
|
||
const categories = category.split('/').map(c => capitalizeWords(c.trim())).filter(c => c);
|
||
categories.forEach(cat => allCategories.add(cat));
|
||
|
||
// Get doel (column F - index 5)
|
||
const doel = row.length >= 6 ? row[5].trim() : "";
|
||
// Get benodigheden (column G - index 6) and convert / to ,
|
||
const benodigheden = row.length >= 7 ? formatBenodigheden(row[6].trim()) : "";
|
||
// Get extra info (column E - index 4)
|
||
const extraInfo = row.length >= 5 ? row[4].trim() : "";
|
||
|
||
result.push({
|
||
id,
|
||
name,
|
||
duration,
|
||
categories,
|
||
category,
|
||
doel,
|
||
benodigheden,
|
||
extraInfo
|
||
});
|
||
|
||
// Store all extra info in a structured way with ID as key
|
||
const infoObj = {};
|
||
if (extraInfo) infoObj.extra = extraInfo;
|
||
if (doel) infoObj.doel = doel;
|
||
if (benodigheden) infoObj.benodigheden = benodigheden;
|
||
if (Object.keys(infoObj).length > 0) {
|
||
activityExtraInfo[id] = infoObj; // Use ID as key instead of name
|
||
}
|
||
}
|
||
}
|
||
|
||
// Store all unique categories
|
||
window.allCategories = Array.from(allCategories).sort();
|
||
return result;
|
||
}
|
||
|
||
// Function to make URLs clickable
|
||
function makeUrlsClickable(text) {
|
||
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
||
return text.replace(urlRegex, function(url) {
|
||
return `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`;
|
||
});
|
||
}
|
||
|
||
// Function to sort activities
|
||
function sortActivities(activities, sortBy) {
|
||
return [...activities].sort((a, b) => {
|
||
if (sortBy === 'name') {
|
||
return a.name.localeCompare(b.name);
|
||
} else if (sortBy === 'category') {
|
||
const aCat = a.categories.length > 0 ? a.categories[0] : '';
|
||
const bCat = b.categories.length > 0 ? b.categories[0] : '';
|
||
if (!aCat && !bCat) return 0;
|
||
if (!aCat) return 1;
|
||
if (!bCat) return -1;
|
||
return aCat.localeCompare(bCat);
|
||
} else if (sortBy === 'duration') {
|
||
return a.duration - b.duration;
|
||
}
|
||
return 0;
|
||
});
|
||
}
|
||
|
||
// Function to filter activities by category
|
||
function filterByCategories(activities, categories) {
|
||
if (categories.length === 0) return activities;
|
||
|
||
return activities.filter(activity => {
|
||
// Include activities with no category
|
||
if (activity.categories.length === 0) return true;
|
||
|
||
// Include activities that match any of the selected categories
|
||
return categories.some(cat => activity.categories.includes(cat));
|
||
});
|
||
}
|
||
|
||
// Function to update URL parameters for multi-day support
|
||
function updateURLParams() {
|
||
const params = new URLSearchParams();
|
||
|
||
console.log('Updating URL params. Current days:', days);
|
||
|
||
// Add days data in a simpler format
|
||
days.forEach((day, dayIndex) => {
|
||
console.log(`Processing day ${dayIndex}:`, day);
|
||
params.set(`s${dayIndex}`, day.startTime);
|
||
|
||
day.activities.forEach((activity, activityIndex) => {
|
||
// Save all activities, but handle empty ones specially
|
||
const activityData = `${activity.name || ''}|${activity.duration}`;
|
||
const paramName = `a${dayIndex}_${activityIndex}`;
|
||
console.log(`Setting param ${paramName} = ${activityData}`);
|
||
|
||
if (activity.id) {
|
||
params.set(paramName, activityData + `|${activity.id}`);
|
||
} else {
|
||
params.set(paramName, activityData);
|
||
}
|
||
});
|
||
});
|
||
|
||
// Update URL without reloading - force immediate update
|
||
const newUrl = `${window.location.pathname}?${params.toString()}`;
|
||
console.log('New URL being set:', newUrl);
|
||
console.log('Params string:', params.toString());
|
||
|
||
// Try multiple methods to force URL update
|
||
try {
|
||
// Method 1: Direct replace
|
||
window.history.replaceState({}, '', newUrl);
|
||
|
||
// Method 2: If that didn't work, try push then replace
|
||
setTimeout(() => {
|
||
if (window.location.href !== newUrl) {
|
||
console.log('URL mismatch detected, trying alternative method');
|
||
window.history.pushState({}, '', newUrl);
|
||
window.history.replaceState({}, '', newUrl);
|
||
}
|
||
}, 0);
|
||
} catch (e) {
|
||
console.error('Error updating URL:', e);
|
||
}
|
||
|
||
console.log('URL updated. Current URL:', window.location.href);
|
||
}
|
||
|
||
// Function to get URL parameters for multi-day support
|
||
function getURLParams() {
|
||
const params = new URLSearchParams(window.location.search);
|
||
const result = {
|
||
days: []
|
||
};
|
||
|
||
// Parse days from URL
|
||
let dayIndex = 0;
|
||
while (true) {
|
||
const startParam = `s${dayIndex}`;
|
||
const startTime = params.get(startParam);
|
||
|
||
if (!startTime) break;
|
||
|
||
const day = {
|
||
startTime: startTime,
|
||
activities: []
|
||
};
|
||
|
||
// Parse activities for this day
|
||
let activityIndex = 0;
|
||
while (true) {
|
||
const activityParam = `a${dayIndex}_${activityIndex}`;
|
||
const activityValue = params.get(activityParam);
|
||
if (!activityValue) break;
|
||
|
||
const parts = activityValue.split('|');
|
||
day.activities.push({
|
||
name: parts[0] || '',
|
||
duration: parseInt(parts[1]) || 60,
|
||
id: parts[2] || undefined
|
||
});
|
||
activityIndex++;
|
||
}
|
||
|
||
result.days.push(day);
|
||
dayIndex++;
|
||
}
|
||
|
||
// If no days in URL, create default day
|
||
if (result.days.length === 0) {
|
||
result.days = [{ startTime: '08:00', activities: [] }];
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
async function initApp() {
|
||
const loadingOverlay = document.getElementById('loading-overlay');
|
||
const loadingText = document.getElementById('loading-text');
|
||
|
||
try {
|
||
loadingText.innerText = "Bezig met ophalen van activiteiten...";
|
||
|
||
const response = await fetch(CSV_URL);
|
||
|
||
if (!response.ok) throw new Error(`Server Error: ${response.status}`);
|
||
|
||
const tsvText = await response.text();
|
||
loadingText.innerText = "Bezig met verwerken...";
|
||
activitiesDatabase = parseTSV(tsvText);
|
||
|
||
if (activitiesDatabase.length > 0) {
|
||
loadingText.innerText = `Geladen: ${activitiesDatabase.length} activiteiten.`;
|
||
}
|
||
|
||
// Load days and activities from URL if present
|
||
const urlParams = getURLParams();
|
||
days = urlParams.days;
|
||
|
||
// Render all days and activities
|
||
renderAllDays();
|
||
|
||
setTimeout(() => {
|
||
loadingOverlay.style.display = 'none';
|
||
}, 500);
|
||
|
||
} catch (error) {
|
||
console.error("Import Error:", error);
|
||
loadingText.innerText = "Verbinding Mislukt.";
|
||
setTimeout(() => {
|
||
loadingOverlay.style.display = 'none';
|
||
alert("Verbinding Mislukt. Controleer internetverbinding en Google Sheet rechten.");
|
||
}, 2500);
|
||
}
|
||
}
|
||
|
||
// Function to render all days
|
||
function renderAllDays() {
|
||
const container = document.getElementById('activitiesContainer');
|
||
container.innerHTML = '';
|
||
|
||
// Remove the main start time container in multi-day mode
|
||
const startTimeContainer = document.querySelector('.start-time-container');
|
||
if (startTimeContainer) {
|
||
startTimeContainer.remove();
|
||
}
|
||
|
||
// Hide the setup-section in multi-day mode
|
||
const setupSection = document.querySelector('.setup-section');
|
||
if (setupSection) {
|
||
setupSection.style.display = days.length === 1 ? 'flex' : 'none';
|
||
}
|
||
|
||
// Hide the main add button in multi-day mode
|
||
const mainAddButton = document.getElementById('mainAddButton');
|
||
if (mainAddButton) {
|
||
mainAddButton.style.display = 'none';
|
||
}
|
||
|
||
// If only one day, render in single-day mode
|
||
if (days.length === 1) {
|
||
renderSingleDayMode();
|
||
return;
|
||
}
|
||
|
||
// Multi-day mode
|
||
days.forEach((day, dayIndex) => {
|
||
// Create day container
|
||
const dayContainer = document.createElement('div');
|
||
dayContainer.className = 'day-container';
|
||
dayContainer.setAttribute('data-day-index', dayIndex);
|
||
|
||
// Create day header
|
||
const dayHeader = document.createElement('div');
|
||
dayHeader.className = 'day-header';
|
||
dayHeader.innerHTML = `
|
||
<div class="day-controls">
|
||
<button class="day-move-btn up" onclick="moveDay(${dayIndex}, -1)" title="Dag Omhoog" ${dayIndex === 0 ? 'disabled' : ''}>
|
||
<i class="fa-solid fa-arrow-up"></i>
|
||
</button>
|
||
<button class="day-move-btn down" onclick="moveDay(${dayIndex}, 1)" title="Dag Omlaag" ${dayIndex === days.length - 1 ? 'disabled' : ''}>
|
||
<i class="fa-solid fa-arrow-down"></i>
|
||
</button>
|
||
</div>
|
||
<h3>Dag ${dayIndex + 1}</h3>
|
||
<div class="day-setup">
|
||
<div class="setup-label">
|
||
<i class="fa-solid fa-clock"></i> Starttijd Dag ${dayIndex + 1}
|
||
</div>
|
||
<input type="time" class="day-start-time" value="${day.startTime}" onchange="updateDayStartTime(${dayIndex}, this.value)">
|
||
${days.length > 1 ? `<button class="btn-delete-day" onclick="deleteDay(${dayIndex})" title="Dag verwijderen" style="background-color: var(--delete-color); color: white; border: none; border-radius: 6px; padding: 8px 12px; cursor: pointer; font-size: 12px; margin-left: 10px;"><i class="fa-solid fa-trash"></i> Verwijder</button>` : ''}
|
||
</div>
|
||
`;
|
||
dayContainer.appendChild(dayHeader);
|
||
|
||
// Create activities container for this day
|
||
const activitiesContainer = document.createElement('div');
|
||
activitiesContainer.className = 'activities-list day-activities';
|
||
activitiesContainer.setAttribute('data-day', dayIndex);
|
||
|
||
// Add activities or empty state
|
||
if (day.activities.length === 0) {
|
||
activitiesContainer.innerHTML = `
|
||
<div style="text-align: center; color: #aaa; padding: 40px; border: 2px dashed #ddd; border-radius: 10px; display: flex; flex-direction: column; align-items: center;">
|
||
<i class="fa-solid fa-list-ul" style="font-size: 3rem; margin-bottom: 15px; color: #ddd;"></i>
|
||
<p>Dag ${dayIndex + 1} is leeg.</p>
|
||
</div>
|
||
`;
|
||
} else {
|
||
day.activities.forEach(activity => {
|
||
const row = createActivityRow(activity, dayIndex);
|
||
activitiesContainer.appendChild(row);
|
||
});
|
||
}
|
||
|
||
dayContainer.appendChild(activitiesContainer);
|
||
|
||
// Add activity button for this day
|
||
const addButton = document.createElement('button');
|
||
addButton.className = 'btn-add';
|
||
addButton.setAttribute('data-day', dayIndex);
|
||
addButton.innerHTML = '<i class="fa-solid fa-plus"></i> Activiteit Toevoegen';
|
||
addButton.onclick = () => addActivityRow(dayIndex);
|
||
dayContainer.appendChild(addButton);
|
||
|
||
container.appendChild(dayContainer);
|
||
});
|
||
|
||
updateExtraInfoDisplay();
|
||
}
|
||
|
||
// Function to render single day mode (original layout)
|
||
function renderSingleDayMode() {
|
||
console.log('Rendering single day mode. Days:', days);
|
||
const container = document.getElementById('activitiesContainer');
|
||
const day = days[0];
|
||
|
||
console.log('Day 0 activities:', day.activities);
|
||
|
||
// Update the main start time input
|
||
const mainStartInput = document.getElementById('dayStartTime');
|
||
if (mainStartInput) {
|
||
mainStartInput.value = day.startTime;
|
||
}
|
||
|
||
// Show the main add button in single day mode
|
||
const mainAddButton = document.getElementById('mainAddButton');
|
||
if (mainAddButton) {
|
||
mainAddButton.style.display = 'block';
|
||
}
|
||
|
||
// Add activities or empty state
|
||
if (day.activities.length === 0) {
|
||
console.log('No activities, showing empty state');
|
||
container.innerHTML = `
|
||
<div style="text-align: center; color: #aaa; padding: 40px; border: 2px dashed #ddd; border-radius: 10px; display: flex; flex-direction: column; align-items: center;">
|
||
<i class="fa-solid fa-list-ul" style="font-size: 3rem; margin-bottom: 15px; color: #ddd;"></i>
|
||
<p>Je planning is leeg.</p>
|
||
</div>
|
||
`;
|
||
} else {
|
||
console.log('Rendering', day.activities.length, 'activities');
|
||
container.innerHTML = '';
|
||
day.activities.forEach((activity, index) => {
|
||
console.log(`Creating row ${index} for activity:`, activity);
|
||
const row = createActivityRow(activity, 0);
|
||
container.appendChild(row);
|
||
});
|
||
}
|
||
|
||
updateExtraInfoDisplay();
|
||
}
|
||
|
||
// Function to create activity row
|
||
function createActivityRow(activity, dayIndex) {
|
||
const row = document.createElement('div');
|
||
row.className = 'activity-row';
|
||
row.setAttribute('data-day', dayIndex);
|
||
|
||
if (activity.id) {
|
||
row.setAttribute('data-id', activity.id);
|
||
}
|
||
|
||
const day = days[dayIndex];
|
||
const startTimeValue = day.startTime || "08:00";
|
||
const defaultDuration = activity.duration || 60;
|
||
const endTimeValue = calculateTime(startTimeValue, defaultDuration);
|
||
|
||
row.innerHTML = `
|
||
<div class="move-controls">
|
||
<button class="move-btn up" onclick="moveRow(this, -1)" title="Naar Boven"><i class="fa-solid fa-arrow-up"></i></button>
|
||
<button class="move-btn down" onclick="moveRow(this, 1)" title="Naar beneden"><i class="fa-solid fa-arrow-down"></i></button>
|
||
</div>
|
||
<div class="time-column">
|
||
<span class="time-label">Start</span>
|
||
<span class="time-value time-start-display">${startTimeValue}</span>
|
||
<span class="time-label">Eind</span>
|
||
<span class="time-value time-end-display">${endTimeValue}</span>
|
||
</div>
|
||
<div class="duration-wrapper">
|
||
<span class="duration-label">Min</span>
|
||
<input type="number" class="duration-input" value="${defaultDuration}" min="1" onchange="handleDurationChange(this)" placeholder="60">
|
||
</div>
|
||
<button class="btn-delete" onclick="deleteRow(this)" title="Verwijderen"><i class="fa-solid fa-trash"></i></button>
|
||
<div class="activity-input-wrapper">
|
||
<input type="text" class="activity-input" placeholder="Activiteit Naam..." value="${activity.name || ''}" oninput="updateActivityData(this)">
|
||
<div class="suggestions-list"></div>
|
||
</div>
|
||
`;
|
||
|
||
initSearchLogic(row);
|
||
|
||
return row;
|
||
}
|
||
|
||
// Function to update activity data from input
|
||
function updateActivityData(input) {
|
||
console.log('updateActivityData called with input value:', input.value);
|
||
const row = input.closest('.activity-row');
|
||
const dayIndex = parseInt(row.getAttribute('data-day'));
|
||
|
||
console.log('Updating activity data:', input.value, 'dayIndex:', dayIndex);
|
||
console.log('Current days structure:', days);
|
||
|
||
let rowIndex;
|
||
|
||
if (days.length === 1) {
|
||
// Single day mode - get all rows and find the index
|
||
const rows = document.querySelectorAll('.activity-row');
|
||
rowIndex = Array.from(rows).indexOf(row);
|
||
console.log('Single day mode - rowIndex:', rowIndex);
|
||
|
||
if (rowIndex >= 0 && rowIndex < days[dayIndex].activities.length) {
|
||
days[dayIndex].activities[rowIndex].name = input.value;
|
||
// Clear the ID when name is edited to remove extra info
|
||
if (days[dayIndex].activities[rowIndex].id) {
|
||
days[dayIndex].activities[rowIndex].id = undefined;
|
||
row.removeAttribute('data-id');
|
||
}
|
||
console.log('Updated activity:', days[dayIndex].activities[rowIndex]);
|
||
console.log('About to call updateURLParams from updateActivityData');
|
||
}
|
||
} else {
|
||
// Multi-day mode - get rows from the specific day container
|
||
const dayContainer = row.closest('.day-container');
|
||
if (dayContainer) {
|
||
const rows = dayContainer.querySelectorAll('.activity-row');
|
||
rowIndex = Array.from(rows).indexOf(row);
|
||
console.log('Multi day mode - rowIndex:', rowIndex);
|
||
|
||
if (rowIndex >= 0 && rowIndex < days[dayIndex].activities.length) {
|
||
days[dayIndex].activities[rowIndex].name = input.value;
|
||
// Clear the ID when name is edited to remove extra info
|
||
if (days[dayIndex].activities[rowIndex].id) {
|
||
days[dayIndex].activities[rowIndex].id = undefined;
|
||
row.removeAttribute('data-id');
|
||
}
|
||
console.log('Updated activity:', days[dayIndex].activities[rowIndex]);
|
||
console.log('About to call updateURLParams from updateActivityData');
|
||
}
|
||
} else {
|
||
console.error('Day container not found in multi-day mode');
|
||
return;
|
||
}
|
||
}
|
||
|
||
updateExtraInfoDisplay();
|
||
updateURLParams();
|
||
console.log('updateActivityData completed');
|
||
}
|
||
|
||
// Function to add activity row to specific day
|
||
function addActivityRow(dayIndex = 0) {
|
||
console.log('Adding activity row to day:', dayIndex);
|
||
console.log('Days before adding:', days);
|
||
|
||
// Add a new activity to the data structure
|
||
const newActivity = { name: '', duration: 60 };
|
||
days[dayIndex].activities.push(newActivity);
|
||
|
||
console.log('Days after adding:', days);
|
||
|
||
// Don't update URL yet - wait until user types something
|
||
// updateURLParams();
|
||
|
||
// Re-render to show the new row
|
||
renderAllDays();
|
||
recalculateEntireSchedule();
|
||
|
||
// Focus on the new input
|
||
setTimeout(() => {
|
||
let targetRow;
|
||
if (days.length === 1) {
|
||
// Single day mode - focus on last activity row
|
||
targetRow = document.querySelector('.activity-row:last-child');
|
||
} else {
|
||
// Multi-day mode - focus on specific day
|
||
const dayContainer = document.querySelector(`[data-day-index="${dayIndex}"]`);
|
||
targetRow = dayContainer.querySelector('.activity-row:last-child');
|
||
}
|
||
|
||
if (targetRow) {
|
||
const input = targetRow.querySelector('.activity-input');
|
||
input.focus();
|
||
}
|
||
}, 100);
|
||
}
|
||
|
||
// Function to delete activity row
|
||
function deleteRow(button) {
|
||
const row = button.closest('.activity-row');
|
||
const dayIndex = parseInt(row.getAttribute('data-day'));
|
||
|
||
if (days.length === 1) {
|
||
// Single day mode - find the activity index
|
||
const rows = document.querySelectorAll('.activity-row');
|
||
const rowIndex = Array.from(rows).indexOf(row);
|
||
|
||
// Remove activity from data structure
|
||
days[dayIndex].activities.splice(rowIndex, 1);
|
||
|
||
// Re-render to show updated state
|
||
renderAllDays();
|
||
recalculateEntireSchedule();
|
||
updateURLParams();
|
||
return;
|
||
}
|
||
|
||
// Multi-day mode
|
||
const dayContainer = row.closest('.day-container');
|
||
const rows = dayContainer.querySelectorAll('.activity-row');
|
||
const rowIndex = Array.from(rows).indexOf(row);
|
||
|
||
// Remove activity from data structure
|
||
days[dayIndex].activities.splice(rowIndex, 1);
|
||
|
||
// Check if this was the last activity of a day (and there are multiple days)
|
||
if (days[dayIndex].activities.length === 0 && days.length > 1) {
|
||
// Automatically delete the day without confirmation
|
||
deleteDay(dayIndex);
|
||
return;
|
||
}
|
||
|
||
renderAllDays();
|
||
recalculateEntireSchedule();
|
||
updateURLParams();
|
||
}
|
||
|
||
// Function to add a new day
|
||
function addDay() {
|
||
console.log('Adding new day. Current days:', days);
|
||
|
||
const newDay = {
|
||
startTime: '08:00',
|
||
activities: []
|
||
};
|
||
days.push(newDay);
|
||
|
||
console.log('Days after adding new day:', days);
|
||
|
||
// Update URL to include the new day structure
|
||
updateURLParams();
|
||
|
||
renderAllDays();
|
||
updateURLParams(); // Update again after rendering
|
||
|
||
showToast(`Dag ${days.length} toegevoegd!`);
|
||
}
|
||
|
||
// Function to move a day up or down
|
||
function moveDay(dayIndex, direction) {
|
||
const newIndex = dayIndex + direction;
|
||
|
||
// Check if the move is valid
|
||
if (newIndex < 0 || newIndex >= days.length) {
|
||
return;
|
||
}
|
||
|
||
// Get the day containers
|
||
const container = document.getElementById('activitiesContainer');
|
||
const dayContainers = Array.from(container.querySelectorAll('.day-container'));
|
||
const currentDayContainer = dayContainers[dayIndex];
|
||
const targetDayContainer = dayContainers[newIndex];
|
||
|
||
// Add animation classes - current moves in direction, target moves opposite
|
||
const currentAnimationClass = direction === 1 ? 'moving-down' : 'moving-up';
|
||
const targetAnimationClass = direction === 1 ? 'moving-up' : 'moving-down';
|
||
|
||
currentDayContainer.classList.add(currentAnimationClass);
|
||
targetDayContainer.classList.add(targetAnimationClass);
|
||
|
||
// Swap the days in the data structure
|
||
const tempDay = days[dayIndex];
|
||
days[dayIndex] = days[newIndex];
|
||
days[newIndex] = tempDay;
|
||
|
||
// After animation completes, re-render
|
||
setTimeout(() => {
|
||
renderAllDays();
|
||
recalculateEntireSchedule();
|
||
updateURLParams();
|
||
showToast(`Dag ${dayIndex + 1} verplaatst naar positie ${newIndex + 1}`);
|
||
}, 500);
|
||
}
|
||
|
||
// Function to delete a day
|
||
function deleteDay(dayIndex) {
|
||
if (days.length <= 1) {
|
||
alert("Je moet minstens één dag hebben.");
|
||
return;
|
||
}
|
||
|
||
if (confirm(`Weet je zeker dat je Dag ${dayIndex + 1} wilt verwijderen? Alle activiteiten op deze dag worden ook verwijderd.`)) {
|
||
days.splice(dayIndex, 1);
|
||
renderAllDays();
|
||
recalculateEntireSchedule();
|
||
updateURLParams();
|
||
showToast(`Dag ${dayIndex + 1} verwijderd!`);
|
||
}
|
||
}
|
||
|
||
// Function to update day start time
|
||
function updateDayStartTime(dayIndex, newStartTime) {
|
||
days[dayIndex].startTime = newStartTime;
|
||
recalculateEntireSchedule();
|
||
updateURLParams();
|
||
}
|
||
|
||
// Updated moveRow function to support cross-day movement
|
||
function moveRow(button, direction) {
|
||
const currentRow = button.closest('.activity-row');
|
||
let currentDayIndex, rows, currentRowIndex;
|
||
|
||
if (days.length === 1) {
|
||
// Single day mode
|
||
currentDayIndex = 0;
|
||
rows = document.querySelectorAll('.activity-row');
|
||
currentRowIndex = Array.from(rows).indexOf(currentRow);
|
||
} else {
|
||
// Multi-day mode
|
||
const dayContainer = currentRow.closest('.day-container');
|
||
if (!dayContainer) {
|
||
console.error('Day container not found in moveRow');
|
||
return;
|
||
}
|
||
currentDayIndex = parseInt(dayContainer.getAttribute('data-day-index'));
|
||
rows = dayContainer.querySelectorAll('.activity-row');
|
||
currentRowIndex = Array.from(rows).indexOf(currentRow);
|
||
}
|
||
|
||
let targetDayIndex = currentDayIndex;
|
||
let targetRowIndex = currentRowIndex + direction;
|
||
|
||
// Check if we need to move to a different day
|
||
if (targetRowIndex < 0 && currentDayIndex > 0) {
|
||
// Move to previous day, end of that day
|
||
targetDayIndex = currentDayIndex - 1;
|
||
targetRowIndex = days[targetDayIndex].activities.length;
|
||
} else if (targetRowIndex >= days[currentDayIndex].activities.length && currentDayIndex < days.length - 1) {
|
||
// Move to next day, beginning of that day
|
||
targetDayIndex = currentDayIndex + 1;
|
||
targetRowIndex = 0;
|
||
}
|
||
|
||
// Only proceed if we're actually moving
|
||
if (targetDayIndex !== currentDayIndex || targetRowIndex !== currentRowIndex) {
|
||
// Find target row for animation
|
||
let targetRow;
|
||
if (targetDayIndex === currentDayIndex) {
|
||
// Same day movement
|
||
const allRows = days.length === 1 ?
|
||
document.querySelectorAll('.activity-row') :
|
||
document.querySelectorAll(`[data-day-index="${currentDayIndex}"] .activity-row`);
|
||
targetRow = allRows[targetRowIndex];
|
||
} else {
|
||
// Cross-day movement
|
||
const targetDayContainer = document.querySelector(`[data-day-index="${targetDayIndex}"]`);
|
||
if (targetDayContainer) {
|
||
const targetRows = targetDayContainer.querySelectorAll('.activity-row');
|
||
targetRow = targetRows[targetRowIndex];
|
||
}
|
||
}
|
||
|
||
// Add animation classes - current moves in direction, target moves opposite
|
||
const currentAnimationClass = direction === 1 ? 'moving-down' : 'moving-up';
|
||
const targetAnimationClass = direction === 1 ? 'moving-up' : 'moving-down';
|
||
|
||
currentRow.classList.add(currentAnimationClass);
|
||
if (targetRow) {
|
||
targetRow.classList.add(targetAnimationClass);
|
||
}
|
||
|
||
// Update data structure
|
||
const activity = days[currentDayIndex].activities.splice(currentRowIndex, 1)[0];
|
||
days[targetDayIndex].activities.splice(targetRowIndex, 0, activity);
|
||
|
||
// After animation completes, re-render
|
||
setTimeout(() => {
|
||
renderAllDays();
|
||
recalculateEntireSchedule();
|
||
updateURLParams();
|
||
}, 400);
|
||
}
|
||
}
|
||
|
||
function handleDurationChange(durationInput) {
|
||
const row = durationInput.closest('.activity-row');
|
||
const dayIndex = parseInt(row.getAttribute('data-day'));
|
||
let rowIndex;
|
||
|
||
if (days.length === 1) {
|
||
// Single day mode - get all rows globally
|
||
const rows = document.querySelectorAll('.activity-row');
|
||
rowIndex = Array.from(rows).indexOf(row);
|
||
} else {
|
||
// Multi-day mode - get rows from the specific day container
|
||
const dayContainer = row.closest('.day-container');
|
||
if (dayContainer) {
|
||
const rows = Array.from(dayContainer.querySelectorAll('.activity-row'));
|
||
rowIndex = rows.indexOf(row);
|
||
} else {
|
||
console.error('Day container not found in handleDurationChange');
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Update duration in data structure
|
||
let duration = parseInt(durationInput.value);
|
||
if (isNaN(duration) || duration < 1) {
|
||
duration = 60;
|
||
durationInput.value = 60;
|
||
}
|
||
|
||
if (rowIndex >= 0 && rowIndex < days[dayIndex].activities.length) {
|
||
days[dayIndex].activities[rowIndex].duration = duration;
|
||
}
|
||
|
||
recalculateEntireSchedule();
|
||
updateExtraInfoDisplay();
|
||
updateURLParams();
|
||
}
|
||
|
||
function initSearchLogic(row) {
|
||
const input = row.querySelector('.activity-input');
|
||
const wrapper = row.querySelector('.activity-input-wrapper');
|
||
const list = row.querySelector('.suggestions-list');
|
||
const durationInput = row.querySelector('.duration-input');
|
||
|
||
// Create sort controls
|
||
const sortControls = document.createElement('div');
|
||
sortControls.className = 'sort-controls';
|
||
sortControls.innerHTML = `
|
||
<button class="sort-btn ${currentSort === 'name' ? 'active' : ''}" onclick="setSort('name')">
|
||
<i class="fa-solid fa-sort-alpha-down"></i> Naam
|
||
</button>
|
||
<button class="sort-btn ${currentSort === 'category' ? 'active' : ''}" onclick="setSort('category')">
|
||
<i class="fa-solid fa-tags"></i> Categorie
|
||
</button>
|
||
<button class="sort-btn ${currentSort === 'duration' ? 'active' : ''}" onclick="setSort('duration')">
|
||
<i class="fa-solid fa-clock"></i> Duur
|
||
</button>
|
||
`;
|
||
|
||
// Create category filter
|
||
const categoryFilter = document.createElement('div');
|
||
categoryFilter.className = 'category-filter';
|
||
|
||
// Add "All" category
|
||
const allChip = document.createElement('span');
|
||
allChip.className = 'category-chip' + (activeCategories.length === 0 ? ' active' : '');
|
||
allChip.innerHTML = `<i class="fa-solid fa-check"></i> Alle`;
|
||
allChip.onclick = () => toggleCategory('');
|
||
categoryFilter.appendChild(allChip);
|
||
|
||
// Add category chips (capitalized)
|
||
window.allCategories.forEach(category => {
|
||
const chip = document.createElement('span');
|
||
chip.className = 'category-chip' + (activeCategories.includes(category) ? ' active' : '');
|
||
chip.innerHTML = `${category} <span class="close-btn">×</span>`;
|
||
chip.onclick = (e) => {
|
||
if (e.target.className === 'close-btn') {
|
||
e.stopPropagation();
|
||
removeCategory(category);
|
||
} else {
|
||
toggleCategory(category);
|
||
}
|
||
};
|
||
categoryFilter.appendChild(chip);
|
||
});
|
||
|
||
function showSuggestions() {
|
||
const query = input.value.toLowerCase().trim();
|
||
list.innerHTML = '';
|
||
|
||
// Clear previous sort controls and category filter
|
||
const existingSortControls = list.querySelector('.sort-controls');
|
||
if (existingSortControls) existingSortControls.remove();
|
||
const existingCategoryFilter = list.querySelector('.category-filter');
|
||
if (existingCategoryFilter) existingCategoryFilter.remove();
|
||
|
||
// Clear filters when searching (not when showing full list)
|
||
if (query.length > 0) {
|
||
activeCategories = [];
|
||
document.querySelectorAll('.category-chip').forEach(chip => {
|
||
chip.classList.remove('active');
|
||
const chipText = chip.textContent.replace('×', '').trim();
|
||
if (chipText === 'Alle') {
|
||
chip.classList.add('active');
|
||
}
|
||
});
|
||
}
|
||
|
||
// Show full list when input is empty
|
||
const matches = query.length === 0 ? activitiesDatabase : activitiesDatabase.filter(item =>
|
||
item.name.toLowerCase().includes(query) ||
|
||
item.categories.some(cat => cat.toLowerCase().includes(query))
|
||
);
|
||
|
||
// Add category filter when showing full list
|
||
if (query.length === 0) {
|
||
list.appendChild(categoryFilter);
|
||
}
|
||
|
||
// Add sort controls when showing full list
|
||
if (query.length === 0) {
|
||
list.appendChild(sortControls);
|
||
}
|
||
|
||
// Filter by active categories
|
||
const filteredMatches = filterByCategories(matches, activeCategories);
|
||
|
||
// Sort the matches
|
||
const sortedMatches = sortActivities(filteredMatches, currentSort);
|
||
|
||
if (sortedMatches.length > 0) {
|
||
sortedMatches.forEach(match => {
|
||
const itemDiv = document.createElement('div');
|
||
itemDiv.className = 'suggestion-item';
|
||
itemDiv.innerHTML = `
|
||
<span>${match.name}${match.extraInfo ? ' <span class="info-badge"><i class="fa-solid fa-info-circle"></i> Info</span>' : ''}</span>
|
||
<span>
|
||
${match.categories.length > 0 ? match.categories.map(cat => `<span class="category-badge">${cat}</span>`).join('') : ''}
|
||
<span class="duration-suggestion">${match.duration} min</span>
|
||
</span>
|
||
`;
|
||
itemDiv.onclick = () => {
|
||
console.log('Suggestion clicked:', match.name);
|
||
input.value = match.name;
|
||
durationInput.value = match.duration;
|
||
// Store the ID in a data attribute on the row
|
||
row.setAttribute('data-id', match.id);
|
||
|
||
// Update data structure
|
||
const dayIndex = parseInt(row.getAttribute('data-day'));
|
||
let rowIndex;
|
||
|
||
if (days.length === 1) {
|
||
// Single day mode - get all rows globally
|
||
const rows = document.querySelectorAll('.activity-row');
|
||
rowIndex = Array.from(rows).indexOf(row);
|
||
} else {
|
||
// Multi-day mode - get rows from the specific day container
|
||
const dayContainer = row.closest('.day-container');
|
||
if (dayContainer) {
|
||
const rows = Array.from(dayContainer.querySelectorAll('.activity-row'));
|
||
rowIndex = rows.indexOf(row);
|
||
} else {
|
||
console.error('Day container not found in multi-day mode');
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (rowIndex >= 0 && rowIndex < days[dayIndex].activities.length) {
|
||
days[dayIndex].activities[rowIndex].name = match.name;
|
||
days[dayIndex].activities[rowIndex].duration = match.duration;
|
||
days[dayIndex].activities[rowIndex].id = match.id;
|
||
} else {
|
||
console.error('Invalid row index:', rowIndex, 'for day', dayIndex);
|
||
}
|
||
|
||
console.log('Updated days structure:', days);
|
||
|
||
list.classList.remove('show');
|
||
handleDurationChange(durationInput);
|
||
input.focus();
|
||
updateExtraInfoDisplay();
|
||
|
||
// Force URL update immediately
|
||
updateURLParams();
|
||
|
||
// Double-check the URL was updated
|
||
setTimeout(() => {
|
||
console.log('Current URL after suggestion click:', window.location.href);
|
||
}, 100);
|
||
};
|
||
list.appendChild(itemDiv);
|
||
});
|
||
list.classList.add('show');
|
||
} else {
|
||
list.innerHTML = '<div style="padding:10px; color:#888; text-align:center;">Geen matches gevonden</div>';
|
||
list.classList.add('show');
|
||
}
|
||
}
|
||
|
||
// Add event listener for input changes to update extra info display
|
||
input.addEventListener('input', () => {
|
||
showSuggestions();
|
||
updateExtraInfoDisplay();
|
||
updateURLParams();
|
||
});
|
||
|
||
input.addEventListener('focus', () => { showSuggestions(); });
|
||
document.addEventListener('click', (e) => { if (!wrapper.contains(e.target)) list.classList.remove('show'); });
|
||
input.addEventListener('keydown', (e) => { if (e.key === 'Escape') { list.classList.remove('show'); input.blur(); } });
|
||
}
|
||
|
||
function updateExtraInfoDisplay() {
|
||
const extraInfoContainer = document.getElementById('extraInfoContainer');
|
||
const extraInfoList = document.getElementById('extraInfoList');
|
||
|
||
// Clear previous content
|
||
extraInfoList.innerHTML = '';
|
||
|
||
// Get all activity names and IDs from the current schedule
|
||
let hasExtraInfo = false;
|
||
|
||
days.forEach((day, dayIndex) => {
|
||
day.activities.forEach((activity, activityIndex) => {
|
||
if (activity.id && activityExtraInfo[activity.id]) {
|
||
hasExtraInfo = true;
|
||
const info = activityExtraInfo[activity.id];
|
||
|
||
const listItem = document.createElement('li');
|
||
listItem.className = 'extra-info-item';
|
||
|
||
// Build the info HTML - use just activity name for single day
|
||
const displayLabel = days.length === 1 ? activity.name : `Dag ${dayIndex + 1} - ${activity.name}`;
|
||
let infoHtml = `<strong>${displayLabel}:</strong><br>`;
|
||
|
||
// Add extra info if available
|
||
if (info.extra) {
|
||
infoHtml += `<div style="margin-top: 5px;">${makeUrlsClickable(info.extra)}</div>`;
|
||
}
|
||
|
||
// Add doel if available
|
||
if (info.doel) {
|
||
infoHtml += `<div style="margin-top: 8px;"><strong>Doel:</strong> ${info.doel}</div>`;
|
||
}
|
||
|
||
// Add benodigheden if available
|
||
if (info.benodigheden) {
|
||
infoHtml += `<div style="margin-top: 5px;"><strong>Benodigheden:</strong> ${info.benodigheden}</div>`;
|
||
}
|
||
|
||
listItem.innerHTML = infoHtml;
|
||
extraInfoList.appendChild(listItem);
|
||
}
|
||
});
|
||
});
|
||
|
||
// Show or hide the container based on whether there's extra info
|
||
extraInfoContainer.style.display = hasExtraInfo ? 'block' : 'none';
|
||
}
|
||
|
||
window.onload = initApp;
|
||
|
||
// Add event listeners
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
// Add event listener to main start time input for single day mode
|
||
const mainStartInput = document.getElementById('dayStartTime');
|
||
if (mainStartInput) {
|
||
mainStartInput.addEventListener('change', () => {
|
||
if (days.length === 1) {
|
||
days[0].startTime = mainStartInput.value;
|
||
recalculateEntireSchedule();
|
||
updateURLParams();
|
||
}
|
||
});
|
||
}
|
||
|
||
// Add event listener to main add day button
|
||
const addDayButton = document.querySelector('[onclick="addDay()"]');
|
||
if (addDayButton) {
|
||
addDayButton.addEventListener('click', updateURLParams);
|
||
}
|
||
});
|
||
|
||
function calculateTime(startTimeString, durationMinutes) {
|
||
const [hours, minutes] = startTimeString.split(':').map(Number);
|
||
const date = new Date();
|
||
date.setHours(hours, minutes, 0);
|
||
date.setMinutes(date.getMinutes() + durationMinutes);
|
||
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
||
}
|
||
|
||
function recalculateEntireSchedule() {
|
||
days.forEach((day, dayIndex) => {
|
||
let currentStartTime = day.startTime;
|
||
|
||
if (days.length === 1) {
|
||
// Single day mode - use main start time input
|
||
const mainStartInput = document.getElementById('dayStartTime');
|
||
if (mainStartInput) {
|
||
currentStartTime = mainStartInput.value;
|
||
day.startTime = currentStartTime;
|
||
}
|
||
|
||
const rows = document.querySelectorAll('.activity-row');
|
||
rows.forEach((row, index) => {
|
||
const startDisplay = row.querySelector('.time-start-display');
|
||
if (startDisplay) startDisplay.innerText = currentStartTime;
|
||
|
||
const durationInput = row.querySelector('.duration-input');
|
||
let duration = 60;
|
||
if (durationInput && durationInput.value > 0) duration = parseInt(durationInput.value);
|
||
|
||
const endTime = calculateTime(currentStartTime, duration);
|
||
const endDisplay = row.querySelector('.time-end-display');
|
||
if (endDisplay) endDisplay.innerText = endTime;
|
||
|
||
currentStartTime = endTime;
|
||
});
|
||
} else {
|
||
// Multi-day mode
|
||
const dayContainer = document.querySelector(`[data-day-index="${dayIndex}"]`);
|
||
if (!dayContainer) return;
|
||
|
||
const rows = dayContainer.querySelectorAll('.activity-row');
|
||
rows.forEach((row, index) => {
|
||
const startDisplay = row.querySelector('.time-start-display');
|
||
if (startDisplay) startDisplay.innerText = currentStartTime;
|
||
|
||
const durationInput = row.querySelector('.duration-input');
|
||
let duration = 60;
|
||
if (durationInput && durationInput.value > 0) duration = parseInt(durationInput.value);
|
||
|
||
const endTime = calculateTime(currentStartTime, duration);
|
||
const endDisplay = row.querySelector('.time-end-display');
|
||
if (endDisplay) endDisplay.innerText = endTime;
|
||
|
||
currentStartTime = endTime;
|
||
});
|
||
}
|
||
});
|
||
|
||
updateExtraInfoDisplay();
|
||
}
|
||
|
||
// Category functions
|
||
function setSort(sortBy) {
|
||
currentSort = sortBy;
|
||
document.querySelectorAll('.sort-btn').forEach(btn => btn.classList.remove('active'));
|
||
document.querySelector(`.sort-btn[onclick="setSort('${sortBy}')"]`).classList.add('active');
|
||
|
||
// Trigger suggestions update for all visible inputs
|
||
document.querySelectorAll('.activity-input:focus').forEach(input => {
|
||
const event = new Event('input');
|
||
input.dispatchEvent(event);
|
||
});
|
||
}
|
||
|
||
function toggleCategory(category) {
|
||
if (category === '') {
|
||
activeCategories = [];
|
||
} else {
|
||
const index = activeCategories.indexOf(category);
|
||
if (index > -1) {
|
||
activeCategories.splice(index, 1);
|
||
} else {
|
||
activeCategories.push(category);
|
||
}
|
||
}
|
||
|
||
// Update UI for all category chips across all suggestion lists
|
||
document.querySelectorAll('.category-chip').forEach(chip => {
|
||
const chipText = chip.textContent.replace('×', '').trim();
|
||
if (chipText === 'Alle') {
|
||
chip.classList.toggle('active', activeCategories.length === 0);
|
||
} else {
|
||
chip.classList.toggle('active', activeCategories.includes(chipText));
|
||
}
|
||
});
|
||
|
||
// Trigger suggestions update for all focused inputs
|
||
document.querySelectorAll('.activity-input:focus').forEach(input => {
|
||
const event = new Event('input', { bubbles: true });
|
||
input.dispatchEvent(event);
|
||
});
|
||
|
||
// Also trigger update for the input that might have the category filter open
|
||
document.querySelectorAll('.activity-input').forEach(input => {
|
||
const wrapper = input.closest('.activity-input-wrapper');
|
||
if (wrapper && wrapper.querySelector('.suggestions-list.show')) {
|
||
const event = new Event('input', { bubbles: true });
|
||
input.dispatchEvent(event);
|
||
}
|
||
});
|
||
}
|
||
|
||
function removeCategory(category) {
|
||
const index = activeCategories.indexOf(category);
|
||
if (index > -1) {
|
||
activeCategories.splice(index, 1);
|
||
}
|
||
|
||
// Update UI
|
||
document.querySelectorAll('.category-chip').forEach(chip => {
|
||
const chipText = chip.textContent.replace('×', '').trim();
|
||
chip.classList.toggle('active', activeCategories.includes(chipText));
|
||
});
|
||
|
||
// Trigger suggestions update
|
||
document.querySelectorAll('.activity-input:focus').forEach(input => {
|
||
const event = new Event('input');
|
||
input.dispatchEvent(event);
|
||
});
|
||
}
|
||
|
||
function generateTextPlan() {
|
||
if (days.length === 0 || days.every(day => day.activities.length === 0)) return "";
|
||
|
||
let text = "Victorie Opkomsten Planner:\n\n";
|
||
const extraInfoMap = {};
|
||
|
||
days.forEach((day, dayIndex) => {
|
||
if (day.activities.length === 0) return;
|
||
|
||
// Use "Opkomst" for single day, "Dag X" for multiple days
|
||
const dayLabel = days.length === 1 ? "Opkomst" : `Dag ${dayIndex + 1}`;
|
||
|
||
text += `=== ${dayLabel} (Start: ${day.startTime}) ===\n`;
|
||
|
||
let currentStartTime = day.startTime;
|
||
day.activities.forEach(activity => {
|
||
const endTime = calculateTime(currentStartTime, activity.duration);
|
||
const activityName = activity.name || "Naamloos";
|
||
text += `${currentStartTime} - ${endTime}: ${activityName}\n`;
|
||
currentStartTime = endTime;
|
||
|
||
// Find matching activity in database using ID
|
||
if (activity.id && activityExtraInfo[activity.id]) {
|
||
// For single day, use just activity name; for multi-day, use "Dag X - activity name"
|
||
let infoKey;
|
||
if (days.length === 1) {
|
||
infoKey = activityName;
|
||
} else {
|
||
infoKey = `Dag ${dayIndex + 1} - ${activityName}`;
|
||
}
|
||
extraInfoMap[infoKey] = activityExtraInfo[activity.id];
|
||
}
|
||
});
|
||
text += "\n";
|
||
});
|
||
|
||
// Add extra info section if any exists
|
||
const extraInfoKeys = Object.keys(extraInfoMap);
|
||
if (extraInfoKeys.length > 0) {
|
||
text += "Activiteit Uitleg:\n";
|
||
extraInfoKeys.forEach(activityKey => {
|
||
const info = extraInfoMap[activityKey];
|
||
text += `\n${activityKey}\n`;
|
||
|
||
if (info.extra) {
|
||
text += `Uitleg: ${info.extra}\n`;
|
||
}
|
||
if (info.doel) {
|
||
text += `Doel: ${info.doel}\n`;
|
||
}
|
||
if (info.benodigheden) {
|
||
text += `Benodigheden: ${info.benodigheden}\n`;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Add URL to the export text (remove the website line)
|
||
text += `\nJe kunt deze planning opslaan en later terug openen met deze link:\n${window.location.href}\n`;
|
||
// Removed: text += "\nopkomstenplanner.marijnjong.de";
|
||
return text;
|
||
}
|
||
|
||
function shareSchedule() {
|
||
const textToShare = generateTextPlan();
|
||
if (!textToShare) { alert("Voeg activiteiten toe voordat je deelt."); return; }
|
||
|
||
if (navigator.share) {
|
||
navigator.share({
|
||
title: 'Victorie Opkomsten Planner',
|
||
text: textToShare,
|
||
url: window.location.href
|
||
}).catch(console.error);
|
||
} else {
|
||
copySchedule();
|
||
}
|
||
}
|
||
|
||
function copySchedule() {
|
||
const textToCopy = generateTextPlan();
|
||
if (!textToCopy) { alert("Voeg activiteiten toe voordat je kopieert."); return; }
|
||
|
||
const textArea = document.createElement("textarea");
|
||
textArea.value = textToCopy;
|
||
textArea.style.top = "0";
|
||
textArea.style.left = "0";
|
||
textArea.style.position = "fixed";
|
||
|
||
document.body.appendChild(textArea);
|
||
textArea.focus();
|
||
textArea.select();
|
||
|
||
try {
|
||
const successful = document.execCommand('copy');
|
||
if (successful) {
|
||
showToast("Gekopieerd naar klembord!");
|
||
} else {
|
||
throw new Error('Kopiëren mislukt');
|
||
}
|
||
} catch (err) {
|
||
console.error('Kopiëren niet mogelijk', err);
|
||
alert("Kopiëren naar klembord is mislukt. Selecteer de tekst handmatig.");
|
||
}
|
||
|
||
document.body.removeChild(textArea);
|
||
}
|
||
|
||
function showToast(message) {
|
||
const x = document.getElementById("toast");
|
||
x.innerText = message;
|
||
x.className = "show";
|
||
setTimeout(function(){ x.className = x.className.replace("show", ""); }, 3000);
|
||
}
|
||
|
||
// Initialize the app
|
||
recalculateEntireSchedule();
|
||
|
||
</script>
|
||
|
||
</body>
|
||
</html> |