mirror of
https://github.com/guyg1474/hcrb.git
synced 2026-10-16 07:13:28 +02:00
Erste Version: HCRB Spieltag-Kalender und Fahrtplaner
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
10400eee59
30 files changed
+3771
No files matched your search
@@ -0,0 +1,519 @@
|
||||
import {
|
||||
$, api, esc, sortByName, seatsLabel, showError, renderHeader, formatDate,
|
||||
} from './common.js';
|
||||
|
||||
renderHeader('fahrt');
|
||||
|
||||
let members = [];
|
||||
let points = [];
|
||||
|
||||
const DEFAULT_DEPARTURE = '07:00';
|
||||
const MEET_BEFORE_MIN = 15;
|
||||
|
||||
// Zustand der aktuellen Fahrt (nur im Browser, wird nicht gespeichert)
|
||||
const trip = {
|
||||
selected: new Set(), // IDs der mitfahrenden Mitglieder
|
||||
bringsCar: new Map(), // ID -> bool, ob das Auto diesmal dabei ist
|
||||
guests: [], // { id, name, hasCar, seats, guest: true }
|
||||
fixed: new Map(), // Mitfahrer-ID -> Fahrer-ID, feste Zuteilung für diese Fahrt
|
||||
};
|
||||
|
||||
let guestCounter = 0;
|
||||
|
||||
// ---------- Treffpunkt und Uhrzeiten ----------
|
||||
function renderPoints() {
|
||||
$('#trip-points-empty').hidden = points.length > 0;
|
||||
$('#trip-point').innerHTML = points.length
|
||||
? points.map((p) => `<option value="${p.id}">${esc(p.name)} – ${esc(p.address)}</option>`).join('')
|
||||
+ '<option value="">(kein Treffpunkt)</option>'
|
||||
: '<option value="">(noch keine Treffpunkte angelegt)</option>';
|
||||
}
|
||||
|
||||
// "07:00" minus 15 Minuten → "06:45" (auch über Mitternacht)
|
||||
function minusMinutes(time, min) {
|
||||
const [h, m] = time.split(':').map(Number);
|
||||
const total = (((h * 60 + m - min) % 1440) + 1440) % 1440;
|
||||
return `${String(Math.floor(total / 60)).padStart(2, '0')}:${String(total % 60).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function setDeparture(time) {
|
||||
$('#trip-departure').value = time;
|
||||
$('#trip-meet').value = minusMinutes(time, MEET_BEFORE_MIN);
|
||||
}
|
||||
|
||||
// Treffzeit folgt der Abfahrt, kann danach aber noch von Hand angepasst werden
|
||||
$('#trip-departure').addEventListener('change', (e) => {
|
||||
if (e.target.value) setDeparture(e.target.value);
|
||||
});
|
||||
|
||||
// ---------- Mitglieder auswählen ----------
|
||||
function renderTripMembers() {
|
||||
$('#trip-members-empty').hidden = members.length > 0;
|
||||
const nameOf = (id) => members.find((m) => m.id === id)?.name;
|
||||
$('#trip-members').innerHTML = sortByName(members).map((m) => {
|
||||
if (!trip.bringsCar.has(m.id)) trip.bringsCar.set(m.id, m.hasCar);
|
||||
const checked = trip.selected.has(m.id);
|
||||
return `
|
||||
<li>
|
||||
<label class="inline name">
|
||||
<input type="checkbox" data-select="${m.id}" ${checked ? 'checked' : ''}> ${esc(m.name)}${m.external ? ' <span class="badge">Extern</span>' : ''}
|
||||
${nameOf(m.ridesWith) ? `<span class="meta">(gern bei ${esc(nameOf(m.ridesWith))})</span>` : ''}
|
||||
</label>
|
||||
<span class="actions">
|
||||
${m.hasCar ? `
|
||||
<label class="inline meta">
|
||||
<input type="checkbox" data-car="${m.id}" ${trip.bringsCar.get(m.id) ? 'checked' : ''} ${checked ? '' : 'disabled'}>
|
||||
Auto dabei (${seatsLabel(m.seats)})
|
||||
</label>
|
||||
${m.alwaysDrives ? '<span class="badge" title="Kommt immer mit eigenem Auto">fährt immer</span>' : ''}` : '<span class="meta">kein Auto</span>'}
|
||||
</span>
|
||||
</li>`;
|
||||
}).join('');
|
||||
renderFixed();
|
||||
}
|
||||
|
||||
$('#trip-members').addEventListener('change', (e) => {
|
||||
const { select, car } = e.target.dataset;
|
||||
if (select) {
|
||||
e.target.checked ? trip.selected.add(select) : trip.selected.delete(select);
|
||||
renderTripMembers();
|
||||
}
|
||||
if (car) {
|
||||
trip.bringsCar.set(car, e.target.checked);
|
||||
renderFixed();
|
||||
}
|
||||
});
|
||||
|
||||
$('#select-all').addEventListener('click', () => {
|
||||
members.forEach((m) => trip.selected.add(m.id));
|
||||
renderTripMembers();
|
||||
});
|
||||
$('#select-none').addEventListener('click', () => {
|
||||
trip.selected.clear();
|
||||
renderTripMembers();
|
||||
});
|
||||
|
||||
// ---------- Gäste ----------
|
||||
$('#guest-hascar').addEventListener('change', (e) => {
|
||||
$('#guest-seats').hidden = !e.target.checked;
|
||||
});
|
||||
|
||||
$('#guest-form').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const name = $('#guest-name').value.trim();
|
||||
if (!name) return;
|
||||
const hasCar = $('#guest-hascar').checked;
|
||||
trip.guests.push({
|
||||
id: 'guest-' + ++guestCounter,
|
||||
name,
|
||||
hasCar,
|
||||
seats: hasCar ? Math.max(1, parseInt($('#guest-seats').value, 10) || 1) : 0,
|
||||
ridesWith: $('#guest-rideswith').value || null,
|
||||
guest: true,
|
||||
});
|
||||
e.target.reset();
|
||||
$('#guest-seats').hidden = true;
|
||||
renderGuests();
|
||||
$('#guest-name').focus();
|
||||
});
|
||||
|
||||
function renderGuests() {
|
||||
const people = [...sortByName(members), ...trip.guests];
|
||||
const nameOf = (id) => people.find((p) => p.id === id)?.name;
|
||||
$('#guest-rideswith').innerHTML = '<option value="">(keine Vorliebe)</option>'
|
||||
+ people.map((p) => `<option value="${p.id}">gern bei ${esc(p.name)}</option>`).join('');
|
||||
$('#guests-list').innerHTML = trip.guests.map((g) => `
|
||||
<li>
|
||||
<span class="name">${esc(g.name)} <span class="badge">Gast</span>
|
||||
${nameOf(g.ridesWith) ? `<span class="meta">(gern bei ${esc(nameOf(g.ridesWith))})</span>` : ''}</span>
|
||||
<span class="meta">${g.hasCar ? `Auto · ${seatsLabel(g.seats)}` : 'kein Auto'}</span>
|
||||
<button class="link danger" data-remove="${g.id}">Entfernen</button>
|
||||
</li>`).join('');
|
||||
renderFixed();
|
||||
}
|
||||
|
||||
$('#guests-list').addEventListener('click', (e) => {
|
||||
const id = e.target.dataset.remove;
|
||||
if (!id) return;
|
||||
trip.guests = trip.guests.filter((g) => g.id !== id);
|
||||
for (const g of trip.guests) if (g.ridesWith === id) g.ridesWith = null;
|
||||
renderGuests();
|
||||
});
|
||||
|
||||
// ---------- Feste Zuteilung ----------
|
||||
// Alle, die diesmal mitfahren (Mitglieder mit Auto-Häkchen der Fahrt, plus Gäste)
|
||||
function currentParticipants() {
|
||||
return [
|
||||
...sortByName(members.filter((m) => trip.selected.has(m.id))).map((m) => ({
|
||||
...m,
|
||||
hasCar: m.hasCar && trip.bringsCar.get(m.id) !== false,
|
||||
})),
|
||||
...trip.guests,
|
||||
];
|
||||
}
|
||||
|
||||
function renderFixed() {
|
||||
const people = currentParticipants();
|
||||
const byId = new Map(people.map((p) => [p.id, p]));
|
||||
const canDrive = (p) => p?.hasCar && p.seats > 0;
|
||||
|
||||
// Einträge entfernen, die nicht mehr passen (abgewählt, Auto nicht dabei, Auto voll)
|
||||
const used = new Map();
|
||||
for (const [passengerId, driverId] of trip.fixed) {
|
||||
const n = (used.get(driverId) || 0) + 1;
|
||||
if (!byId.has(passengerId) || !canDrive(byId.get(driverId)) || trip.fixed.has(driverId)
|
||||
|| n > byId.get(driverId).seats) {
|
||||
trip.fixed.delete(passengerId);
|
||||
} else {
|
||||
used.set(driverId, n);
|
||||
}
|
||||
}
|
||||
|
||||
const fixedDrivers = new Set(trip.fixed.values());
|
||||
const passengers = people.filter((p) => !trip.fixed.has(p.id) && !fixedDrivers.has(p.id));
|
||||
const drivers = people.filter((p) => canDrive(p) && !trip.fixed.has(p.id) && (used.get(p.id) || 0) < p.seats);
|
||||
|
||||
$('#fixed-passenger').innerHTML = passengers.length
|
||||
? passengers.map((p) => `<option value="${p.id}">${esc(p.name)}</option>`).join('')
|
||||
: '<option value="">(niemand mehr frei)</option>';
|
||||
$('#fixed-driver').innerHTML = drivers.length
|
||||
? drivers.map((d) => `<option value="${d.id}">${esc(d.name)} (${d.seats - (used.get(d.id) || 0)} frei)</option>`).join('')
|
||||
: '<option value="">(kein Auto dabei)</option>';
|
||||
$('#fixed-form button').disabled = !passengers.length || !drivers.length;
|
||||
|
||||
$('#fixed-list').innerHTML = [...trip.fixed].map(([passengerId, driverId]) => `
|
||||
<li>
|
||||
<span class="name">${esc(byId.get(passengerId).name)} <span class="muted">fährt bei</span> ${esc(byId.get(driverId).name)}</span>
|
||||
<button class="link danger" data-unfix="${passengerId}">Entfernen</button>
|
||||
</li>`).join('');
|
||||
}
|
||||
|
||||
$('#fixed-form').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const passengerId = $('#fixed-passenger').value;
|
||||
const driverId = $('#fixed-driver').value;
|
||||
if (!passengerId || !driverId || passengerId === driverId) return;
|
||||
trip.fixed.set(passengerId, driverId);
|
||||
renderFixed();
|
||||
});
|
||||
|
||||
$('#fixed-list').addEventListener('click', (e) => {
|
||||
const id = e.target.dataset.unfix;
|
||||
if (!id) return;
|
||||
trip.fixed.delete(id);
|
||||
renderFixed();
|
||||
});
|
||||
|
||||
// ---------- Zurücksetzen ----------
|
||||
function resetTrip() {
|
||||
trip.selected.clear();
|
||||
trip.bringsCar.clear();
|
||||
trip.guests = [];
|
||||
trip.fixed.clear();
|
||||
$('#trip-title').value = '';
|
||||
$('#trip-date').value = '';
|
||||
$('#trip-point').selectedIndex = 0;
|
||||
setDeparture(DEFAULT_DEPARTURE);
|
||||
$('#opt-min-cars').checked = true;
|
||||
$('#guest-form').reset();
|
||||
$('#guest-seats').hidden = true;
|
||||
$('#result').innerHTML = '';
|
||||
$('#from-event').hidden = true;
|
||||
history.replaceState(null, '', location.pathname);
|
||||
renderTripMembers();
|
||||
renderGuests();
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
$('#reset-btn').addEventListener('click', resetTrip);
|
||||
|
||||
// ---------- Einteilung berechnen ----------
|
||||
/**
|
||||
* Verteilt die Teilnehmer auf Autos.
|
||||
* - Feste Zuteilung (fixed: Mitfahrer-ID -> Fahrer-ID): der Fahrer fährt sicher, der Mitfahrer sitzt sicher bei ihm.
|
||||
* - Wer immer mit eigenem Auto kommt (alwaysDrives) und es dabeihat, fährt auf jeden Fall.
|
||||
* - minCars: weitere Autos nur so viele wie nötig (größte zuerst), sonst alle Autos.
|
||||
* Wer lieber bei einem anderen Autobesitzer mitfährt (ridesWith), wird erst zuletzt Fahrer.
|
||||
* - Wunsch-Mitfahrer: wer bei einem Fahrer mitfahren möchte, kommt zuerst in dessen Auto;
|
||||
* Mitfahrer mit Vorliebe füreinander kommen nach Möglichkeit ins selbe Auto.
|
||||
* - Übrige Mitfahrer: zuerst bekommt jedes Auto jemanden (Immer-Fahrer zuerst), danach gleichmäßig auffüllen.
|
||||
*/
|
||||
function planTrip(participants, minCars, fixed = new Map()) {
|
||||
const byId = new Map(participants.map((p) => [p.id, p]));
|
||||
const hasUsableCar = (p) => !!(p?.hasCar && p.seats > 0);
|
||||
const prefersOtherCar = (p) => hasUsableCar(byId.get(p.ridesWith));
|
||||
const fixedDrivers = new Set(fixed.values());
|
||||
const mustDrive = (p) => p.alwaysDrives || fixedDrivers.has(p.id);
|
||||
|
||||
// Fest zugeteilte Mitfahrer fahren nie selbst
|
||||
const carOwners = participants.filter((p) => hasUsableCar(p) && !fixed.has(p.id));
|
||||
const forced = carOwners.filter(mustDrive).sort((a, b) => b.seats - a.seats);
|
||||
const optional = carOwners.filter((p) => !mustDrive(p))
|
||||
.sort((a, b) => prefersOtherCar(a) - prefersOtherCar(b) || b.seats - a.seats);
|
||||
|
||||
let drivers = [...forced, ...optional];
|
||||
if (minCars) {
|
||||
drivers = [...forced];
|
||||
let capacity = forced.reduce((sum, d) => sum + 1 + d.seats, 0);
|
||||
for (const owner of optional) {
|
||||
if (capacity >= participants.length) break;
|
||||
drivers.push(owner);
|
||||
capacity += 1 + owner.seats;
|
||||
}
|
||||
}
|
||||
|
||||
const driverIds = new Set(drivers.map((d) => d.id));
|
||||
const cars = drivers.map((d) => ({ driver: d, passengers: [] }));
|
||||
const carOf = new Map(cars.map((c) => [c.driver.id, c]));
|
||||
const unassigned = [];
|
||||
const free = (car) => car.driver.seats - car.passengers.length;
|
||||
const place = (p, car) => { car.passengers.push(p); carOf.set(p.id, car); };
|
||||
|
||||
// Auto mit den wenigsten Mitfahrern, bei Gleichstand Immer-Fahrer, dann die meisten freien Plätze
|
||||
function pickCar(size) {
|
||||
let best = null;
|
||||
for (const car of cars) {
|
||||
if (free(car) < size) continue;
|
||||
if (!best
|
||||
|| car.passengers.length < best.passengers.length
|
||||
|| (car.passengers.length === best.passengers.length
|
||||
&& (car.driver.alwaysDrives && !best.driver.alwaysDrives
|
||||
|| (!!car.driver.alwaysDrives === !!best.driver.alwaysDrives && free(car) > free(best))))) {
|
||||
best = car;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
// 0. Feste Zuteilung für diese Fahrt
|
||||
for (const p of participants) {
|
||||
const car = carOf.get(fixed.get(p.id));
|
||||
if (car && car.driver.id === fixed.get(p.id) && free(car) > 0) place(p, car);
|
||||
}
|
||||
|
||||
// 1. Wer bei einem Fahrer mitfahren möchte, kommt in dessen Auto
|
||||
let rest = [];
|
||||
for (const p of participants) {
|
||||
if (carOf.has(p.id)) continue;
|
||||
const car = carOf.get(p.ridesWith);
|
||||
if (car && car.driver.id === p.ridesWith && free(car) > 0) place(p, car);
|
||||
else rest.push(p);
|
||||
}
|
||||
|
||||
// 2. Übrige Mitfahrer in Gruppen zusammenfassen, die über Vorlieben verbunden sind
|
||||
const groupOf = new Map(rest.map((p) => [p.id, [p]]));
|
||||
for (const p of rest) {
|
||||
const a = groupOf.get(p.id);
|
||||
const b = groupOf.get(p.ridesWith);
|
||||
if (!b || a === b) continue;
|
||||
a.push(...b);
|
||||
for (const q of b) groupOf.set(q.id, a);
|
||||
}
|
||||
const groups = [...new Set(groupOf.values())].sort((a, b) => b.length - a.length);
|
||||
|
||||
// 3. Gruppen verteilen – bevorzugt ins Auto, in dem die Wunschperson schon sitzt
|
||||
for (const group of groups) {
|
||||
const wished = group.map((p) => carOf.get(p.ridesWith)).find((c) => c && free(c) >= group.length);
|
||||
const car = wished || pickCar(group.length);
|
||||
if (car) {
|
||||
group.forEach((p) => place(p, car));
|
||||
continue;
|
||||
}
|
||||
for (const p of group) {
|
||||
const own = carOf.get(p.ridesWith);
|
||||
const c = own && free(own) > 0 ? own : pickCar(1);
|
||||
c ? place(p, c) : unassigned.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
return { cars, unassigned };
|
||||
}
|
||||
|
||||
// ---------- Ergebnis ----------
|
||||
const badges = (p) => (p.guest ? ' <span class="badge">Gast</span>' : '')
|
||||
+ (p.external ? ' <span class="badge">Extern</span>' : '');
|
||||
|
||||
const sameMeeting = (a, b) => a.pointId === b.pointId && a.meet === b.meet && a.departure === b.departure;
|
||||
|
||||
function meetingLines({ pointId, meet, departure }) {
|
||||
const point = points.find((p) => p.id === pointId);
|
||||
return [
|
||||
point && `Treffpunkt: ${point.name}, ${point.address}`,
|
||||
meet && `Treffzeit: ${meet} Uhr`,
|
||||
departure && `Abfahrt: ${departure} Uhr`,
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
function meetingHtml({ pointId, meet, departure }) {
|
||||
const point = points.find((p) => p.id === pointId);
|
||||
let html = '<dl class="meeting">';
|
||||
if (point) {
|
||||
const maps = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(point.address)}`;
|
||||
html += `<dt>Treffpunkt</dt><dd>${esc(point.name)}
|
||||
<small><a href="${maps}" target="_blank" rel="noopener">${esc(point.address)}</a></small></dd>`;
|
||||
}
|
||||
if (meet) html += `<dt>Treffzeit</dt><dd>${esc(meet)} Uhr</dd>`;
|
||||
if (departure) html += `<dt>Abfahrt</dt><dd>${esc(departure)} Uhr</dd>`;
|
||||
return html + '</dl>';
|
||||
}
|
||||
|
||||
// Haben alle Autos denselben Treffpunkt und dieselben Zeiten, steht das einmal oben, sonst bei jedem Auto
|
||||
function commonMeeting(carMeet, defaults) {
|
||||
if (!carMeet.length) return defaults;
|
||||
return carMeet.every((m) => sameMeeting(m, carMeet[0])) ? carMeet[0] : null;
|
||||
}
|
||||
|
||||
function buildText(heading, { cars, unassigned }, carMeet, defaults) {
|
||||
const lines = [`Fahrgemeinschaften${heading ? ': ' + heading : ''}`, ''];
|
||||
const common = commonMeeting(carMeet, defaults);
|
||||
if (common) {
|
||||
const ml = meetingLines(common);
|
||||
if (ml.length) lines.push(...ml, '');
|
||||
}
|
||||
cars.forEach((car, i) => {
|
||||
if (i > 0 && !common) lines.push('');
|
||||
lines.push(`Fahrzeug ${i + 1} – Fahrer: ${car.driver.name} (${car.passengers.length}/${car.driver.seats} belegt)`);
|
||||
if (!common) meetingLines(carMeet[i]).forEach((l) => lines.push(` ${l}`));
|
||||
car.passengers.forEach((p) => lines.push(` • ${p.name}`));
|
||||
if (!car.passengers.length) lines.push(' • (keine Mitfahrer)');
|
||||
});
|
||||
if (unassigned.length) {
|
||||
lines.push('', `OHNE PLATZ: ${unassigned.map((p) => p.name).join(', ')}`);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
$('#plan-btn').addEventListener('click', () => {
|
||||
const participants = currentParticipants();
|
||||
|
||||
const result = $('#result');
|
||||
if (!participants.length) {
|
||||
result.innerHTML = '<p class="warn">Bitte wähle mindestens eine Person aus.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const plan = planTrip(participants, $('#opt-min-cars').checked, trip.fixed);
|
||||
const heading = [$('#trip-title').value.trim(), formatDate($('#trip-date').value)].filter(Boolean).join(' – ');
|
||||
const totalSeats = plan.cars.reduce((s, c) => s + 1 + c.driver.seats, 0);
|
||||
|
||||
// Treffpunkt und Zeiten pro Auto: Standard-Treffpunkt des Fahrers, sonst der Treffpunkt der Fahrt
|
||||
const defaults = {
|
||||
pointId: $('#trip-point').value,
|
||||
meet: $('#trip-meet').value,
|
||||
departure: $('#trip-departure').value,
|
||||
};
|
||||
const carMeet = plan.cars.map((car) => ({
|
||||
...defaults,
|
||||
pointId: points.some((p) => p.id === car.driver.meetingPoint) ? car.driver.meetingPoint : defaults.pointId,
|
||||
}));
|
||||
|
||||
const pointOptions = (selected) => points
|
||||
.map((p) => `<option value="${p.id}" ${p.id === selected ? 'selected' : ''}>${esc(p.name)}</option>`).join('')
|
||||
+ `<option value="" ${selected ? '' : 'selected'}>(kein Treffpunkt)</option>`;
|
||||
|
||||
let html = heading ? `<h3>${esc(heading)}</h3>` : '';
|
||||
html += '<div id="meeting-common"></div>';
|
||||
html += `<p class="muted">${participants.length} Personen · ${plan.cars.length} ${plan.cars.length === 1 ? 'Auto' : 'Autos'} · ${totalSeats} Sitzplätze</p>`;
|
||||
|
||||
html += '<div class="cars">' + plan.cars.map((car, i) => `
|
||||
<div class="car">
|
||||
<h3>Auto ${i + 1}: ${esc(car.driver.name)}${badges(car.driver)}</h3>
|
||||
<div class="muted">${car.passengers.length} / ${car.driver.seats} Plätze belegt</div>
|
||||
<div class="car-meet no-print">
|
||||
<label>Treffpunkt <select data-car-point="${i}">${pointOptions(carMeet[i].pointId)}</select></label>
|
||||
<div class="grid2">
|
||||
<label>Treffzeit <input type="time" data-car-meet="${i}" value="${esc(carMeet[i].meet)}"></label>
|
||||
<label>Abfahrt <input type="time" data-car-dep="${i}" value="${esc(carMeet[i].departure)}"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div data-car-info="${i}"></div>
|
||||
${car.passengers.length
|
||||
? `<ol>${car.passengers.map((p) => `<li>${esc(p.name)}${badges(p)}${trip.fixed.get(p.id) === car.driver.id ? ' <span class="badge">fest</span>' : ''}</li>`).join('')}</ol>`
|
||||
: '<p class="muted">keine Mitfahrer</p>'}
|
||||
</div>`).join('') + '</div>';
|
||||
|
||||
if (plan.unassigned.length) {
|
||||
const missing = plan.unassigned.length;
|
||||
html += `<div class="warn"><strong>Nicht genug Plätze!</strong> Es fehlen ${seatsLabel(missing)}.<br>
|
||||
Ohne Platz: ${plan.unassigned.map((p) => esc(p.name)).join(', ')}</div>`;
|
||||
}
|
||||
|
||||
html += `
|
||||
<div class="row wrap no-print">
|
||||
<button id="copy-btn">Als Text kopieren</button>
|
||||
<button id="print-btn">Drucken</button>
|
||||
<button id="reset-after-btn">Neue Fahrt</button>
|
||||
</div>`;
|
||||
|
||||
result.innerHTML = html;
|
||||
|
||||
function renderMeetings() {
|
||||
const common = commonMeeting(carMeet, defaults);
|
||||
$('#meeting-common').innerHTML = common ? meetingHtml(common) : '';
|
||||
carMeet.forEach((m, i) => {
|
||||
$(`[data-car-info="${i}"]`).innerHTML = common ? '' : meetingHtml(m);
|
||||
});
|
||||
}
|
||||
renderMeetings();
|
||||
|
||||
result.querySelector('.cars').addEventListener('change', (e) => {
|
||||
const { carPoint, carMeet: meetIdx, carDep } = e.target.dataset;
|
||||
if (carPoint !== undefined) carMeet[carPoint].pointId = e.target.value;
|
||||
if (meetIdx !== undefined) carMeet[meetIdx].meet = e.target.value;
|
||||
if (carDep !== undefined) {
|
||||
// Treffzeit folgt der Abfahrt wie oben bei der Fahrt
|
||||
const m = carMeet[carDep];
|
||||
m.departure = e.target.value;
|
||||
if (m.departure) {
|
||||
m.meet = minusMinutes(m.departure, MEET_BEFORE_MIN);
|
||||
$(`[data-car-meet="${carDep}"]`).value = m.meet;
|
||||
}
|
||||
}
|
||||
$('#copy-btn').textContent = 'Als Text kopieren';
|
||||
renderMeetings();
|
||||
});
|
||||
|
||||
$('#copy-btn').addEventListener('click', async (e) => {
|
||||
const text = buildText(heading, plan, carMeet, defaults);
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
e.target.textContent = 'Kopiert';
|
||||
} catch {
|
||||
prompt('Text zum Kopieren:', text);
|
||||
}
|
||||
});
|
||||
$('#print-btn').addEventListener('click', () => window.print());
|
||||
$('#reset-after-btn').addEventListener('click', resetTrip);
|
||||
});
|
||||
|
||||
// ---------- Übernahme aus Kalender-Termin ----------
|
||||
async function applyEventFromUrl() {
|
||||
const eventId = new URLSearchParams(location.search).get('termin');
|
||||
if (!eventId) return;
|
||||
let ev;
|
||||
try {
|
||||
ev = await api('GET', `/events/${encodeURIComponent(eventId)}`);
|
||||
} catch {
|
||||
showError(new Error('Der Termin wurde nicht gefunden.'));
|
||||
return;
|
||||
}
|
||||
const known = ev.attendees.filter((id) => members.some((m) => m.id === id));
|
||||
known.forEach((id) => trip.selected.add(id));
|
||||
$('#trip-title').value = ev.title;
|
||||
$('#trip-date').value = ev.date;
|
||||
const notice = $('#from-event');
|
||||
notice.innerHTML = `Übernommen aus dem Kalender: <strong>${esc(ev.title)}</strong> am ${formatDate(ev.date, true)} –
|
||||
${known.length} ${known.length === 1 ? 'Zusage ist' : 'Zusagen sind'} schon als „kommt mit“ ausgewählt.`;
|
||||
notice.hidden = false;
|
||||
}
|
||||
|
||||
// ---------- Start ----------
|
||||
async function init() {
|
||||
[members, points] = await Promise.all([api('GET', '/members'), api('GET', '/meeting-points')]);
|
||||
renderPoints();
|
||||
await applyEventFromUrl();
|
||||
renderTripMembers();
|
||||
renderGuests();
|
||||
}
|
||||
|
||||
init().catch(showError);
|
||||
Reference in new issue
Block a user