feat: CRM-Stufen (Lead/Kunde) + InvoiceNinja-Sync (eine Pflege)
- Zwei Kundenstufen: Lead (potenziell) und fester Kunde; Lead braucht nur Unternehmen + E-Mail und erzeugt automatisch ein Akquise-Ticket - Akquise-Worksheet-Ergebnis (In Kontakt/Zugesagt/Abgesagt) sichtbar in WSIDs; "Zugesagt" macht den Lead automatisch zum festen Kunden, "Abgesagt" -> verloren - Kunde wird nur in Appwrite gepflegt und automatisch als InvoiceNinja-Client angelegt/aktualisiert (beim Upgrade bzw. bei Datenpflege) - Rechnungen nur fuer feste Kunden mit vollstaendigen Daten (Strasse/PLZ/Stadt) - Kundenliste mit Stufen-Filter, neue Rechnungs-/Adressfelder Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
7d764ce6bc
commit
a1cb90372f
8 files changed
+296
-80
No files matched your search
@@ -5,7 +5,7 @@ import { useEmployees } from '../hooks/useEmployees'
|
||||
|
||||
// Fallback-Werte falls Config nicht geladen werden kann
|
||||
const DEFAULT_TICKET_TYPES = [
|
||||
'Home Office', 'Holidays', 'Trip', 'Supportrequest', 'Change Request',
|
||||
'Akquise', 'Home Office', 'Holidays', 'Trip', 'Supportrequest', 'Change Request',
|
||||
'Maintenance', 'Project', 'Webpage', 'Controlling', 'Development', 'Documentation',
|
||||
'Meeting/Conference', 'IT Management', 'IT Security', 'Procurement',
|
||||
'Rollout', 'Emergency Call', 'Other Services'
|
||||
|
||||
@@ -30,6 +30,9 @@ const RESPONSE_LEVELS = [
|
||||
'n/a'
|
||||
]
|
||||
|
||||
// Akquise-Tickets: Ergebnis des Kundenkontakts (statt technischer Status)
|
||||
const ACQUISITION_STATUS = ['In Kontakt', 'Zugesagt', 'Abgesagt']
|
||||
|
||||
export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCreate }) {
|
||||
const { user } = useAuth()
|
||||
const today = new Date().toLocaleDateString('de-DE', {
|
||||
@@ -55,12 +58,18 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
||||
const [error, setError] = useState('')
|
||||
const [autoCalculate, setAutoCalculate] = useState(true)
|
||||
|
||||
const isAcquisition = workorder?.type === 'Akquise'
|
||||
const statusOptions = isAcquisition ? ACQUISITION_STATUS : STATUS_OPTIONS
|
||||
const defaultStatus = isAcquisition
|
||||
? (ACQUISITION_STATUS.includes(workorder?.status) ? workorder.status : 'In Kontakt')
|
||||
: (workorder?.status || 'Open')
|
||||
|
||||
// Reset form wenn Modal geöffnet wird
|
||||
useEffect(() => {
|
||||
if (isOpen && workorder) {
|
||||
setFormData({
|
||||
serviceType: 'Remote',
|
||||
newStatus: workorder.status || 'Open',
|
||||
newStatus: defaultStatus,
|
||||
newResponseLevel: workorder.responseLevel || '',
|
||||
totalTime: 0,
|
||||
startDate: today,
|
||||
@@ -189,17 +198,22 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">New Status</label>
|
||||
<label className="form-label">{isAcquisition ? 'Akquise-Ergebnis' : 'New Status'}</label>
|
||||
<select
|
||||
className="form-control"
|
||||
value={formData.newStatus}
|
||||
onChange={(e) => handleChange('newStatus', e.target.value)}
|
||||
required
|
||||
>
|
||||
{STATUS_OPTIONS.map(status => (
|
||||
{statusOptions.map(status => (
|
||||
<option key={status} value={status}>{status}</option>
|
||||
))}
|
||||
</select>
|
||||
{isAcquisition && (
|
||||
<small style={{ color: '#a0aec0', fontSize: '12px' }}>
|
||||
"Zugesagt" macht den Lead automatisch zum festen Kunden.
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { FaFilePdf, FaPlus, FaLink, FaSpinner, FaRotate } from 'react-icons/fa6'
|
||||
import { databases, DATABASE_ID, COLLECTIONS } from '../lib/appwrite'
|
||||
import { integrationsApi, formatMoney, INVOICE_STATUS } from '../lib/integrationsApi'
|
||||
import { useInvoices } from '../hooks/useInvoices'
|
||||
import { customerStage, isInvoiceReady } from '../lib/leads'
|
||||
|
||||
const card = {
|
||||
background: 'rgba(45, 55, 72, 0.5)',
|
||||
@@ -74,7 +75,15 @@ export default function InvoicePanel({ ticket }) {
|
||||
|
||||
{customerError && <div className="text-red" style={{ marginBottom: 8 }}>{customerError}</div>}
|
||||
|
||||
{!clientId ? (
|
||||
{customer && customerStage(customer) === 'lead' ? (
|
||||
<p style={{ color: '#a0aec0', margin: 0 }}>
|
||||
Dieser Kunde ist noch ein Lead. Erst zum festen Kunden machen (im Kunden-Bereich, vollstaendige Daten noetig) - dann sind Rechnungen moeglich.
|
||||
</p>
|
||||
) : !clientId && !isInvoiceReady(customer) ? (
|
||||
<p style={{ color: '#a0aec0', margin: 0 }}>
|
||||
Rechnungsdaten unvollstaendig. Bitte im Kunden-Bereich Unternehmen, E-Mail, Strasse, PLZ und Stadt ergaenzen.
|
||||
</p>
|
||||
) : !clientId ? (
|
||||
<LinkClient customer={customer} onLinked={loadCustomer} />
|
||||
) : (
|
||||
<InvoiceList
|
||||
@@ -177,7 +186,7 @@ function LinkClient({ customer, onLinked }) {
|
||||
<div key={c.id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px', borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
|
||||
<div>
|
||||
<strong style={{ color: 'var(--dark-text)' }}>{c.name}</strong>
|
||||
{c.number ? <span style={{ color: '#a0aec0' }}> · {c.number}</span> : null}
|
||||
{c.number ? <span style={{ color: '#a0aec0' }}> � {c.number}</span> : null}
|
||||
{c.email ? <div style={{ color: '#a0aec0', fontSize: '12px' }}>{c.email}</div> : null}
|
||||
</div>
|
||||
<button className="btn btn-green" style={{ margin: 0 }} disabled={saving} onClick={() => saveMapping(c.id)}>
|
||||
@@ -248,7 +257,7 @@ function InvoiceList({ ticket, clientId, invoices, loading, error, createInvoice
|
||||
</thead>
|
||||
<tbody>
|
||||
{invoices.map((inv) => {
|
||||
const st = INVOICE_STATUS[inv.status_id] || { label: '—', color: '#a0aec0' }
|
||||
const st = INVOICE_STATUS[inv.status_id] || { label: '�', color: '#a0aec0' }
|
||||
const overdue = Number(inv.balance) > 0 && inv.due_date && new Date(inv.due_date) < new Date()
|
||||
return (
|
||||
<tr key={inv.id}>
|
||||
|
||||
@@ -18,6 +18,7 @@ import Tabs from './ui/Tabs'
|
||||
import Badge from './ui/Badge'
|
||||
import { StatusPill, PriorityPill } from './ui/StatusPill'
|
||||
import { useWorksheets } from '../hooks/useWorksheets'
|
||||
import { databases, DATABASE_ID, COLLECTIONS } from '../lib/appwrite'
|
||||
|
||||
const TABS = [
|
||||
{ id: 'details', label: 'Details' },
|
||||
@@ -52,6 +53,19 @@ export default function TicketCard({ ticket, onUpdate }) {
|
||||
responseLevel: worksheetData.newResponseLevel || ticket.responseLevel,
|
||||
})
|
||||
}
|
||||
// Akquise-Automatik: Ergebnis im Worksheet steuert den Kundenstatus
|
||||
if (result.success && ticket.type === 'Akquise' && ticket.customerId) {
|
||||
const outcome = worksheetData.newStatus
|
||||
const next = outcome === 'Zugesagt' ? 'customer' : outcome === 'Abgesagt' ? 'lost' : null
|
||||
if (next) {
|
||||
try {
|
||||
await databases.updateDocument(DATABASE_ID, COLLECTIONS.CUSTOMERS, ticket.customerId, {
|
||||
customerStatus: next,
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
} catch { /* nicht kritisch */ }
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user