274 lines
9.8 KiB
TypeScript
274 lines
9.8 KiB
TypeScript
'use client'
|
|
|
|
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()
|
|
|
|
const [form, setForm] = useState({
|
|
company_name: '',
|
|
company_street: '',
|
|
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()
|
|
.then(data => {
|
|
setForm({
|
|
company_name: data.company_name || '',
|
|
company_street: data.company_street || '',
|
|
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))
|
|
}, [getCompanySettings])
|
|
|
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const { name, value } = e.target
|
|
setForm(prev => ({ ...prev, [name]: value }))
|
|
setSaved(false)
|
|
}
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
setSaving(true)
|
|
setSaved(false)
|
|
setSaveError('')
|
|
try {
|
|
// 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 {
|
|
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">
|
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-900" />
|
|
Loading settings…
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label htmlFor="company_name" className="block text-sm font-medium text-gray-700 mb-1">
|
|
Company Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="company_name"
|
|
name="company_name"
|
|
value={form.company_name}
|
|
onChange={handleChange}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
placeholder="ProfitPlanet GmbH"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="company_street" className="block text-sm font-medium text-gray-700 mb-1">
|
|
Street
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="company_street"
|
|
name="company_street"
|
|
value={form.company_street}
|
|
onChange={handleChange}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
placeholder="Musterstraße 1"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="company_postal_city" className="block text-sm font-medium text-gray-700 mb-1">
|
|
Postal Code & City
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="company_postal_city"
|
|
name="company_postal_city"
|
|
value={form.company_postal_city}
|
|
onChange={handleChange}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
placeholder="12345 Berlin"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="company_country" className="block text-sm font-medium text-gray-700 mb-1">
|
|
Country
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="company_country"
|
|
name="company_country"
|
|
value={form.company_country}
|
|
onChange={handleChange}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
placeholder="Germany"
|
|
/>
|
|
</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"
|
|
disabled={saving}
|
|
className={`px-5 py-2 rounded-lg text-sm font-semibold text-white transition-colors ${
|
|
saving ? 'bg-gray-400 cursor-not-allowed' : 'bg-blue-900 hover:bg-blue-800'
|
|
}`}
|
|
>
|
|
{saving ? 'Saving…' : 'Save'}
|
|
</button>
|
|
{saved && (
|
|
<span className="text-sm text-green-600 font-medium">Saved successfully</span>
|
|
)}
|
|
</div>
|
|
</form>
|
|
)
|
|
}
|