diff --git a/public/kalkulation.css b/public/kalkulation.css
index 79daa98..8a3f25f 100644
--- a/public/kalkulation.css
+++ b/public/kalkulation.css
@@ -297,6 +297,16 @@ textarea.kal-input { min-height: 84px; }
background: var(--color-neutral-100); color: var(--color-neutral-700);
}
.term-status.fixed { background: var(--color-accent-2-100); color: var(--color-accent-2-800); }
+.term-save { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-top: 14px; }
+.term-save button:disabled { opacity: 0.45; cursor: not-allowed; }
+.term-save-hint { font-size: 13px; color: var(--color-neutral-600); flex: 1; min-width: 200px; text-wrap: pretty; }
+.term-save-hint.done { color: var(--color-accent-2-700); font-weight: 700; }
+.term-save-hint.error { color: var(--color-accent-800); font-weight: 700; }
+.slot:disabled { opacity: 0.4; cursor: not-allowed; text-decoration: line-through; }
+.slot.taken-own {
+ border-color: var(--color-accent-2-500); background: var(--color-accent-2-100);
+ color: var(--color-accent-2-800); text-decoration: none; opacity: 1;
+}
/* ------------------------------------------------------------ Kundenportal --- */
.portal-box {
diff --git a/public/kalkulation.html b/public/kalkulation.html
index 35e4f17..8c1bbc3 100644
--- a/public/kalkulation.html
+++ b/public/kalkulation.html
@@ -111,6 +111,13 @@
Noch kein Termin. Wähle einen Tag und eine Uhrzeit — beides landet in der Zusammenfassung.
+
+
+
+ Trägt den Termin am Akquise-Ticket ein — damit steht er im Planboard.
+
diff --git a/public/kalkulation.js b/public/kalkulation.js
index ede86dd..c0c99d1 100644
--- a/public/kalkulation.js
+++ b/public/kalkulation.js
@@ -123,6 +123,7 @@ const STAFFEL = [{ max: 5, r: 150 }, { max: 15, r: 130 }, { max: 40, r: 115 }, {
const PEOPLE = {
justin: {
name: 'Justin', role: 'Website, Angebote, Betreuung', initial: 'J',
+ email: 'justin@webklar.com',
slots: {
1: ['09:00', '10:00', '11:00', '16:00', '17:00'],
2: ['09:00', '10:00', '16:00', '17:00'],
@@ -133,6 +134,7 @@ const PEOPLE = {
},
kenso: {
name: 'Kenso', role: 'Shop, Software, Automatisierung', initial: 'K',
+ email: 'kenso@webklar.com',
slots: { 2: ['14:00', '15:00', '18:00'], 4: ['14:00', '15:00', '18:00'], 6: ['10:00', '11:00'] },
},
}
@@ -162,6 +164,11 @@ const state = {
let customer = null
let storageKey = 'wk-kalkulation:neu'
+/* Schon vergebene Termine (aus den Akquise-Tickets), damit nichts doppelt
+ gebucht wird. Wird beim Laden und nach jedem Speichern aktualisiert. */
+let booked = []
+let savedAppointment = null
+
/* --------------------------------------------------------------- Helfer --- */
function escapeHtml(str) {
return String(str ?? '')
@@ -179,10 +186,11 @@ function eur(n) {
return Number(n || 0).toLocaleString('de-DE', { maximumFractionDigits: 0 }) + ' €'
}
-async function api(path) {
+async function api(path, options = {}) {
const response = await fetch(path, {
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
+ ...options,
})
const data = await response.json().catch(() => ({}))
if (!response.ok) throw new Error(data.error || `Fehler ${response.status}`)
@@ -285,14 +293,31 @@ function totals() {
}
/* ---------------------------------------------------------------- Termin --- */
+/* Feste Zeitfenster der Person, ergaenzt um Zeiten, die in WOMS bereits am
+ Ticket stehen (jemand kann dort auch von Hand einen Termin setzen). */
function slotsFor(personId, date) {
- return PEOPLE[personId].slots[date.getDay()] || []
+ const base = PEOPLE[personId].slots[date.getDay()] || []
+ const key = dayKey(date)
+ const email = PEOPLE[personId].email
+ const extra = booked
+ .filter((a) => a.date === key && a.employeeEmail === email && a.time && !base.includes(a.time))
+ .map((a) => a.time)
+ return extra.length ? [...new Set([...base, ...extra])].sort() : base
}
function dayKey(date) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
}
+/* Ist die Zeit beim aktuell gewaehlten Mitarbeiter schon vergeben?
+ 'own' = der Termin dieses Kunden (der darf natuerlich stehen bleiben). */
+function slotTaken(dateKey, time) {
+ const email = PEOPLE[state.person].email
+ const hit = booked.find((a) => a.date === dateKey && a.time === time && a.employeeEmail === email)
+ if (!hit) return null
+ return hit.customerId && customer && hit.customerId === customer.id ? 'own' : 'other'
+}
+
function terminDays() {
const today = new Date()
today.setHours(0, 0, 0, 0)
@@ -300,13 +325,34 @@ function terminDays() {
for (let i = 1; i <= 14; i += 1) {
const d = new Date(today)
d.setDate(today.getDate() + i)
- list.push({ key: dayKey(d), date: d, slots: slotsFor(state.person, d) })
+ const key = dayKey(d)
+ const slots = slotsFor(state.person, d)
+ list.push({
+ key,
+ date: d,
+ slots,
+ free: slots.filter((s) => slotTaken(key, s) !== 'other'),
+ })
}
return list
}
+async function loadAppointments() {
+ const days = terminDays()
+ if (!days.length) return
+ try {
+ const { appointments } = await api(
+ `/api/portal-admin/appointments?from=${days[0].key}&to=${days[days.length - 1].key}`
+ )
+ booked = appointments || []
+ } catch (err) {
+ console.warn('Belegte Termine konnten nicht geladen werden:', err.message)
+ booked = []
+ }
+}
+
function selectedDay() {
- return terminDays().find((d) => d.key === state.day && d.slots.length) || null
+ return terminDays().find((d) => d.key === state.day && d.free.length) || null
}
function longDay(date) {
@@ -360,6 +406,7 @@ function summaryText() {
if (term) {
lines.push('', 'TERMIN', term)
if (state.termNote) lines.push(`Thema: ${state.termNote}`)
+ if (savedAppointment?.woid) lines.push(`Eingetragen in Ticket #${savedAppointment.woid}`)
}
if (state.notes) lines.push('', 'NOTIZEN', state.notes)
if (state.portalEmail || state.portalPass) {
@@ -475,7 +522,7 @@ function renderTermin() {
const days = terminDays()
el('days-label').textContent = `Freie Tage — ${PEOPLE[state.person].name}`
el('day-list').innerHTML = days.map((d) => {
- const free = d.slots.length
+ const free = d.free.length
const on = state.day === d.key && free > 0
return `