274 lines
9.9 KiB
TypeScript
274 lines
9.9 KiB
TypeScript
'use client'
|
|
|
|
import { useTranslation } from '../../../i18n/useTranslation'
|
|
import { useEffect, useRef, useState } from 'react'
|
|
import useContractManagement from '../hooks/useContractManagement'
|
|
|
|
const LOGO_ACCEPTED_TYPES = ['image/png', 'image/jpeg', 'image/webp', 'image/svg+xml']
|
|
const MAX_LOGO_BYTES = 1024 * 1024
|
|
|
|
type CompanySettingsForm = {
|
|
company_name: string
|
|
company_street: string
|
|
company_postal_city: string
|
|
company_country: string
|
|
company_logo_base64: string | null
|
|
company_logo_mime_type: string | null
|
|
}
|
|
|
|
function fileToBase64Payload(file: File) {
|
|
return new Promise<{ base64: string; mimeType: string }>((resolve, reject) => {
|
|
const reader = new FileReader()
|
|
reader.onload = () => {
|
|
const result = typeof reader.result === 'string' ? reader.result : ''
|
|
const match = result.match(/^data:(image\/[a-zA-Z0-9.+-]+);base64,(.+)$/)
|
|
if (!match) {
|
|
reject(new Error('invalid_data_url'))
|
|
return
|
|
}
|
|
resolve({ mimeType: match[1], base64: match[2] })
|
|
}
|
|
reader.onerror = () => reject(new Error('read_failed'))
|
|
reader.readAsDataURL(file)
|
|
})
|
|
}
|
|
|
|
export default function CompanySettingsPanel() {
|
|
const { t } = useTranslation()
|
|
const { getCompanySettings, updateCompanySettings } = useContractManagement()
|
|
const logoInputRef = useRef<HTMLInputElement | null>(null)
|
|
|
|
const [form, setForm] = useState<CompanySettingsForm>({
|
|
company_name: '',
|
|
company_street: '',
|
|
company_postal_city: '',
|
|
company_country: '',
|
|
company_logo_base64: null,
|
|
company_logo_mime_type: null,
|
|
})
|
|
const [loading, setLoading] = useState(true)
|
|
const [saving, setSaving] = useState(false)
|
|
const [saved, setSaved] = useState(false)
|
|
const [saveError, setSaveError] = useState<string>('')
|
|
const [logoError, setLogoError] = 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 || '',
|
|
company_logo_base64: data.company_logo_base64 || null,
|
|
company_logo_mime_type: data.company_logo_mime_type || null,
|
|
})
|
|
})
|
|
.catch(() => {})
|
|
.finally(() => setLoading(false))
|
|
}, [getCompanySettings])
|
|
|
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const { name, value } = e.target
|
|
setForm((prev) => ({ ...prev, [name]: value }))
|
|
setSaved(false)
|
|
}
|
|
|
|
const handleLogoChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0]
|
|
setLogoError('')
|
|
if (!file) return
|
|
|
|
if (!LOGO_ACCEPTED_TYPES.includes(file.type)) {
|
|
setLogoError(t('autofix.k2bd38d5e'))
|
|
if (logoInputRef.current) logoInputRef.current.value = ''
|
|
return
|
|
}
|
|
|
|
if (file.size > MAX_LOGO_BYTES) {
|
|
setLogoError(t('autofix.k394b7f42'))
|
|
if (logoInputRef.current) logoInputRef.current.value = ''
|
|
return
|
|
}
|
|
|
|
try {
|
|
const { base64, mimeType } = await fileToBase64Payload(file)
|
|
setForm((prev) => ({
|
|
...prev,
|
|
company_logo_base64: base64,
|
|
company_logo_mime_type: mimeType,
|
|
}))
|
|
setSaved(false)
|
|
} catch {
|
|
setLogoError(t('autofix.k8a1d4c20'))
|
|
} finally {
|
|
if (logoInputRef.current) logoInputRef.current.value = ''
|
|
}
|
|
}
|
|
|
|
const handleRemoveLogo = () => {
|
|
setForm((prev) => ({
|
|
...prev,
|
|
company_logo_base64: null,
|
|
company_logo_mime_type: null,
|
|
}))
|
|
setLogoError('')
|
|
setSaved(false)
|
|
if (logoInputRef.current) logoInputRef.current.value = ''
|
|
}
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
setSaving(true)
|
|
setSaved(false)
|
|
setSaveError('')
|
|
try {
|
|
await updateCompanySettings(form)
|
|
setSaved(true)
|
|
setTimeout(() => setSaved(false), 3000)
|
|
} catch {
|
|
setSaveError(t('autofix.k95a16b2b'))
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
const logoPreviewSrc = form.company_logo_base64
|
|
? `data:${form.company_logo_mime_type || 'image/png'};base64,${form.company_logo_base64}`
|
|
: null
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center gap-2 py-4 text-sm text-gray-500">
|
|
<div className="h-4 w-4 animate-spin rounded-full border-b-2 border-blue-900" />
|
|
{t('autofix.k81a1b900')}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
<div className="rounded-2xl border border-slate-200 bg-slate-50/80 p-4 shadow-sm">
|
|
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
|
|
<div className="space-y-1">
|
|
<h3 className="text-lg font-semibold text-slate-900">{t('autofix.k0198ce13')}</h3>
|
|
<p className="text-sm text-slate-500">{t('autofix.k03d7361d')}</p>
|
|
<p className="text-xs text-slate-400">{t('autofix.k1c2b0975')}</p>
|
|
</div>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<input
|
|
ref={logoInputRef}
|
|
type="file"
|
|
accept={LOGO_ACCEPTED_TYPES.join(',')}
|
|
onChange={handleLogoChange}
|
|
className="hidden"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => logoInputRef.current?.click()}
|
|
className="rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 transition hover:border-slate-400 hover:bg-slate-100"
|
|
>
|
|
{logoPreviewSrc ? t('autofix.k7d3f0e11') : t('autofix.k089f42a1')}
|
|
</button>
|
|
{logoPreviewSrc && (
|
|
<button
|
|
type="button"
|
|
onClick={handleRemoveLogo}
|
|
className="rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-semibold text-red-700 transition hover:bg-red-100"
|
|
>
|
|
{t('autofix.k0d8e2d01')}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-4 rounded-2xl border border-dashed border-slate-300 bg-white p-4">
|
|
{logoPreviewSrc ? (
|
|
<img
|
|
src={logoPreviewSrc}
|
|
alt={t('autofix.k0198ce13')}
|
|
className="max-h-24 max-w-full object-contain"
|
|
/>
|
|
) : (
|
|
<div className="text-sm text-slate-500">{t('autofix.k432b8a12')}</div>
|
|
)}
|
|
</div>
|
|
|
|
{logoError && <div className="mt-3 text-sm font-medium text-red-600">{logoError}</div>}
|
|
</div>
|
|
|
|
<div>
|
|
<h3 className="mb-3 flex items-center gap-2 text-lg font-semibold text-slate-900">
|
|
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" /></svg>
|
|
{t('autofix.kaa8bbc8e')}
|
|
</h3>
|
|
<p className="mb-4 text-sm text-slate-500">{t('autofix.k15bea9bb')}</p>
|
|
|
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
<div>
|
|
<label htmlFor="company_name" className="mb-1 block text-sm font-medium text-gray-700">{t('autofix.k33918465')}</label>
|
|
<input
|
|
type="text"
|
|
id="company_name"
|
|
name="company_name"
|
|
value={form.company_name}
|
|
onChange={handleChange}
|
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-transparent focus:ring-2 focus:ring-blue-500"
|
|
placeholder={t('autofix.k91e69df1')}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="company_street" className="mb-1 block text-sm font-medium text-gray-700">Street</label>
|
|
<input
|
|
type="text"
|
|
id="company_street"
|
|
name="company_street"
|
|
value={form.company_street}
|
|
onChange={handleChange}
|
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-transparent focus:ring-2 focus:ring-blue-500"
|
|
placeholder={t('autofix.k81c7c2f2')}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="company_postal_city" className="mb-1 block text-sm font-medium text-gray-700">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 rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-transparent focus:ring-2 focus:ring-blue-500"
|
|
placeholder={t('autofix.k93165aea')}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="company_country" className="mb-1 block text-sm font-medium text-gray-700">Country</label>
|
|
<input
|
|
type="text"
|
|
id="company_country"
|
|
name="company_country"
|
|
value={form.company_country}
|
|
onChange={handleChange}
|
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-transparent focus:ring-2 focus:ring-blue-500"
|
|
placeholder="Germany"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{saveError && <div className="text-sm font-medium text-red-600">{saveError}</div>}
|
|
|
|
<div className="flex items-center gap-3">
|
|
<button
|
|
type="submit"
|
|
disabled={saving}
|
|
className={`rounded-lg px-5 py-2 text-sm font-semibold text-white transition-colors ${saving ? 'cursor-not-allowed bg-gray-400' : 'bg-blue-900 hover:bg-blue-800'}`}
|
|
>
|
|
{saving ? t('autofix.kac6cedc7') : 'Save'}
|
|
</button>
|
|
{saved && <span className="text-sm font-medium text-green-600">{t('autofix.ka29ac729')}</span>}
|
|
</div>
|
|
</form>
|
|
)
|
|
}
|