PETER JÄCKEL [Scan+Order]
Die Anmeldung bleibt auf diesem Gerät 30 Tage aktiv.
Automatisch
V1.9.1.15 · Beta
HalloWas möchtest Du jetzt machen?

Mitteilungen

System-Mitteilungen werden geprüft …
Für System-Mitteilungen muss Scan+Order als Web-App auf dem Home-Bildschirm installiert sein.
📖 Anleitung öffnen

To-do

To-do anlegen

Mitteilung

To-do bearbeiten

Mitteilung löschen

To-do wirklich löschen?

Das To-do wird aus der Mitteilungszentrale entfernt.

Message

Nachrichten

Hier entsteht die interne Nachrichtenfunktion für Mitarbeiter, Admin, Rackjobber und Filialen – auch an mehrere Empfänger gleichzeitig.
V1.9.1.15 · Beta
Inventur: 0 Artikel · 0 Stück

Artikel finden

VORRÄTIGEOL
Standard-EK12,99 €
Sonderpreis
UVP24,99 €
—

Neue Bestellung

Kunden auswählen.

Aktuelle Bestellung

Kd.-Nr.: 123456
Kunde: Beispiel-Shop · Berlin

Bestellung importieren

Auftrags-Historie

Noch keine Aufträge.

Lieferungen

Lieferungen werden geladen …

Retoure

Zuerst den Kunden auswählen.

Retoure scannen

0 Artikel · 0 Stück
Artikel scannen. Nach jedem erfolgreichen Scan startet der nächste Scan automatisch.

Retouren-Übersicht

Filial-Info

Rückstand

MinMax

Ziel: 1–2 Wochen
So entstehen die Vorschläge
Ø der letzten vier abgeschlossenen Wochen. Min: Bedarf bis zur nächsten Lieferung, aufgerundet + 1 Stück. Max: Bedarf für zwei Wochen, aufgerundet + 2 Stück. 0/0 bleibt inaktiv und wird nicht automatisch reaktiviert.
„Auf 0/0“ deaktiviert den Artikel. Eine Bestellmenge wird dadurch nicht hinzugefügt.
Noch keine Artikeländerungen.

MinMax-Änderungen prüfen

Die Werte werden dauerhaft gespeichert und als Änderung mit Menge 0 in den Bestellentwurf übernommen. Bestehende Bestellmengen bleiben erhalten.

Umsätze

Umsatzübersicht

Monatliche Entwicklung

Monatswerte

Gruppen Entwicklung

Monatliche Entwicklung Gruppe

Monatswerte Gruppe

Entwicklung je Filiale

Filiale antippen, um die monatliche Entwicklung im Detail zu öffnen.

Layout

Planogramm anzeigen, bearbeiten und als PDF teilen. Bestellung und Änderungen werden separat als Excel exportiert.Layout anzeigen, als PDF teilen oder drucken. Bearbeiten ist am iPhone nicht möglich.
Import unterstützt die beiden bisherigen Excel-Arten: ein gemeinsames Blatt „Layout“ mit Wand-/Tower-Flächen sowie die Büro-Datei mit einer eigenen Excel-Seite je Fläche.

Einstellungen

Allgemein

EOL-Aktualisierung
Offizielle EOL-Liste automatisch beim Start prüfen.
EOL: wird geprüft …
Datenstand: wird geprüft …

Konto & Anmeldung

Anmeldung

Automatische Anmeldung: 30 Tage auf diesem Gerät.

Passwort ändern

Benutzerdaten

E-Mail / SMTP

SMTP-Tests

Automatischer Import

import@scan-order.de
Excel-Dateien einfach an diese Adresse senden. report@scan-order.de kann als Alias auf dasselbe Import-Postfach zeigen. Erkannte Tabellen werden automatisch importiert. Für die Kundenzuordnung – insbesondere bei Layouts – muss die 6-stellige Kd.-Nr. im Excel-Dateinamen oder im E-Mail-Betreff stehen.

Layouts werden automatisch als Entwurf dem Kunden zugeordnet. Der manuelle Upload unter Layout bleibt zusätzlich erhalten.

Erfolgreich importierte E-Mails werden in Importiert abgelegt und nach 30 Tagen automatisch gelöscht. Fehlerhafte Importe bleiben in Import Fehler zur Kontrolle erhalten.

Bilddatei auswählen

Für Layout und Layout-PDF dauerhaft das optimale FTP-Bild einer Art.-Nr. festlegen. Ohne Auswahl verwendet Scan+Order automatisch _1.

Gruppen-Entwicklung

Neue Händlergruppe

Mehrere Filialen eines Händlers zu einer Gruppe zusammenfassen. Die Kundenliste liefert über Unternehmen automatische Vorschläge; die Zuordnung kann jederzeit manuell ergänzt oder geändert werden.

Vorschläge aus Kundenliste

Unternehmen mit mehreren aktuell bekannten Filialen.

Händlergruppen

Werbegruppen-Entwicklung

Neue Werbegruppe

Werbegruppen können mehrere Händlergruppen und zusätzlich einzelne Filialen enthalten. Beispiel: Händler A mit 4 Shops + Händler B mit 2 Shops + Händler C mit 1 Shop = 7 Filialen in einer Werbegruppe.
Enthaltene Filialen

Werbegruppen

