179 lines
12 KiB
TypeScript
179 lines
12 KiB
TypeScript
// /opt/erp-system/app/settings/page.tsx
|
|
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { Settings, Save, Percent, Euro, Mail, Inbox, Hash } from 'lucide-react';
|
|
|
|
export default function SettingsPage() {
|
|
const [data, setData] = useState({
|
|
hourlyRate: 0, taxRate: 0, companyName: '', companyInfo: '',
|
|
smtpHost: '', smtpPort: 587, smtpUser: '', smtpPass: '', smtpFrom: '', hasSmtpPass: false,
|
|
imapHost: '', imapPort: 993, imapUser: '', imapPass: '', hasImapPass: false,
|
|
nextQuoteNumber: 1, nextOrderNumber: 1, nextDeliveryNumber: 1, nextInvoiceNumber: 1, nextCreditNoteNumber: 1,
|
|
defaultQuoteValidityDays: 14
|
|
});
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
const [msg, setMsg] = useState('');
|
|
|
|
useEffect(() => {
|
|
fetch('/api/settings').then(res => res.json()).then(d => {
|
|
setData({
|
|
hourlyRate: d.hourlyRate || 0, taxRate: d.taxRate || 0, companyName: d.companyName || '', companyInfo: d.companyInfo || '',
|
|
smtpHost: d.smtpHost || '', smtpPort: d.smtpPort || 587, smtpUser: d.smtpUser || '', smtpPass: '', smtpFrom: d.smtpFrom || '', hasSmtpPass: d.hasSmtpPass,
|
|
imapHost: d.imapHost || '', imapPort: d.imapPort || 993, imapUser: d.imapUser || '', imapPass: '', hasImapPass: d.hasImapPass,
|
|
nextQuoteNumber: d.nextQuoteNumber || 1, nextOrderNumber: d.nextOrderNumber || 1, nextDeliveryNumber: d.nextDeliveryNumber || 1, nextInvoiceNumber: d.nextInvoiceNumber || 1, nextCreditNoteNumber: d.nextCreditNoteNumber || 1,
|
|
defaultQuoteValidityDays: d.defaultQuoteValidityDays || 14
|
|
});
|
|
setLoading(false);
|
|
});
|
|
}, []);
|
|
|
|
const handleSave = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
const res = await fetch('/api/settings', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
if (res.ok) {
|
|
setMsg('Einstellungen erfolgreich gespeichert.');
|
|
setTimeout(() => setMsg(''), 3000);
|
|
setData(prev => ({
|
|
...prev,
|
|
smtpPass: '', hasSmtpPass: prev.smtpPass ? true : prev.hasSmtpPass,
|
|
imapPass: '', hasImapPass: prev.imapPass ? true : prev.hasImapPass
|
|
}));
|
|
}
|
|
};
|
|
|
|
if (loading) return <div className="p-8">Lade Konfiguration...</div>;
|
|
|
|
return (
|
|
<div className="max-w-4xl mx-auto space-y-6 pb-12">
|
|
<h1 className="text-2xl font-bold text-slate-900 flex items-center gap-2">
|
|
<Settings className="w-6 h-6 text-indigo-600" /> Systemeinstellungen
|
|
</h1>
|
|
|
|
{msg && <div className="p-4 bg-emerald-50 text-emerald-700 border border-emerald-200 rounded-lg font-medium">{msg}</div>}
|
|
|
|
<form onSubmit={handleSave} className="bg-white rounded-xl shadow-sm border border-slate-200 divide-y divide-slate-100">
|
|
|
|
{/* Abrechnung */}
|
|
<div className="p-6 space-y-4">
|
|
<h2 className="font-semibold text-slate-800">Abrechnungsparameter</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1 flex items-center gap-2">
|
|
<Euro className="w-4 h-4" /> Standard-Stundensatz
|
|
</label>
|
|
<input type="number" step="0.01" className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none" value={data.hourlyRate} onChange={e => setData({...data, hourlyRate: parseFloat(e.target.value)})} />
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1 flex items-center gap-2">
|
|
<Percent className="w-4 h-4" /> Mehrwertsteuer (%)
|
|
</label>
|
|
<input type="number" step="0.1" className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none" value={data.taxRate} onChange={e => setData({...data, taxRate: parseFloat(e.target.value)})} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* SMTP */}
|
|
<div className="p-6 space-y-4">
|
|
<h2 className="font-semibold text-slate-800 flex items-center gap-2">
|
|
<Mail className="w-5 h-5" /> Ausgehende E-Mails (SMTP)
|
|
</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<div className="md:col-span-3">
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">SMTP Server (Host)</label>
|
|
<input type="text" className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none" value={data.smtpHost} onChange={e => setData({...data, smtpHost: e.target.value})} />
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Port</label>
|
|
<input type="number" className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none" value={data.smtpPort} onChange={e => setData({...data, smtpPort: parseInt(e.target.value)})} />
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">SMTP Benutzername</label>
|
|
<input type="text" className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none" value={data.smtpUser} onChange={e => setData({...data, smtpUser: e.target.value})} />
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">SMTP Passwort</label>
|
|
<input type="password" placeholder={data.hasSmtpPass ? "****** (Gespeichert)" : "Passwort eingeben"} className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none" value={data.smtpPass} onChange={e => setData({...data, smtpPass: e.target.value})} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* IMAP */}
|
|
<div className="p-6 space-y-4">
|
|
<h2 className="font-semibold text-slate-800 flex items-center gap-2">
|
|
<Inbox className="w-5 h-5" /> Eingehende E-Mails (IMAP)
|
|
</h2>
|
|
<p className="text-sm text-slate-500">Diese E-Mail-Adresse wird überwacht, um automatisch Tickets aus eingehenden E-Mails zu erstellen.</p>
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<div className="md:col-span-3">
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">IMAP Server (Host)</label>
|
|
<input type="text" placeholder="z.B. imap.strato.de" className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none" value={data.imapHost} onChange={e => setData({...data, imapHost: e.target.value})} />
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Port</label>
|
|
<input type="number" placeholder="993" className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none" value={data.imapPort} onChange={e => setData({...data, imapPort: parseInt(e.target.value)})} />
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">IMAP Benutzername</label>
|
|
<input type="text" placeholder="z.B. support@deine-firma.de" className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none" value={data.imapUser} onChange={e => setData({...data, imapUser: e.target.value})} />
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">IMAP Passwort</label>
|
|
<input type="password" placeholder={data.hasImapPass ? "****** (Gespeichert)" : "Passwort eingeben"} className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none" value={data.imapPass} onChange={e => setData({...data, imapPass: e.target.value})} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Nummernkreise */}
|
|
<div className="p-6 space-y-4">
|
|
<h2 className="font-semibold text-slate-800 flex items-center gap-2"><Hash className="w-4 h-4 text-indigo-600" /> Nummernkreise (Warenwirtschaft)</h2>
|
|
<p className="text-sm text-slate-500">Laufende Nummern für Belege. Format: <span className="font-mono bg-slate-100 px-1 rounded">ANG-{new Date().getFullYear()}-{data.nextQuoteNumber.toString().padStart(4, '0')}</span></p>
|
|
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Angebot (ANG)</label>
|
|
<input type="number" min="1" className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none" value={data.nextQuoteNumber} onChange={e => setData({...data, nextQuoteNumber: parseInt(e.target.value) || 1})} />
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">AB</label>
|
|
<input type="number" min="1" className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none" value={data.nextOrderNumber} onChange={e => setData({...data, nextOrderNumber: parseInt(e.target.value) || 1})} />
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Lieferschein (LS)</label>
|
|
<input type="number" min="1" className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none" value={data.nextDeliveryNumber} onChange={e => setData({...data, nextDeliveryNumber: parseInt(e.target.value) || 1})} />
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Rechnung (RE)</label>
|
|
<input type="number" min="1" className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none" value={data.nextInvoiceNumber} onChange={e => setData({...data, nextInvoiceNumber: parseInt(e.target.value) || 1})} />
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Korrektur (RK)</label>
|
|
<input type="number" min="1" className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none" value={data.nextCreditNoteNumber} onChange={e => setData({...data, nextCreditNoteNumber: parseInt(e.target.value) || 1})} />
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mt-2">
|
|
<div className="col-span-2">
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Standard-Gültigkeit Angebot (Tage)</label>
|
|
<input type="number" min="1" className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none" value={data.defaultQuoteValidityDays} onChange={e => setData({...data, defaultQuoteValidityDays: parseInt(e.target.value) || 14})} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-6 bg-slate-50 flex justify-end">
|
|
<button type="submit" className="bg-indigo-600 text-white px-6 py-2.5 rounded-lg hover:bg-indigo-700 transition flex items-center gap-2 font-bold shadow-sm">
|
|
<Save className="w-4 h-4" /> Alle Einstellungen speichern
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|