Initial commit - ERP System
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { ArrowLeft, Plus, Trash2, Search } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useToast } from '../../components/ToastProvider';
|
||||
|
||||
const docTypes = [
|
||||
{ value: 'QUOTE', label: 'Angebot' },
|
||||
{ value: 'ORDER_CONFIRMATION', label: 'Auftragsbestätigung' },
|
||||
{ value: 'DELIVERY_NOTE', label: 'Lieferschein' },
|
||||
{ value: 'INVOICE', label: 'Rechnung' },
|
||||
];
|
||||
|
||||
export default function NewSalesDocPage() {
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const [type, setType] = useState('QUOTE');
|
||||
const [customerId, setCustomerId] = useState<number | null>(null);
|
||||
const [customerSearch, setCustomerSearch] = useState('');
|
||||
const [customerResults, setCustomerResults] = useState<any[]>([]);
|
||||
const [selectedCustomer, setSelectedCustomer] = useState<any>(null);
|
||||
const [notes, setNotes] = useState('');
|
||||
const [validUntil, setValidUntil] = useState('');
|
||||
const [items, setItems] = useState<any[]>([{ description: '', quantity: 1, unitPrice: 0, productId: null }]);
|
||||
const [products, setProducts] = useState<any[]>([]);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/products').then(r => r.json()).then(setProducts).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const searchCustomers = async (q: string) => {
|
||||
setCustomerSearch(q);
|
||||
if (q.length < 2) { setCustomerResults([]); return; }
|
||||
const res = await fetch(`/api/customers/search?q=${encodeURIComponent(q)}`);
|
||||
if (res.ok) setCustomerResults(await res.json());
|
||||
};
|
||||
|
||||
const selectCustomer = (c: any) => {
|
||||
setSelectedCustomer(c);
|
||||
setCustomerId(c.id);
|
||||
setCustomerSearch(c.companyName || `${c.firstName} ${c.lastName}`);
|
||||
setCustomerResults([]);
|
||||
};
|
||||
|
||||
const updateItem = (idx: number, field: string, value: any) => {
|
||||
const newItems = [...items];
|
||||
newItems[idx] = { ...newItems[idx], [field]: value };
|
||||
// Auto-fill price from product
|
||||
if (field === 'productId' && value) {
|
||||
const prod = products.find(p => p.id === parseInt(value));
|
||||
if (prod) {
|
||||
newItems[idx].unitPrice = prod.salePrice;
|
||||
newItems[idx].description = prod.name;
|
||||
}
|
||||
}
|
||||
setItems(newItems);
|
||||
};
|
||||
|
||||
const addItem = () => setItems([...items, { description: '', quantity: 1, unitPrice: 0, productId: null }]);
|
||||
const removeItem = (idx: number) => setItems(items.filter((_, i) => i !== idx));
|
||||
|
||||
const subtotal = items.reduce((sum, i) => sum + (i.quantity * i.unitPrice), 0);
|
||||
const tax = subtotal * 0.19;
|
||||
const total = subtotal + tax;
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!customerId) { toast('Bitte Kunde auswählen', 'error'); return; }
|
||||
if (items.length === 0 || !items[0].description) { toast('Bitte mindestens eine Position', 'error'); return; }
|
||||
setSaving(true);
|
||||
|
||||
const res = await fetch('/api/sales', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
type, customerId, notes, validUntil: validUntil || null,
|
||||
items: items.map(i => ({
|
||||
description: i.description, quantity: parseFloat(i.quantity) || 1,
|
||||
unitPrice: parseFloat(i.unitPrice) || 0,
|
||||
productId: i.productId ? parseInt(i.productId) : null
|
||||
}))
|
||||
})
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const doc = await res.json();
|
||||
toast('Beleg erstellt', 'success');
|
||||
router.push(`/sales/${doc.id}`);
|
||||
} else {
|
||||
toast('Fehler beim Erstellen', 'error');
|
||||
}
|
||||
setSaving(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto space-y-6 pb-12 animate-fade-in-up">
|
||||
<div className="flex items-center gap-4">
|
||||
<button onClick={() => router.push('/sales')} className="p-2 hover:bg-slate-200 rounded-lg text-slate-600 transition">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<h1 className="text-2xl font-bold text-slate-900">Neuen Beleg erstellen</h1>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Left: Details */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<div className="bg-white p-6 rounded-2xl shadow-lg shadow-slate-200/50 border border-slate-100">
|
||||
<h2 className="font-semibold text-slate-800 mb-4">Belegdaten</h2>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Belegart *</label>
|
||||
<select className="w-full border border-slate-300 p-2.5 rounded-xl focus-ring outline-none bg-white" value={type} onChange={e => setType(e.target.value)}>
|
||||
{docTypes.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Gültig bis</label>
|
||||
<input type="date" className="w-full border border-slate-300 p-2.5 rounded-xl focus-ring outline-none" value={validUntil} onChange={e => setValidUntil(e.target.value)} />
|
||||
</div>
|
||||
<div className="col-span-2 relative">
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Kunde *</label>
|
||||
<div className="relative">
|
||||
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
|
||||
<input type="text" placeholder="Kunde suchen..." className="w-full pl-9 border border-slate-300 p-2.5 rounded-xl focus-ring outline-none" value={customerSearch} onChange={e => searchCustomers(e.target.value)} />
|
||||
</div>
|
||||
{customerResults.length > 0 && (
|
||||
<div className="absolute z-20 left-0 right-0 mt-1 bg-white border border-slate-200 rounded-xl shadow-xl max-h-48 overflow-y-auto">
|
||||
{customerResults.map(c => (
|
||||
<button key={c.id} onClick={() => selectCustomer(c)} className="w-full text-left px-4 py-3 hover:bg-indigo-50 transition text-sm">
|
||||
<span className="font-semibold">{c.companyName || `${c.firstName} ${c.lastName}`}</span>
|
||||
<span className="text-slate-400 ml-2">{c.email}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{selectedCustomer && <p className="text-xs text-emerald-600 mt-1 font-medium">✓ {selectedCustomer.companyName || `${selectedCustomer.firstName} ${selectedCustomer.lastName}`}</p>}
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Notizen</label>
|
||||
<textarea rows={2} className="w-full border border-slate-300 p-2.5 rounded-xl focus-ring outline-none resize-none" value={notes} onChange={e => setNotes(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Positions */}
|
||||
<div className="bg-white p-6 rounded-2xl shadow-lg shadow-slate-200/50 border border-slate-100">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="font-semibold text-slate-800">Positionen</h2>
|
||||
<button type="button" onClick={addItem} className="text-indigo-600 hover:text-indigo-800 transition flex items-center gap-1 text-sm font-medium">
|
||||
<Plus className="w-4 h-4" /> Position
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{items.map((item, idx) => (
|
||||
<div key={idx} className="grid grid-cols-12 gap-2 items-end p-3 bg-slate-50 rounded-xl border border-slate-100">
|
||||
<div className="col-span-3">
|
||||
<label className="block text-[10px] font-medium text-slate-500 mb-0.5">Produkt</label>
|
||||
<select className="w-full border border-slate-300 p-2 rounded-lg text-sm bg-white" value={item.productId || ''} onChange={e => updateItem(idx, 'productId', e.target.value || null)}>
|
||||
<option value="">Freitext</option>
|
||||
{products.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-span-4">
|
||||
<label className="block text-[10px] font-medium text-slate-500 mb-0.5">Beschreibung *</label>
|
||||
<input type="text" required className="w-full border border-slate-300 p-2 rounded-lg text-sm" value={item.description} onChange={e => updateItem(idx, 'description', e.target.value)} />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="block text-[10px] font-medium text-slate-500 mb-0.5">Menge</label>
|
||||
<input type="number" step="0.01" min="0.01" className="w-full border border-slate-300 p-2 rounded-lg text-sm" value={item.quantity} onChange={e => updateItem(idx, 'quantity', e.target.value)} />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="block text-[10px] font-medium text-slate-500 mb-0.5">Einzelpreis €</label>
|
||||
<input type="number" step="0.01" className="w-full border border-slate-300 p-2 rounded-lg text-sm" value={item.unitPrice} onChange={e => updateItem(idx, 'unitPrice', e.target.value)} />
|
||||
</div>
|
||||
<div className="col-span-1 flex justify-center">
|
||||
{items.length > 1 && (
|
||||
<button type="button" onClick={() => removeItem(idx)} className="p-2 text-slate-400 hover:text-red-600 rounded-lg transition">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Summary */}
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white p-6 rounded-2xl shadow-lg shadow-slate-200/50 border border-slate-100 sticky top-6">
|
||||
<h2 className="font-semibold text-slate-800 mb-4">Zusammenfassung</h2>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="flex justify-between text-slate-600">
|
||||
<span>Netto</span><span className="font-semibold">{subtotal.toFixed(2)} €</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-slate-600">
|
||||
<span>MwSt (19%)</span><span className="font-semibold">{tax.toFixed(2)} €</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-lg font-bold text-slate-900 pt-3 border-t border-slate-200">
|
||||
<span>Gesamt</span><span>{total.toFixed(2)} €</span>
|
||||
</div>
|
||||
<div className="text-xs text-slate-400">{items.filter(i => i.description).length} Position(en)</div>
|
||||
</div>
|
||||
<button onClick={handleSubmit} disabled={saving}
|
||||
className="w-full mt-6 bg-indigo-600 text-white py-3 rounded-xl font-bold hover:bg-indigo-700 transition disabled:opacity-50">
|
||||
{saving ? 'Erstelle...' : 'Beleg erstellen'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user