Merge branch 'bigTypeShii' of https://git.profit-planet.partners/Seazn/profit-planet-frontend into bigTypeShii
This commit is contained in:
commit
1e5b3b77c8
@ -30,7 +30,9 @@
|
||||
|
||||
.doc {
|
||||
width: 100%;
|
||||
max-width: 760px;
|
||||
margin: 0 auto;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.header {
|
||||
@ -89,7 +91,7 @@
|
||||
|
||||
.grid2 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@ -110,8 +112,8 @@
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 180px 1fr;
|
||||
gap: 8px;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 2px;
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px dashed #e5e7eb;
|
||||
}
|
||||
@ -136,7 +138,30 @@
|
||||
}
|
||||
|
||||
.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 {
|
||||
color: var(--muted);
|
||||
@ -169,6 +194,7 @@
|
||||
vertical-align: middle;
|
||||
border-radius: 2px;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
@ -184,12 +210,6 @@
|
||||
transform: translate(-50%, -60%) rotate(40deg);
|
||||
}
|
||||
|
||||
.prefContact {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
@ -338,9 +358,11 @@
|
||||
<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>
|
||||
</div>
|
||||
<div style="text-align:right; font-size: 10pt;" class="muted">
|
||||
<div>Vertragsnummer: <span class="fill">{{contractNumber}}</span></div>
|
||||
<div>Datum: <span class="fill">{{currentDate}}</span></div>
|
||||
<div class="metaGrid muted">
|
||||
<div class="metaLabel">Vertragsnummer:</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>
|
||||
|
||||
@ -356,23 +378,6 @@
|
||||
</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>
|
||||
<div class="box">
|
||||
<div class="grid2">
|
||||
@ -397,6 +402,8 @@
|
||||
<div class="hint">Rechnungsadresse: <strong>{{invoiceSameAsShippingMark}} wie Lieferadresse</strong></div>
|
||||
</div>
|
||||
|
||||
<div class="pageBreak"></div>
|
||||
|
||||
<h2>Rechnungsadresse (falls abweichend)</h2>
|
||||
<div class="box">
|
||||
<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">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">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>
|
||||
|
||||
@ -441,8 +441,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pageBreak"></div>
|
||||
|
||||
<h2>Angebote</h2>
|
||||
<div class="box">
|
||||
<p class="para">
|
||||
@ -477,18 +475,11 @@
|
||||
<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,
|
||||
<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:
|
||||
</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>
|
||||
Der BIO Kaffee-Tee wird automatisch im monatlichen Abstand fakturiert und innerhalb von drei bis fünf Werktagen an den Kunden geliefert.</p>
|
||||
</div>
|
||||
|
||||
<div class="pageBreak"></div>
|
||||
|
||||
<h2>Zahlungsart</h2>
|
||||
<div class="box">
|
||||
<div class="checkline">
|
||||
@ -501,8 +492,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pageBreak"></div>
|
||||
|
||||
<div class="legal">
|
||||
<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>
|
||||
@ -521,7 +510,11 @@
|
||||
<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>
|
||||
|
||||
<div class="pageBreak"></div>
|
||||
</div>
|
||||
|
||||
<div class="pageBreak"></div>
|
||||
|
||||
<div class="legal">
|
||||
|
||||
<h2>§ 5 Wartung und Reparatur</h2>
|
||||
<p>(1) Wartung und Reparaturen der Kaffeemaschinen sind in der Gestellung wie folgt enthalten:</p>
|
||||
@ -541,7 +534,11 @@
|
||||
<h2>§ 7 Eigentumsverhältnisse</h2>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<div class="pageBreak"></div>
|
||||
</div>
|
||||
|
||||
<div class="pageBreak"></div>
|
||||
|
||||
<div class="legal">
|
||||
|
||||
<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>(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>
|
||||
</div>
|
||||
|
||||
<div class="pageBreak"></div>
|
||||
|
||||
<div class="legal">
|
||||
|
||||
<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>
|
||||
@ -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>(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>
|
||||
<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>(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>
|
||||
<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>
|
||||
@ -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>(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>
|
||||
|
||||
<div class="pageBreak"></div>
|
||||
|
||||
|
||||
<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>(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>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="pageBreak"></div>
|
||||
|
||||
<div class="legal"></div>
|
||||
|
||||
<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>
|
||||
@ -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>(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>
|
||||
<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>
|
||||
@ -654,7 +663,11 @@
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@ -3,6 +3,33 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
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() {
|
||||
const { getCompanySettings, updateCompanySettings } = useContractManagement()
|
||||
|
||||
@ -12,9 +39,14 @@ export default function CompanySettingsPanel() {
|
||||
company_postal_city: '',
|
||||
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 [saving, setSaving] = useState(false)
|
||||
const [saved, setSaved] = useState(false)
|
||||
const [saveError, setSaveError] = useState<string>('')
|
||||
|
||||
useEffect(() => {
|
||||
getCompanySettings()
|
||||
@ -25,6 +57,11 @@ export default function CompanySettingsPanel() {
|
||||
company_postal_city: data.company_postal_city || '',
|
||||
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(() => {})
|
||||
.finally(() => setLoading(false))
|
||||
@ -40,17 +77,81 @@ export default function CompanySettingsPanel() {
|
||||
e.preventDefault()
|
||||
setSaving(true)
|
||||
setSaved(false)
|
||||
setSaveError('')
|
||||
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)
|
||||
setTimeout(() => setSaved(false), 3000)
|
||||
} catch {
|
||||
// silent
|
||||
setSaveError('Could not save settings.')
|
||||
} finally {
|
||||
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) {
|
||||
return (
|
||||
<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 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">
|
||||
<button
|
||||
type="submit"
|
||||
|
||||
@ -28,6 +28,31 @@ export type CompanyStamp = {
|
||||
|
||||
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 {
|
||||
return typeof FormData !== 'undefined' && body instanceof FormData;
|
||||
}
|
||||
@ -515,18 +540,57 @@ export default function useContractManagement() {
|
||||
}, [authorizedFetch]);
|
||||
|
||||
// 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 () => {
|
||||
return authorizedFetch<{ company_name: string; company_street: string; company_postal_city: string; company_country: string }>(
|
||||
'/api/admin/company-settings', { method: 'GET' }
|
||||
);
|
||||
return authorizedFetch<CompanySettings>('/api/admin/company-settings', { method: 'GET' });
|
||||
}, [authorizedFetch]);
|
||||
|
||||
const updateCompanySettings = useCallback(async (data: {
|
||||
company_name: string; company_street: string; company_postal_city: string; company_country: string;
|
||||
}) => {
|
||||
return authorizedFetch<{ company_name: string; company_street: string; company_postal_city: string; company_country: string }>(
|
||||
'/api/admin/company-settings', { method: 'PUT', body: JSON.stringify(data) }
|
||||
);
|
||||
const updateCompanySettings = useCallback(async (data: Partial<CompanySettings>) => {
|
||||
// Debug request body in browser console (redacts base64 values)
|
||||
try {
|
||||
// IMPORTANT: `data` is the real payload object; `redacted` is for logs only.
|
||||
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]);
|
||||
|
||||
return {
|
||||
|
||||
@ -10,6 +10,7 @@ import { useShippingFees } from '../hooks/useShippingFees';
|
||||
import { useAboContractTemplateHtml } from './hooks/useAboContractTemplateHtml'
|
||||
import SignaturePad from './components/SignaturePad'
|
||||
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
|
||||
|
||||
@ -77,6 +78,8 @@ export default function SummaryPage() {
|
||||
signingCity: '',
|
||||
});
|
||||
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 [taxRate, setTaxRate] = useState(0.07); // minimal fallback only
|
||||
const [vatRates, setVatRates] = useState<{ code: string; rate: number | null }[]>([]);
|
||||
@ -223,18 +226,43 @@ export default function SummaryPage() {
|
||||
const usableHeight = pageHeight - marginTop - marginBottom
|
||||
|
||||
const renderCanvasToPdf = (canvas: HTMLCanvasElement, pageIndex: number) => {
|
||||
const imgData = canvas.toDataURL('image/png')
|
||||
const imgWidth = usableWidth
|
||||
const imgHeight = (canvas.height * imgWidth) / canvas.width
|
||||
const imgWidthPt = usableWidth
|
||||
const pxPerPt = canvas.width / imgWidthPt
|
||||
const sliceHeightPx = Math.max(1, Math.floor(usableHeight * pxPerPt))
|
||||
|
||||
// If a single chunk is too tall, fall back to slicing within that chunk.
|
||||
const sliceCount = Math.max(1, Math.ceil(imgHeight / usableHeight))
|
||||
for (let i = 0; i < sliceCount; i++) {
|
||||
const isFirstPageForChunk = i === 0
|
||||
const isFirstOverall = pageIndex === 0 && isFirstPageForChunk
|
||||
let yPx = 0
|
||||
let sliceIndex = 0
|
||||
while (yPx < canvas.height) {
|
||||
const remainingPx = canvas.height - yPx
|
||||
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()
|
||||
const y = marginTop - i * usableHeight
|
||||
pdf.addImage(imgData, 'PNG', marginX, y, imgWidth, imgHeight)
|
||||
pdf.addImage(imgData, 'PNG', marginX, marginTop, imgWidthPt, imgHeightPt)
|
||||
|
||||
yPx += currentSliceHeightPx
|
||||
sliceIndex++
|
||||
}
|
||||
}
|
||||
|
||||
@ -502,6 +530,9 @@ export default function SummaryPage() {
|
||||
setSubmitError(null)
|
||||
setSubmitLoading(true)
|
||||
try {
|
||||
const recipientEmail = form.recipientEmail.trim()
|
||||
const recipientName = form.recipientName.trim()
|
||||
|
||||
const payload = {
|
||||
items: selectedEntries.map(entry => ({
|
||||
coffeeId: entry.coffee.id,
|
||||
@ -540,6 +571,59 @@ export default function SummaryPage() {
|
||||
// NEW: explicit JSON preview to match request body
|
||||
console.info('[SummaryPage] subscribeAbo payload JSON:', JSON.stringify(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);
|
||||
try { sessionStorage.removeItem('coffeeSelections'); } catch {}
|
||||
try { sessionStorage.removeItem('coffeeAboSizeCapsules'); } catch {}
|
||||
@ -895,6 +979,23 @@ export default function SummaryPage() {
|
||||
Subscription created.
|
||||
</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">
|
||||
<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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user