Kundenseite: Reiter in der Adresse, Uebersicht mit Umlaut, Protokoll der Kundenansicht
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8c7a3fcced
commit
370724a9a7
6 files changed
+120
-7
No files matched your search
@@ -0,0 +1,56 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { FaSpinner, FaRotateRight } from 'react-icons/fa6'
|
||||
import { Card, Button, EmptyState } from '../ui'
|
||||
import { getCustomerViewLog } from '../../lib/teamApi'
|
||||
import { formatViewLogTime, viewLogLabel } from '../../lib/viewLog'
|
||||
|
||||
// Reiter „Protokoll“: wer wann die Kundenansicht dieses Kunden geöffnet oder darin etwas geändert hat
|
||||
export default function CustomerViewLog({ customerId }) {
|
||||
const [entries, setEntries] = useState(null)
|
||||
const [error, setError] = useState(null)
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setError(null)
|
||||
setEntries(null)
|
||||
try {
|
||||
const { entries: list } = await getCustomerViewLog(customerId)
|
||||
setEntries(list || [])
|
||||
} catch (err) {
|
||||
setError(err.message || 'Protokoll konnte nicht geladen werden.')
|
||||
}
|
||||
}, [customerId])
|
||||
|
||||
useEffect(() => { load() }, [load])
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card>
|
||||
<div className="flex items-center gap-2 wrap">
|
||||
<span className="muted">{error}</span>
|
||||
<Button variant="ghost" onClick={load}><FaRotateRight /> Erneut versuchen</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
if (!entries) return <Card><FaSpinner className="spinner" /></Card>
|
||||
if (entries.length === 0) {
|
||||
return <Card><EmptyState title="Noch keine Einträge" hint="Hier steht, wer die Kundenansicht geöffnet oder darin etwas geändert hat." /></Card>
|
||||
}
|
||||
|
||||
return (
|
||||
<Card pad={false}>
|
||||
<div className="list">
|
||||
{entries.map((e) => (
|
||||
<div key={e.id} className="list-row">
|
||||
<span className="mono muted" style={{ minWidth: 130 }}>{formatViewLogTime(e.at)}</span>
|
||||
<span style={{ minWidth: 120, fontWeight: 600 }}>{e.staffName || '–'}</span>
|
||||
<div className="grow">
|
||||
<div>{viewLogLabel(e.action)}</div>
|
||||
{e.detail && <div className="muted">{e.detail}</div>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -2,10 +2,12 @@
|
||||
// die nur Admins freigibt: für alle anderen gibt es den Reiter nicht.
|
||||
|
||||
const BASE_TABS = [
|
||||
{ id: 'overview', label: 'Uebersicht' },
|
||||
{ id: 'overview', label: 'Übersicht' },
|
||||
{ id: 'tickets', label: 'Tickets' },
|
||||
{ id: 'invoices', label: 'Rechnungen', adminOnly: true },
|
||||
{ id: 'projects', label: 'Projekte' },
|
||||
// Protokoll der Kundenansicht (wer wann das Portal des Kunden geöffnet oder darin etwas geändert hat): alle Mitarbeiter
|
||||
{ id: 'protokoll', label: 'Protokoll' },
|
||||
]
|
||||
|
||||
/** Reiter für Tabs: [{ id, label }]; „Rechnungen“ nur für Admins. */
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// Protokoll der Kundenansicht (Portal-Collection customerViewLog): Beschriftung und Zeit. Rein, unter Node testbar.
|
||||
|
||||
const LABELS = {
|
||||
geoeffnet: 'Kundenansicht geöffnet',
|
||||
beendet: 'Kundenansicht beendet',
|
||||
abgelaufen: 'Kundenansicht abgelaufen',
|
||||
news_angelegt: 'News angelegt',
|
||||
news_bild: 'News-Foto hochgeladen',
|
||||
news_geaendert: 'News geändert',
|
||||
news_geloescht: 'News gelöscht',
|
||||
}
|
||||
|
||||
/** Deutsche Beschriftung einer Aktion; unbekannte Aktionen erscheinen roh. */
|
||||
export const viewLogLabel = (action) => LABELS[action] || action
|
||||
|
||||
/** ISO-Zeit -> "dd.mm.yyyy hh:mm" in deutscher Zeit; leer bleibt leer. */
|
||||
export function formatViewLogTime(iso) {
|
||||
if (!iso) return ''
|
||||
const parts = Object.fromEntries(
|
||||
new Intl.DateTimeFormat('de-DE', {
|
||||
timeZone: 'Europe/Berlin', day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', hourCycle: 'h23',
|
||||
}).formatToParts(new Date(iso)).map((p) => [p.type, p.value])
|
||||
)
|
||||
return `${parts.day}.${parts.month}.${parts.year} ${parts.hour}:${parts.minute}`
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useParams, Link, useNavigate } from 'react-router-dom'
|
||||
import { useParams, Link, useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { FaSpinner, FaStar, FaArrowLeft, FaFloppyDisk, FaArrowUp, FaKey, FaArrowUpRightFromSquare, FaBoxArchive, FaBoxOpen, FaTrash } from 'react-icons/fa6'
|
||||
import { databases, DATABASE_ID, COLLECTIONS, Query } from '../lib/appwrite'
|
||||
import { PageHeader, Card, Button, Badge, Tabs, EmptyState, Field } from '../components/ui'
|
||||
@@ -12,6 +12,7 @@ import { useAuth } from '../context/AuthContext'
|
||||
import { useWebsiteProjects } from '../hooks/useWebsiteProjects'
|
||||
import { customerTabs, resolveCustomerTab } from '../lib/customerTabs'
|
||||
import { customerStage, isArchived, isInvoiceReady, setCustomerArchived, upgradeToCustomer } from '../lib/leads'
|
||||
import CustomerViewLog from '../components/customer/CustomerViewLog'
|
||||
|
||||
const PORTAL_URL = 'https://project.webklar.com'
|
||||
|
||||
@@ -20,7 +21,13 @@ export default function CustomerDetailPage() {
|
||||
const [customer, setCustomer] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(null)
|
||||
const [tab, setTab] = useState('overview')
|
||||
// Reiter steht in der Adresse (/customers/<id>?tab=protokoll), damit Sammellisten und Team-Mails direkt hineinspringen
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const tab = searchParams.get('tab') || 'overview'
|
||||
const setTab = useCallback(
|
||||
(next) => setSearchParams(next === 'overview' ? {} : { tab: next }, { replace: true }),
|
||||
[setSearchParams]
|
||||
)
|
||||
const { isAdmin } = useAuth()
|
||||
const navigate = useNavigate()
|
||||
|
||||
@@ -93,6 +100,7 @@ export default function CustomerDetailPage() {
|
||||
{activeTab === 'tickets' && <CustomerTickets customerId={customer.$id} />}
|
||||
{activeTab === 'invoices' && <CustomerInvoices customer={customer} onShowOverview={() => setTab('overview')} />}
|
||||
{activeTab === 'projects' && <CustomerProjects customerId={customer.$id} />}
|
||||
{activeTab === 'protokoll' && <CustomerViewLog customerId={customer.$id} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user