profit-planet-frontend/src/app/admin/contract-management/components/companySettingsPanel.tsx
2026-03-16 20:04:13 +01:00

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 &amp; 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>
)
}