export default { ALL_VALUE: "__ALL__", CORE_COLUMNS: [ "Vorgang", "Name", "Vorname", "E-Mail", "Email", "Geb.-Datum", "Zus_Wunsch", "Skipassnummer" ], getWorkbook() { const file = Dateiupload.files?.[0]; if (!file) { return []; } const rawData = file.data; if ( rawData === null || rawData === undefined || rawData === "" ) { return []; } if (typeof rawData === "string") { try { return JSON.parse(rawData); } catch (error) { return []; } } return rawData; }, getSheetRows() { const workbook = this.getWorkbook(); if ( !Array.isArray(workbook) || workbook.length === 0 ) { return []; } /* * Appsmith liefert Excel-Dateien meist so: * * [ * { * name: "!Leistungsliste", * data: [ * ["Vorgang", "Name", "Vorname"], * ["98555", "Statz", "Hannah"] * ] * } * ] */ if ( workbook[0] !== null && typeof workbook[0] === "object" && !Array.isArray(workbook[0]) && Array.isArray(workbook[0].data) ) { const preferredSheet = workbook.find(sheet => String(sheet?.name ?? "") .toLowerCase() .includes("leistungsliste") ); const selectedSheet = preferredSheet ?? workbook[0]; return Array.isArray(selectedSheet?.data) ? selectedSheet.data : []; } /* * Alternativ können die Zeilen direkt geliefert werden. */ return workbook; }, createUniqueHeaders(headerRow) { const usedHeaders = {}; return headerRow.map((value, index) => { let header = String(value ?? "") .replace(/\s+/g, " ") .trim(); if (!header) { header = `Spalte_${index + 1}`; } usedHeaders[header] = (usedHeaders[header] ?? 0) + 1; if (usedHeaders[header] > 1) { return `${header}_${usedHeaders[header]}`; } return header; }); }, rowsAsObjects() { const rows = this.getSheetRows(); if ( !Array.isArray(rows) || rows.length === 0 ) { return []; } /* * Daten liegen bereits als Objekte vor. */ if ( rows[0] !== null && typeof rows[0] === "object" && !Array.isArray(rows[0]) ) { return rows.filter(row => Object.values(row).some(value => String(value ?? "").trim() !== "" ) ); } /* * Daten liegen als Zeilen-Arrays vor. * Erste Zeile wird als Überschrift verwendet. */ if (!Array.isArray(rows[0])) { return []; } const headers = this.createUniqueHeaders(rows[0]); return rows .slice(1) .filter(row => Array.isArray(row) && row.some(value => String(value ?? "").trim() !== "" ) ) .map(row => { const result = {}; headers.forEach((header, index) => { result[header] = row[index] ?? ""; }); return result; }); }, cleanText(value) { return String(value ?? "").trim(); }, isBooked(value) { const normalized = String(value ?? "") .trim() .toLowerCase(); const emptyValues = [ "", "0", "false", "nein", "no", "n", "null", "undefined", "-" ]; return !emptyValues.includes(normalized); }, formatExcelDate(value) { if ( value === null || value === undefined || value === "" ) { return ""; } const numericValue = Number(value); /* * Excel-Datumswerte sind fortlaufende Nummern. * Beispiel: 41495 wird zu einem echten Datum. */ if ( Number.isFinite(numericValue) && numericValue > 20000 && numericValue < 100000 ) { const milliseconds = Math.round( (numericValue - 25569) * 86400 * 1000 ); const date = new Date(milliseconds); if (!Number.isNaN(date.getTime())) { const day = String( date.getUTCDate() ).padStart(2, "0"); const month = String( date.getUTCMonth() + 1 ).padStart(2, "0"); const year = date.getUTCFullYear(); return `${day}.${month}.${year}`; } } return this.cleanText(value); }, getServiceColumns() { const rows = this.rowsAsObjects(); if (rows.length === 0) { return []; } return Object.keys(rows[0]) .filter(column => !this.CORE_COLUMNS.includes(column) ) .filter(column => !column.startsWith("Spalte_") ); }, getSavedState() { const state = appsmith.store?.checkinState; if ( state !== null && typeof state === "object" && !Array.isArray(state) ) { return state; } return {}; }, createPersonId(rawRow, index) { const vorgang = this.cleanText(rawRow["Vorgang"]); const name = this.cleanText(rawRow["Name"]); const vorname = this.cleanText(rawRow["Vorname"]); const email = this.cleanText( rawRow["E-Mail"] ?? rawRow["Email"] ); const gebDatum = this.cleanText(rawRow["Geb.-Datum"]); return [ vorgang, name, vorname, email, gebDatum, index ] .join("|") .toLowerCase() .replace(/\s+/g, "_"); }, internalRows() { const rawRows = this.rowsAsObjects(); const serviceColumns = this.getServiceColumns(); const savedState = this.getSavedState(); return rawRows.map((rawRow, index) => { const id = this.createPersonId(rawRow, index); const state = savedState[id] ?? {}; const bookedServices = serviceColumns.filter(column => this.isBooked(rawRow[column]) ); return { id, vorgang: this.cleanText( rawRow["Vorgang"] ), name: this.cleanText( rawRow["Name"] ), vorname: this.cleanText( rawRow["Vorname"] ), email: this.cleanText( rawRow["E-Mail"] ?? rawRow["Email"] ), gebDatum: this.formatExcelDate( rawRow["Geb.-Datum"] ), zimmer: state.zimmer ?? this.cleanText( rawRow["Zus_Wunsch"] ), schluessel: Number( state.schluessel ?? 0 ), schluesselRetour: Boolean( state.schluesselRetour ?? false ), leistungenListe: bookedServices, leistungen: bookedServices.join(" · "), skipassnummer: state.skipassnummer ?? this.cleanText( rawRow["Skipassnummer"] ), notiz: state.notiz ?? "", stirnlampe: Number( state.stirnlampe ?? 0 ), stirnlampeRetour: Boolean( state.stirnlampeRetour ?? false ), nachbuchungen: Array.isArray( state.nachbuchungen ) ? state.nachbuchungen : [], status: state.status ?? "offen", reiseStatus: state.reiseStatus ?? "aktiv", checkedIn: Boolean( state.checkedIn ?? false ), checkedInAt: state.checkedInAt ?? "", checkedInBy: state.checkedInBy ?? "" }; }); }, vorgangOptions() { const vorgaenge = [ ...new Set( this.internalRows() .map(row => row.vorgang) .filter(Boolean) ) ].sort((a, b) => a.localeCompare( b, "de", { numeric: true } ) ); return [ { label: "Alle Gruppen", value: this.ALL_VALUE }, ...vorgaenge.map(vorgang => ({ label: vorgang, value: vorgang })) ]; }, leistungOptions() { const services = [ ...new Set( this.internalRows() .flatMap(row => row.leistungenListe ?? [] ) .filter(Boolean) ) ].sort((a, b) => a.localeCompare(b, "de") ); return [ { label: "Alle Leistungen", value: this.ALL_VALUE }, ...services.map(service => ({ label: service, value: service })) ]; }, filteredInternalRows() { let rows = [ ...this.internalRows() ]; /* * Suche */ const searchText = this.cleanText( inpSuche.text ).toLowerCase(); if (searchText) { rows = rows.filter(row => { const searchableText = [ row.vorgang, row.name, row.vorname, row.email, row.gebDatum, row.zimmer, row.skipassnummer, row.leistungen ] .join(" ") .toLowerCase(); return searchableText.includes( searchText ); }); } /* * Vorgangs-/Gruppenfilter */ const selectedVorgang = selVorgang.selectedOptionValue || this.ALL_VALUE; if ( selectedVorgang !== this.ALL_VALUE ) { rows = rows.filter(row => row.vorgang === String(selectedVorgang) ); } /* * Check-in-Filter */ const selectedCheckin = selCheckin.selectedOptionValue || this.ALL_VALUE; if ( selectedCheckin === "checked" ) { rows = rows.filter(row => row.checkedIn === true ); } if ( selectedCheckin === "pending" ) { rows = rows.filter(row => row.checkedIn === false ); } /* * Reisestatusfilter */ const selectedReiseStatus = selReiseStatus.selectedOptionValue || this.ALL_VALUE; if ( selectedReiseStatus !== this.ALL_VALUE ) { rows = rows.filter(row => row.reiseStatus === selectedReiseStatus ); } /* * Leistungsfilter */ const selectedLeistung = selLeistung.selectedOptionValue || this.ALL_VALUE; if ( selectedLeistung !== this.ALL_VALUE ) { rows = rows.filter(row => row.leistungenListe.includes( selectedLeistung ) ); } return rows; }, filteredRows() { return this.filteredInternalRows() .map(row => ({ id: row.id, Vorgang: row.vorgang, Name: row.name, Vorname: row.vorname, "E-Mail": row.email, "Geb.-Datum": row.gebDatum, Zimmer: row.zimmer, Schlüssel: row.schluessel, "Schlüssel retour": row.schluesselRetour, "Skipass / Leistungen": row.leistungen, Skipassnummer: row.skipassnummer, Notiz: row.notiz, Stirnlampe: row.stirnlampe, "Stirnlampe retour": row.stirnlampeRetour, Nachbuchungen: row.nachbuchungen.length, Status: row.status, Reisestatus: row.reiseStatus, Eingecheckt: row.checkedIn, "Check-in Zeitpunkt": row.checkedInAt, "Check-in Benutzer": row.checkedInBy })); }, stats() { const rows = this.internalRows(); return { gesamt: rows.length, eingecheckt: rows.filter(row => row.checkedIn === true ).length, ausstehend: rows.filter(row => row.checkedIn === false ).length, gruppen: new Set( rows .map(row => row.vorgang) .filter(Boolean) ).size }; }, async savePersonState( personId, changes ) { if (!personId) { showAlert( "Teilnehmer-ID fehlt.", "error" ); return false; } const currentState = { ...this.getSavedState() }; currentState[personId] = { ...(currentState[personId] ?? {}), ...changes, updatedAt: new Date().toISOString(), updatedBy: appsmith.user?.email || "lokaler Benutzer" }; await storeValue( "checkinState", currentState, true ); return true; }, async toggleCheckin(tableRow) { if (!tableRow?.id) { showAlert( "Teilnehmer konnte nicht ermittelt werden.", "error" ); return false; } const person = this.internalRows().find(row => row.id === tableRow.id ); if (!person) { showAlert( "Teilnehmer wurde nicht gefunden.", "error" ); return false; } const newCheckedIn = !person.checkedIn; await this.savePersonState( person.id, { checkedIn: newCheckedIn, checkedInAt: newCheckedIn ? new Date().toISOString() : "", checkedInBy: newCheckedIn ? ( appsmith.user?.email || "lokaler Benutzer" ) : "" } ); showAlert( newCheckedIn ? `${person.vorname} ${person.name} wurde eingecheckt.` : `Check-in von ${person.vorname} ${person.name} wurde aufgehoben.`, newCheckedIn ? "success" : "warning" ); return true; }, async saveUpdatedRow(updatedRow) { if (!updatedRow?.id) { showAlert( "Die bearbeitete Person konnte nicht ermittelt werden.", "error" ); return false; } const person = this.internalRows().find(row => row.id === updatedRow.id ); if (!person) { showAlert( "Teilnehmer wurde nicht gefunden.", "error" ); return false; } const schluesselValue = Number(updatedRow["Schlüssel"]); const stirnlampeValue = Number(updatedRow["Stirnlampe"]); await this.savePersonState( person.id, { zimmer: this.cleanText( updatedRow["Zimmer"] ?? person.zimmer ), schluessel: Number.isFinite(schluesselValue) ? schluesselValue : person.schluessel, schluesselRetour: Boolean( updatedRow["Schlüssel retour"] ?? person.schluesselRetour ), skipassnummer: this.cleanText( updatedRow["Skipassnummer"] ?? person.skipassnummer ), notiz: this.cleanText( updatedRow["Notiz"] ?? person.notiz ), stirnlampe: Number.isFinite(stirnlampeValue) ? stirnlampeValue : person.stirnlampe, stirnlampeRetour: Boolean( updatedRow["Stirnlampe retour"] ?? person.stirnlampeRetour ), status: this.cleanText( updatedRow["Status"] ?? person.status ), reiseStatus: this.cleanText( updatedRow["Reisestatus"] ?? person.reiseStatus ) } ); showAlert( "Änderungen gespeichert.", "success" ); return true; }, async resetCheckinState() { await removeValue( "checkinState" ); showAlert( "Gespeicherte Check-in-Daten wurden zurückgesetzt.", "success" ); return true; }, async checkinButtonClick() { return await this.toggleCheckin( Teilnehmer.triggeredRow ); } };