ASDOJHASOJDHOLASD

This commit is contained in:
DeathKaioken 2026-03-15 19:57:02 +01:00
parent f47441ff41
commit e7db384d58
6 changed files with 841 additions and 432 deletions

806
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -27,7 +27,9 @@
"country-flag-icons": "^1.6.13",
"country-select-js": "^2.1.0",
"gsap": "^3.14.2",
"html2canvas": "^1.4.1",
"intl-tel-input": "^26.4.1",
"jspdf": "^4.2.0",
"lucide-react": "^0.574.0",
"motion": "^12.34.1",
"next": "^16.1.6",
@ -65,4 +67,4 @@
"tailwindcss": "^4.1.18",
"typescript": "^5"
}
}
}

View File

@ -163,7 +163,10 @@
width: 14px;
height: 14px;
border: 1px solid var(--ink);
display: inline-block;
display: inline-flex;
align-items: center;
justify-content: center;
vertical-align: middle;
border-radius: 2px;
position: relative;
flex: 0 0 auto;
@ -172,13 +175,20 @@
.checkbox.checked::after {
content: "";
position: absolute;
left: 3px;
top: 0px;
left: 50%;
top: 50%;
width: 6px;
height: 10px;
border-right: 2px solid var(--ink);
border-bottom: 2px solid var(--ink);
transform: rotate(40deg);
transform: translate(-50%, -60%) rotate(40deg);
}
.prefContact {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
table {
@ -257,6 +267,51 @@
body { background: #fff; }
.box { background: #fff; }
thead th { background: #f3f4f6; }
/* Keep important blocks from being split across pages */
.header,
.grid2,
.box,
table,
thead,
tbody,
tr,
.sigGrid,
.signature-grid,
.signature-cell,
.signature-section {
break-inside: avoid;
page-break-inside: avoid;
-webkit-column-break-inside: avoid;
}
/* Keep labels/rows together */
.row {
break-inside: avoid;
page-break-inside: avoid;
}
/* Keep headings with the first lines of the following content */
h1, h2, h3 {
break-after: avoid-page;
page-break-after: avoid;
}
/* Improve paragraph pagination */
p {
orphans: 3;
widows: 3;
}
/* Repeat table header on new pages (Chromium/Puppeteer) */
thead { display: table-header-group; }
/* Explicit page breaks */
.pageBreak {
break-before: page;
page-break-before: always;
margin-top: 0;
}
}
</style>
</head>
@ -265,7 +320,7 @@
<div class="header">
<div class="brand">
<p class="title">ABO Vertrag</p>
<p class="subtitle">Kaffee-/Tee-Service & automatische Wiederbestellungen</p>
<p class="subtitle">Angebot auf Abschluss eines Kauf- Mietvertrages Kaffee-Service- Kapsel</p>
</div>
<div class="company">
<div><strong>PROFIT PLANET GMBH</strong></div>
@ -335,11 +390,6 @@
<div class="row"><div class="label">Mobil</div><div class="value"><span class="fill">{{shippingMobile}}</span></div></div>
<div class="row"><div class="label">E-Mail-Adresse</div><div class="value"><span class="fill wide">{{shippingEmail}}</span></div></div>
<div class="row">
<div class="label">Bevorzugte Kontaktaufnahme</div>
<div class="value">
<span class="check"><span class="checkbox {{shippingPrefPhoneClass}}"></span> Telefon</span>
<span class="check" style="margin-left: 12px;"><span class="checkbox {{shippingPrefEmailClass}}"></span> E-Mail</span>
</div>
</div>
</div>
</div>
@ -365,9 +415,9 @@
<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">
<div class="value prefContact">
<span class="check"><span class="checkbox {{invoicePrefPhoneClass}}"></span> Telefon</span>
<span class="check" style="margin-left: 12px;"><span class="checkbox {{invoicePrefEmailClass}}"></span> E-Mail</span>
<span class="check"><span class="checkbox {{invoicePrefEmailClass}}"></span> E-Mail</span>
</div>
</div>
</div>
@ -376,7 +426,6 @@
<div class="checkline" style="margin-top: 6px;">
<span class="check"><span class="checkbox {{fnCheckedClass}}"></span> FN: <span class="fill">{{fnNumber}}</span></span>
<span class="check"><span class="checkbox {{atuCheckedClass}}"></span> ATU: <span class="fill">{{atuNumber}}</span></span>
<span class="muted" style="margin-left:auto; font-size: 9.5pt;">(falls zutreffend ausfüllen)</span>
</div>
</div>
@ -392,6 +441,8 @@
</div>
</div>
<div class="pageBreak"></div>
<h2>Angebote</h2>
<div class="box">
<p class="para">
@ -426,7 +477,7 @@
<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 (zutreffendes bitte ankreuzen)
Der BIO Kaffee-Tee wird automatisch im Abstand von:
</p>
<div class="checkline" style="margin-top: 2px;">
@ -470,6 +521,8 @@
<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>
<h2>§ 5 Wartung und Reparatur</h2>
<p>(1) Wartung und Reparaturen der Kaffeemaschinen sind in der Gestellung wie folgt enthalten:</p>
<p>(a) Alle Wartungsarbeiten und Reparaturen werden werktags, von Montag bis Freitag zu den üblichen Arbeitszeiten (09:00 - 17:00 Uhr) telefonisch: 0043 676 3440274 oder schriftlich an office@profit-planet.com durchgeführt. Dienstleistungen an Wochenenden und Feiertagen sind ausgeschlossen und können nur gegen einen Aufpreis vom Kunden selbst bei dem von uns benannten, autorisierten Servicepartner beauftragt werden.</p>
@ -488,6 +541,8 @@
<h2>§ 7 Eigentumsverhältnisse</h2>
<p>Die gelieferten Maschinen bleiben Eigentum von Profit Planet GmbH.</p>
<div class="pageBreak"></div>
<h2>§ 8 Interne Bestellsysteme und Bestellungen, Datenschutz</h2>
<p>Weiteres stimme ich dem Erhalt von exklusiven Angeboten und Informationen wie folgt zu:</p>
<p><span class="check"><span class="checkbox"></span></span> Ich stimme zu, dass die angegebenen Daten von der Profit Planet GmbH verarbeitet und zur Information über exklusive Angebote und sonstige Informationen über E-Mail-Newsletters verwendet werden. Die Zustimmung kann jederzeit per E-Mail an office@profit-planet.com widerrufen werden. Ich akzeptiere hiermit die Datenschutzbestimmungen.</p>
@ -546,6 +601,8 @@
<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>
@ -559,6 +616,8 @@
<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>
@ -577,6 +636,8 @@
<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>

View File

@ -0,0 +1,46 @@
'use client'
import { useEffect, useState } from 'react'
export function useAboContractTemplateHtml() {
const [html, setHtml] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
let active = true
;(async () => {
setLoading(true)
setError(null)
try {
const res = await fetch('/templates/abo-contract-template.html', {
method: 'GET',
headers: { Accept: 'text/html' },
cache: 'no-store',
})
const text = await res.text().catch(() => '')
if (!res.ok) {
throw new Error(text || `Failed to load contract template: ${res.status}`)
}
if (active) setHtml(text || null)
} catch (e: any) {
if (active) {
setHtml(null)
setError(e?.message || 'Failed to load contract template preview.')
}
} finally {
if (active) setLoading(false)
}
})()
return () => {
active = false
}
}, [])
return { html, loading, error }
}

View File

@ -7,17 +7,52 @@ import { getStandardVatRate, getVatRates } from './hooks/getTaxRate';
import { subscribeAbo } from './hooks/subscribeAbo';
import useAuthStore from '../../store/authStore'
import { useShippingFees } from '../hooks/useShippingFees';
import { useAboActiveContractHtml } from './hooks/useAboActiveContractHtml'
import { useAboContractTemplateHtml } from './hooks/useAboContractTemplateHtml'
import SignaturePad from './components/SignaturePad'
import { Dialog, DialogActions, DialogBody, DialogTitle } from '../../components/dialog'
const COLORS = ['#1C2B4A', '#233357', '#2A3B66', '#314475', '#3A4F88', '#5B6C9A']; // dark blue palette
function extractTemplateVariables(templateHtml: string | null | undefined): string[] {
if (!templateHtml) return []
const vars = new Set<string>()
const re = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g
let match: RegExpExecArray | null
while ((match = re.exec(templateHtml)) !== null) {
if (match[1]) vars.add(match[1])
}
return Array.from(vars).sort((a, b) => a.localeCompare(b))
}
function escapeHtml(value: string): string {
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
}
function hashString(value: string): number {
// djb2
let hash = 5381
for (let i = 0; i < value.length; i++) {
hash = ((hash << 5) + hash) ^ value.charCodeAt(i)
}
return hash >>> 0
}
export default function SummaryPage() {
const router = useRouter();
const { coffees, loading, error } = useActiveCoffees();
const user = useAuthStore(state => state.user)
const { feeByPieceCount, loading: shippingLoading, error: shippingError } = useShippingFees();
const { html: contractHtml, loading: contractLoading, error: contractError } = useAboActiveContractHtml()
const { html: contractHtml, loading: contractLoading, error: contractError } = useAboContractTemplateHtml()
const [isContractPreviewOpen, setIsContractPreviewOpen] = useState(false)
const [contractPdfUrl, setContractPdfUrl] = useState<string>('')
const [contractPdfKey, setContractPdfKey] = useState<string>('')
const [contractPdfLoading, setContractPdfLoading] = useState(false)
const [contractPdfError, setContractPdfError] = useState<string | null>(null)
const [selections, setSelections] = useState<Record<string, number>>({});
const [selectedPlanCapsules, setSelectedPlanCapsules] = useState<60 | 120>(120);
const [isForSelf, setIsForSelf] = useState(true);
@ -44,6 +79,208 @@ export default function SummaryPage() {
const [submitLoading, setSubmitLoading] = useState(false);
const initialCountryRef = useRef(form.country)
const templateVariableNames = useMemo(() => extractTemplateVariables(contractHtml), [contractHtml])
const templateVariableNamesKey = useMemo(() => templateVariableNames.join('|'), [templateVariableNames])
const [contractVariables, setContractVariables] = useState<Record<string, string>>({})
useEffect(() => {
if (!templateVariableNamesKey) return
setContractVariables(prev => {
let changed = false
const next: Record<string, string> = { ...prev }
for (const name of templateVariableNames) {
if (next[name] === undefined) {
next[name] = ''
changed = true
}
}
return changed ? next : prev
})
}, [templateVariableNamesKey, templateVariableNames])
const populatedContractHtml = useMemo(() => {
if (!contractHtml) return null
// Replace placeholders with escaped user-entered values so placeholders never show.
return contractHtml.replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (_whole, varName: string) => {
if (varName === 'signatureImage' && !contractVariables[varName] && signatureDataUrl) {
const safeUrl = String(signatureDataUrl)
if (safeUrl.startsWith('data:image/')) {
const src = escapeHtml(safeUrl)
return `<img alt="Signature" src="${src}" style="max-width: 100%; max-height: 120px;" />`
}
}
const value = contractVariables[varName] ?? ''
return escapeHtml(String(value))
})
}, [contractHtml, contractVariables, signatureDataUrl])
const contractPdfCacheKey = useMemo(() => {
if (!populatedContractHtml) return ''
return String(hashString(populatedContractHtml))
}, [populatedContractHtml])
useEffect(() => {
// Cleanup blob URL when it changes or on unmount.
return () => {
if (contractPdfUrl) URL.revokeObjectURL(contractPdfUrl)
}
}, [contractPdfUrl])
const closeContractPreview = () => {
setIsContractPreviewOpen(false)
setContractPdfError(null)
setContractPdfLoading(false)
setContractPdfKey('')
if (contractPdfUrl) {
URL.revokeObjectURL(contractPdfUrl)
setContractPdfUrl('')
}
}
const openContractPreview = async () => {
if (!populatedContractHtml) return
setIsContractPreviewOpen(true)
setContractPdfError(null)
// Reuse only if the populated HTML hasn't changed.
if (contractPdfUrl && contractPdfKey === contractPdfCacheKey) return
if (contractPdfUrl) {
URL.revokeObjectURL(contractPdfUrl)
setContractPdfUrl('')
}
setContractPdfLoading(true)
try {
const [jsPdfMod, html2canvasMod] = await Promise.all([import('jspdf'), import('html2canvas')])
const jsPDF: any = (jsPdfMod as any).jsPDF || (jsPdfMod as any).default
const html2canvas: any = (html2canvasMod as any).default || html2canvasMod
const parser = new DOMParser()
const doc = parser.parseFromString(populatedContractHtml, 'text/html')
const styles = Array.from(doc.querySelectorAll('style'))
.map(s => s.textContent || '')
.join('\n')
const bodyHtml = doc.body?.innerHTML || ''
const wrapper = document.createElement('div')
wrapper.style.position = 'fixed'
wrapper.style.left = '-10000px'
wrapper.style.top = '0'
wrapper.style.width = '794px' // approx A4 width at 96dpi
wrapper.style.background = '#ffffff'
wrapper.innerHTML = `<style>${styles}</style>${bodyHtml}`
document.body.appendChild(wrapper)
try {
const pdf = new jsPDF({ orientation: 'p', unit: 'pt', format: 'a4' })
const pageWidth = pdf.internal.pageSize.getWidth()
const pageHeight = pdf.internal.pageSize.getHeight()
const marginX = 24
const marginTop = 24
const marginBottom = 24
const usableWidth = pageWidth - marginX * 2
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
// 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
if (!isFirstOverall) pdf.addPage()
const y = marginTop - i * usableHeight
pdf.addImage(imgData, 'PNG', marginX, y, imgWidth, imgHeight)
}
}
// Split at explicit .pageBreak markers (in document order, even when nested)
// to avoid cutting content between pages.
const docRoot = wrapper.querySelector('.doc') as HTMLElement | null
const pageRoot = docRoot ?? wrapper
const breakEls = Array.from(pageRoot.querySelectorAll('.pageBreak')) as HTMLElement[]
if (breakEls.length === 0) {
const canvas: HTMLCanvasElement = await html2canvas(wrapper, {
scale: Math.min(2, window.devicePixelRatio || 1),
backgroundColor: '#ffffff',
useCORS: true,
})
renderCanvasToPdf(canvas, 0)
} else {
const range = document.createRange()
range.setStart(pageRoot, 0)
const fragments: DocumentFragment[] = []
for (const br of breakEls) {
range.setEndBefore(br)
const frag = range.cloneContents()
if (frag.childNodes.length > 0) fragments.push(frag)
range.setStartAfter(br)
}
range.setEnd(pageRoot, pageRoot.childNodes.length)
const lastFrag = range.cloneContents()
if (lastFrag.childNodes.length > 0) fragments.push(lastFrag)
if (fragments.length === 0) {
const canvas: HTMLCanvasElement = await html2canvas(wrapper, {
scale: Math.min(2, window.devicePixelRatio || 1),
backgroundColor: '#ffffff',
useCORS: true,
})
renderCanvasToPdf(canvas, 0)
} else {
for (let pageIndex = 0; pageIndex < fragments.length; pageIndex++) {
const pageWrapper = document.createElement('div')
pageWrapper.style.position = 'fixed'
pageWrapper.style.left = '-10000px'
pageWrapper.style.top = '0'
pageWrapper.style.width = '794px'
pageWrapper.style.background = '#ffffff'
pageWrapper.innerHTML = `<style>${styles}</style>`
const pageDoc = document.createElement('div')
pageDoc.className = docRoot?.className || 'doc'
pageDoc.appendChild(fragments[pageIndex])
pageWrapper.appendChild(pageDoc)
document.body.appendChild(pageWrapper)
try {
const canvas: HTMLCanvasElement = await html2canvas(pageWrapper, {
scale: Math.min(2, window.devicePixelRatio || 1),
backgroundColor: '#ffffff',
useCORS: true,
})
renderCanvasToPdf(canvas, pageIndex)
} finally {
document.body.removeChild(pageWrapper)
}
}
}
}
const blob = pdf.output('blob') as Blob
const url = URL.createObjectURL(blob)
setContractPdfUrl(url)
setContractPdfKey(contractPdfCacheKey)
} finally {
document.body.removeChild(wrapper)
}
} catch (e: any) {
setContractPdfError(e?.message || 'Failed to generate PDF preview.')
} finally {
setContractPdfLoading(false)
}
}
useEffect(() => {
try {
const raw = sessionStorage.getItem('coffeeSelections');
@ -456,9 +693,9 @@ export default function SummaryPage() {
{/* Contract preview + signature (frontend only for now) */}
<div className="mt-6 border-t border-gray-200 pt-6">
<h3 className="text-base font-semibold text-gray-900 mb-2">Contract preview (ABO)</h3>
<h3 className="text-base font-semibold text-gray-900 mb-2">Contract template preview (ABO)</h3>
<p className="text-xs text-gray-600 mb-3">
This is the currently active ABO contract template for your account.
This is the ABO contract HTML template (populated from the fields below, frontend-only).
</p>
{contractLoading ? (
@ -469,19 +706,38 @@ export default function SummaryPage() {
<div className="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900">
Contract preview could not be loaded: {contractError}
</div>
) : contractHtml ? (
<div className="rounded-lg border border-gray-300 bg-white overflow-hidden">
<iframe
title="ABO Contract Preview"
className="w-full h-[520px]"
srcDoc={contractHtml}
sandbox="allow-same-origin"
referrerPolicy="no-referrer"
/>
</div>
) : populatedContractHtml ? (
<>
{templateVariableNames.length > 0 && (
<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="grid gap-4 sm:grid-cols-2">
{templateVariableNames.map(varName => (
<div key={varName}>
<label className="block text-xs font-medium mb-1 text-gray-700">{varName}</label>
<input
value={contractVariables[varName] ?? ''}
onChange={e =>
setContractVariables(prev => ({ ...prev, [varName]: e.target.value }))
}
className="w-full rounded border px-3 py-2 bg-white border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#1C2B4A]"
/>
</div>
))}
</div>
</div>
)}
<button
type="button"
onClick={openContractPreview}
className="inline-flex items-center justify-center rounded-md bg-[#1C2B4A] text-white px-4 py-2 text-sm font-semibold hover:bg-[#1C2B4A]/90"
>
Open preview
</button>
</>
) : (
<div className="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700">
No active ABO contract is available.
Contract template is not available.
</div>
)}
@ -490,6 +746,42 @@ export default function SummaryPage() {
</div>
</div>
<Dialog open={isContractPreviewOpen} onClose={closeContractPreview} size="5xl">
<DialogTitle>ABO contract preview (PDF)</DialogTitle>
<DialogBody>
{contractPdfError ? (
<div className="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900">
PDF preview could not be generated: {contractPdfError}
</div>
) : contractPdfLoading ? (
<div className="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700">
Generating PDF preview
</div>
) : contractPdfUrl ? (
<div className="rounded-lg border border-gray-300 bg-white overflow-hidden">
<iframe
title="ABO Contract PDF Preview"
className="w-full h-[75vh]"
src={contractPdfUrl}
/>
</div>
) : (
<div className="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700">
No PDF preview available.
</div>
)}
</DialogBody>
<DialogActions>
<button
type="button"
onClick={closeContractPreview}
className="rounded-md border border-gray-300 px-4 py-2 text-sm font-semibold hover:bg-gray-50"
>
Close
</button>
</DialogActions>
</Dialog>
<button
onClick={submit}
disabled={!canSubmit || submitLoading}

View File

@ -25,13 +25,13 @@ export function Dialog({
'as' | 'className'
>) {
return (
<Headless.Dialog {...props}>
<Headless.Dialog {...props} className="relative z-50">
<Headless.DialogBackdrop
transition
className="fixed inset-0 flex w-screen justify-center overflow-y-auto bg-zinc-950/25 px-2 py-2 transition duration-100 focus:outline-0 data-closed:opacity-0 data-enter:ease-out data-leave:ease-in sm:px-6 sm:py-8 lg:px-8 lg:py-16 dark:bg-zinc-950/50"
className="fixed inset-0 z-50 flex w-screen justify-center overflow-y-auto bg-zinc-950/25 backdrop-blur-sm px-2 py-2 transition duration-100 focus:outline-0 data-closed:opacity-0 data-enter:ease-out data-leave:ease-in sm:px-6 sm:py-8 lg:px-8 lg:py-16 dark:bg-zinc-950/50"
/>
<div className="fixed inset-0 w-screen overflow-y-auto pt-6 sm:pt-0">
<div className="fixed inset-0 z-50 w-screen overflow-y-auto pt-6 sm:pt-0">
<div className="grid min-h-full grid-rows-[1fr_auto] justify-items-center sm:grid-rows-[1fr_auto_3fr] sm:p-4">
<Headless.DialogPanel
transition