Angebote in der Oberflaeche: schicken und im Portal zusagen
VERWALTUNG
Der Angebotsbereich steht in der Anfrage-Detailansicht ueber dem
direkten Annehmen. Der uebliche Weg ist: Angebot raus, Kunde sagt zu,
Projekt entsteht -- direkt anzunehmen ist der Sonderfall (man hat schon
telefoniert). Stuende der Sonderfall oben, waere er der naheliegende
Griff, und man verschenkte den Beleg, den eine Zusage im Portal erzeugt.
Vorbelegt sind Paket, Preis, Anzahlung, Laufzeit, Ablaufdatum und der
komplette Leistungstext. Zu tun bleibt: Paket bestaetigen, Preis
bestaetigen. Der Leistungstext ist eingeklappt statt die Seite zu
fluten, aber aenderbar.
Ein Paketwechsel laedt ALLES neu -- auch den Leistungstext. Ein
stehengebliebener Text vom vorigen Paket waere der schlimmste Fall: Er
ginge unbemerkt hinaus, und im Angebot staende etwas, das zum Preis
nicht passt. Genau das prueft der Test.
PORTAL
Die Angebotskarte steht ganz oben, vor allen Projekten: Ein Angebot ist
das Einzige im Portal, das eine ENTSCHEIDUNG verlangt -- alles andere
ist Auskunft. Der eingefrorene Leistungstext wird lesbar aufbereitet
(Zwischenueberschriften, Punkte), nicht als Textblock hingeworfen.
Vor der Zusage wird gefragt, und die Frage nennt die Folgen ('verbindlich',
'Anzahlung'). Nicht aus Foermlichkeit: Hier entsteht ein Vertrag, und ein
Knopf, der beim Danebentippen einen Auftrag ausloest, waere eine Falle.
Beim Ablehnen wird NICHT gefragt -- das ist folgenlos.
ZWEI FEHLER GEFUNDEN, BEIDE IM NORMALFALL
- Die Angebotskarten standen im Zweig 'es gibt Projekte'. Ein Kunde mit
offenem Angebot hat aber typischerweise noch KEIN Projekt -- genau der
Normalfall -- und saeh damit nichts. Der Server lieferte das Angebot,
die Seite zeichnete es nur nie. Nur im Browsertest sichtbar.
- Eine Antwort ohne 'nachrichten' liess das Postfach werfen, und weil der
Fehler den Aufbau abbrach, erschien danach GAR NICHTS mehr -- auch nicht
das Angebot, das ueber allem stehen sollte. Dieselbe Sorte wie zuvor im
Cockpit: Ein halber Server ist ein realistischer Fall.
Neun Sprachschluessel in fuenf Sprachen ergaenzt.
Geprueft mit 54 Pruefungen auf Computer und Handy, die messen, was
tatsaechlich an den Server geht. Alle bestehenden weiter gruen.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+125
-1
@@ -123,6 +123,14 @@
|
||||
.vw-meld-weg:focus-visible { outline: 2px solid rgb(var(--wd-blau-rgb) / .95); outline-offset: 2px; }
|
||||
.vw-meld-alle { margin: .1rem 0 1rem; }
|
||||
|
||||
.vw-ag-details { margin: .6rem 0 .8rem; }
|
||||
.wd .vw-ag-details summary {
|
||||
cursor: pointer; font-size: .86rem; color: var(--wd-text-soft);
|
||||
padding: .3rem 0; min-height: 44px; display: flex; align-items: center;
|
||||
}
|
||||
.wd .vw-ag-details summary:hover { color: var(--wd-text); }
|
||||
.wd .vw-ag-details textarea { font-family: var(--font-body); font-size: .86rem; line-height: 1.5; }
|
||||
|
||||
.vw-abbruch-rechnung {
|
||||
padding: .7rem .85rem; margin-bottom: .8rem;
|
||||
border-radius: var(--wd-radius-m, 14px);
|
||||
@@ -2288,7 +2296,50 @@
|
||||
(a.kunde_id
|
||||
? '<div class="vw-abschnitt"><h3>Kunde</h3>' +
|
||||
'<p class="wd-klein wd-leise">Aus dieser Anfrage wurde bereits ein Kundenzugang erstellt. Du findest ihn oben unter „Kunden“.</p></div>'
|
||||
: '<div class="vw-abschnitt"><h3>Entscheiden</h3>' +
|
||||
: '<div class="vw-abschnitt"><h3>Angebot schicken</h3>' +
|
||||
'<div class="vw-uebernahme" id="vw-angebot">' +
|
||||
/* Das Angebot steht VOR dem direkten Annehmen.
|
||||
|
||||
Der übliche Weg ist: Angebot raus, Kunde sagt zu, Projekt
|
||||
entsteht. Direkt anzunehmen ist der Sonderfall (man hat
|
||||
schon telefoniert). Stünde der Sonderfall oben, wäre er
|
||||
der naheliegende Griff — und man verschenkte den Beleg,
|
||||
den eine Zusage im Portal erzeugt. */
|
||||
'<p class="wd-klein wd-leise" style="margin-top:0">Der Leistungsumfang kommt ' +
|
||||
'aus der Standardliste des Pakets — du wählst nur Paket und Preis. Der Kunde ' +
|
||||
'sagt im Portal mit einem Klick zu; die Anzahlung entsteht dabei von selbst.</p>' +
|
||||
'<div class="wd-grid wd-grid--2" style="gap:.7rem">' +
|
||||
'<div class="wd-feld">' +
|
||||
'<label for="vw-ag-paket">Paket</label>' +
|
||||
'<select id="vw-ag-paket">' +
|
||||
'<option value="onepager">Onepager</option>' +
|
||||
'<option value="website">Website</option>' +
|
||||
'<option value="shop">Onlineshop</option>' +
|
||||
'<option value="verwaltung">Verwaltungsbereich</option>' +
|
||||
'<option value="betreuung">Laufende Betreuung</option>' +
|
||||
'<option value="unklar">Noch unklar</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'<div class="wd-feld">' +
|
||||
'<label for="vw-ag-preis">Preis in Euro</label>' +
|
||||
'<input type="number" id="vw-ag-preis" min="1" step="1" inputmode="numeric" />' +
|
||||
'<span class="wd-feld-hilfe" id="vw-ag-anzahlung">…</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<details class="vw-ag-details">' +
|
||||
'<summary>Leistungsumfang ansehen und ändern</summary>' +
|
||||
'<div class="wd-feld" style="margin-top:.6rem">' +
|
||||
'<textarea id="vw-ag-leistungen" rows="12"></textarea>' +
|
||||
'<span class="wd-feld-hilfe">Kommt aus der Standardliste. Was du hier ' +
|
||||
'änderst, wird beim Absenden festgeschrieben.</span>' +
|
||||
'</div>' +
|
||||
'</details>' +
|
||||
'<p class="wd-klein wd-leise" id="vw-ag-gueltig"></p>' +
|
||||
'<button type="button" class="wd-btn wd-btn--haupt" id="vw-ag-senden" ' +
|
||||
'style="width:100%">Angebot schicken</button>' +
|
||||
'</div></div>' +
|
||||
|
||||
'<div class="vw-abschnitt"><h3>Oder direkt entscheiden</h3>' +
|
||||
'<div class="vw-uebernahme" id="vw-annahme">' +
|
||||
"<h4>Annehmen — und alles läuft</h4>" +
|
||||
/* Was genau passiert, steht DA, bevor man drückt. Ein Knopf,
|
||||
@@ -2447,6 +2498,79 @@
|
||||
return t.length === 3 ? t[2] + "." + t[1] + "." + t[0] : String(iso);
|
||||
}
|
||||
|
||||
/* ---- Angebot ----
|
||||
|
||||
Die Vorschau füllt alles vor. Wechselt man das Paket, wird neu
|
||||
geladen: Preis, Laufzeit UND Leistungstext hängen daran. Ein
|
||||
stehengebliebener Text vom vorigen Paket wäre der schlimmste Fall --
|
||||
er ginge unbemerkt so hinaus, und im Angebot stünde etwas, das zum
|
||||
Preis nicht passt. */
|
||||
var agKasten = blatt.querySelector("#vw-angebot");
|
||||
if (agKasten) {
|
||||
var agPaket = blatt.querySelector("#vw-ag-paket");
|
||||
var agPreis = blatt.querySelector("#vw-ag-preis");
|
||||
var agLeist = blatt.querySelector("#vw-ag-leistungen");
|
||||
var agGueltig = blatt.querySelector("#vw-ag-gueltig");
|
||||
var agAnz = blatt.querySelector("#vw-ag-anzahlung");
|
||||
var agGueltigBis = null;
|
||||
|
||||
var agAnzahlungZeigen = function () {
|
||||
var wert = Number(agPreis.value);
|
||||
agAnz.textContent = Number.isFinite(wert) && wert > 0
|
||||
? "30 % Anzahlung: " + (Math.round(wert * 30) / 100).toFixed(2).replace(".", ",") + " €"
|
||||
: "Die 30 % Anzahlung wird daraus berechnet.";
|
||||
};
|
||||
|
||||
var agLaden = async function (paket) {
|
||||
try {
|
||||
var v = await api("/webdesign/admin/anfragen/" + encodeURIComponent(a.id)
|
||||
+ "/angebot-vorschau" + (paket ? "?paket=" + encodeURIComponent(paket) : ""));
|
||||
if (!v || !v.ok) return;
|
||||
agPaket.value = v.paket;
|
||||
agPreis.value = v.preisEuro;
|
||||
agLeist.value = v.leistungen || "";
|
||||
agGueltigBis = v.gueltigBis;
|
||||
agGueltig.textContent = "Läuft " + v.werktage + " Werktage · gültig bis "
|
||||
+ datumKurz(v.gueltigBis);
|
||||
agAnzahlungZeigen();
|
||||
if (v.schonUnterwegs) {
|
||||
agGueltig.textContent = "Achtung: " + v.schonUnterwegs.nummer
|
||||
+ " ist bereits unterwegs. Zieh es zurück, bevor du ein neues schickst.";
|
||||
}
|
||||
} catch (f) {
|
||||
agGueltig.textContent = "Vorschlag konnte nicht geladen werden: " + f.message;
|
||||
}
|
||||
};
|
||||
agLaden(null);
|
||||
agPaket.addEventListener("change", function () { agLaden(agPaket.value); });
|
||||
agPreis.addEventListener("input", agAnzahlungZeigen);
|
||||
|
||||
blatt.querySelector("#vw-ag-senden").addEventListener("click", async function (e) {
|
||||
var preis = Number(agPreis.value);
|
||||
if (!Number.isFinite(preis) || preis <= 0) {
|
||||
window.WD.meldung(meldung, "Bitte einen Preis eintragen.", "fehler");
|
||||
return;
|
||||
}
|
||||
window.WD.knopfSperren(e.target, true, "Wird geschickt…");
|
||||
try {
|
||||
var r = await api("/webdesign/admin/anfragen/" + encodeURIComponent(a.id) + "/angebot", {
|
||||
method: "POST",
|
||||
body: { paket: agPaket.value, preisEuro: preis,
|
||||
leistungen: agLeist.value, gueltigBis: agGueltigBis },
|
||||
});
|
||||
schliesseDetail();
|
||||
await ladeAnfragen();
|
||||
window.WD.meldung(meldung,
|
||||
"Angebot " + r.nummer + " ist raus: " + r.preisText + " · Anzahlung "
|
||||
+ r.anzahlungText + " · gültig bis " + datumKurz(r.gueltigBis)
|
||||
+ ". " + (r.hinweis || ""), "ok");
|
||||
} catch (f) {
|
||||
window.WD.knopfSperren(e.target, false);
|
||||
window.WD.meldung(meldung, "Angebot konnte nicht raus: " + f.message, "fehler");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var annahmeKasten = blatt.querySelector("#vw-annahme");
|
||||
if (annahmeKasten) {
|
||||
var terminFeld = blatt.querySelector("#vw-an-termin");
|
||||
|
||||
Reference in New Issue
Block a user