');w.document.close();} layoutMsg('PDF wird vorbereitet · Produktbilder aller Wände werden geladen …'); await layoutPrefetchAllMedia(l,{preloadImages:false,onProgress:(done,total)=>{if(inFrame){layoutPreviewSetStatus('Layout-PDF wird vorbereitet …',`Produktbilder laden: ${done} / ${total}`);}else{try{const p=w.document.getElementById('p');if(p)p.textContent=`Produktbilder laden: ${done} / ${total}`;}catch(e){}}}}); if(!inFrame&&w.closed)return; layoutMsg(''); const productCard=(cell,c,r)=>{const a=layoutArticle(cell.articleNo),live=LAYOUT_STATE.liveStatus.get(String(cell.articleNo||'')),deleted=!!live?.deleted,e=layoutEffectiveEol(cell.articleNo),media=LAYOUT_STATE.mediaCache.get(String(cell.articleNo||'')),img=media?.images?.[0]?new URL(media.images[0],location.href).href:'',name=live?.name||a?.name||'',ek=Number(a?.ek||0),uvp=Number(live?.uvp??a?.uvp??0),nameClass=(name||'').length>92?' xlong':(name||'').length>62?' long':'';return `
${img?``:'
'}
${esc(name||cell.articleNo||'')}
${deleted?'
🚮 GELÖSCHT
':e?'
EOL
':''}
${esc(cell.articleNo||'—')} · Min ${esc(cell.min||0)} / Max ${esc(cell.max||0)}
EK ${ek.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})} € · UVP ${uvp.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})} €
${cell.previousArticleNo?``:''}
`;}; const marketingCard=(m,wall,zone)=>{const a=layoutArticle(m.articleNo),media=LAYOUT_STATE.mediaCache.get(String(m.articleNo||'')),img=media?.images?.[0]?new URL(media.images[0],location.href).href:'',name=a?.name||'Marketingartikel';const style=zone==="grid"?`grid-column:${Number(m.col||0)+1}/span ${Math.max(1,Number(m.colSpan||1))};grid-row:${Number(m.row||0)+1}/span ${Math.max(1,Number(m.rowSpan||1))};`:`grid-column:${Number(m.col||0)+1}/span ${Math.max(1,Number(m.colSpan||wall.columns?.length||1))};`;return `
${img?``:''}
${esc(m.articleNo||'')}${esc(name)}
`;}; const pdfMaxCols=Math.max(1,...(l.walls||[]).map(w=>Number(w.columns?.length||0)));const pdfMaxRows=Math.max(1,...(l.walls||[]).map(w=>Number(w.rows||0)));const pdfGap=.55;const pdfCellW=Math.min(33.6,(286-((pdfMaxCols-1)*pdfGap))/pdfMaxCols);const pdfCellH=Math.min(37.2,(151-((pdfMaxRows-1)*pdfGap))/pdfMaxRows); const walls=(l.walls||[]).map(wall=>{const cols=wall.columns?.length||0,rows=wall.rows||0,areas=layoutMarketingAreas(wall),top=areas.filter(a=>a.zone==="top"),bottom=areas.filter(a=>a.zone==="bottom"),gridAreas=areas.filter(a=>a.zone==="grid"),zoneCount=(top.length?1:0)+(bottom.length?1:0),zoneHeight=zoneCount>1?23:30,otherHeight=(wall.marketing||[]).length?7:0,estimatedGridHeight=177-17-(zoneCount*zoneHeight)-otherHeight,estimatedCellHeight=estimatedGridHeight/Math.max(1,rows),compact=pdfCellW<22||pdfCellH<22?' compact':'';const covered=new Set();for(const m of gridAreas)for(let rr=m.row;rrmarketingCard(m,wall,"grid")).join('');for(let r=0;rWeitere Marketingartikel: ${(wall.marketing||[]).map(m=>`${esc(m.qty||1)}× ${esc(m.articleNo)}`).join(' · ')}
`:'';return `
${esc(l.customer?.kdnr||'')} · ${esc(l.customer?.name||'')}${esc(l.title||'Layout')}
${new Date().toLocaleDateString('de-DE')}

${esc(wall.no||'')} · ${esc(wall.name||'')}

${cols} Spalten · ${rows} Reihen
V${esc(l.version||1)}
${other}${top.length?`
${top.map(m=>marketingCard(m,wall,"top")).join('')}
`:''}
${cells}
${bottom.length?`
${bottom.map(m=>marketingCard(m,wall,"bottom")).join('')}
`:''}
`}).join(''); const previewHtml=`${esc(l.customer?.kdnr||'')} Layout${walls} `; if(inFrame){const frame=$("layoutPreviewFrame");if(!frame)return;layoutPreviewSetStatus("Vorschau wird aufgebaut …","Alle Wände werden im Frame vorbereitet.");frame.classList.remove("ready");frame.srcdoc=previewHtml;}else{w.document.open();w.document.write(previewHtml);w.document.close();} } $("layoutCustomerSearchBtn").onclick=searchLayoutCustomer;$("layoutCustomerSearch").addEventListener("keydown",e=>{if(e.key==="Enter")searchLayoutCustomer();});$("layoutSelect").onchange=loadSelectedLayout;$("layoutNewBtn").onclick=startNewLayout;$("layoutEditBtn").onclick=startEditCurrentLayout;$("layoutUndoBtn").onclick=undoLayoutChange;$("layoutDeleteBtn").onclick=deleteSelectedLayout;$("layoutImportBtn").onclick=()=>{if(!layoutCustomer()){layoutMsg("Bitte zuerst die Filiale suchen.",true);$("layoutCustomerSearch").focus();return;}$("layoutImportFile").click();};$("layoutImportFile").onchange=e=>{const f=e.target.files?.[0];if(f)importLegacyLayoutFile(f);};$("layoutSaveBtn").onclick=saveLayout;$("layoutPublishBtn").onclick=publishLayout;$("layoutAddWallToggle").onclick=()=>{$("layoutAddWallPanel").style.display=$("layoutAddWallPanel").style.display==='none'?'block':'none';};$("layoutAddWallBtn").onclick=addLayoutWall;$("layoutExcelBtn").onclick=exportLayoutExcel;$("layoutPdfBtn").onclick=printLayoutPdf;$("homeLayout").onclick=()=>openLayoutMode('current'); document.addEventListener("keydown",e=>{if((e.ctrlKey||e.metaKey)&&!e.shiftKey&&String(e.key).toLowerCase()==="z"&&$("layout")?.classList.contains("active")&&layoutCanEdit()){e.preventDefault();undoLayoutChange();}}); try{ history=normalizeHistoryRecords(JSON.parse(localStorage.getItem("scanorder_history")||"[]")); }catch(e){history=[]} function openHistoryDefault(){ historyCustomerFilter=""; historyCustomerMode=""; $("historyCustomerScope").style.display="none"; // Beim Öffnen von „Aufträge“ immer mit „Aufträge offen“ starten. historyFilter="orders-open"; document.querySelectorAll(".history-filter").forEach(b=>{ const active=b.dataset.filter===historyFilter; b.classList.toggle("active",active); b.setAttribute("aria-pressed",active?"true":"false"); }); renderHistory(); show("history"); syncHistoryWithServer(true); } document.querySelectorAll(".history-filter").forEach(b=>{ b.onclick=()=>setHistoryFilter(b.dataset.filter); }); $("historyDeliveriesBtn").onclick=()=>showDeliveries(null,"history"); document.querySelectorAll(".nav-item").forEach(b=>b.onclick=()=>{ if(b.dataset.target==="layout"){openLayoutMode("current");return} if(b.dataset.target==="order"){closeCamera();setScannerFlow("idle");if(!currentCustomer){openCustomerModal();return}} if(b.dataset.target==="returnCustomer"){startReturn();return} if(b.dataset.target==="history"){openHistoryDefault();return} if(b.dataset.target==="scan")setScannerFlow("article"); if(b.dataset.target==="settings")return settingsHome(); show(b.dataset.target); }); document.querySelectorAll(".side-nav-item").forEach(b=>b.onclick=()=>{ // Sofortiges visuelles Feedback, noch bevor eine Seite Daten lädt. document.querySelectorAll("#desktopSidebar .side-nav-item").forEach(n=>n.classList.remove("active")); b.classList.add("active"); openSidebarGroupForElement(b); if(b.dataset.layoutMode) return openLayoutMode(b.dataset.layoutMode); if(b.dataset.settingsPanel) return openSettingsPanel(b.dataset.settingsPanel); const action=b.dataset.action||""; if(action==="admin-config") return openAdminConfig(); if(action==="customer-sales"&&isRackjobberUser())return;if(action==="customer-sales") return openSideCustomerView("sales"); if(action==="customer-minmax") return openSideCustomerView("minmax"); if(action==="customer-backlog") return openSideCustomerView("backlog"); if(action==="deliveries") return showDeliveries(null,"history"); const target=b.dataset.target; if(!target)return; if(target==="order"){closeCamera();setScannerFlow("idle");if(!currentCustomer){openCustomerModal();return}} if(target==="returnCustomer"){startReturn();return} if(target==="history"){openHistoryDefault();return} if(target==="scan")setScannerFlow("article"); show(target); if(target==="scan" && APP_LAYOUT_MODE==="desktop") focusManualArticleEntry(); }); function openAdminConfig(){ const url=new URL("admin/",document.baseURI).href; window.location.assign(url); } function settingsHome(){ show("settings"); const hub=$("settingsHub");if(hub)hub.style.display=""; document.querySelectorAll(".settings-panel").forEach(p=>p.classList.remove("active")); } function openSettingsPanel(name){ if(["import","images","groups","adgroups"].includes(String(name))&&!isStaffUser()){settingsHome();return;} show("settings"); const hub=$("settingsHub");if(hub)hub.style.display="none"; document.querySelectorAll(".settings-panel").forEach(p=>p.classList.toggle("active",p.dataset.settingsPage===name)); document.querySelectorAll("[data-settings-panel]").forEach(b=>b.classList.toggle("active",b.dataset.settingsPanel===name)); $("content").scrollTop=0; if(name==="import")loadImportMailStatus?.(); if(name==="groups"||name==="adgroups")loadBranchGroups(); } const IMAGE_CHOICE={article:"",selected:""}; async function imageChoiceApi(action,data={}){await AUTH_READY;await ensureTokenLogin();const fd=new FormData();fd.append("action",action);Object.entries(data).forEach(([k,v])=>fd.append(k,String(v??"")));const r=await fetch(scanOrderApiUrl("ftp_image_choice.php"),{method:"POST",body:fd,credentials:"same-origin",cache:"no-store"});const d=await r.json().catch(()=>({ok:false,message:"Ungültige Serverantwort."}));if(!r.ok||!d.ok)throw new Error(d.message||"Bildauswahl konnte nicht geladen werden.");return d;} function renderImageChoices(d){const grid=$("imageChoiceGrid"),st=$("imageChoiceStatus");if(!grid||!st)return;IMAGE_CHOICE.article=String(d.article||"");IMAGE_CHOICE.selected=String(d.selected||"");const files=Array.isArray(d.files)?d.files:[];st.classList.remove("error");st.textContent=files.length?`${files.length} Bilddatei${files.length===1?"":"en"} gefunden${IMAGE_CHOICE.selected?` · ausgewählt: ${IMAGE_CHOICE.selected}`:" · Standard: _1"}`:"Keine FTP-Bilder zu dieser Art.-Nr. gefunden.";grid.innerHTML=files.map(x=>``).join("");grid.querySelectorAll("[data-image-file]").forEach(b=>b.onclick=async()=>{try{st.textContent="Auswahl wird gespeichert …";const r=await imageChoiceApi("save",{article:IMAGE_CHOICE.article,file:b.dataset.imageFile});LAYOUT_STATE.mediaCache.delete(IMAGE_CHOICE.article);renderImageChoices(r);}catch(e){st.classList.add("error");st.textContent=e.message;}});} async function loadImageChoices(){const input=$("imageChoiceArticle"),st=$("imageChoiceStatus"),grid=$("imageChoiceGrid");if(!input||!st||!grid)return;const article=String(input.value||"").trim();if(!article){st.classList.add("error");st.textContent="Bitte Art.-Nr. eingeben.";return;}st.classList.remove("error");st.textContent="FTP-Bilder werden gesucht …";grid.innerHTML="";try{renderImageChoices(await imageChoiceApi("list",{article}));}catch(e){st.classList.add("error");st.textContent=e.message;}} if($("imageChoiceSearchBtn"))$("imageChoiceSearchBtn").onclick=loadImageChoices;if($("imageChoiceArticle"))$("imageChoiceArticle").addEventListener("keydown",e=>{if(e.key==="Enter")loadImageChoices();}); document.querySelectorAll("[data-open-settings]").forEach(b=>b.onclick=()=>openSettingsPanel(b.dataset.openSettings)); document.querySelectorAll("[data-settings-home]").forEach(b=>b.onclick=settingsHome); if($("adminConfigBtn"))$("adminConfigBtn").onclick=openAdminConfig; const GROUP_STATE={groups:[],adGroups:[],branchSelected:new Set(),adSelectedCustomers:new Set(),adSelectedGroups:new Set(),loaded:false,loading:false,loadPromise:null}; async function groupApi(action,data={},_retry=false){await AUTH_READY;await ensureTokenLogin();const fd=new FormData();fd.append("action",action);Object.entries(data).forEach(([k,v])=>fd.append(k,typeof v==="string"?v:JSON.stringify(v)));const r=await fetch("api/groups.php",{method:"POST",body:fd,credentials:"same-origin",cache:"no-store"});const d=await r.json().catch(()=>({ok:false,message:"Ungültige Serverantwort."}));if(authFailureResponse(r.status,d)&&!_retry){const u=await ensureTokenLogin(true);if(u)return groupApi(action,data,true);}if(!r.ok&&!d.message)d.message="Gruppen-Server nicht erreichbar.";return d;} function customerLabel(c){return `${c.kdnr||""} · ${c.name||""}${c.city?" · "+c.city:""}`;} function groupCustomers(){return (DATA.customers||[]).slice().sort((a,b)=>customerLabel(a).localeCompare(customerLabel(b),"de",{numeric:true,sensitivity:"base"}));} function findGroupCustomer(k){return groupCustomers().find(c=>String(c.kdnr)===String(k));} function normalizeGroupSearch(v){return String(v||"").toLocaleLowerCase("de-DE").normalize("NFD").replace(/[\u0300-\u036f]/g,"");} function groupMemberIds(g){const raw=Array.isArray(g?.customers)?g.customers:Array.isArray(g?.customer_ids)?g.customer_ids:Array.isArray(g?.members)?g.members:[];const stored=[...new Set(raw.map(String).filter(Boolean))];if(stored.length)return stored;const source=String(g?.source_company||"").trim();if(!source)return [];const want=normalizeGroupSearch(source);return groupCustomers().filter(c=>normalizeGroupSearch(c.unternehmen||"")===want).map(c=>String(c.kdnr));} function renderCustomerSearchResults(inputId,resultId,selectedSet,onToggle){const input=$(inputId),wrap=$(resultId);if(!input||!wrap)return;const q=normalizeGroupSearch(input.value);if(!q){wrap.innerHTML="";return;}const rows=groupCustomers().filter(c=>normalizeGroupSearch([c.kdnr,c.name,c.city,c.unternehmen].join(" ")).includes(q)).slice(0,30);wrap.innerHTML=rows.map(c=>{const k=String(c.kdnr),chosen=selectedSet.has(k);return ``;}).join("")||'
Keine passende Filiale.
';wrap.querySelectorAll("[data-kdnr]").forEach(b=>b.onclick=()=>{const k=String(b.dataset.kdnr),adding=!selectedSet.has(k);onToggle(k,adding);renderCustomerSearchResults(inputId,resultId,selectedSet,onToggle);});} function renderGroupSelected(){const wrap=$("branchGroupSelected");if(!wrap)return;const ids=[...GROUP_STATE.branchSelected];wrap.innerHTML=`
${ids.length?`✓ ${ids.length} Filiale${ids.length===1?"":"n"} ausgewählt`:"Noch keine Filialen gewählt."}
`+ids.map(k=>{const c=findGroupCustomer(k);return `${esc(c?customerLabel(c):k)} `}).join("");wrap.querySelectorAll("[data-remove-group-customer]").forEach(b=>b.onclick=()=>{GROUP_STATE.branchSelected.delete(b.dataset.removeGroupCustomer);setBranchGroupMode("");renderGroupSelected();renderCustomerSearchResults("branchGroupCustomerSearch","branchGroupCustomerResults",GROUP_STATE.branchSelected,branchCustomerToggle);renderBranchSuggestions();});} function setBranchGroupMode(source=""){$("branchGroupCompanySource").value=source||"";$("branchGroupMode").textContent=source?`Automatische Gruppe · wird bei jeder Kundenlisten-Aktualisierung über Unternehmen „${source}“ synchronisiert.`:"Manuelle Gruppe · die ausgewählten Filialen bleiben fest zugeordnet.";} function resetBranchGroup(){GROUP_STATE.branchSelected.clear();$("branchGroupId").value="";$("branchGroupName").value="";$("branchGroupCustomerSearch").value="";$("branchGroupCustomerResults").innerHTML="";$("branchGroupFormTitle").textContent="Neue Händlergruppe";setBranchGroupMode("");renderGroupSelected();} function editBranchGroup(id){const g=GROUP_STATE.groups.find(x=>x.id===id);if(!g)return;GROUP_STATE.branchSelected=new Set(groupMemberIds(g));$("branchGroupId").value=g.id;$("branchGroupName").value=g.name||"";$("branchGroupFormTitle").textContent="Händlergruppe bearbeiten";setBranchGroupMode(g.source_company||"");renderGroupSelected();$("branchGroupName").scrollIntoView({behavior:"smooth",block:"center"});} function branchSuggestions(){const map=new Map();for(const c of groupCustomers()){if(Number(c.sperre||0))continue;const company=String(c.unternehmen||"").trim();if(!company)continue;const key=normalizeGroupSearch(company);if(!map.has(key))map.set(key,{name:company,customers:[]});map.get(key).customers.push(String(c.kdnr));}return [...map.values()].filter(x=>x.customers.length>1).sort((a,b)=>a.name.localeCompare(b.name,"de",{sensitivity:"base"}));} function renderBranchSuggestions(){const wrap=$("branchGroupSuggestions");if(!wrap)return;const existingNames=new Set(GROUP_STATE.groups.map(g=>normalizeGroupSearch(g.name))),active=normalizeGroupSearch($("branchGroupCompanySource")?.value||"");const sug=branchSuggestions().slice(0,80);wrap.innerHTML=sug.map((x,i)=>{const key=normalizeGroupSearch(x.name),saved=existingNames.has(key),selected=active&&active===key;return ``;}).join("")||'Keine Mehrfilial-Unternehmen in der aktuellen Kundenliste gefunden.';wrap.querySelectorAll("[data-suggestion]").forEach(b=>b.onclick=()=>{const x=sug[Number(b.dataset.suggestion)];if(!x)return;$("branchGroupName").value=x.name;GROUP_STATE.branchSelected=new Set(x.customers);setBranchGroupMode(x.name);renderGroupSelected();renderBranchSuggestions();});} function renderBranchGroupList(){const wrap=$("branchGroupList");if(!wrap)return;wrap.innerHTML=GROUP_STATE.groups.map(g=>{const ids=groupMemberIds(g);return `

${esc(g.name)}

${ids.length} Filiale(n)${g.source_company?` · automatisch: ${esc(g.source_company)}`:""}
`;}).join("")||'
Noch keine Händlergruppen angelegt.
';wrap.querySelectorAll("[data-edit-group]").forEach(b=>b.onclick=()=>editBranchGroup(b.dataset.editGroup));wrap.querySelectorAll("[data-delete-group]").forEach(b=>b.onclick=()=>deleteBranchGroup(b.dataset.deleteGroup));} async function saveBranchGroup(){const st=$("branchGroupStatus");st.classList.remove("error");st.textContent="Gruppe wird gespeichert …";const d=await groupApi("save_group",{id:$("branchGroupId").value,name:$("branchGroupName").value,source_company:$("branchGroupCompanySource").value,customers:[...GROUP_STATE.branchSelected]});if(!d.ok){st.classList.add("error");st.textContent=d.message;return;}st.textContent="✓ "+d.message;resetBranchGroup();await loadBranchGroups(true);} async function deleteBranchGroup(id){if(!confirm("Händlergruppe wirklich löschen?"))return;const d=await groupApi("delete_group",{id});if(!d.ok){alert(d.message);return;}await loadBranchGroups(true);} function renderAdGroupOptions(){const wrap=$("adGroupGroupOptions");if(!wrap)return;wrap.innerHTML=GROUP_STATE.groups.map(g=>{const chosen=GROUP_STATE.adSelectedGroups.has(String(g.id)),count=groupMemberIds(g).length;return ``;}).join("")||'
Zuerst Händlergruppen anlegen.
';wrap.querySelectorAll('input[type="checkbox"]').forEach(ch=>ch.onchange=()=>{ch.checked?GROUP_STATE.adSelectedGroups.add(ch.value):GROUP_STATE.adSelectedGroups.delete(ch.value);renderAdResolved();});} function renderAdSelectedCustomers(){const wrap=$("adGroupSelectedCustomers");if(!wrap)return;const ids=[...GROUP_STATE.adSelectedCustomers];wrap.innerHTML=`
${ids.length?`✓ ${ids.length} zusätzliche Filiale${ids.length===1?"":"n"} ausgewählt`:"Keine zusätzlichen Einzel-Filialen."}
`+ids.map(k=>{const c=findGroupCustomer(k);return `${esc(c?customerLabel(c):k)} `}).join("");wrap.querySelectorAll("[data-remove-ad-customer]").forEach(b=>b.onclick=()=>{GROUP_STATE.adSelectedCustomers.delete(b.dataset.removeAdCustomer);renderAdSelectedCustomers();renderAdResolved();renderCustomerSearchResults("adGroupCustomerSearch","adGroupCustomerResults",GROUP_STATE.adSelectedCustomers,adCustomerToggle);});} function resolvedAdCustomerIds(){const ids=new Set([...GROUP_STATE.adSelectedCustomers]);for(const id of GROUP_STATE.adSelectedGroups){const g=GROUP_STATE.groups.find(x=>String(x.id)===String(id));for(const k of groupMemberIds(g))ids.add(String(k));}return [...ids];} function renderAdResolved(){const wrap=$("adGroupResolved");if(!wrap)return;const ids=resolvedAdCustomerIds();wrap.innerHTML=`
${ids.length?`✓ Insgesamt ${ids.length} Filiale${ids.length===1?"":"n"} enthalten`:"Noch keine Filialen enthalten."}
`+ids.map(k=>{const c=findGroupCustomer(k);return `${esc(c?customerLabel(c):k)}`}).join("");renderAdGroupOptions();} function resetAdGroup(){GROUP_STATE.adSelectedCustomers.clear();GROUP_STATE.adSelectedGroups.clear();$("adGroupId").value="";$("adGroupName").value="";$("adGroupCustomerSearch").value="";$("adGroupCustomerResults").innerHTML="";$("adGroupFormTitle").textContent="Neue Werbegruppe";renderAdSelectedCustomers();renderAdGroupOptions();renderAdResolved();} function editAdGroup(id){const g=GROUP_STATE.adGroups.find(x=>x.id===id);if(!g)return;GROUP_STATE.adSelectedCustomers=new Set((g.customers||[]).map(String));GROUP_STATE.adSelectedGroups=new Set((g.group_ids||[]).map(String));$("adGroupId").value=g.id;$("adGroupName").value=g.name||"";$("adGroupFormTitle").textContent="Werbegruppe bearbeiten";renderAdSelectedCustomers();renderAdGroupOptions();renderAdResolved();} function renderAdGroupList(){const wrap=$("adGroupList");if(!wrap)return;wrap.innerHTML=GROUP_STATE.adGroups.map(g=>`

${esc(g.name)}

${(g.resolved_customers||[]).length} Filiale(n) · ${(g.group_ids||[]).length} Händlergruppe(n)
`).join("")||'
Noch keine Werbegruppen angelegt.
';wrap.querySelectorAll("[data-edit-adgroup]").forEach(b=>b.onclick=()=>editAdGroup(b.dataset.editAdgroup));wrap.querySelectorAll("[data-delete-adgroup]").forEach(b=>b.onclick=()=>deleteAdGroup(b.dataset.deleteAdgroup));} async function saveAdGroup(){const st=$("adGroupStatus");st.classList.remove("error");st.textContent="Werbegruppe wird gespeichert …";const d=await groupApi("save_ad_group",{id:$("adGroupId").value,name:$("adGroupName").value,group_ids:[...GROUP_STATE.adSelectedGroups],customers:[...GROUP_STATE.adSelectedCustomers]});if(!d.ok){st.classList.add("error");st.textContent=d.message;return;}st.textContent="✓ "+d.message;resetAdGroup();await loadBranchGroups(true);} async function deleteAdGroup(id){if(!confirm("Werbegruppe wirklich löschen?"))return;const d=await groupApi("delete_ad_group",{id});if(!d.ok){alert(d.message);return;}await loadBranchGroups(true);} async function loadBranchGroups(force=false){if(!isStaffUser())return false;if(GROUP_STATE.loaded&&!force){renderBranchGroupList();renderBranchSuggestions();renderAdGroupList();renderAdGroupOptions();renderAdResolved();return true;}if(GROUP_STATE.loading&&GROUP_STATE.loadPromise)return GROUP_STATE.loadPromise;GROUP_STATE.loading=true;GROUP_STATE.loadPromise=(async()=>{const d=await groupApi("list");if(!d.ok){const st=$("branchGroupStatus");if(st){st.classList.add("error");st.textContent=d.message;}return false;}GROUP_STATE.groups=d.groups||[];GROUP_STATE.adGroups=d.ad_groups||[];GROUP_STATE.loaded=true;renderBranchGroupList();renderBranchSuggestions();renderAdGroupList();renderAdGroupOptions();renderGroupSelected();renderAdSelectedCustomers();renderAdResolved();return true;})().finally(()=>{GROUP_STATE.loading=false;GROUP_STATE.loadPromise=null;});return GROUP_STATE.loadPromise;} function branchCustomerToggle(k,adding){adding?GROUP_STATE.branchSelected.add(String(k)):GROUP_STATE.branchSelected.delete(String(k));setBranchGroupMode("");renderGroupSelected();renderBranchSuggestions();} function adCustomerToggle(k,adding){adding?GROUP_STATE.adSelectedCustomers.add(String(k)):GROUP_STATE.adSelectedCustomers.delete(String(k));renderAdSelectedCustomers();renderAdResolved();} $("branchGroupCustomerSearch").addEventListener("input",()=>renderCustomerSearchResults("branchGroupCustomerSearch","branchGroupCustomerResults",GROUP_STATE.branchSelected,branchCustomerToggle)); $("adGroupCustomerSearch").addEventListener("input",()=>renderCustomerSearchResults("adGroupCustomerSearch","adGroupCustomerResults",GROUP_STATE.adSelectedCustomers,adCustomerToggle)); $("branchGroupSaveBtn").onclick=saveBranchGroup;$("branchGroupResetBtn").onclick=resetBranchGroup;$("adGroupSaveBtn").onclick=saveAdGroup;$("adGroupResetBtn").onclick=resetAdGroup;setBranchGroupMode(""); async function importMailApi(action,data={},_retry=false){await AUTH_READY;await ensureTokenLogin();const fd=new FormData();fd.append("action",action);Object.entries(data).forEach(([k,v])=>fd.append(k,v));const r=await fetch("api/import_mail.php",{method:"POST",body:fd,credentials:"same-origin",cache:"no-store"});const d=await r.json().catch(()=>({ok:false,message:"Ungültige Serverantwort."}));if(authFailureResponse(r.status,d)&&!_retry){const u=await ensureTokenLogin(true);if(u)return importMailApi(action,data,true);}if(!r.ok&&!d.message)d.message="Import-Server nicht erreichbar.";return d;} function importMailStatusText(state){if(!state)return "";const when=state.last_run?new Date(state.last_run).toLocaleString("de-DE"):"noch nie";const errs=Array.isArray(state.last_errors)?state.last_errors:[];const runs=Array.isArray(state.runs)?state.runs:[];const last=runs.length?runs[runs.length-1]:{};const typeNames={eso:"ESO / MinMax",backlog:"Rückstand",revenue:"Umsatz",price:"Preisliste / Artikel",stock:"Bestand",layout:"Layout",delivery:"Lieferungen täglich"};const files=Array.isArray(last.files)?last.files:[];const fileText=files.slice(0,8).map(f=>`✓ ${f.file||"Excel"}: ${typeNames[f.type]||f.type||"importiert"}${f.rows!=null?` · ${f.rows} Zeilen`:""}`).join("\n");const dup=Number(last.verified_duplicates||0);const reval=Number(last.revalidated_imported||0);const extra=[fileText,dup?`↻ ${dup} bereits verifizierte(r) Anhang/Anhänge erkannt.`:"",reval?`↻ ${reval} Mail(s) aus „Importiert“ erneut geprüft.`:""].filter(Boolean).join("\n");const details=errs.slice(0,4).map(e=>`• ${e.file||e.subject||"Mail"}: ${e.error||"Importfehler"}`).join("\n");return `Letzte Prüfung: ${when}\n${state.last_message||""}${extra?`\n\n${extra}`:""}${details?`\n\n${details}`:""}`;} async function loadImportMailStatus(){if(!isStaffUser())return;const st=$("importMailStatus"),detail=$("importMailDetail"),wrap=$("importMailConnectWrap");try{const d=await importMailApi("status");if(!d.ok)throw new Error(d.message);const errs=Array.isArray(d.state?.last_errors)?d.state.last_errors:[];st.classList.remove("error","warn");if(!d.configured)st.textContent="Noch nicht verbunden.";else if(errs.length){st.classList.add("warn");st.textContent=`🟠 Import aktiv · ${errs.length} Importfehler zur Kontrolle.`;}else st.textContent="🟢 IMAP-Import aktiv · letzte Prüfung ohne Importfehler.";if(wrap)wrap.style.display=d.configured?"none":"block";detail.textContent=importMailStatusText(d.state);}catch(e){st.classList.remove("warn");st.classList.add("error");st.textContent="🔴 "+e.message;if(wrap)wrap.style.display="block";}}; async function saveImportMail(){const st=$("importMailStatus"),detail=$("importMailDetail");st.classList.remove("error");st.textContent="IMAP-Postfach wird geprüft …";try{const d=await importMailApi("save",{password:$("importMailPassword").value});if(!d.ok)throw new Error(d.message);$("importMailPassword").value="";st.textContent="🟢 "+d.message;await loadImportMailStatus();}catch(e){st.classList.add("error");st.textContent="🔴 "+e.message;detail.textContent="";}} async function runImportMail({silent=false}={}){if(!isStaffUser())return;const st=$("importMailStatus"),detail=$("importMailDetail");if(!silent){st.classList.remove("error");st.textContent="Import-Postfach wird geprüft …";}try{const d=await importMailApi("run");if(!d.ok)throw new Error(d.message);st.classList.remove("error");if(!silent)st.textContent="🟢 "+d.message;detail.textContent=importMailStatusText(d.state);if((d.imports||[]).length){await loadData();renderCustomerMinMaxEditor?.();if((d.imports||[]).some(x=>x.type==="layout")&&$("pageLayout")?.classList.contains("active"))await refreshLayoutList(true);if((d.imports||[]).some(x=>x.type==="delivery")&&$("deliveries")?.classList.contains("active"))await showDeliveries(DELIVERY_STATE.customer,DELIVERY_STATE.returnScreen);}}catch(e){if(!silent){st.classList.add("error");st.textContent="🔴 "+e.message;}}} $("importMailSaveBtn").onclick=saveImportMail;$("importMailRunBtn").onclick=()=>runImportMail(); let IMPORT_MAIL_TIMER=null;function scheduleImportMailCheck(){if(AUTH_USER?.role!=="admin")return;loadImportMailStatus();const key="scanorder_import_mail_last_check";const last=Number(localStorage.getItem(key)||0);if(Date.now()-last>10*60*1000){localStorage.setItem(key,String(Date.now()));setTimeout(()=>runImportMail({silent:true}),3000);}if(!IMPORT_MAIL_TIMER)IMPORT_MAIL_TIMER=setInterval(()=>runImportMail({silent:true}),15*60*1000);} // V1.8.73 Step 10: Auto-Login/Initialisierung startet hinter dem Opening; Retoure übernimmt nach Neustart keine offene Filiale automatisch. // V1.8.73 Step 7: Mitarbeiterrolle geprüft; Staff-Stammdaten abgesichert und Filial-Retourenadresse beim Übernehmen erhalten. // V1.8.73 Step 6: Geteilte Filial-Vorgänge werden beim Betreuer sichtbar gekennzeichnet; Step-4-Historienlogik bleibt unverändert. // V1.8.73 Step 4: Filial-Bestellungen/-Retouren werden serverseitig mit dem zugewiesenen Mitarbeiter/Admin geteilt; Änderungen bleiben am originalen Filial-Vorgang. // V1.8.73: TomTom-Ziel wird vor dem App-Start auf Koordinaten aufgelöst und lokal zwischengespeichert; visuelle Navi-Leiste bleibt unverändert. // V1.8.63: Filial-Info zeigt rechts nur die drei bereitgestellten App-Symbole untereinander; keine zusätzliche Freifläche. // V1.8.62: Filial-Info zeigt Apple Karten, Google Maps und TomTom rechts neben der Adresse als vertikale Navi-Tasten. // V1.8.81: Filiale darf Umsätze der eigenen zugeordneten Filialen sehen und diese in Gruppen Entwicklung vergleichen; Rackjobber bleibt ohne Umsatzrechte. // V1.8.61: Rücksprung aus Monatlicher/Gruppen-Umsatzentwicklung verwendet den tatsächlich angezeigten Kundenkontext. // V1.8.60: Gruppen-Umsatzbutton korrigiert; globale Gruppen/Werbegruppen für Mitarbeiter+Admin; Mitarbeiterrechte in der App wie Admin außer Admin-Config; einheitliches Tastenfeedback. // V1.8.57: vierstellige Art.-Nr. als Präfixsuche; NEU-Status 6 Wochen ab Anlagedatum der Preisliste. // V1.8.56: zentrale 30-Tage-Token-Anmeldung für Gruppen, Werbegruppen, Admin und weitere geschützte APIs; Login zeigt sofort Fortschritt. // V1.8.55: kompaktere Einstellungen; Historien-Button zweizeilig; Admin-Dashboard trennt Kennzahlen und Aktionen klarer. // V1.8.54: Einstellungen als Untermenü; Filialgruppen und Werbegruppen; Admin-Config-Navigation repariert. // V1.8.36: tägliche Lieferliste per E-Mail-Import, zentrale 6-Monats-Lieferhistorie, UPS-Tracking und automatische Auftragszuordnung. // V1.8.36: Filial-Info wieder im 2-Spalten-Raster; Import-Erfolg eindeutig grün; iOS-Safari-Footer bleibt im sichtbaren Viewport. // V1.8.36: Lieferartikel als kompakte Karten; UPS-Tracking öffnet direkt extern statt im blockierten iframe. // V1.8.36: MinMax-Aktionen oben und unten; der untere Aktionsbereich bleibt beim langen Scrollen sticky erreichbar. // V1.8.36: In aktuellen Aufträgen stehen Bearbeiten und Löschen pro Position kompakt nebeneinander. // V1.8.54: neue Rolle Filiale mit Kd.-Nr.-Begrenzung, festem Außendienst-Empfänger und speicherbarer Reply-To-Adresse. // V1.8.54: Filial-Auswahl bleibt innerhalb der Karte und entspricht der Button-Höhe. // V1.8.54: Umsatzimporte verschiedener Stichtage/Jahre werden zusammengeführt statt gegenseitig überschrieben. // V1.8.54: Benutzerdaten-Refresh lädt Rolle, E-Mail und Filialzuordnungen live vom Server. // V1.8.54: EOL nur mit gültigem EOL-Datum; Homepage-Status kann EOL bestätigen/aufheben, aber nicht ohne Datum erzeugen. // V1.8.54: Retoure „An mich“ ohne Kunden-CC und ohne Pflichtprüfung der Kunden-E-Mail. // V1.8.54: Direktbuttons der Filial-Info auch auf iPad/Desktop vollständig sichtbar. // V1.8.54: Layout nutzt zentrale EOL-Regel; robuste Blockerkennung; Spalten kopieren/einfügen und zwischen Flächen verschieben. // V1.8.54: Admin-Config direkt unter Einstellungen; Zugangsdaten-Mailbackend + Kurzanleitung-PDF erneut vollständig im Update. // V1.8.54: Aktiver Auftrag bleibt fest an seine Kd.-Nr. gebunden; Filial-Info/Layouts/Umsätze ändern den Auftrag nicht mehr. // V1.8.54: „Kunde wechseln“ kann einen geöffneten Auftrag bewusst und ohne Positionsverlust neu zuordnen. // V1.8.54: Auftrags-/Retourenkarten einheitlich; feste Aktionsreihen am Kartenfuß, UPS immer in eigener unterer Reihe. // V1.8.54: Bestellung und Retoure besitzen getrennte Scanner-Kontexte; parallele offene Vorgänge beeinflussen sich nicht. $("messagePhotoViewerClose")?.addEventListener("click",closeMessagePhotoViewer); $("messagePhotoViewerShare")?.addEventListener("click",shareMessagePhoto); ["messageEditTitle","messageEditText","messageEditPriority"].forEach(id=>$(id)?.addEventListener("input",()=>{})); function updatePageJumpControls(){const c=$("content"),j=$("pageJumpControls");if(!c||!j)return;const overlayOpen=!!document.querySelector(".message-photo-viewer.on,.todo-leave-overlay.on,.photo-modal.open,[role=dialog][open]");const longPage=c.scrollHeight>c.clientHeight+120;j.classList.toggle("is-hidden",!longPage||overlayOpen)} $("pageJumpTop")?.addEventListener("click",()=>$("content")?.scrollTo({top:0,behavior:"smooth"})); $("pageJumpBottom")?.addEventListener("click",()=>{const c=$("content");if(c)c.scrollTo({top:c.scrollHeight,behavior:"smooth"})}); $("content")?.addEventListener("scroll",updatePageJumpControls,{passive:true}); window.addEventListener("resize",updatePageJumpControls); if(window.ResizeObserver&&$("content")){new ResizeObserver(updatePageJumpControls).observe($("content"));} new MutationObserver(()=>requestAnimationFrame(updatePageJumpControls)).observe($("content"),{subtree:true,childList:true,attributes:true,attributeFilter:["class","style"]}); requestAnimationFrame(updatePageJumpControls); $("messagePhotoViewerDelete")?.addEventListener("click",deleteMessageViewerPhoto); // Native-style photo gestures: pinch, double tap, pan when zoomed, swipe at 1x. let MESSAGE_ZOOM=1,MESSAGE_PAN_X=0,MESSAGE_PAN_Y=0,MESSAGE_GESTURE=null,MESSAGE_LAST_TAP=0; function applyMessagePhotoZoom(){if($("messagePhotoViewerVideo")?.style.display==="block")return;const img=$("messagePhotoViewerImage");if(img){img.style.transform=`translate3d(${MESSAGE_PAN_X}px,${MESSAGE_PAN_Y}px,0) scale(${MESSAGE_ZOOM})`;img.style.transformOrigin="center center"}} function resetMessagePhotoZoom(){MESSAGE_ZOOM=1;MESSAGE_PAN_X=0;MESSAGE_PAN_Y=0;MESSAGE_GESTURE=null;applyMessagePhotoZoom()} function messageTouchDistance(t){return Math.hypot(t[0].clientX-t[1].clientX,t[0].clientY-t[1].clientY)} const messageStage=$("messagePhotoViewerStage"); messageStage?.addEventListener("touchstart",e=>{ if(e.touches.length>=2){MESSAGE_GESTURE={kind:"pinch",distance:messageTouchDistance(e.touches),zoom:MESSAGE_ZOOM,panX:MESSAGE_PAN_X,panY:MESSAGE_PAN_Y};return} const t=e.touches[0];if(!t)return; MESSAGE_GESTURE={kind:"single",x:t.clientX,y:t.clientY,panX:MESSAGE_PAN_X,panY:MESSAGE_PAN_Y,moved:false}; },{passive:false}); messageStage?.addEventListener("touchmove",e=>{ if(!MESSAGE_GESTURE)return; if(e.touches.length>=2){e.preventDefault();if(MESSAGE_GESTURE.kind!=="pinch")MESSAGE_GESTURE={kind:"pinch",distance:messageTouchDistance(e.touches),zoom:MESSAGE_ZOOM};MESSAGE_ZOOM=Math.max(1,Math.min(5,MESSAGE_GESTURE.zoom*messageTouchDistance(e.touches)/Math.max(1,MESSAGE_GESTURE.distance)));if(MESSAGE_ZOOM===1){MESSAGE_PAN_X=0;MESSAGE_PAN_Y=0}applyMessagePhotoZoom();return} if(e.touches.length===1&&MESSAGE_GESTURE.kind==="single"){ const t=e.touches[0],dx=t.clientX-MESSAGE_GESTURE.x,dy=t.clientY-MESSAGE_GESTURE.y; if(Math.abs(dx)+Math.abs(dy)>8)MESSAGE_GESTURE.moved=true; if(MESSAGE_ZOOM>1){e.preventDefault();const stage=messageStage.getBoundingClientRect();const img=$("messagePhotoViewerImage"),maxX=(Math.min(stage.width,img?.clientWidth||stage.width)*(MESSAGE_ZOOM-1))/2,maxY=(Math.min(stage.height,img?.clientHeight||stage.height)*(MESSAGE_ZOOM-1))/2;MESSAGE_PAN_X=Math.max(-maxX,Math.min(maxX,MESSAGE_GESTURE.panX+dx));MESSAGE_PAN_Y=Math.max(-maxY,Math.min(maxY,MESSAGE_GESTURE.panY+dy));applyMessagePhotoZoom()} } },{passive:false}); messageStage?.addEventListener("touchend",e=>{ const g=MESSAGE_GESTURE;if(!g)return; if(g.kind==="pinch"){if(e.touches.length===1){const t=e.touches[0];MESSAGE_GESTURE={kind:"single",x:t.clientX,y:t.clientY,panX:MESSAGE_PAN_X,panY:MESSAGE_PAN_Y,moved:true}}else if(e.touches.length===0){if(MESSAGE_ZOOM<=1.02)resetMessagePhotoZoom();else MESSAGE_GESTURE=null}return} if(e.touches.length)return; const t=e.changedTouches[0],dx=t.clientX-g.x,dy=t.clientY-g.y; if(MESSAGE_ZOOM===1&&Math.abs(dx)>55&&Math.abs(dx)>Math.abs(dy)*1.2){stepMessagePhotoViewer(dx<0?1:-1);MESSAGE_LAST_TAP=0} else if(!g.moved&&Math.abs(dx)<15&&Math.abs(dy)<15){const now=Date.now();if(now-MESSAGE_LAST_TAP<330){if(MESSAGE_ZOOM>1)resetMessagePhotoZoom();else{MESSAGE_ZOOM=2.5;applyMessagePhotoZoom()}MESSAGE_LAST_TAP=0}else MESSAGE_LAST_TAP=now} MESSAGE_GESTURE=null; },{passive:true}); messageStage?.addEventListener("touchcancel",()=>{MESSAGE_GESTURE=null},{passive:true}); // Desktop/iPad trackpad: Ctrl+wheel zooms the image instead of the page. messageStage?.addEventListener("wheel",e=>{if(!e.ctrlKey)return;e.preventDefault();MESSAGE_ZOOM=Math.max(1,Math.min(5,MESSAGE_ZOOM*Math.exp(-e.deltaY*.012)));if(MESSAGE_ZOOM===1){MESSAGE_PAN_X=0;MESSAGE_PAN_Y=0}applyMessagePhotoZoom()},{passive:false}); const APP_VERSION="V1.9.1.8.3.19"; // V1.8.36: Historie synchronisiert browserübergreifend über Server + 30-Tage-Token. // V1.8.36: Reine Art.-Nr./EAN-Listen erhalten Menge 1 je Vorkommen; keine EAN-Endziffern mehr als Menge interpretieren. // V1.8.25: E-Mail-Import verschiebt nur verifizierte Excel-Imports nach „Importiert“; bereits verschobene Mails werden einmalig neu validiert und der konkrete Import wird im Setup protokolliert. // V1.8.25: speicherschonender XLSX-Parser + robuste JSON-Serverantwort für grosse Importdateien. // V1.8.16: MinMax-Bearbeitung aus V1.7 in V1.8 integriert: Suche/Filter, 0/0, Verkaufs-Vorschläge, dauerhafte Speicherung und Übernahme in den Bestellentwurf. // V1.8.15: iPhone/iPad Kamera wird bei jedem Öffnen frisch initialisiert; // Login aus V1.6 mit sicherem 30-Tage-Persistenz-Token bleibt unverändert. // V1.8.10: Alle Wand-/Marketingbilder im Hintergrund vorladen; PDF wartet vor dem Drucken auf sämtliche Bilder. // V1.8.8: Marketingbilder orientieren sich automatisch an der Fläche (Rotation gegen Uhrzeigersinn bei nötigem Richtungswechsel); Zeilen können zusätzlich nach oben/unten verschoben werden. // V1.8.2: iPad/Desktop mit fixer linker Navigation, Logo in Sidebar, voller Arbeitsfläche rechts und iPhone-Designsprache; Feierabend quer. // V1.8.1: Responsive iPad-/Desktop-Grundlayout plus Kamerawechsel Front/Rück am iPad/Smartphone; Desktop bewusst ohne Webcam-Scanner. // V1.6.10.12: Tendenzlogik auf ±5 % Schwelle umgestellt: ↑ grün, ↓ rot, → gelb innerhalb ±5 %. // V1.6.10.11: Umsatz-Tendenzpfeile farbig (↑ grün, ↓ rot); Gruppenentwicklung ergänzt um klickbare Filialübersicht mit Detailansicht. // V1.6.10.10: Feierabend-Bild responsiv: Smartphone im Hochformat nutzt feierabend-mobile.jpg; iPad/Desktop behalten feierabend.jpg. // V1.6.10.9: Doppelte Tendenz-Anzeige in Umsatzübersicht entfernt; Pfeil an der Differenz bleibt als alleinige Tendenzanzeige. // V1.6.10.8: Umsatzübersicht kompakter; Differenz/Tendenz im 2×2-Feld. Unternehmensgruppen aus „Unternehmen“ mit aggregierter Gruppen-Entwicklung. // V1.6.10.7: Retouren-Excel übernimmt Außendienst aus dem aktuell angemeldeten Benutzerkonto. // V1.6.10.6: Basis V1.6.10.5 unverändert fortgeführt; Android/Samsung nutzt jetzt den nativen BarcodeDetector für EAN-13 parallel zu ZBar auf demselben sichtbaren Scanbereich. // V1.6.10.3: Shop-Slug mit & korrigiert. // V1.6.10.2: Live-Shopstatus hat Vorrang; sichtbarer EOL-Status, 404=gelöscht. // V1.6.10.1: Produktbilder und Live-EOL-Status robust wiederhergestellt. // V1.6.10: Umsatzübersicht Kunden-Info 2×2. // V1.6.9.6: Art.-Nr. 06xxx = Bestückungszubehör; kein Shop-Löschstatus, Bestand ausschließlich aus der Preisliste. // V1.6.9.5: Auftrags-Historie wird pro angemeldetem Benutzer serverseitig synchronisiert und browser-/geraeteuebergreifend bereitgestellt. // V1.6.9.4: Offizielle EOL-Liste wird serverseitig automatisch aktualisiert; EOL-Start + 3 Monate = keine GU mehr ab. // V1.6.9.3: Retouren-Service-Mail nutzt die Gesamtstückzahl und ein reduziertes Anschreiben. // V1.6.9.1: PDF-Import robuster + EOL-Kennzeichnung in allen Artikel-Uebersichten. // Enthält weiterhin den Scanner-State-Fix aus V1.6.8.5 und den Retouren-Excel-Fix aus V1.6.8.4. // V1.6.8.3: Mailrouting serverseitig: Mitarbeiter-CC und Reply-To auf users.email. async function checkVersion(){ const badges=document.querySelectorAll(".version-badge,.camera-version"); const normalizeVersion=v=>{ const s=String(v||"").trim(); return s.startsWith("V")?s:"V"+s; }; try{ const r=await fetch("./version.json?ts="+Date.now(),{cache:"no-store",credentials:"include"}); if(!r.ok) throw new Error("HTTP "+r.status); const j=await r.json(); const server=normalizeVersion(j.version); badges.forEach(b=>b.textContent=server===APP_VERSION?`${APP_VERSION} · ✓ aktuell`:`${APP_VERSION} · ⚠ Server ${server||"unbekannt"}`); }catch(e){ badges.forEach(b=>b.textContent=`${APP_VERSION} · ? Serverprüfung`); } } checkVersion(); async function showDataVersion(){ try{const r=await fetch('./data/v16_import/manifest.json?ts='+Date.now(),{cache:'no-store'}); if(!r.ok)throw new Error(); const m=await r.json(); $("dataVersionStatus").textContent=`Datenstand: ${m.importWeek||m.importDate||m.generatedAt||'unbekannt'}`;} catch(e){ $("dataVersionStatus").textContent='Datenstand: lokale Daten'; } } // V1.6.8.2: Einheitliche App-Shell. Kopf- und Fußzeile bleiben an ihrer // festen Position; bei Tastatureingaben wird ausschließlich der Innenbereich gescrollt. let stableAppHeight=Math.round(window.innerHeight||document.documentElement.clientHeight||0); function applyStableAppHeight(){ if(!stableAppHeight) stableAppHeight=Math.round(window.innerHeight||document.documentElement.clientHeight||0); document.documentElement.style.setProperty('--app-height',stableAppHeight+'px'); } function syncKeyboardInset(){ const vv=window.visualViewport; const visibleBottom=vv?(vv.offsetTop+vv.height):(window.innerHeight||stableAppHeight); const inset=Math.max(0,Math.round(stableAppHeight-visibleBottom)); document.documentElement.style.setProperty('--keyboard-inset',inset+'px'); } function refreshStableAppHeight(){ const active=document.activeElement; const editing=!!(active&&active.matches&&active.matches('input,textarea,select')); if(!editing){ const h=Math.round(window.innerHeight||document.documentElement.clientHeight||stableAppHeight); if(h>0) stableAppHeight=h; applyStableAppHeight(); } syncKeyboardInset(); } applyStableAppHeight();syncKeyboardInset(); function handleVisualViewportChange(){ const active=document.activeElement; const editing=!!(active&&active.matches&&active.matches('input,textarea,select')); const vv=window.visualViewport; if(!editing&&vv){ const h=Math.round(vv.height+vv.offsetTop); if(h>320){stableAppHeight=h;applyStableAppHeight();} } syncKeyboardInset(); } if(window.visualViewport){visualViewport.addEventListener('resize',handleVisualViewportChange);visualViewport.addEventListener('scroll',handleVisualViewportChange);} window.addEventListener('resize',refreshStableAppHeight); window.addEventListener('orientationchange',()=>setTimeout(()=>{stableAppHeight=Math.round(window.innerHeight||document.documentElement.clientHeight||stableAppHeight);applyStableAppHeight();syncKeyboardInset();},350)); document.addEventListener('focusout',()=>setTimeout(refreshStableAppHeight,120)); document.addEventListener('focusin',e=>{ if(!e.target.matches('input,textarea'))return; setTimeout(()=>{ syncKeyboardInset(); const c=$("content"); if(!c)return; const r=e.target.getBoundingClientRect(); const vv=window.visualViewport; const nav=$("bottomNav"); const header=document.querySelector('.app-header'); const visibleTop=vv?vv.offsetTop:0; const visibleBottom=vv?(vv.offsetTop+vv.height):(window.innerHeight||stableAppHeight); const topLimit=Math.max((header?.getBoundingClientRect().bottom||0)+8,visibleTop+8); const navTop=nav?.getBoundingClientRect().top||stableAppHeight; const bottomLimit=Math.min(navTop-8,visibleBottom-12); let delta=0; if(r.bottom>bottomLimit) delta=r.bottom-bottomLimit; else if(r.top1)c.scrollBy({top:delta,behavior:'smooth'}); },140); }); let APP_INIT_PROMISE=null; // V1.8.73 STEP11 FAST START: // Nur Auth/Session + Benutzer/Rolle blockieren den sichtbaren App-Start. // Daten, Gruppen, Historie und EOL werden danach parallel im Hintergrund geladen. // Hintergrundjobs dürfen die aktuelle Navigation niemals auf Home zurücksetzen. let APP_BACKGROUND_INIT_PROMISE=null; function initializeAuthenticatedApp({force=false}={}){ if(APP_BACKGROUND_INIT_PROMISE&&!force)return APP_BACKGROUND_INIT_PROMISE; // Home genau einmal beim echten Login/App-Start setzen, bevor Hintergrundjobs beginnen. renderOrder(); show("home"); showDataVersion(); checkV16ImportErrors(); APP_BACKGROUND_INIT_PROMISE=(async()=>{ // STEP21.2: Mitteilungen haben beim Start höchste Priorität. // Den geräteübergreifenden Erledigt-Status sofort nach Auth laden, noch bevor // die übrigen Hintergrundbereiche fertig sind. Der Badge wird anschließend // nach jedem relevanten Datenpaket sofort neu berechnet. const messagePriorityJob=(async()=>{ if(!isStaffUser())return; try{ await ensureMessageServerState(); await refreshMessages(); }catch(e){console.warn("Mitteilungen konnten beim Start nicht vorab synchronisiert werden",e);} })(); const dataJob=(async()=>{ try{ // V1.8.81: einmalige, vom normalen Mailimport getrennte ESO-Historienmigration. // KW 37–40 = Verkaufsmenge aktuelle Woche; aktueller MinMax-/Artikelstand ausschließlich 04.10.2026. try{await protectedFetchJson('api/eso_history_v1881.php?ts='+Date.now(),{},false);}catch(e){console.warn('ESO-Historie V1.8.81',e);} await loadData(); setupFilialePickers?.(); renderOrder(); const hint=$("hint"); if(hint)hint.textContent=`${DATA.articles.length.toLocaleString("de-DE")} Artikel verfügbar.`; // Import-/Umsatzmeldungen sofort in den Badge übernehmen. await refreshMessages(); }catch(e){ const hint=$("hint");if(hint)hint.textContent="Daten konnten nicht geladen werden."; console.error("STEP11 Hintergrunddaten",e); } })(); const groupsJob=(async()=>{ if(!isStaffUser())return; try{await loadBranchGroups(true);}catch(e){console.warn("Globale Gruppen konnten im Hintergrund nicht geladen werden",e);} })(); const historyJob=(async()=>{ try{ await initHistoryForUser(); renderHistory(); // Offene Aufträge/Retouren sofort in den Badge übernehmen – unabhängig // davon, auf welcher Seite der Nutzer inzwischen arbeitet. await refreshMessages(); // Reminder erst nach geladener Historie und nur wenn der Benutzer noch auf Home ist. const active=document.querySelector(".screen.active"); if(active&&active.id==="home")checkOpenReminder(); }catch(e){console.warn("Historie konnte im Hintergrund nicht geladen werden",e);} })(); const eolJob=(async()=>{ try{await syncEolData({cacheOnly:true,silent:true});} catch(e){console.warn("EOL-Servercache nicht erreichbar",e);} if(eolAutoEnabled())setTimeout(()=>syncEolData({silent:true}).catch(e=>console.warn("EOL-Hintergrundaktualisierung",e)),300); })(); await Promise.allSettled([messagePriorityJob,dataJob,groupsJob,historyJob,eolJob]); return true; })(); if(force)APP_BACKGROUND_INIT_PROMISE.finally(()=>{APP_BACKGROUND_INIT_PROMISE=null;}); return APP_BACKGROUND_INIT_PROMISE; } (async()=>{ const user=await AUTH_READY; if(!user)return; // Auth ist fertig: Oberfläche sofort freigeben. Hintergrundinitialisierung nicht abwarten. initializeAuthenticatedApp(); document.getElementById('authOverlay').classList.add('hidden'); })();