fmlfmlfmlfm

This commit is contained in:
DeathKaioken 2026-03-16 20:04:13 +01:00
parent c8092eb83c
commit 46211c5924
4 changed files with 400 additions and 85 deletions

View File

@ -30,7 +30,9 @@
.doc { .doc {
width: 100%; width: 100%;
max-width: 760px;
margin: 0 auto; margin: 0 auto;
padding: 0 16px;
} }
.header { .header {
@ -89,7 +91,7 @@
.grid2 { .grid2 {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr;
gap: 10px; gap: 10px;
} }
@ -110,8 +112,8 @@
.row { .row {
display: grid; display: grid;
grid-template-columns: 180px 1fr; grid-template-columns: 1fr;
gap: 8px; gap: 2px;
padding: 4px 0; padding: 4px 0;
border-bottom: 1px dashed #e5e7eb; border-bottom: 1px dashed #e5e7eb;
} }
@ -136,7 +138,30 @@
} }
.fill.wide { min-width: 280px; } .fill.wide { min-width: 280px; }
.fill.full { display: inline-block; min-width: 100%; } .fill.full { display: block; width: 100%; min-width: 0; }
.metaGrid {
display: grid;
grid-template-columns: auto auto;
justify-content: end;
align-items: end;
column-gap: 8px;
row-gap: 4px;
text-align: right;
font-size: 10pt;
}
.metaGrid .metaLabel {
white-space: nowrap;
}
.metaGrid .metaValue {
text-align: right;
}
.metaGrid .fill {
min-width: 160px;
}
.hint { .hint {
color: var(--muted); color: var(--muted);
@ -169,6 +194,7 @@
vertical-align: middle; vertical-align: middle;
border-radius: 2px; border-radius: 2px;
position: relative; position: relative;
top: 2px;
flex: 0 0 auto; flex: 0 0 auto;
} }
@ -184,12 +210,6 @@
transform: translate(-50%, -60%) rotate(40deg); transform: translate(-50%, -60%) rotate(40deg);
} }
.prefContact {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
table { table {
width: 100%; width: 100%;
@ -338,9 +358,11 @@
<h1>Vertrag über automatische Wiederbestellungen (ABO)</h1> <h1>Vertrag über automatische Wiederbestellungen (ABO)</h1>
<p class="para muted" style="margin: 4px 0 0;">Bitte alle Felder vollständig ausfüllen und Zutreffendes ankreuzen.</p> <p class="para muted" style="margin: 4px 0 0;">Bitte alle Felder vollständig ausfüllen und Zutreffendes ankreuzen.</p>
</div> </div>
<div style="text-align:right; font-size: 10pt;" class="muted"> <div class="metaGrid muted">
<div>Vertragsnummer: <span class="fill">{{contractNumber}}</span></div> <div class="metaLabel">Vertragsnummer:</div>
<div>Datum: <span class="fill">{{currentDate}}</span></div> <div class="metaValue"><span class="fill">{{contractNumber}}</span></div>
<div class="metaLabel">Datum:</div>
<div class="metaValue"><span class="fill">{{currentDate}}</span></div>
</div> </div>
</div> </div>
@ -356,23 +378,6 @@
</div> </div>
</div> </div>
<div class="grid2" style="margin-top: 10px;">
<div class="box">
<div class="boxTitle">Affiliate</div>
<div class="row">
<div class="label">AFFILIATE NAME</div>
<div class="value"><span class="fill wide">{{affiliateName}}</span></div>
</div>
</div>
<div class="box">
<div class="boxTitle">Client</div>
<div class="row">
<div class="label">CLIENT NAME</div>
<div class="value"><span class="fill wide">{{clientName}}</span></div>
</div>
</div>
</div>
<h2>Lieferadresse</h2> <h2>Lieferadresse</h2>
<div class="box"> <div class="box">
<div class="grid2"> <div class="grid2">
@ -397,6 +402,8 @@
<div class="hint">Rechnungsadresse: <strong>{{invoiceSameAsShippingMark}} wie Lieferadresse</strong></div> <div class="hint">Rechnungsadresse: <strong>{{invoiceSameAsShippingMark}} wie Lieferadresse</strong></div>
</div> </div>
<div class="pageBreak"></div>
<h2>Rechnungsadresse (falls abweichend)</h2> <h2>Rechnungsadresse (falls abweichend)</h2>
<div class="box"> <div class="box">
<div class="grid2"> <div class="grid2">
@ -413,13 +420,6 @@
<div class="row"><div class="label">Telefonnummer</div><div class="value"><span class="fill">{{invoicePhone}}</span></div></div> <div class="row"><div class="label">Telefonnummer</div><div class="value"><span class="fill">{{invoicePhone}}</span></div></div>
<div class="row"><div class="label">Mobil</div><div class="value"><span class="fill">{{invoiceMobile}}</span></div></div> <div class="row"><div class="label">Mobil</div><div class="value"><span class="fill">{{invoiceMobile}}</span></div></div>
<div class="row"><div class="label">E-Mail-Adresse</div><div class="value"><span class="fill wide">{{invoiceEmail}}</span></div></div> <div class="row"><div class="label">E-Mail-Adresse</div><div class="value"><span class="fill wide">{{invoiceEmail}}</span></div></div>
<div class="row">
<div class="label">Bevorzugte Kontaktaufnahme</div>
<div class="value prefContact">
<span class="check"><span class="checkbox {{invoicePrefPhoneClass}}"></span> Telefon</span>
<span class="check"><span class="checkbox {{invoicePrefEmailClass}}"></span> E-Mail</span>
</div>
</div>
</div> </div>
</div> </div>
@ -441,8 +441,6 @@
</div> </div>
</div> </div>
<div class="pageBreak"></div>
<h2>Angebote</h2> <h2>Angebote</h2>
<div class="box"> <div class="box">
<p class="para"> <p class="para">
@ -477,18 +475,11 @@
<p class="para" style="margin-top: 10px;"> <p class="para" style="margin-top: 10px;">
Bei Angabe einer automatischen Wiederbestellung, gemäß den Regelungen in nachstehendem Punkt 3, erhält der Kunde in regelmäßigen Abständen, Bei Angabe einer automatischen Wiederbestellung, gemäß den Regelungen in nachstehendem Punkt 3, erhält der Kunde in regelmäßigen Abständen,
<strong>BEGINNEND AM (Unterzeichnung des Vertrages)</strong> vorstehend eingetragene BIO Kaffee-Teemenge für die Dauer des Vertrages oder bis zum Widerruf der automatischen Wiederbestellung. <strong>BEGINNEND AM (Unterzeichnung des Vertrages)</strong> vorstehend eingetragene BIO Kaffee-Teemenge für die Dauer des Vertrages oder bis zum Widerruf der automatischen Wiederbestellung.
Der BIO Kaffee-Tee wird automatisch im Abstand von: Der BIO Kaffee-Tee wird automatisch im monatlichen Abstand fakturiert und innerhalb von drei bis fünf Werktagen an den Kunden geliefert.</p>
</p>
<div class="checkline" style="margin-top: 2px;">
<span class="check"><span class="checkbox {{intervalMonthlyClass}}"></span> 1 Monat</span>
<span class="check"><span class="checkbox {{intervalTwoMonthlyClass}}"></span> 2 Monate</span>
<span class="check"><span class="checkbox {{intervalQuarterlyClass}}"></span> 3 Monate</span>
</div>
<p class="para">fakturiert und innerhalb von drei bis fünf Werktagen an den Kunden geliefert.</p>
</div> </div>
<div class="pageBreak"></div>
<h2>Zahlungsart</h2> <h2>Zahlungsart</h2>
<div class="box"> <div class="box">
<div class="checkline"> <div class="checkline">
@ -501,8 +492,6 @@
</div> </div>
</div> </div>
<div class="pageBreak"></div>
<div class="legal"> <div class="legal">
<h2>§ 1 Vertragsgegenstand/Geltung der Allgemeinen Geschäftsbedingungen</h2> <h2>§ 1 Vertragsgegenstand/Geltung der Allgemeinen Geschäftsbedingungen</h2>
<p>(1) Die Allgemeinen Geschäftsbedingungen (AGB, siehe unten) der Profit Planet GmbH sind verbindlicher Bestandteil dieses Vertrages. Abweichende, entgegenstehende oder ergänzende Allgemeine Geschäftsbedingungen des Kunden werden nur dann und insoweit Vertragsbestandteil, als Profit Planet GmbH ihrer Geltung ausdrücklich zugestimmt hat. Dieses Zustimmungserfordernis gilt in jedem Fall, beispielsweise auch dann, wenn Profit Planet GmbH in Kenntnis der AGB des Kunden die Lieferung an ihn vorbehaltlos ausführt.</p> <p>(1) Die Allgemeinen Geschäftsbedingungen (AGB, siehe unten) der Profit Planet GmbH sind verbindlicher Bestandteil dieses Vertrages. Abweichende, entgegenstehende oder ergänzende Allgemeine Geschäftsbedingungen des Kunden werden nur dann und insoweit Vertragsbestandteil, als Profit Planet GmbH ihrer Geltung ausdrücklich zugestimmt hat. Dieses Zustimmungserfordernis gilt in jedem Fall, beispielsweise auch dann, wenn Profit Planet GmbH in Kenntnis der AGB des Kunden die Lieferung an ihn vorbehaltlos ausführt.</p>
@ -521,7 +510,11 @@
<p>(2) Der Kaffee wird wiederkehrend zugestellt laut Bestellung.</p> <p>(2) Der Kaffee wird wiederkehrend zugestellt laut Bestellung.</p>
<p>(3) Verstößt der Kunde gegen seine Verpflichtung aus Abs. 1, so ist die Profit Planet GmbH zur außerordentlichen fristlosen Kündigung aus wichtigem Grund berechtigt. Darüber hinaus vereinbaren die Parteien die Zahlung einer verschuldensunabhängigen Vertragsstrafe durch den Kunden an die Profit Planet GmbH in angemessener Höhe, wobei die Profit Planet GmbH die Höhe nach billigem Ermessen bestimmen wird und die Angemessenheit der Vertragsstrafe im Streitfall von dem zuständigen Gericht überprüft werden kann. Die Geltendmachung weiteren Schadensersatzes bleibt vorbehalten.</p> <p>(3) Verstößt der Kunde gegen seine Verpflichtung aus Abs. 1, so ist die Profit Planet GmbH zur außerordentlichen fristlosen Kündigung aus wichtigem Grund berechtigt. Darüber hinaus vereinbaren die Parteien die Zahlung einer verschuldensunabhängigen Vertragsstrafe durch den Kunden an die Profit Planet GmbH in angemessener Höhe, wobei die Profit Planet GmbH die Höhe nach billigem Ermessen bestimmen wird und die Angemessenheit der Vertragsstrafe im Streitfall von dem zuständigen Gericht überprüft werden kann. Die Geltendmachung weiteren Schadensersatzes bleibt vorbehalten.</p>
<div class="pageBreak"></div> </div>
<div class="pageBreak"></div>
<div class="legal">
<h2>§ 5 Wartung und Reparatur</h2> <h2>§ 5 Wartung und Reparatur</h2>
<p>(1) Wartung und Reparaturen der Kaffeemaschinen sind in der Gestellung wie folgt enthalten:</p> <p>(1) Wartung und Reparaturen der Kaffeemaschinen sind in der Gestellung wie folgt enthalten:</p>
@ -541,7 +534,11 @@
<h2>§ 7 Eigentumsverhältnisse</h2> <h2>§ 7 Eigentumsverhältnisse</h2>
<p>Die gelieferten Maschinen bleiben Eigentum von Profit Planet GmbH.</p> <p>Die gelieferten Maschinen bleiben Eigentum von Profit Planet GmbH.</p>
<div class="pageBreak"></div> </div>
<div class="pageBreak"></div>
<div class="legal">
<h2>§ 8 Interne Bestellsysteme und Bestellungen, Datenschutz</h2> <h2>§ 8 Interne Bestellsysteme und Bestellungen, Datenschutz</h2>
<p>Weiteres stimme ich dem Erhalt von exklusiven Angeboten und Informationen wie folgt zu:</p> <p>Weiteres stimme ich dem Erhalt von exklusiven Angeboten und Informationen wie folgt zu:</p>
@ -578,7 +575,11 @@
<p class="footerNote">Informationen über Inhaltsstoffe, Nährwertangaben etc. finden Sie auf der Lieferanten-Homepage www.lanaturalifestyle.com oder unter der Telefonnummer: 0043 552 322 960.</p> <p class="footerNote">Informationen über Inhaltsstoffe, Nährwertangaben etc. finden Sie auf der Lieferanten-Homepage www.lanaturalifestyle.com oder unter der Telefonnummer: 0043 552 322 960.</p>
<div class="pageBreak"></div> </div>
<div class="pageBreak"></div>
<div class="legal">
<h2>Allgemeine Geschäftsbedingungen Kaffee-Service</h2> <h2>Allgemeine Geschäftsbedingungen Kaffee-Service</h2>
@ -594,6 +595,11 @@
<p>(1) Unsere Angebote sind freibleibend und unverbindlich. Dies gilt auch, wenn wir dem Käufer Kataloge, technische Dokumentationen (z.B. Zeichnungen, Pläne, Berechnungen, Kalkulationen, Verweisungen auf DIN-Normen), sonstige Produktbeschreibungen oder Unterlagen auch in elektronischer Form überlassen haben, an denen wir uns Eigentums- und Urheberrechte vorbehalten.</p> <p>(1) Unsere Angebote sind freibleibend und unverbindlich. Dies gilt auch, wenn wir dem Käufer Kataloge, technische Dokumentationen (z.B. Zeichnungen, Pläne, Berechnungen, Kalkulationen, Verweisungen auf DIN-Normen), sonstige Produktbeschreibungen oder Unterlagen auch in elektronischer Form überlassen haben, an denen wir uns Eigentums- und Urheberrechte vorbehalten.</p>
<p>(2) Die Bestellung der Ware durch den Käufer gilt als verbindliches Vertragsangebot. Sofern sich aus der Bestellung nichts anderes ergibt, sind wir berechtigt, dieses Vertragsangebot innerhalb von 30 Tagen nach seinem Zugang bei uns anzunehmen.</p> <p>(2) Die Bestellung der Ware durch den Käufer gilt als verbindliches Vertragsangebot. Sofern sich aus der Bestellung nichts anderes ergibt, sind wir berechtigt, dieses Vertragsangebot innerhalb von 30 Tagen nach seinem Zugang bei uns anzunehmen.</p>
<p>(3) Die Annahme kann entweder schriftlich (z.B. durch Auftragsbestätigung) oder durch Auslieferung der Ware an den Käufer erklärt werden.</p> <p>(3) Die Annahme kann entweder schriftlich (z.B. durch Auftragsbestätigung) oder durch Auslieferung der Ware an den Käufer erklärt werden.</p>
</div>
<div class="pageBreak"></div>
<div class="legal">
<h3>§ 3 Lieferfrist und Lieferverzug</h3> <h3>§ 3 Lieferfrist und Lieferverzug</h3>
<p>(1) Die Lieferfrist wird individuell vereinbart bzw. von uns bei Annahme der Bestellung angegeben. Sofern dies nicht der Fall ist, beträgt die Lieferfrist ca. 14 21 Tage ab Vertragsschluss.</p> <p>(1) Die Lieferfrist wird individuell vereinbart bzw. von uns bei Annahme der Bestellung angegeben. Sofern dies nicht der Fall ist, beträgt die Lieferfrist ca. 14 21 Tage ab Vertragsschluss.</p>
@ -601,13 +607,13 @@
<p>(3) Der Eintritt unseres Lieferverzugs bestimmt sich nach den gesetzlichen Vorschriften. In jedem Fall ist aber eine Mahnung durch den Kunden erforderlich.</p> <p>(3) Der Eintritt unseres Lieferverzugs bestimmt sich nach den gesetzlichen Vorschriften. In jedem Fall ist aber eine Mahnung durch den Kunden erforderlich.</p>
<p>(4) Die Rechte des Kunden und unsere gesetzlichen Rechte, insbesondere bei einem Ausschluss der Leistungspflicht (z.B. aufgrund Unmöglichkeit oder Unzumutbarkeit der Leistung und/oder Nacherfüllung), bleiben unberührt.</p> <p>(4) Die Rechte des Kunden und unsere gesetzlichen Rechte, insbesondere bei einem Ausschluss der Leistungspflicht (z.B. aufgrund Unmöglichkeit oder Unzumutbarkeit der Leistung und/oder Nacherfüllung), bleiben unberührt.</p>
<div class="pageBreak"></div>
<h3>§ 4 Lieferung, Gefahrübergang, Abnahme, Annahmeverzug</h3> <h3>§ 4 Lieferung, Gefahrübergang, Abnahme, Annahmeverzug</h3>
<p>(1) Die Lieferung erfolgt ab Lager, wo auch der Erfüllungsort für die Lieferung und eine etwaige Nacherfüllung ist. Auf Verlangen und Kosten des Käufers wird die Ware an einen anderen Bestimmungsort versandt (Versendungskauf). Soweit nicht etwas anderes vereinbart ist, sind wir berechtigt, die Art der Versendung (insbesondere Transportunternehmen, Versandweg, Verpackung) selbst zu bestimmen.</p> <p>(1) Die Lieferung erfolgt ab Lager, wo auch der Erfüllungsort für die Lieferung und eine etwaige Nacherfüllung ist. Auf Verlangen und Kosten des Käufers wird die Ware an einen anderen Bestimmungsort versandt (Versendungskauf). Soweit nicht etwas anderes vereinbart ist, sind wir berechtigt, die Art der Versendung (insbesondere Transportunternehmen, Versandweg, Verpackung) selbst zu bestimmen.</p>
<p>(2) Die Gefahr des zufälligen Untergangs und der zufälligen Verschlechterung der Ware geht spätestens mit der Übergabe auf den Käufer über. Beim Versendungskauf geht die Gefahr des zufälligen Untergangs und einer zufälligen Verschlechterung der Ware während des Transports für Unternehmer bereits mit Übergabe der Ware an den Spediteur/Frachtführer über; für Verbraucher geht die Gefahr erst über, wenn die Ware dem Verbraucher oder einem von diesem benannten Dritten (der nicht Frachtführer ist) übergeben wurde. Hat der Verbraucher den Beförderungsvertrag selbst ohne unsere Auswahlmöglichkeit beauftragt, so geht die Gefahr bereits mit Übergabe der Ware an den Beförderer über.</p> <p>(2) Die Gefahr des zufälligen Untergangs und der zufälligen Verschlechterung der Ware geht spätestens mit der Übergabe auf den Käufer über. Beim Versendungskauf geht die Gefahr des zufälligen Untergangs und einer zufälligen Verschlechterung der Ware während des Transports für Unternehmer bereits mit Übergabe der Ware an den Spediteur/Frachtführer über; für Verbraucher geht die Gefahr erst über, wenn die Ware dem Verbraucher oder einem von diesem benannten Dritten (der nicht Frachtführer ist) übergeben wurde. Hat der Verbraucher den Beförderungsvertrag selbst ohne unsere Auswahlmöglichkeit beauftragt, so geht die Gefahr bereits mit Übergabe der Ware an den Beförderer über.</p>
<p>(3) Kommt der Käufer in Annahmeverzug, unterlässt er eine Mitwirkungshandlung oder verzögert sich unsere Lieferung aus anderen, vom Käufer zu vertretenden Gründen, so sind wir berechtigt, Ersatz des hieraus entstehenden Schadens einschließlich Mehraufwendungen (z.B. Lagerkosten) zu verlangen. Der Nachweis eines höheren Schadens und unsere gesetzlichen Ansprüche (insbesondere Ersatz von Mehraufwendungen, angemessene Entschädigung, Kündigung) bleiben unberührt.</p> <p>(3) Kommt der Käufer in Annahmeverzug, unterlässt er eine Mitwirkungshandlung oder verzögert sich unsere Lieferung aus anderen, vom Käufer zu vertretenden Gründen, so sind wir berechtigt, Ersatz des hieraus entstehenden Schadens einschließlich Mehraufwendungen (z.B. Lagerkosten) zu verlangen. Der Nachweis eines höheren Schadens und unsere gesetzlichen Ansprüche (insbesondere Ersatz von Mehraufwendungen, angemessene Entschädigung, Kündigung) bleiben unberührt.</p>
</div>
<div class="pageBreak"></div>
<div class="legal">
<h3>§ 5 Preise und Zahlungsbedingungen</h3> <h3>§ 5 Preise und Zahlungsbedingungen</h3>
<p>(1) Sofern im Einzelfall nichts anderes vereinbart ist, gelten unsere jeweils zum Zeitpunkt des Vertragsschlusses aktuellen Preise, und zwar ab Lager, zzgl. gesetzlicher Umsatzsteuer.</p> <p>(1) Sofern im Einzelfall nichts anderes vereinbart ist, gelten unsere jeweils zum Zeitpunkt des Vertragsschlusses aktuellen Preise, und zwar ab Lager, zzgl. gesetzlicher Umsatzsteuer.</p>
<p>(2) Beim Versendungskauf trägt der Käufer die Transportkosten ab Lager und die Kosten einer ggf. vom Käufer gewünschten Transportversicherung. Sofern wir nicht die im Einzelfall tatsächlich entstandenen Transportkosten in Rechnung stellen, gilt eine Transportkostenpauschale (ausschließlich Transportversicherung) iHv 200 EUR als vereinbart. Etwaige Zölle, Gebühren, Steuern und sonstige öffentliche Abgaben trägt der Käufer.</p> <p>(2) Beim Versendungskauf trägt der Käufer die Transportkosten ab Lager und die Kosten einer ggf. vom Käufer gewünschten Transportversicherung. Sofern wir nicht die im Einzelfall tatsächlich entstandenen Transportkosten in Rechnung stellen, gilt eine Transportkostenpauschale (ausschließlich Transportversicherung) iHv 200 EUR als vereinbart. Etwaige Zölle, Gebühren, Steuern und sonstige öffentliche Abgaben trägt der Käufer.</p>
@ -615,13 +621,18 @@
<p>(4) Mit Ablauf vorstehender Zahlungsfrist kommt der Käufer in Verzug. Der Kaufpreis ist während des Verzugs zum jeweils geltenden gesetzlichen Verzugszinssatz zu verzinsen. Wir behalten uns die Geltendmachung eines weitergehenden Verzugsschadens vor. Gegenüber Kaufleuten bleibt unser Anspruch auf den kaufmännischen Fälligkeitszins (§ 352 UGB) unberührt.</p> <p>(4) Mit Ablauf vorstehender Zahlungsfrist kommt der Käufer in Verzug. Der Kaufpreis ist während des Verzugs zum jeweils geltenden gesetzlichen Verzugszinssatz zu verzinsen. Wir behalten uns die Geltendmachung eines weitergehenden Verzugsschadens vor. Gegenüber Kaufleuten bleibt unser Anspruch auf den kaufmännischen Fälligkeitszins (§ 352 UGB) unberührt.</p>
<p>(5) Dem Käufer stehen Aufrechnungs- oder Zurückbehaltungsrechte nur insoweit zu, als sein Anspruch rechtskräftig festgestellt oder unbestritten ist. Bei Mängeln der Lieferung bleiben die Gegenrechte des Käufers insbesondere gem. § 7 dieser AGB unberührt. Gegenüber Verbrauchern gilt diese Einschränkung nicht für Ansprüche, die in rechtlichem Zusammenhang mit ihrer Verbindlichkeit stehen.</p> <p>(5) Dem Käufer stehen Aufrechnungs- oder Zurückbehaltungsrechte nur insoweit zu, als sein Anspruch rechtskräftig festgestellt oder unbestritten ist. Bei Mängeln der Lieferung bleiben die Gegenrechte des Käufers insbesondere gem. § 7 dieser AGB unberührt. Gegenüber Verbrauchern gilt diese Einschränkung nicht für Ansprüche, die in rechtlichem Zusammenhang mit ihrer Verbindlichkeit stehen.</p>
<p>(6) Wird nach Vertragsabschluss erkennbar, dass unser Anspruch auf Zahlung des Kaufpreises durch mangelnde Zahlungsfähigkeit oder drohende Zahlungsunfähigkeit des Käufers gefährdet ist etwa durch Antrag auf Eröffnung eines Insolvenzverfahrens oder vergleichbare Umstände , sind wir berechtigt, unsere Leistung zu verweigern und dem Käufer eine angemessene Frist zur Erbringung der Gegenleistung oder zur Sicherheitsleistung zu setzen. Nach fruchtlosem Ablauf dieser Frist sind wir berechtigt, vom Vertrag zurückzutreten.</p> <p>(6) Wird nach Vertragsabschluss erkennbar, dass unser Anspruch auf Zahlung des Kaufpreises durch mangelnde Zahlungsfähigkeit oder drohende Zahlungsunfähigkeit des Käufers gefährdet ist etwa durch Antrag auf Eröffnung eines Insolvenzverfahrens oder vergleichbare Umstände , sind wir berechtigt, unsere Leistung zu verweigern und dem Käufer eine angemessene Frist zur Erbringung der Gegenleistung oder zur Sicherheitsleistung zu setzen. Nach fruchtlosem Ablauf dieser Frist sind wir berechtigt, vom Vertrag zurückzutreten.</p>
<div class="pageBreak"></div>
<h3>§ 6 Eigentumsvorbehalt</h3> <h3>§ 6 Eigentumsvorbehalt</h3>
<p>(1) Bis zur vollständigen Bezahlung aller unserer gegenwärtigen und künftigen Forderungen aus dem Kaufvertrag und einer laufenden Geschäftsbeziehung (gesicherte Forderungen) behalten wir uns das Eigentum an den verkauften Waren vor.</p> <p>(1) Bis zur vollständigen Bezahlung aller unserer gegenwärtigen und künftigen Forderungen aus dem Kaufvertrag und einer laufenden Geschäftsbeziehung (gesicherte Forderungen) behalten wir uns das Eigentum an den verkauften Waren vor.</p>
<p>(2) Die unter Eigentumsvorbehalt stehenden Waren dürfen vor vollständiger Bezahlung der gesicherten Forderungen weder an Dritte verpfändet, noch zur Sicherheit übereignet werden. Der Käufer hat uns unverzüglich schriftlich zu benachrichtigen, wenn ein Antrag auf Eröffnung eines Insolvenzverfahrens gestellt oder soweit Zugriffe Dritter (zB Pfändungen) auf die uns gehörenden Waren erfolgen.</p> <p>(2) Die unter Eigentumsvorbehalt stehenden Waren dürfen vor vollständiger Bezahlung der gesicherten Forderungen weder an Dritte verpfändet, noch zur Sicherheit übereignet werden. Der Käufer hat uns unverzüglich schriftlich zu benachrichtigen, wenn ein Antrag auf Eröffnung eines Insolvenzverfahrens gestellt oder soweit Zugriffe Dritter (zB Pfändungen) auf die uns gehörenden Waren erfolgen.</p>
<p>(3) Bei vertragswidrigem Verhalten des Käufers, insbesondere bei Nichtzahlung des fälligen Kaufpreises, sind wir berechtigt, nach den gesetzlichen Vorschriften vom Vertrag zurückzutreten oder/und die Ware auf Grund des Eigentumsvorbehalts heraus zu verlangen. Das Herausgabeverlangen beinhaltet nicht zugleich die Erklärung des Rücktritts; wir sind vielmehr berechtigt, lediglich die Ware heraus zu verlangen und uns den Rücktritt vorzubehalten. Zahlt der Käufer den fälligen Kaufpreis nicht, dürfen wir diese Rechte nur geltend machen, wenn wir dem Käufer zuvor erfolglos eine angemessene Frist zur Zahlung gesetzt haben oder eine derartige Fristsetzung nach den gesetzlichen Vorschriften entbehrlich ist.</p> <p>(3) Bei vertragswidrigem Verhalten des Käufers, insbesondere bei Nichtzahlung des fälligen Kaufpreises, sind wir berechtigt, nach den gesetzlichen Vorschriften vom Vertrag zurückzutreten oder/und die Ware auf Grund des Eigentumsvorbehalts heraus zu verlangen. Das Herausgabeverlangen beinhaltet nicht zugleich die Erklärung des Rücktritts; wir sind vielmehr berechtigt, lediglich die Ware heraus zu verlangen und uns den Rücktritt vorzubehalten. Zahlt der Käufer den fälligen Kaufpreis nicht, dürfen wir diese Rechte nur geltend machen, wenn wir dem Käufer zuvor erfolglos eine angemessene Frist zur Zahlung gesetzt haben oder eine derartige Fristsetzung nach den gesetzlichen Vorschriften entbehrlich ist.</p>
</div>
<div class="pageBreak"></div>
<div class="legal"></div>
<h3>§ 7 Sachmängel</h3> <h3>§ 7 Sachmängel</h3>
<p>(1) In dringenden Fällen, z.B. bei Gefährdung der Betriebssicherheit oder zur Abwehr unverhältnismäßiger Schäden, hat der Käufer das Recht, den Mangel selbst zu beseitigen und von uns Ersatz der hierzu objektiv erforderlichen (angemessenen) Aufwendungen zu verlangen. Von einer derartigen Selbstvornahme sind wir unverzüglich nach Möglichkeit vorher zu benachrichtigen. Das Selbstvornahmerecht besteht nicht, wenn wir berechtigt wären, eine entsprechende Nacherfüllung nach den gesetzlichen Vorschriften zu verweigern.</p> <p>(1) In dringenden Fällen, z.B. bei Gefährdung der Betriebssicherheit oder zur Abwehr unverhältnismäßiger Schäden, hat der Käufer das Recht, den Mangel selbst zu beseitigen und von uns Ersatz der hierzu objektiv erforderlichen (angemessenen) Aufwendungen zu verlangen. Von einer derartigen Selbstvornahme sind wir unverzüglich nach Möglichkeit vorher zu benachrichtigen. Das Selbstvornahmerecht besteht nicht, wenn wir berechtigt wären, eine entsprechende Nacherfüllung nach den gesetzlichen Vorschriften zu verweigern.</p>
@ -636,8 +647,6 @@
<p>(5) Ein Rücktritt oder eine Kündigung wegen Pflichtverletzung, die nicht auf einem Mangel der Ware beruht, ist nur zulässig, wenn wir diese zu vertreten haben. Ein darüber hinausgehendes freies Rücktritts- oder Kündigungsrecht des Käufers wird soweit rechtlich zulässig ausgeschlossen.</p> <p>(5) Ein Rücktritt oder eine Kündigung wegen Pflichtverletzung, die nicht auf einem Mangel der Ware beruht, ist nur zulässig, wenn wir diese zu vertreten haben. Ein darüber hinausgehendes freies Rücktritts- oder Kündigungsrecht des Käufers wird soweit rechtlich zulässig ausgeschlossen.</p>
<p>(6) Die zwingenden Bestimmungen des Produkthaftungsgesetzes sowie die Haftung für vorsätzliches oder grob fahrlässiges Verhalten bleiben von den vorstehenden Regelungen unberührt.</p> <p>(6) Die zwingenden Bestimmungen des Produkthaftungsgesetzes sowie die Haftung für vorsätzliches oder grob fahrlässiges Verhalten bleiben von den vorstehenden Regelungen unberührt.</p>
<div class="pageBreak"></div>
<h3>§ 9 Verjährung</h3> <h3>§ 9 Verjährung</h3>
<p>(1) Bei Verträgen mit Unternehmern im Sinne des § 1 KSchG wird die gesetzliche Gewährleistungsfrist für bewegliche Sachen gemäß § 933 ABGB auf ein Jahr ab Übergabe verkürzt. Dies gilt nicht bei Arglist oder bei Übernahme einer Garantie für die Beschaffenheit der Ware.</p> <p>(1) Bei Verträgen mit Unternehmern im Sinne des § 1 KSchG wird die gesetzliche Gewährleistungsfrist für bewegliche Sachen gemäß § 933 ABGB auf ein Jahr ab Übergabe verkürzt. Dies gilt nicht bei Arglist oder bei Übernahme einer Garantie für die Beschaffenheit der Ware.</p>
<p>(2) Die in Abs. 1 genannte Fristverkürzung gilt nicht für Ansprüche des Käufers wegen Schäden aus der Verletzung des Lebens, des Körpers oder der Gesundheit, bei grob fahrlässigem oder vorsätzlichem Verhalten oder bei Ansprüchen nach dem Produkthaftungsgesetz.</p> <p>(2) Die in Abs. 1 genannte Fristverkürzung gilt nicht für Ansprüche des Käufers wegen Schäden aus der Verletzung des Lebens, des Körpers oder der Gesundheit, bei grob fahrlässigem oder vorsätzlichem Verhalten oder bei Ansprüchen nach dem Produkthaftungsgesetz.</p>
@ -654,7 +663,11 @@
<p class="muted"><strong>Stand der Allgemeinen Geschäftsbedingungen Profit Planet Kaffee-Service: 01.08.2025</strong></p> <p class="muted"><strong>Stand der Allgemeinen Geschäftsbedingungen Profit Planet Kaffee-Service: 01.08.2025</strong></p>
<div class="pageBreak"></div> </div>
<div class="pageBreak"></div>
<div class="legal">
<h2>Informationen für Verbraucher über das Rücktrittsrecht (Widerrufsrecht)</h2> <h2>Informationen für Verbraucher über das Rücktrittsrecht (Widerrufsrecht)</h2>
<p><strong>Widerrufsrecht:</strong> Sie haben das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen. Die Widerrufsfrist beträgt vierzehn Tage ab dem Tag, an dem Sie (oder ein von Ihnen benannter Dritter, der nicht der Beförderer ist) die erste Ware im Rahmen dieses Vertrages in Besitz genommen haben. Bei einem Vertrag über Dienstleistungen (z.B. Miete einer Kaffeemaschine) beginnt die Widerrufsfrist mit dem Tag des Vertragsabschlusses.</p> <p><strong>Widerrufsrecht:</strong> Sie haben das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen. Die Widerrufsfrist beträgt vierzehn Tage ab dem Tag, an dem Sie (oder ein von Ihnen benannter Dritter, der nicht der Beförderer ist) die erste Ware im Rahmen dieses Vertrages in Besitz genommen haben. Bei einem Vertrag über Dienstleistungen (z.B. Miete einer Kaffeemaschine) beginnt die Widerrufsfrist mit dem Tag des Vertragsabschlusses.</p>

View File

@ -3,6 +3,33 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import useContractManagement from '../hooks/useContractManagement' import useContractManagement from '../hooks/useContractManagement'
function fileToDataUrl(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onerror = () => reject(new Error('Failed to read file'))
reader.onload = () => {
const result = reader.result
if (typeof result === 'string') resolve(result)
else reject(new Error('Invalid file result'))
}
reader.readAsDataURL(file)
})
}
function summarizeForLog(payload: Record<string, any>) {
const out: Record<string, any> = {}
for (const [k, v] of Object.entries(payload)) {
if (typeof v === 'string' && (k.toLowerCase().includes('base64') || k.toLowerCase().includes('qr_code'))) {
out[k] = { kind: 'base64', len: v.length, head: v.slice(0, 32) }
} else if (typeof v === 'string' && v.length > 200) {
out[k] = { kind: 'string', len: v.length, head: v.slice(0, 32) }
} else {
out[k] = v
}
}
return out
}
export default function CompanySettingsPanel() { export default function CompanySettingsPanel() {
const { getCompanySettings, updateCompanySettings } = useContractManagement() const { getCompanySettings, updateCompanySettings } = useContractManagement()
@ -12,9 +39,14 @@ export default function CompanySettingsPanel() {
company_postal_city: '', company_postal_city: '',
company_country: '', company_country: '',
}) })
const [hasQr60, setHasQr60] = useState(false)
const [hasQr120, setHasQr120] = useState(false)
const [qr60DataUrl, setQr60DataUrl] = useState<string>('')
const [qr120DataUrl, setQr120DataUrl] = useState<string>('')
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [saved, setSaved] = useState(false) const [saved, setSaved] = useState(false)
const [saveError, setSaveError] = useState<string>('')
useEffect(() => { useEffect(() => {
getCompanySettings() getCompanySettings()
@ -25,6 +57,11 @@ export default function CompanySettingsPanel() {
company_postal_city: data.company_postal_city || '', company_postal_city: data.company_postal_city || '',
company_country: data.company_country || '', company_country: data.company_country || '',
}) })
const qr60 = (data as any)?.qr_code_60_base64 ?? (data as any)?.qrCode60Base64
const qr120 = (data as any)?.qr_code_120_base64 ?? (data as any)?.qrCode120Base64
setHasQr60(!!qr60)
setHasQr120(!!qr120)
}) })
.catch(() => {}) .catch(() => {})
.finally(() => setLoading(false)) .finally(() => setLoading(false))
@ -40,17 +77,81 @@ export default function CompanySettingsPanel() {
e.preventDefault() e.preventDefault()
setSaving(true) setSaving(true)
setSaved(false) setSaved(false)
setSaveError('')
try { try {
await updateCompanySettings(form) // IMPORTANT: send `payload` (full strings), not the redacted log view.
const payload: any = { ...form }
if (qr60DataUrl) payload.qr_code_60_base64 = qr60DataUrl
if (qr120DataUrl) payload.qr_code_120_base64 = qr120DataUrl
// For logging only (redacted); never send this object.
const logPayload: any = summarizeForLog(payload)
try {
const qr60 = payload.qr_code_60_base64
const qr120 = payload.qr_code_120_base64
console.info('[CompanySettingsPanel] updateCompanySettings payload', {
logPayload,
keys: Object.keys(payload),
jsonLength: JSON.stringify(payload).length,
qrFieldTypes: {
qr_code_60_base64: qr60 ? typeof qr60 : null,
qr_code_120_base64: qr120 ? typeof qr120 : null,
},
qrFieldLengths: {
qr_code_60_base64: typeof qr60 === 'string' ? qr60.length : null,
qr_code_120_base64: typeof qr120 === 'string' ? qr120.length : null,
},
})
if (qr60 && typeof qr60 !== 'string') console.warn('[CompanySettingsPanel] qr_code_60_base64 is not a string!', qr60)
if (qr120 && typeof qr120 !== 'string') console.warn('[CompanySettingsPanel] qr_code_120_base64 is not a string!', qr120)
} catch {}
await updateCompanySettings(payload)
setSaved(true) setSaved(true)
setTimeout(() => setSaved(false), 3000) setTimeout(() => setSaved(false), 3000)
} catch { } catch {
// silent setSaveError('Could not save settings.')
} finally { } finally {
setSaving(false) setSaving(false)
} }
} }
const handleQrUpload = async (which: '60' | '120', file: File | null) => {
setSaved(false)
setSaveError('')
if (!file) return
// Backend accepts 10MB JSON, but base64 expands the payload.
// Keep a conservative limit to avoid 413 Payload Too Large.
const MAX_FILE_BYTES = 7_000_000
if (file.size > MAX_FILE_BYTES) {
setSaveError('QR image is too large. Please upload a smaller PNG.')
return
}
if (file.type && file.type !== 'image/png') {
setSaveError('Please upload a PNG file for the QR code.')
return
}
try {
const dataUrl = await fileToDataUrl(file)
// Normalize to raw base64, to match other endpoints (e.g. company stamp upload)
const m = dataUrl.match(/^data:(.+?);base64,(.*)$/)
const base64 = m ? m[2] : dataUrl
if (which === '60') {
setQr60DataUrl(base64)
setHasQr60(true)
} else {
setQr120DataUrl(base64)
setHasQr120(true)
}
} catch {
setSaveError('Could not read QR image file.')
}
}
if (loading) { if (loading) {
return ( return (
<div className="flex items-center gap-2 text-sm text-gray-500 py-4"> <div className="flex items-center gap-2 text-sm text-gray-500 py-4">
@ -121,6 +222,38 @@ export default function CompanySettingsPanel() {
</div> </div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Invoice QR Code (60 pcs)</label>
<input
type="file"
accept="image/png"
onChange={e => handleQrUpload('60', e.target.files?.[0] || null)}
className="block w-full text-sm text-gray-700 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-900 hover:file:bg-blue-100"
/>
<div className="mt-1 text-xs text-gray-500">
{qr60DataUrl ? 'Selected (will be saved on Save)' : hasQr60 ? 'Already uploaded' : 'Not uploaded'}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Invoice QR Code (120 pcs)</label>
<input
type="file"
accept="image/png"
onChange={e => handleQrUpload('120', e.target.files?.[0] || null)}
className="block w-full text-sm text-gray-700 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-900 hover:file:bg-blue-100"
/>
<div className="mt-1 text-xs text-gray-500">
{qr120DataUrl ? 'Selected (will be saved on Save)' : hasQr120 ? 'Already uploaded' : 'Not uploaded'}
</div>
</div>
</div>
{saveError && (
<div className="text-sm text-red-600 font-medium">{saveError}</div>
)}
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<button <button
type="submit" type="submit"

View File

@ -28,6 +28,31 @@ export type CompanyStamp = {
type Json = Record<string, any>; type Json = Record<string, any>;
function redactLongOrBase64ish(value: any) {
if (typeof value !== 'string') return value;
const len = value.length;
const head = value.slice(0, 32);
// If it's long, treat it as potentially sensitive / base64 and only log metadata.
if (len > 200) {
return { kind: 'string', len, head };
}
return value;
}
function redactJsonForLogs(input: any): any {
if (!input || typeof input !== 'object') return redactLongOrBase64ish(input);
if (Array.isArray(input)) return input.map(redactJsonForLogs);
const out: Record<string, any> = {};
for (const [k, v] of Object.entries(input)) {
if (typeof v === 'string' && (k.toLowerCase().includes('base64') || k.toLowerCase().includes('qr_code'))) {
out[k] = { kind: 'base64', len: v.length, head: v.slice(0, 32) };
} else {
out[k] = redactJsonForLogs(v);
}
}
return out;
}
function isFormData(body: any): body is FormData { function isFormData(body: any): body is FormData {
return typeof FormData !== 'undefined' && body instanceof FormData; return typeof FormData !== 'undefined' && body instanceof FormData;
} }
@ -515,18 +540,57 @@ export default function useContractManagement() {
}, [authorizedFetch]); }, [authorizedFetch]);
// Company settings (invoice address info) // Company settings (invoice address info)
type CompanySettings = {
company_name?: string
company_street?: string
company_postal_city?: string
company_country?: string
// NEW: QR codes for invoices (base64 or data URL)
qr_code_60_base64?: string | null
qr_code_120_base64?: string | null
// NEW: allow camelCase too (backend supports both)
qrCode60Base64?: string | null
qrCode120Base64?: string | null
}
const getCompanySettings = useCallback(async () => { const getCompanySettings = useCallback(async () => {
return authorizedFetch<{ company_name: string; company_street: string; company_postal_city: string; company_country: string }>( return authorizedFetch<CompanySettings>('/api/admin/company-settings', { method: 'GET' });
'/api/admin/company-settings', { method: 'GET' }
);
}, [authorizedFetch]); }, [authorizedFetch]);
const updateCompanySettings = useCallback(async (data: { const updateCompanySettings = useCallback(async (data: Partial<CompanySettings>) => {
company_name: string; company_street: string; company_postal_city: string; company_country: string; // Debug request body in browser console (redacts base64 values)
}) => { try {
return authorizedFetch<{ company_name: string; company_street: string; company_postal_city: string; company_country: string }>( // IMPORTANT: `data` is the real payload object; `redacted` is for logs only.
'/api/admin/company-settings', { method: 'PUT', body: JSON.stringify(data) } const json = JSON.stringify(data);
); const redacted = redactJsonForLogs(data);
const qr60 = (data as any)?.qr_code_60_base64 ?? (data as any)?.qrCode60Base64;
const qr120 = (data as any)?.qr_code_120_base64 ?? (data as any)?.qrCode120Base64;
console.info('[CM][company-settings] PUT body', {
redacted,
jsonLength: json.length,
keys: Object.keys(data || {}),
qrFieldTypes: {
qr_code_60_base64: qr60 ? typeof qr60 : null,
qr_code_120_base64: qr120 ? typeof qr120 : null,
},
qrFieldLengths: {
qr_code_60_base64: typeof qr60 === 'string' ? qr60.length : null,
qr_code_120_base64: typeof qr120 === 'string' ? qr120.length : null,
},
});
if (qr60 && typeof qr60 !== 'string') {
console.warn('[CM][company-settings] qr_code_60_base64 is not a string!', qr60);
}
if (qr120 && typeof qr120 !== 'string') {
console.warn('[CM][company-settings] qr_code_120_base64 is not a string!', qr120);
}
} catch {}
return authorizedFetch<CompanySettings>('/api/admin/company-settings', {
method: 'PUT',
body: JSON.stringify(data),
});
}, [authorizedFetch]); }, [authorizedFetch]);
return { return {

View File

@ -10,6 +10,7 @@ import { useShippingFees } from '../hooks/useShippingFees';
import { useAboContractTemplateHtml } from './hooks/useAboContractTemplateHtml' import { useAboContractTemplateHtml } from './hooks/useAboContractTemplateHtml'
import SignaturePad from './components/SignaturePad' import SignaturePad from './components/SignaturePad'
import { Dialog, DialogActions, DialogBody, DialogTitle } from '../../components/dialog' import { Dialog, DialogActions, DialogBody, DialogTitle } from '../../components/dialog'
import { createReferralLink } from '../../referral-management/hooks/generateReferralLink'
const COLORS = ['#1C2B4A', '#233357', '#2A3B66', '#314475', '#3A4F88', '#5B6C9A']; // dark blue palette const COLORS = ['#1C2B4A', '#233357', '#2A3B66', '#314475', '#3A4F88', '#5B6C9A']; // dark blue palette
@ -72,6 +73,8 @@ export default function SummaryPage() {
recipientNotes: '', recipientNotes: '',
}); });
const [showThanks, setShowThanks] = useState(false); const [showThanks, setShowThanks] = useState(false);
const [guestMailtoHref, setGuestMailtoHref] = useState<string>('')
const [guestInviteLink, setGuestInviteLink] = useState<string>('')
const [confetti, setConfetti] = useState<{ left: number; delay: number; color: string }[]>([]); const [confetti, setConfetti] = useState<{ left: number; delay: number; color: string }[]>([]);
const [taxRate, setTaxRate] = useState(0.07); // minimal fallback only const [taxRate, setTaxRate] = useState(0.07); // minimal fallback only
const [vatRates, setVatRates] = useState<{ code: string; rate: number | null }[]>([]); const [vatRates, setVatRates] = useState<{ code: string; rate: number | null }[]>([]);
@ -188,18 +191,43 @@ export default function SummaryPage() {
const usableHeight = pageHeight - marginTop - marginBottom const usableHeight = pageHeight - marginTop - marginBottom
const renderCanvasToPdf = (canvas: HTMLCanvasElement, pageIndex: number) => { const renderCanvasToPdf = (canvas: HTMLCanvasElement, pageIndex: number) => {
const imgData = canvas.toDataURL('image/png') const imgWidthPt = usableWidth
const imgWidth = usableWidth const pxPerPt = canvas.width / imgWidthPt
const imgHeight = (canvas.height * imgWidth) / canvas.width const sliceHeightPx = Math.max(1, Math.floor(usableHeight * pxPerPt))
// If a single chunk is too tall, fall back to slicing within that chunk. let yPx = 0
const sliceCount = Math.max(1, Math.ceil(imgHeight / usableHeight)) let sliceIndex = 0
for (let i = 0; i < sliceCount; i++) { while (yPx < canvas.height) {
const isFirstPageForChunk = i === 0 const remainingPx = canvas.height - yPx
const isFirstOverall = pageIndex === 0 && isFirstPageForChunk const currentSliceHeightPx = Math.min(sliceHeightPx, remainingPx)
const sliceCanvas = document.createElement('canvas')
sliceCanvas.width = canvas.width
sliceCanvas.height = currentSliceHeightPx
const ctx = sliceCanvas.getContext('2d')
if (!ctx) break
ctx.drawImage(
canvas,
0,
yPx,
canvas.width,
currentSliceHeightPx,
0,
0,
canvas.width,
currentSliceHeightPx
)
const imgData = sliceCanvas.toDataURL('image/png')
const imgHeightPt = currentSliceHeightPx / pxPerPt
const isFirstOverall = pageIndex === 0 && sliceIndex === 0
if (!isFirstOverall) pdf.addPage() if (!isFirstOverall) pdf.addPage()
const y = marginTop - i * usableHeight pdf.addImage(imgData, 'PNG', marginX, marginTop, imgWidthPt, imgHeightPt)
pdf.addImage(imgData, 'PNG', marginX, y, imgWidth, imgHeight)
yPx += currentSliceHeightPx
sliceIndex++
} }
} }
@ -472,6 +500,9 @@ export default function SummaryPage() {
setSubmitError(null) setSubmitError(null)
setSubmitLoading(true) setSubmitLoading(true)
try { try {
const recipientEmail = form.recipientEmail.trim()
const recipientName = form.recipientName.trim()
const payload = { const payload = {
items: selectedEntries.map(entry => ({ items: selectedEntries.map(entry => ({
coffeeId: entry.coffee.id, coffeeId: entry.coffee.id,
@ -501,6 +532,59 @@ export default function SummaryPage() {
// NEW: explicit JSON preview to match request body // NEW: explicit JSON preview to match request body
console.info('[SummaryPage] subscribeAbo payload JSON:', JSON.stringify(payload)) console.info('[SummaryPage] subscribeAbo payload JSON:', JSON.stringify(payload))
await subscribeAbo(payload) await subscribeAbo(payload)
// TEMP: Guest email workaround (ignore contract/PDF for mail)
// Open an email draft to the recipient when subscription is for someone else.
if (!isForSelf && recipientEmail) {
try {
// A referral token is required for /register, so we generate a 1-time referral link.
const refRes = await createReferralLink({ expiresInDays: 7, maxUses: 1 })
const refBody: any = (refRes as any)?.body
const refCode =
refBody?.data?.code ||
refBody?.data?.token ||
refBody?.data?.ref ||
refBody?.code ||
refBody?.token ||
refBody?.ref ||
''
const origin = typeof window !== 'undefined' ? window.location.origin : ''
const guestLink = refCode
? (origin
? `${origin}/register?ref=${encodeURIComponent(String(refCode))}&guest=true`
: `/register?ref=${encodeURIComponent(String(refCode))}&guest=true`)
: ''
setGuestInviteLink(guestLink)
if (!guestLink) {
console.warn('[SummaryPage] Guest invite: could not generate referral token/link', { refBody })
setGuestMailtoHref('')
} else {
const subject = 'Profit Planet Guest access for your coffee abonnement'
const body =
`Hallo${recipientName ? ` ${recipientName}` : ''},\n\n` +
`du wurdest eingeladen, um Zugriff auf dein Kaffee-Abonnement zu erhalten.\n\n` +
`Bitte registriere dich hier als Gast:\n${guestLink}\n\n` +
`Liebe Grüße\nProfit Planet`
const mailto = `mailto:${recipientEmail}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`
setGuestMailtoHref(mailto)
try {
window.location.href = mailto
} catch {}
}
} catch (e) {
console.warn('[SummaryPage] Guest invite: failed to create referral link', e)
setGuestMailtoHref('')
setGuestInviteLink('')
}
} else {
setGuestMailtoHref('')
setGuestInviteLink('')
}
setShowThanks(true); setShowThanks(true);
try { sessionStorage.removeItem('coffeeSelections'); } catch {} try { sessionStorage.removeItem('coffeeSelections'); } catch {}
try { sessionStorage.removeItem('coffeeAboSizeCapsules'); } catch {} try { sessionStorage.removeItem('coffeeAboSizeCapsules'); } catch {}
@ -711,7 +795,7 @@ export default function SummaryPage() {
{templateVariableNames.length > 0 && ( {templateVariableNames.length > 0 && (
<div className="mb-4 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3"> <div className="mb-4 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3">
<div className="text-sm font-semibold text-gray-900 mb-2">Contract variables</div> <div className="text-sm font-semibold text-gray-900 mb-2">Contract variables</div>
<div className="grid gap-4 sm:grid-cols-2"> <div className="grid gap-4">
{templateVariableNames.map(varName => ( {templateVariableNames.map(varName => (
<div key={varName}> <div key={varName}>
<label className="block text-xs font-medium mb-1 text-gray-700">{varName}</label> <label className="block text-xs font-medium mb-1 text-gray-700">{varName}</label>
@ -884,9 +968,30 @@ export default function SummaryPage() {
</div> </div>
<h3 className="text-2xl font-bold">Thanks for your subscription!</h3> <h3 className="text-2xl font-bold">Thanks for your subscription!</h3>
<p className="mt-1 text-sm text-gray-600"> <p className="mt-1 text-sm text-gray-600">
{isForSelf ? 'Subscription created.' : 'Subscription created, invitation sent.'} {isForSelf
? 'Subscription created.'
: guestMailtoHref
? 'Subscription created. Email draft opened for the guest invite.'
: 'Subscription created. Guest invite email could not be prepared.'}
</p> </p>
{!isForSelf && guestMailtoHref && (
<div className="mt-4">
<a
href={guestMailtoHref}
className="inline-flex items-center justify-center rounded-lg bg-[#1C2B4A] text-white px-4 py-2 font-semibold hover:bg-[#1C2B4A]/90"
>
Open guest email draft again
</a>
</div>
)}
{!isForSelf && guestInviteLink && (
<div className="mt-3 text-xs text-gray-600 break-words">
Guest registration link: <a className="underline" href={guestInviteLink} target="_blank" rel="noreferrer">{guestInviteLink}</a>
</div>
)}
<div className="mt-6 grid gap-3 sm:grid-cols-2"> <div className="mt-6 grid gap-3 sm:grid-cols-2">
<button onClick={() => { setShowThanks(false); backToSelection(); }} className="rounded-lg bg-[#1C2B4A] text-white px-4 py-2 font-semibold hover:bg-[#1C2B4A]/90"> <button onClick={() => { setShowThanks(false); backToSelection(); }} className="rounded-lg bg-[#1C2B4A] text-white px-4 py-2 font-semibold hover:bg-[#1C2B4A]/90">
Back to selection Back to selection