Unnötige Datein gelöscht. DB normalisiert
This commit is contained in:
1 parent
da64472251
commit
4e083bee73
32 files changed
+971
-5542
No files matched your search
Vendored
BIN
Binary file not shown.
+1
-2
@@ -5,7 +5,7 @@
|
||||
und verwaltet wöchentliche Sitzungen (Sitzungswahl, -anlage, -duplikat und -löschung im
|
||||
seitlichen Menü) je Abteilung (ELT, HLS, Architektur). Sie lädt das gemeinsame Stylesheet
|
||||
styles.css sowie der Reihe nach die Module js/globals.js, js/utils.js, js/ui.js,
|
||||
js/date-picker.js, js/csv.js, js/server.js, js/store.js, js/weeks.js, danach die
|
||||
js/date-picker.js, js/server.js, js/store.js, js/weeks.js, danach die
|
||||
tab-spezifischen Module js/board/board.js, js/people/people.js,
|
||||
js/projects/projects.js, js/load/load.js sowie js/gantt.js, js/showmode.js,
|
||||
js/tabs-auth.js, js/pdf.js und js/init.js; crypto-js und pdfmake kommen von einem
|
||||
@@ -227,7 +227,6 @@
|
||||
<script src="/js/utils.js?v=5"></script>
|
||||
<script src="/js/ui.js?v=5"></script>
|
||||
<script src="/js/date-picker.js?v=5"></script>
|
||||
<script src="/js/csv.js?v=5"></script>
|
||||
<script src="/js/server.js?v=5"></script>
|
||||
<script src="/js/store.js?v=5"></script>
|
||||
<script src="/js/weeks.js?v=5"></script>
|
||||
|
||||
Vendored
BIN
Binary file not shown.
@@ -53,7 +53,7 @@ function formatStatus(command, value){
|
||||
}
|
||||
|
||||
// peopleByRole liegt jetzt im gemeinsamen Modul js/utils.js (benötigt auch
|
||||
// csv.js für die Projektplan-Seite, die board.js nicht lädt).
|
||||
// die Projektplan-Seite, die board.js nicht lädt).
|
||||
|
||||
// roleClass: erzeugt CSS-Klassen für eine Rolle bzw. ein Rollen-Array
|
||||
// (Rollenname kleingeschrieben, Leerzeichen → Bindestrich). Parameter: `role`
|
||||
@@ -85,7 +85,7 @@ function renderBoard(){
|
||||
let h2 = `<tr>`;
|
||||
ROLES.forEach(r=>{
|
||||
if(grouped[r].length===0) h2 += `<th class="rot ${roleClass(r)}">–</th>`;
|
||||
grouped[r].forEach(entry=> h2 += `<th class="rot ${roleClass(entry.role)}">${entry.person.name}</th>`);
|
||||
grouped[r].forEach(entry=> h2 += `<th class="rot ${roleClass(entry.role)}">${esc(entry.person.name)}</th>`);
|
||||
});
|
||||
LPHS.forEach(l=> h2 += `<th class="rot">${l}</th>`);
|
||||
h2 += `</tr>`;
|
||||
|
||||
@@ -1,351 +0,0 @@
|
||||
// -----------------------------
|
||||
// Projektübersicht — CSV-Serialisierung & Parsing
|
||||
// Hinweis: weekToCsv() und parseSessionCsv() sind invers zueinander.
|
||||
// Das Spaltenlayout darf nicht geändert werden, ohne beide Funktionen anzupassen.
|
||||
// -----------------------------
|
||||
// Diese Datei enthält das gesamte CSV-Handling (Serialisieren und Parsen) und
|
||||
// wird nach js/date-picker.js von beiden HTML-Einstiegspunkten geladen. Sie
|
||||
// stellt die globalen Funktionen weekToCsv/parseSessionCsv, das Personen-CSV
|
||||
// (globalPeopleToCsv/parseGlobalPeopleCsv) sowie Helfer wie weekSortKey und
|
||||
// decodeWeekLabel bereit. Hauptnutzer sind js/store.js (Laden/Speichern aller
|
||||
// Sitzungen), js/weeks.js (Wochen-Klonen) und js/plan/plan-state.js (savePlan),
|
||||
// die die CSV-Zeichenketten über js/server.js austauschen.
|
||||
|
||||
// filenameForWeek: leitet aus dem Wochenlabel einen Dateinamen ab
|
||||
// (alle Nicht-Wort-Zeichen werden durch _ ersetzt) und hängt .csv an.
|
||||
// Parameter: label (String). Rückgabe: String wie 'KW_30___2026.csv'.
|
||||
function filenameForWeek(label){
|
||||
return (label.replace(/[^\w]/g,'_'))+'.csv';
|
||||
}
|
||||
|
||||
// csvEscape: maskiert ein CSV-Feld, falls es Sonderzeichen (;, ", Zeilenumbruch) enthält
|
||||
// Parameter: v (beliebiger Wert). Rückgabe: String, bei Bedarf in Anführungszeichen.
|
||||
function csvEscape(v){
|
||||
const s = (v === null || v === undefined ? '' : String(v));
|
||||
if(/[;"\n\r]/.test(s)) return '"' + s.replace(/"/g,'""') + '"';
|
||||
return s;
|
||||
}
|
||||
|
||||
// globalPeopleToCsv: serialisiert die globalen Personen (aller Abteilungen) als CSV-Text
|
||||
// Parameter: people (Array von Person-Objekten). Rückgabe: UTF-8-BOM-CSV-String.
|
||||
function globalPeopleToCsv(people){
|
||||
const rows = [['NAME', 'Rollen', 'Voller Name', 'E-Mail', 'Abwesenheiten']];
|
||||
people.slice().sort((a,b) => a.name.localeCompare(b.name)).forEach(p => {
|
||||
const absences = Array.isArray(p.absences) ? JSON.stringify(p.absences) : '';
|
||||
rows.push([p.name, (p.role || []).join(','), p.fullName || '', p.email || '', absences]);
|
||||
});
|
||||
return '\ufeff' + rows.map(r => r.map(csvEscape).join(';')).join('\n');
|
||||
}
|
||||
|
||||
// parseGlobalPeopleCsv: parst den _people.csv-Inhalt zurück in Person-Objekte
|
||||
// Parameter: text (String). Rückgabe: Array von Person-Objekten (mit stabiler ID).
|
||||
function parseGlobalPeopleCsv(text){
|
||||
if(!text) return [];
|
||||
const rows = parseCsvText(text);
|
||||
if(!rows.length) return [];
|
||||
const people = [];
|
||||
rows.slice(1).forEach(row => {
|
||||
const name = (row[0] || '').trim();
|
||||
if(!name) return;
|
||||
const roles = String(row[1] || 'Planung').split(',').map(x => x.trim()).filter(Boolean);
|
||||
const fullName = (row[2] || '').trim() || name;
|
||||
const email = (row[3] || '').trim();
|
||||
let absences = [];
|
||||
if(row[4]){
|
||||
try {
|
||||
const parsed = JSON.parse(row[4]);
|
||||
if(Array.isArray(parsed)) absences = parsed;
|
||||
} catch(e) {
|
||||
// Ignoriere fehlerhafte JSON-Daten
|
||||
}
|
||||
}
|
||||
people.push({ id: personIdForName(name), name, fullName, email, role: roles, absences });
|
||||
});
|
||||
return people;
|
||||
}
|
||||
|
||||
// weekToCsv: serialisiert eine komplette Sitzung (Woche) in das feste CSV-Spaltenlayout
|
||||
// Parameter: w (Wochen-Objekt mit label, date, locked, people, attendance, projects).
|
||||
// Rückgabe: UTF-8-BOM-CSV-String. Invers zu parseSessionCsv().
|
||||
function weekToCsv(w){
|
||||
// allPeople enthält für das Board jede (Person, Rolle)-Kombination einmal.
|
||||
const grouped = peopleByRole(w);
|
||||
const allPeople = [];
|
||||
ROLES.forEach(r => grouped[r].forEach(entry => allPeople.push(entry)));
|
||||
const rows = [];
|
||||
// Metadaten-Zeile: Label, Datum und Sperrstatus der Sitzung
|
||||
rows.push(['WOCHE', w.label || '', w.date || '', w.locked ? '1' : '']);
|
||||
// PERSONEN-Zeile: Pro Spalte Name|Rolle|Voller Name, damit beim Laden
|
||||
// die Zuordnung pro Rolle eindeutig bleibt und der volle Name erhalten ist.
|
||||
// Der volle Name wird immer als dritter Teil gespeichert (auch leer),
|
||||
// damit er beim Bearbeiten nicht versehentlich verloren geht.
|
||||
const peopleMeta = allPeople.map(e => `${e.person.name}|${e.role}|${e.person.fullName || ''}`);
|
||||
rows.push(['PERSONEN', '', '', ...peopleMeta, ...LPHS.map(() => ''), '','','','','']);
|
||||
rows.push(['Pr.-Nr.','Bezeichnung','Status', ...allPeople.map(e => e.person.name), ...LPHS, 'Gruppe','Index','Plan','Aufwand','Aktive Phasen']);
|
||||
rows.push(['ANWESENHEIT','', ...allPeople.map(e => w.attendance && w.attendance[e.person.id] ? 'X' : ''), ...LPHS.map(() => ''),'','','','','']);
|
||||
w.projects.forEach(p => {
|
||||
const arr = [p.nr || '', p.name || '', p.status || ''];
|
||||
allPeople.forEach(entry => {
|
||||
const s = p.assign[assignKey(entry.person.id, entry.role)];
|
||||
arr.push(s === 1 ? 'X' : s === 2 ? '(X)' : '');
|
||||
});
|
||||
LPHS.forEach(l => {
|
||||
const v = p.lph[l];
|
||||
arr.push(v !== undefined && v !== '' ? String(v).replace(/%/g,'') + '%' : '');
|
||||
});
|
||||
arr.push(p.group || '');
|
||||
arr.push(p.order !== undefined ? String(p.order) : '');
|
||||
arr.push(p.plan ? JSON.stringify(p.plan) : '');
|
||||
arr.push(p.aufwandProLP ? JSON.stringify(p.aufwandProLP) : '');
|
||||
arr.push(p.aktuelleLP ? JSON.stringify(p.aktuelleLP) : '');
|
||||
rows.push(arr);
|
||||
});
|
||||
return '\ufeff' + rows.map(r => r.map(csvEscape).join(';')).join('\n');
|
||||
}
|
||||
|
||||
|
||||
// parseCsvText: entfernt ein (mehrfach kodiertes) BOM und zerlegt den Text in Zeilen-Arrays
|
||||
// Parameter: text (String). Rückgabe: Array aus Zeilen, jede ein Feld-Array.
|
||||
function parseCsvText(text){
|
||||
// Manche ältere CSV-Dateien enthalten ein UTF-8-BOM gefolgt von einem
|
||||
// literalen "" (dreifach kodiert). Beides am Anfang entfernen.
|
||||
return text.replace(/^[\ufeff\u00EF\u00BB\u00BF]+/,'').split(/\r?\n/).filter(line => line.trim() !== '').map(parseCsvLine);
|
||||
}
|
||||
|
||||
// Toleranter CSV-Zeilenparser. Versteht sauber gequotetes CSV (wie es
|
||||
// csvEscape/weekToCsv erzeugen). Semikolons innerhalb eines quoted Feldes sind
|
||||
// Feldinhalt und kein Trennzeichen – so bleibt weekToCsv()/parseSessionCsv()
|
||||
// auch bei Status-Texten, die mit ";" enden, invers.
|
||||
// parseCsvLine: zerlegt eine einzelne CSV-Zeile in ein Feld-Array
|
||||
// Parameter: line (String). Rückgabe: Array der Felder.
|
||||
function parseCsvLine(line){
|
||||
const result = [];
|
||||
let current = '';
|
||||
let inQuotes = false;
|
||||
let hasUnclosedQuotes = false;
|
||||
let i = 0;
|
||||
while(i < line.length){
|
||||
const c = line[i];
|
||||
const n = line[i+1];
|
||||
if(inQuotes){
|
||||
if(c === '"' && n === '"'){
|
||||
current += '"'; i += 2;
|
||||
} else if(c === '"' && (n === ';' || n === undefined)){
|
||||
result.push(current);
|
||||
current = '';
|
||||
inQuotes = false;
|
||||
i += (n === ';' ? 2 : 1);
|
||||
} else {
|
||||
current += c; i++;
|
||||
}
|
||||
} else {
|
||||
if(c === ';'){
|
||||
result.push(current);
|
||||
current = ''; i++;
|
||||
} else if(c === '"'){
|
||||
inQuotes = true; i++;
|
||||
} else {
|
||||
current += c; i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(inQuotes) hasUnclosedQuotes = true;
|
||||
result.push(current);
|
||||
if(hasUnclosedQuotes){
|
||||
console.warn('CSV-Zeile enthält nicht geschlossene Anführungszeichen:', line.slice(0,80));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// decodeWeekLabel: rekonstruiert das Wochenlabel aus einem Dateinamen
|
||||
// (ersetzt _ durch Leerzeichen und ergänzt das Layout "KW XX / YYYY").
|
||||
// Parameter: filename (String). Rückgabe: Label-String.
|
||||
function decodeWeekLabel(filename){
|
||||
const base = filename.replace(/\.csv$/i,'');
|
||||
let label = base.replace(/_+/g,' ').trim();
|
||||
if(/^KW \d+ \d+$/.test(label)){
|
||||
label = label.replace(/^KW (\d+) (\d+)$/, 'KW $1 / $2');
|
||||
}
|
||||
return label || base;
|
||||
}
|
||||
|
||||
// Erzeugt einen Sortierschlüssel aus einem Wochenlabel, sodass z.B.
|
||||
// "KW 9 / 2026" vor "KW 10 / 2026" kommt.
|
||||
// weekSortKey: liefert einen nach Jahr und Kalenderwoche sortierbaren Schlüssel
|
||||
// Parameter: label (String). Rückgabe: String (Jahr-KW) oder kleingeschriebenes Label.
|
||||
function weekSortKey(label){
|
||||
const m = String(label || '').match(/KW\s*(\d+)[^\d]+(\d{4})/i);
|
||||
if(m){
|
||||
const kw = m[1].padStart(2,'0');
|
||||
return `${m[2]}-${kw}`;
|
||||
}
|
||||
return String(label || '').toLowerCase();
|
||||
}
|
||||
|
||||
|
||||
// parseSessionCsv: parst die CSV-Zeilen einer Sitzung zurück in ein Wochen-Objekt
|
||||
// Parameter: filename (String), rows (Array von Feld-Arrays aus parseCsvText).
|
||||
// Rückgabe: Wochen-Objekt {id, label, date, people, projects, attendance, locked}
|
||||
// oder null bei leerer Eingabe. Invers zu weekToCsv().
|
||||
function parseSessionCsv(filename, rows){
|
||||
if(!rows || rows.length === 0) return null;
|
||||
let label, date, headerRowIdx = 0, attendanceRowIdx = 1, projectStartIdx = 2;
|
||||
let locked = false;
|
||||
let metaPeopleRow = null;
|
||||
if(rows[0][0] === 'WOCHE'){
|
||||
label = rows[0][1] || decodeWeekLabel(filename);
|
||||
date = rows[0][2] || '';
|
||||
locked = (rows[0][3] || '').trim() === '1';
|
||||
if(rows[1] && rows[1][0] === 'PERSONEN'){
|
||||
metaPeopleRow = rows[1];
|
||||
headerRowIdx = 2; attendanceRowIdx = 3;
|
||||
} else {
|
||||
headerRowIdx = 1; attendanceRowIdx = 2;
|
||||
}
|
||||
} else {
|
||||
label = decodeWeekLabel(filename);
|
||||
date = '';
|
||||
}
|
||||
projectStartIdx = attendanceRowIdx + 1;
|
||||
|
||||
const header = rows[headerRowIdx];
|
||||
if(!header || header.length < 4) return { id: uid(), label, date, people: [], projects: [], attendance: {} };
|
||||
|
||||
const lphStart = header.indexOf('LPH 1');
|
||||
const groupIdx = header.indexOf('Gruppe');
|
||||
const orderIdx = header.indexOf('Index');
|
||||
const planIdx = header.indexOf('Plan');
|
||||
const aufwandIdx = header.indexOf('Aufwand');
|
||||
const aktiveIdx = header.indexOf('Aktive Phasen');
|
||||
const peopleCols = header.slice(3, lphStart === -1 ? header.length : lphStart);
|
||||
|
||||
const people = [];
|
||||
const colMap = [];
|
||||
const personMeta = metaPeopleRow ? metaPeopleRow.slice(3, lphStart === -1 ? metaPeopleRow.length : lphStart) : [];
|
||||
const personByName = {};
|
||||
const nameOccurrences = {};
|
||||
peopleCols.forEach((name, idx) => {
|
||||
const n = (name || '').trim();
|
||||
if(!n){ colMap.push(null); return; }
|
||||
|
||||
let roles = ['Planung'];
|
||||
let fullName = n;
|
||||
if(personMeta[idx]){
|
||||
const parts = String(personMeta[idx]).split('|');
|
||||
const rawRoles = parts[1] || '';
|
||||
const parsedRoles = rawRoles.split(',').map(x => x.trim()).filter(x => ROLES.includes(x));
|
||||
if(parsedRoles.length > 0) roles = parsedRoles;
|
||||
// Voller Name ist alles ab dem zweiten Pipe; damit sind auch Pipe-Zeichen
|
||||
// im Namen möglich. Leere Strings werden bewusst übernommen.
|
||||
if(parts.length >= 3) fullName = parts.slice(2).join('|').trim();
|
||||
} else {
|
||||
const occurrence = nameOccurrences[n] || 0;
|
||||
roles = [ROLES[Math.min(occurrence, ROLES.length - 1)]];
|
||||
nameOccurrences[n] = occurrence + 1;
|
||||
}
|
||||
|
||||
// Gleiche Person (gleiches Kürzel) wird im Board pro Rolle mehrfach
|
||||
// dargestellt, soll aber im Datenmodell nur einmal existieren.
|
||||
let person = personByName[n];
|
||||
if(!person){
|
||||
person = { id: personIdForName(n), name: n, fullName: fullName, role: [] };
|
||||
personByName[n] = person;
|
||||
people.push(person);
|
||||
}
|
||||
// Überschreibe den vollen Namen mit dem Wert aus der CSV; die CSV ist
|
||||
// hier die führende Datenquelle für den vollständigen Namen.
|
||||
person.fullName = fullName;
|
||||
// Sammle alle bekannten Rollen der Person; die aktuelle Spaltenrolle
|
||||
// wird für die Zuordnung über colMap verwendet.
|
||||
roles.forEach(r => { if(!person.role.includes(r)) person.role.push(r); });
|
||||
|
||||
colMap.push({ person, role: roles[0] });
|
||||
});
|
||||
|
||||
const projects = [];
|
||||
for(let rIdx = projectStartIdx; rIdx < rows.length; rIdx++){
|
||||
const row = rows[rIdx];
|
||||
if(row.length < 4) continue;
|
||||
const nr = row[0] || '';
|
||||
const name = row[1] || '';
|
||||
const status = row[2] || '';
|
||||
if(!nr.trim() && !name.trim()) continue;
|
||||
|
||||
const assign = {};
|
||||
peopleCols.forEach((_, i) => {
|
||||
const entry = colMap[i];
|
||||
if(!entry) return;
|
||||
const v = (row[3+i] || '').trim();
|
||||
const key = assignKey(entry.person.id, entry.role);
|
||||
if(v === 'X') assign[key] = 1;
|
||||
else if(v === '(X)') assign[key] = 2;
|
||||
});
|
||||
|
||||
const lph = {};
|
||||
if(lphStart !== -1){
|
||||
LPHS.forEach((l, j) => {
|
||||
const raw = row[lphStart + j];
|
||||
if(raw !== undefined && raw !== ''){
|
||||
const num = String(raw).replace(/%/g,'');
|
||||
if(!isNaN(parseFloat(num)) && isFinite(num)) lph[l] = num;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let group = 'Aktive Projekte';
|
||||
if(groupIdx >= 0 && row[groupIdx]) group = row[groupIdx].trim() || group;
|
||||
else if(row.length > 3) group = row[row.length - 1].trim() || group;
|
||||
|
||||
let order = rIdx - projectStartIdx;
|
||||
if(orderIdx >= 0 && row[orderIdx] !== undefined && row[orderIdx] !== ''){
|
||||
const parsed = parseInt(row[orderIdx], 10);
|
||||
if(!isNaN(parsed)) order = parsed;
|
||||
}
|
||||
|
||||
let plan = null;
|
||||
if(planIdx >= 0 && row[planIdx]){
|
||||
try {
|
||||
const parsed = JSON.parse(row[planIdx]);
|
||||
if(parsed && typeof parsed === 'object') plan = parsed;
|
||||
} catch(e) {
|
||||
console.warn('Plan-Daten für Projekt konnten nicht geparst werden:', nr, name, e);
|
||||
}
|
||||
}
|
||||
|
||||
let aufwandProLP;
|
||||
if(aufwandIdx >= 0 && row[aufwandIdx]){
|
||||
try {
|
||||
const parsed = JSON.parse(row[aufwandIdx]);
|
||||
if(parsed && typeof parsed === 'object') aufwandProLP = parsed;
|
||||
} catch(e) {
|
||||
console.warn('LP-Aufwand für Projekt konnten nicht geparst werden:', nr, name, e);
|
||||
}
|
||||
}
|
||||
|
||||
let aktuelleLP;
|
||||
if(aktiveIdx >= 0 && row[aktiveIdx]){
|
||||
try {
|
||||
const parsed = JSON.parse(row[aktiveIdx]);
|
||||
if(Array.isArray(parsed)) aktuelleLP = parsed;
|
||||
else if(typeof parsed === 'string') aktuelleLP = [parsed];
|
||||
} catch(e) {
|
||||
console.warn('Aktive Phasen für Projekt konnten nicht geparst werden:', nr, name, e);
|
||||
}
|
||||
}
|
||||
|
||||
projects.push({ id: uid(), nr, name, status, group, lph, assign, order, plan, aufwandProLP, aktuelleLP });
|
||||
}
|
||||
|
||||
const attendance = {};
|
||||
const attRow = rows[attendanceRowIdx];
|
||||
if(attRow && attRow[0] === 'ANWESENHEIT'){
|
||||
peopleCols.forEach((_, i) => {
|
||||
const entry = colMap[i];
|
||||
if(entry && (attRow[2+i] || '').trim() === 'X'){
|
||||
attendance[entry.person.id] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { id: uid(), label, date, people, projects, attendance, locked };
|
||||
}
|
||||
+2
-2
@@ -335,8 +335,8 @@ function buildGanttHtml(projects, start, end, options = {}){
|
||||
function openProjectPlan(pid){
|
||||
const w = currentWeek(); if(!w) return;
|
||||
const p = w.projects.find(x => x.id === pid); if(!p) return;
|
||||
// IDs sind nicht persistent (werden bei jedem CSV-Laden neu erzeugt).
|
||||
// Daher werden stabile Werte (Label, Projekt-Nr.) in der URL übergeben.
|
||||
// Projekt-IDs sind in der DB stabil, aber die Plan-Seite ist eine separate
|
||||
// Route. Daher werden stabile Werte (Label, Projekt-Nr.) in der URL übergeben.
|
||||
// Absoluter Pfad mit Abteilung und Projektname als Route-Segment, damit der
|
||||
// Aufruf auch von Deep-Link-URLs wie "/HLS/Auslastung" funktioniert.
|
||||
const url = new URL('/' + currentDepartment + '/' + PLAN_SLUG + '/' + encodeURIComponent(p.name || ''), window.location.origin);
|
||||
|
||||
@@ -114,16 +114,20 @@
|
||||
const todos = Array.isArray(plan.todos) ? plan.todos : [];
|
||||
const milestones = Array.isArray(plan.milestones) ? plan.milestones : [];
|
||||
const todoIds = todos.map(t => t.id);
|
||||
const milestoneIds = milestones.map(m => m.id);
|
||||
|
||||
// Alle beweglichen Zeilen in aktueller DOM-Reihenfolge
|
||||
const rows = [...chart.querySelectorAll(DRAGGABLE_GANTT_ROW_SELECTOR)];
|
||||
const todoRowIds = rows.filter(r => r.classList.contains('gantt-todo')).map(r => r.dataset.todoId).filter(Boolean);
|
||||
const milestoneRowIds = rows.filter(r => r.classList.contains('gantt-milestone-row')).map(r => r.dataset.milestoneId).filter(Boolean);
|
||||
|
||||
// Sicherheitscheck: Anzahl muss stimmen
|
||||
// Sicherheitscheck: Anzahl muss stimmen. Nicht jedes Meilenstein wird als
|
||||
// eigene .gantt-milestone-row gerendert – nur die mit einer TODO-basierten
|
||||
// Position (before:/after:) sind eigene Zeilen. Meilensteine ohne Position
|
||||
// liegen in der Projektzeile und werden hier nicht umsortiert, deshalb nur
|
||||
// die positionierten zählen.
|
||||
if(todoRowIds.length !== todoIds.length) return;
|
||||
if(milestoneRowIds.length !== milestoneIds.length) return;
|
||||
const positionedIds = milestones.filter(m => m.position && /^(before|after):/.test(m.position)).map(m => m.id);
|
||||
if(milestoneRowIds.length !== positionedIds.length) return;
|
||||
|
||||
// TODOs neu ordnen
|
||||
const newTodos = [];
|
||||
|
||||
@@ -66,20 +66,17 @@ window.PlanApp = {
|
||||
|
||||
// bindToolbarButtons: verbindet die Toolbar-Buttons (TODO hinzufügen,
|
||||
// Auto-Schedule, Kritischer Pfad) mit ihren globalen Funktionen der
|
||||
// anderen plan-Module.
|
||||
// anderen plan-Module. Die Buttons binden ihre Handler bereits über das
|
||||
// onclick-Attribut in project-plan.html; hier werden nur Lücken gefüllt,
|
||||
// falls ein Button-Element fehlt.
|
||||
function bindToolbarButtons(){
|
||||
const addTodoBtn = document.getElementById('addTodoBtn');
|
||||
if(addTodoBtn) addTodoBtn.addEventListener('click', () => addTodo());
|
||||
const autoScheduleBtn = document.getElementById('autoScheduleBtn');
|
||||
if(autoScheduleBtn) autoScheduleBtn.addEventListener('click', () => autoScheduleTodos());
|
||||
const criticalPathBtn = document.getElementById('criticalPathBtn');
|
||||
if(criticalPathBtn) criticalPathBtn.addEventListener('click', () => toggleCriticalPath());
|
||||
}
|
||||
|
||||
// loadPlanData: lädt alle Sitzungen und die globale Personenliste der
|
||||
// Abteilung über die Server-API, sortiert die Wochen chronologisch und
|
||||
// sucht daraus die Ziel-Woche bzw. das Ziel-Projekt. Zeigt Fehlermeldungen
|
||||
// der Reihenfolge nach im Seitenkopf an und startet sonst renderPlanPage().
|
||||
// Abteilung über die Server-API (JSON-Dokumente), sortiert die Wochen
|
||||
// chronologisch und sucht daraus die Ziel-Woche bzw. das Ziel-Projekt. Zeigt
|
||||
// Fehlermeldungen der Reihenfolge nach im Seitenkopf an und startet sonst
|
||||
// renderPlanPage().
|
||||
function loadPlanData(){
|
||||
if(!window.fetch){
|
||||
document.getElementById('planSubtitle').textContent = 'Netzwerk nicht verfügbar.';
|
||||
@@ -90,12 +87,15 @@ window.PlanApp = {
|
||||
.then(r => { if(!r.ok) throw new Error('Server HTTP '+r.status); return r.json(); })
|
||||
.then(data => {
|
||||
db = { weeks: [], currentWeek: null, globalPeople: {} };
|
||||
db.globalPeople[app.department] = parseGlobalPeopleCsv(data.people);
|
||||
const parsed = (data.sessions || []).map(s => ({ filename: s.filename, rows: parseCsvText(s.content) }));
|
||||
parsed.forEach(s => {
|
||||
const w = parseSessionCsv(s.filename, s.rows);
|
||||
if(w) db.weeks.push(w);
|
||||
});
|
||||
db.globalPeople[app.department] = Array.isArray(data.people) ? data.people : [];
|
||||
db.weeks = (Array.isArray(data.weeks) ? data.weeks : [])
|
||||
.filter(w => w && typeof w.label === 'string')
|
||||
.map(w => {
|
||||
if(!Array.isArray(w.people)) w.people = [];
|
||||
if(!Array.isArray(w.projects)) w.projects = [];
|
||||
if(!w.attendance) w.attendance = {};
|
||||
return w;
|
||||
});
|
||||
db.weeks.sort((a,b) => weekSortKey(a.label).localeCompare(weekSortKey(b.label)));
|
||||
app.week = db.weeks.find(w => w.label === app.weekLabel);
|
||||
if(!app.week){
|
||||
@@ -179,18 +179,15 @@ window.PlanApp = {
|
||||
// Speichern: nur diese Sitzung; globale Personenliste nicht überschreiben
|
||||
function savePlan(){
|
||||
if(!app.week || !app.project) return;
|
||||
const filename = filenameForWeek(app.week.label);
|
||||
const csv = weekToCsv(app.week);
|
||||
sendCsvToServer(filename, csv).catch(err => console.error('Speichern fehlgeschlagen:', err));
|
||||
sendWeekToServer(app.week).catch(err => console.error('Speichern fehlgeschlagen:', err));
|
||||
}
|
||||
|
||||
// saveGlobalPeople: schreibt die globale Personenliste der Abteilung als
|
||||
// _people.csv auf den Server, aber nur wenn sie nicht leer ist.
|
||||
// JSON-Array auf den Server, aber nur wenn sie nicht leer ist.
|
||||
function saveGlobalPeople(){
|
||||
const people = db.globalPeople[app.department] || [];
|
||||
if(people.length > 0){
|
||||
const peopleCsv = globalPeopleToCsv(people);
|
||||
sendCsvToServer('_people.csv', peopleCsv).catch(err => console.error('Speichern Personen fehlgeschlagen:', err));
|
||||
sendPeopleToServer(people).catch(err => console.error('Speichern Personen fehlgeschlagen:', err));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ function editProjectActiveLP(id, value){
|
||||
function renderProjects(){
|
||||
const w = currentWeek();
|
||||
if(!w) return;
|
||||
const locked = isWeekLocked(w);
|
||||
let html="";
|
||||
const groups = {};
|
||||
w.projects.forEach(p=>{ (groups[p.group]||(groups[p.group]=[])).push(p); });
|
||||
@@ -71,10 +72,10 @@ function renderProjects(){
|
||||
|
||||
sortGroups(Object.keys(groups)).forEach(g=>{
|
||||
html += `<div class="section-title">${esc(g)}</div>`;
|
||||
html += `<div class="projects-sortable" data-group="${g}" ondragover="handleDragOver(event)" ondrop="handleDrop(event)">`;
|
||||
html += `<div class="projects-sortable" data-group="${g}" ${locked ? '' : 'ondragover="handleDragOver(event)" ondrop="handleDrop(event)"'}>`;
|
||||
groups[g].forEach(p=>{
|
||||
html += `<div class="person-list-item" draggable="true" data-project-id="${p.id}" ondragstart="handleDragStart(event)" ondragend="handleDragEnd(event)">
|
||||
<span class="drag-handle" style="cursor:grab;color:#999;margin-right:8px">⋮⋮</span>
|
||||
html += `<div class="person-list-item" ${locked ? '' : 'draggable="true" ondragstart="handleDragStart(event)" ondragend="handleDragEnd(event)"'} data-project-id="${p.id}">
|
||||
${locked ? '' : `<span class="drag-handle" style="cursor:grab;color:#999;margin-right:8px">⋮⋮</span>`}
|
||||
<input type="text" value="${esc(p.nr)}" onchange="editProjectField('${p.id}','nr',this.value)" style="width:100px" placeholder="Proj-Nr">
|
||||
<input type="text" value="${esc(p.name)}" onchange="editProjectField('${p.id}','name',this.value)" style="width:200px" placeholder="Bezeichnung">
|
||||
<select onchange="editProjectField('${p.id}','group',this.value)">
|
||||
|
||||
+31
-17
@@ -1,13 +1,15 @@
|
||||
// -----------------------------
|
||||
// Projektübersicht — Server-Kommunikation (CSV-API)
|
||||
// Projektübersicht — Server-Kommunikation (JSON-API)
|
||||
// -----------------------------
|
||||
// Diese Datei kapselt die HTTP-Kommunikation mit dem Express-Server (siehe
|
||||
// server.js im Projekt-Root) und wird nach js/csv.js von beiden
|
||||
// server.js im Projekt-Root) und wird nach js/date-picker.js von beiden
|
||||
// HTML-Einstiegspunkten geladen. Sie stellt baseUrl, blobToDataUrl,
|
||||
// sendCsvToServer und deleteCsvFromServer bereit. Genutzt wird sie von
|
||||
// js/store.js (syncAllToServer, Speichern/Löschen aller Wochen),
|
||||
// js/weeks.js (Wochen-Löschung) und js/plan/plan-state.js (savePlan schreibt
|
||||
// direkt die aktuelle Woche über sendCsvToServer).
|
||||
// sendWeekToServer, sendPeopleToServer und deleteWeekFromServer bereit.
|
||||
// Genutzt wird sie von js/store.js (syncAllToServer, Speichern aller Wochen
|
||||
// und der Personenliste), js/weeks.js (Wochen-Löschung) und
|
||||
// js/plan/plan-state.js (savePlan schreibt direkt die aktuelle Woche über
|
||||
// sendWeekToServer). Die Daten werden als strukturierte JSON-Objekte an die
|
||||
// SQLite-API des Servers übergeben (keine CSV-Strings mehr).
|
||||
|
||||
// baseUrl: dynamische Server-URL, damit die App auch von anderen Geraeten im Netzwerk erreichbar ist.
|
||||
// Fallback auf localhost:3003, wenn die Seite als lokale Datei geladen wurde.
|
||||
@@ -31,24 +33,36 @@ function blobToDataUrl(blob){
|
||||
});
|
||||
}
|
||||
|
||||
// sendCsvToServer: POSTet eine Speicherwoche im CSV-Format an <baseUrl>/save-csv
|
||||
// Parameter: filename (String), csvContent (String). Rückgabe: Promise mit Server-Antwort.
|
||||
function sendCsvToServer(filename, csvContent){
|
||||
// sendWeekToServer: POSTet ein komplettes Wochen-Objekt (JSON) an
|
||||
// <baseUrl>/save-week. Parameter: week (Objekt). Rückgabe: Promise mit Server-Antwort.
|
||||
function sendWeekToServer(week){
|
||||
if(!window.fetch) return Promise.reject(new Error('fetch nicht verfügbar'));
|
||||
return fetch(baseUrl() + '/save-csv',{
|
||||
return fetch(baseUrl() + '/save-week',{
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json; charset=utf-8'},
|
||||
body:JSON.stringify({filename,csv:csvContent,department:currentDepartment})
|
||||
body:JSON.stringify({week, department:currentDepartment})
|
||||
}).then(r=>{ if(!r.ok) throw new Error('Server HTTP '+r.status); return r.json(); });
|
||||
}
|
||||
|
||||
// deleteCsvFromServer: fordert das Löschen einer Speicherwoche über POST /delete-csv an
|
||||
// Parameter: filename (String). Rückgabe: Promise mit Server-Antwort.
|
||||
function deleteCsvFromServer(filename){
|
||||
// sendPeopleToServer: POSTet die globale Personenliste einer Abteilung als
|
||||
// JSON-Array an <baseUrl>/save-people. Parameter: people (Array).
|
||||
// Rückgabe: Promise mit Server-Antwort.
|
||||
function sendPeopleToServer(people){
|
||||
if(!window.fetch) return Promise.reject(new Error('fetch nicht verfügbar'));
|
||||
return fetch(baseUrl() + '/delete-csv', {
|
||||
return fetch(baseUrl() + '/save-people',{
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json; charset=utf-8'},
|
||||
body:JSON.stringify({people, department:currentDepartment})
|
||||
}).then(r=>{ if(!r.ok) throw new Error('Server HTTP '+r.status); return r.json(); });
|
||||
}
|
||||
|
||||
// deleteWeekFromServer: fordert das Löschen einer Woche über POST /delete-week an
|
||||
// Parameter: label (String, das Wochen-Label). Rückgabe: Promise mit Server-Antwort.
|
||||
function deleteWeekFromServer(label){
|
||||
if(!window.fetch) return Promise.reject(new Error('fetch nicht verfügbar'));
|
||||
return fetch(baseUrl() + '/delete-week', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json; charset=utf-8'},
|
||||
body: JSON.stringify({ filename, department: currentDepartment })
|
||||
body: JSON.stringify({ label, department: currentDepartment })
|
||||
}).then(r=>{ if(!r.ok) throw new Error('Server HTTP '+r.status); return r.json(); });
|
||||
}
|
||||
}
|
||||
+33
-32
@@ -1,18 +1,20 @@
|
||||
// -----------------------------
|
||||
// Projektübersicht — Daten laden, Speichern, Abteilung wechseln
|
||||
// -----------------------------
|
||||
// Diese Datei bildet die Schnittstelle zum Server: Sie lädt alle Sitzungs-CSVs
|
||||
// Diese Datei bildet die Schnittstelle zum Server: Sie lädt alle Sitzungen
|
||||
// beim Start, speichert den kompletten Datenbestand (alle Wochen + globale
|
||||
// Personenliste) und verwaltet den Wechsel zwischen Abteilungen (ELT/HLS/ARCH).
|
||||
// Sie wird von `Test.html` nach `js/csv.js` und `js/server.js` geladen und stellt
|
||||
// Die Daten liegen serverseitig in einer SQLite-Datenbank und werden als
|
||||
// strukturierte JSON-Objekte über die API ausgetauscht (keine CSV-Strings).
|
||||
// Sie wird von `Test.html` nach `js/date-picker.js` und `js/server.js` geladen und stellt
|
||||
// die globalen Funktionen `save()`, `syncAllToServer()`, `switchDepartment()`,
|
||||
// `load()`, `seed()` und `demoProj()` bereit. Aufrufer sind u.a. `js/weeks.js`,
|
||||
// `js/board/board.js`, `js/people/people.js`, `js/projects/projects.js` und `js/load/load.js`; das Speichern
|
||||
// ist über `saveSyncTimer` (in `js/globals.js` deklariert) um 500 ms debounced.
|
||||
|
||||
// save: synchronisiert die aktuellen Daten als CSV-Dateien zum Server.
|
||||
// Jede Sitzung wird als eigene CSV-Datei gespeichert. Personen und Rollen werden
|
||||
// in der Sitzungscsv selbst abgelegt.
|
||||
// save: synchronisiert die aktuellen Daten zum Server.
|
||||
// Jede Sitzung wird als eigenes JSON-Dokument gespeichert. Personen und Rollen
|
||||
// werden in der Sitzung selbst abgelegt.
|
||||
function save(){
|
||||
scheduleSessionSync();
|
||||
}
|
||||
@@ -25,31 +27,28 @@ function scheduleSessionSync(){
|
||||
saveSyncTimer = setTimeout(() => { syncAllToServer(); saveSyncTimer = null; }, 500);
|
||||
}
|
||||
|
||||
// syncAllToServer: schreibt die gesamte Datenbank als CSV-Dateien zum Server.
|
||||
// Zuerst wird die globale Personenliste (`_people.csv`) gespeichert, danach jede
|
||||
// Sitzung einzeln über `filenameForWeek()`/`weekToCsv()` und `sendCsvToServer()`.
|
||||
// syncAllToServer: schreibt die gesamte Datenbank als JSON-Dokumente zum Server.
|
||||
// Zuerst wird die globale Personenliste (`save-people`) gespeichert, danach jede
|
||||
// Sitzung einzeln über `sendWeekToServer()`.
|
||||
// Gibt nichts zurück; Fehler werden nur per console.error protokolliert.
|
||||
function syncAllToServer(){
|
||||
// Zuerst globale Personenliste speichern
|
||||
saveGlobalPeopleToServer();
|
||||
// Dann jede Sitzung speichern
|
||||
db.weeks.forEach(w => {
|
||||
const filename = filenameForWeek(w.label);
|
||||
const csv = weekToCsv(w);
|
||||
sendCsvToServer(filename, csv)
|
||||
.then(res => { if(res && res.ok) console.log('Sitzung gespeichert:', filename); })
|
||||
.catch(err => { console.error('Sitzung konnte nicht gespeichert werden:', filename, err.message || err); });
|
||||
sendWeekToServer(w)
|
||||
.then(res => { if(res && res.ok) console.log('Sitzung gespeichert:', w.label); })
|
||||
.catch(err => { console.error('Sitzung konnte nicht gespeichert werden:', w.label, err.message || err); });
|
||||
});
|
||||
}
|
||||
|
||||
// saveGlobalPeopleToServer: serialisiert die globale Personenliste der aktuellen
|
||||
// Abteilung per `globalPeopleToCsv()` und speichert sie als `_people.csv`.
|
||||
// saveGlobalPeopleToServer: speichert die globale Personenliste der aktuellen
|
||||
// Abteilung über `sendPeopleToServer()` (JSON-Array).
|
||||
// Leere Personenlisten werden übersprungen (kein Schreibvorgang).
|
||||
function saveGlobalPeopleToServer(){
|
||||
const people = db.globalPeople[currentDepartment] || [];
|
||||
if(people.length === 0) return;
|
||||
const csv = globalPeopleToCsv(people);
|
||||
sendCsvToServer('_people.csv', csv)
|
||||
sendPeopleToServer(people)
|
||||
.then(res => { if(res && res.ok) console.log('Globale Personenliste gespeichert'); })
|
||||
.catch(err => { console.error('Globale Personenliste konnte nicht gespeichert werden:', err.message || err); });
|
||||
}
|
||||
@@ -62,7 +61,7 @@ function switchDepartment(dept){
|
||||
if(!DEPARTMENTS[dept]) return;
|
||||
// Laufenden Speicher-Debounce der alten Abteilung verwerfen, damit dessen
|
||||
// Sync nicht nach dem Reset mit dem neuen currentDepartment feuert und die
|
||||
// CSV der falschen Abteilung schreibt.
|
||||
// Daten der falschen Abteilung schreibt.
|
||||
if(saveSyncTimer){ clearTimeout(saveSyncTimer); saveSyncTimer = null; }
|
||||
currentDepartment = dept;
|
||||
saveUiState();
|
||||
@@ -84,11 +83,11 @@ function switchDepartment(dept){
|
||||
load();
|
||||
}
|
||||
|
||||
// load: liest alle CSV-Dateien der aktuellen Abteilung über `/load-all` vom
|
||||
// Server ein, parst Personen und Sitzungen (mit `parseSessionCsv()`) und baut
|
||||
// daraus `db.weeks` auf. Sortiert nach `weekSortKey()`, wendet `applyAutoLock()`
|
||||
// an und rendert die Ansicht. Kein Parameter, keine Rückgabe; bei Fehlern oder
|
||||
// fehlenden Daten werden Beispieldaten über `seed()` geladen.
|
||||
// load: liest alle Sitzungen und die globale Personenliste der aktuellen
|
||||
// Abteilung über `/load-all` als JSON vom Server und baut daraus `db.weeks`
|
||||
// auf. Sortiert nach `weekSortKey()`, wendet `applyAutoLock()` an und rendert
|
||||
// die Ansicht. Kein Parameter, keine Rückgabe; bei Fehlern oder fehlenden
|
||||
// Daten werden Beispieldaten über `seed()` geladen.
|
||||
function load(){
|
||||
if(!window.fetch){
|
||||
alert('Ihr Browser unterstützt keine Netzwerk-Anfragen. Bitte verwenden Sie einen aktuellen Browser.');
|
||||
@@ -106,18 +105,20 @@ function load(){
|
||||
.then(data => {
|
||||
if(currentDepartment !== requestedDepartment) return;
|
||||
// Globale Personenliste laden
|
||||
db.globalPeople[currentDepartment] = parseGlobalPeopleCsv(data.people);
|
||||
db.globalPeople[currentDepartment] = Array.isArray(data.people) ? data.people : [];
|
||||
|
||||
const parsedSessions = (data.sessions || []).map(s => ({ filename: s.filename, rows: parseCsvText(s.content) }));
|
||||
|
||||
db.weeks = [];
|
||||
parsedSessions.forEach(s => {
|
||||
const w = parseSessionCsv(s.filename, s.rows);
|
||||
if(w) db.weeks.push(w);
|
||||
});
|
||||
db.weeks = (Array.isArray(data.weeks) ? data.weeks : [])
|
||||
.filter(w => w && typeof w.label === 'string')
|
||||
.map(w => {
|
||||
// Felder absichern, damit alte/kaputte Dokumente nicht crashen
|
||||
if(!Array.isArray(w.people)) w.people = [];
|
||||
if(!Array.isArray(w.projects)) w.projects = [];
|
||||
if(!w.attendance) w.attendance = {};
|
||||
return w;
|
||||
});
|
||||
|
||||
if(db.weeks.length === 0){
|
||||
console.warn('Server lieferte 0 Sitzungsdateien. Es werden Beispieldaten (seed) angezeigt. Pruefen Sie, ob der Ordner sessions/ mitkopiert wurde.');
|
||||
console.warn('Server lieferte 0 Sitzungen. Es werden Beispieldaten (seed) angezeigt. Pruefen Sie, ob die SQLite-Datenbank sessions/projektuebersicht.db existiert.');
|
||||
seed();
|
||||
}
|
||||
else {
|
||||
|
||||
+14
-3
@@ -5,7 +5,7 @@
|
||||
// ID-, Sortier- und Datumsfunktionen) und wird nach js/globals.js von beiden
|
||||
// HTML-Einstiegspunkten geladen. Sie stellt ausschließlich globale Funktionen
|
||||
// ohne eigenen Zustand bereit; genutzt wird sie praktisch von allen Modulen,
|
||||
// u. a. js/csv.js, js/board/board.js, js/load/load.js sowie den Plan-Modulen unter js/plan/*
|
||||
// u. a. js/board/board.js, js/load/load.js sowie den Plan-Modulen unter js/plan/*
|
||||
// (z. B. für ISO-Datumsformate und Werktage) und von js/gantt.js.
|
||||
|
||||
// departmentParam: gibt ein Query-String-Fragment fuer die aktuelle Abteilung zurueck.
|
||||
@@ -38,8 +38,7 @@ function sortGroups(arr){
|
||||
// peopleByRole: gruppiert Personen nach Rolle zurück in ein Objekt.
|
||||
// Parameter `target`: Woche(n) mit `people`-Array (oder undefined → aktuelle
|
||||
// Woche). Rückgabe: Objekt `{ Rolle: [{person, role}] }` mit Deduplizierung.
|
||||
// Liegt im gemeinsamen Modul, weil auch csv.js (weekToCsv für savePlan auf der
|
||||
// Projektplan-Seite) und pdf.js darauf zugreifen.
|
||||
// Liegt im gemeinsamen Modul, weil board/load/pdf auf sie zugreifen.
|
||||
function peopleByRole(target){
|
||||
const m = {}; ROLES.forEach(r=>m[r]=[]);
|
||||
const people = [];
|
||||
@@ -65,6 +64,18 @@ function peopleByRole(target){
|
||||
return m;
|
||||
}
|
||||
|
||||
// weekSortKey: liefert einen nach Jahr und Kalenderwoche sortierbaren Schlüssel,
|
||||
// sodass z.B. "KW 9 / 2026" vor "KW 10 / 2026" kommt.
|
||||
// Parameter: label (String). Rückgabe: String (Jahr-KW) oder kleingeschriebenes Label.
|
||||
function weekSortKey(label){
|
||||
const m = String(label || '').match(/KW\s*(\d+)[^\d]+(\d{4})/i);
|
||||
if(m){
|
||||
const kw = m[1].padStart(2,'0');
|
||||
return `${m[2]}-${kw}`;
|
||||
}
|
||||
return String(label || '').toLowerCase();
|
||||
}
|
||||
|
||||
// Persistente ID fuer eine Person aus dem Kuerzel, damit Anwesenheit und
|
||||
// Zuordnungen auch nach einem Neuladen stabil bleiben.
|
||||
// Parameter: name (String). Rückgabe: String wie 'p_ab'.
|
||||
|
||||
+10
-10
@@ -124,9 +124,9 @@ function unlockCurrentWeek(skipAuth = false){
|
||||
}
|
||||
|
||||
// deleteWeek: löscht die aktuell ausgewählte Sitzung (nur wenn mehr als 1
|
||||
// vorhanden) inkl. ihrer CSV-Datei auf dem Server. Parameter `skipAuth=true`
|
||||
// überspringt die Passwortabfrage. Danach wird die letzte verbleibende Woche
|
||||
// aktiviert und neu gerendert. Keine Rückgabe.
|
||||
// vorhanden) auf dem Server. Parameter `skipAuth=true` überspringt die
|
||||
// Passwortabfrage. Danach wird die letzte verbleibende Woche aktiviert und neu
|
||||
// gerendert. Keine Rückgabe.
|
||||
function deleteWeek(skipAuth = false){
|
||||
if(db.weeks.length<=1){ alert("Mindestens eine Sitzung muss bestehen bleiben."); return; }
|
||||
if(!skipAuth && !isAuthenticated){
|
||||
@@ -138,13 +138,13 @@ function deleteWeek(skipAuth = false){
|
||||
}
|
||||
if(!confirm("Diese Sitzung wirklich löschen?")) { requestedAction = null; return; }
|
||||
const current = currentWeek();
|
||||
const filename = current ? filenameForWeek(current.label) : null;
|
||||
if(filename){
|
||||
console.log('Lösche CSV-Datei auf Server:', filename);
|
||||
deleteCsvFromServer(filename).then(res=>{
|
||||
if(res && res.ok){ console.log('CSV-Datei gelöscht:', filename); }
|
||||
else { alert('CSV-Löschung fehlgeschlagen: ' + (res && res.error ? res.error : 'unbekannter Fehler')); }
|
||||
}).catch(err=>{ alert('CSV-Löschung fehlgeschlagen: ' + (err.message || err)); });
|
||||
if(current){
|
||||
const label = current.label;
|
||||
console.log('Lösche Woche auf Server:', label);
|
||||
deleteWeekFromServer(label).then(res=>{
|
||||
if(res && res.ok){ console.log('Woche gelöscht:', label); }
|
||||
else { alert('Wochen-Löschung fehlgeschlagen: ' + (res && res.error ? res.error : 'unbekannter Fehler')); }
|
||||
}).catch(err=>{ alert('Wochen-Löschung fehlgeschlagen: ' + (err.message || err)); });
|
||||
}
|
||||
db.weeks = db.weeks.filter(w=>w.id!==db.currentWeek);
|
||||
db.weeks.sort((a,b) => weekSortKey(a.label).localeCompare(weekSortKey(b.label)));
|
||||
|
||||
-3279
File diff suppressed because it is too large.
Load diff
@@ -6,7 +6,7 @@
|
||||
in js/gantt.js in einem neuen Tab geöffnet und per Deep-Link-Route
|
||||
"/<Abteilung>/Projektplan/<Projektname>" adressiert; Woche und Projekt-Nr.
|
||||
kommen als URL-Parameter (weekLabel, projNr). Sie lädt styles.css und ausschließlich die gemeinsamen
|
||||
Module js/globals.js, js/utils.js, js/ui.js, js/date-picker.js, js/csv.js, js/server.js
|
||||
Module js/globals.js, js/utils.js, js/ui.js, js/date-picker.js, js/server.js
|
||||
und js/gantt.js sowie danach die Plan-Module js/plan/plan-state.js,
|
||||
js/plan/plan-build-gantt.js, js/plan/plan-gantt.js, js/plan/plan-drag.js,
|
||||
js/plan/plan-modals.js und js/plan/plan-schedule.js. WICHTIG: store, weeks, board, people,
|
||||
@@ -318,7 +318,6 @@
|
||||
<script src="/js/utils.js?v=5"></script>
|
||||
<script src="/js/ui.js?v=5"></script>
|
||||
<script src="/js/date-picker.js?v=5"></script>
|
||||
<script src="/js/csv.js?v=5"></script>
|
||||
<script src="/js/server.js?v=5"></script>
|
||||
<script src="/js/gantt.js?v=5"></script>
|
||||
<script src="/js/plan/plan-state.js?v=5"></script>
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user