feat: Komplett-Redesign + Ausbau zur Plattform
- Ruhiges Dark-Designsystem (Tokens, Karten, Badges/Pills, KPIs) in global.css, animierter PixelBlast-Hintergrund entfernt (kleineres Bundle) - Aufgeraeumter App-Rahmen: feste Sidebar mit sichtbaren Labels, gruppierter Navigation, aktivem Zustand und Collapse - Wiederverwendbare UI-Bausteine unter src/components/ui/ - Ticketliste als scanbare Karten (TicketCard) statt rowSpan-Tabelle - Neue Bereiche: Kunden (CRM-360), Finanzen-Cockpit, Dokumente-Browser - Dashboard als echtes Cockpit (Ticket-/Finanz-/Dokument-Kennzahlen) - Alle Seiten auf einheitlichen PageHeader/Container/Tokens umgestellt Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
8f6927fbb0
commit
d20a4de117
28 files changed
+1639
-840
No files matched your search
@@ -10,13 +10,13 @@ const card = {
|
||||
border: '1px solid rgba(16, 185, 129, 0.2)',
|
||||
}
|
||||
|
||||
export default function DocumentsPanel({ ticket }) {
|
||||
export default function DocumentsPanel({ ticket, initialScope = 'ticket' }) {
|
||||
const [docs, setDocs] = useState([])
|
||||
const [count, setCount] = useState(0)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
const [query, setQuery] = useState('')
|
||||
const [scope, setScope] = useState('ticket') // 'ticket' | 'customer'
|
||||
const [scope, setScope] = useState(initialScope) // 'ticket' | 'customer'
|
||||
const [showUpload, setShowUpload] = useState(false)
|
||||
const [busyId, setBusyId] = useState(null)
|
||||
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="footer">
|
||||
<p>
|
||||
WOMS 2.0 - Work Order Management System |
|
||||
Powered by <a href="https://appwrite.io" target="_blank" rel="noopener noreferrer">Appwrite</a> & React
|
||||
</p>
|
||||
<p className="text-small text-grey">
|
||||
© {new Date().getFullYear()} - All rights reserved
|
||||
<p className="faint" style={{ fontSize: 13, margin: 0 }}>
|
||||
Webklar Ticket-Plattform · © {new Date().getFullYear()}
|
||||
</p>
|
||||
</footer>
|
||||
)
|
||||
|
||||
+88
-140
@@ -1,174 +1,122 @@
|
||||
import { useState } from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { motion } from 'motion/react'
|
||||
import {
|
||||
IconTicket,
|
||||
IconDeviceDesktop,
|
||||
IconChartBar,
|
||||
IconCalendar,
|
||||
IconFolders,
|
||||
IconFileText,
|
||||
IconBook,
|
||||
import {
|
||||
IconTicket,
|
||||
IconUsers,
|
||||
IconReportMoney,
|
||||
IconFiles,
|
||||
IconFolders,
|
||||
IconCalendar,
|
||||
IconLayoutDashboard,
|
||||
IconReport,
|
||||
IconDeviceDesktop,
|
||||
IconSettings,
|
||||
IconLogout
|
||||
IconBook,
|
||||
IconLogout,
|
||||
IconLayoutSidebarLeftCollapse,
|
||||
IconLayoutSidebarLeftExpand,
|
||||
} from '@tabler/icons-react'
|
||||
import { Sidebar, SidebarBody, SidebarLink } from './ModernSidebar'
|
||||
|
||||
export default function Navbar() {
|
||||
export default function Sidebar() {
|
||||
const { user, logout, isAdmin } = useAuth()
|
||||
const navigate = useNavigate()
|
||||
const [open, setOpen] = useState(false)
|
||||
const location = useLocation()
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout()
|
||||
navigate('/login')
|
||||
}
|
||||
|
||||
const links = [
|
||||
const groups = [
|
||||
{
|
||||
label: 'Tickets',
|
||||
href: '/tickets',
|
||||
icon: <IconTicket className="icon" />,
|
||||
label: 'Betrieb',
|
||||
links: [
|
||||
{ label: 'Tickets', href: '/tickets', icon: <IconTicket /> },
|
||||
{ label: 'Planboard', href: '/planboard', icon: <IconCalendar /> },
|
||||
{ label: 'Projekte', href: '/projects', icon: <IconFolders /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Assets',
|
||||
href: '/assets',
|
||||
icon: <IconDeviceDesktop className="icon" />,
|
||||
label: 'CRM',
|
||||
links: [{ label: 'Kunden', href: '/customers', icon: <IconUsers /> }],
|
||||
},
|
||||
{
|
||||
label: 'Dashboard',
|
||||
href: '/dashboard',
|
||||
icon: <IconChartBar className="icon" />,
|
||||
label: 'Finanzen',
|
||||
links: [{ label: 'Finanzen', href: '/finance', icon: <IconReportMoney /> }],
|
||||
},
|
||||
{
|
||||
label: 'Planboard',
|
||||
href: '/planboard',
|
||||
icon: <IconCalendar className="icon" />,
|
||||
label: 'Dokumente',
|
||||
links: [{ label: 'Dokumente', href: '/documents', icon: <IconFiles /> }],
|
||||
},
|
||||
{
|
||||
label: 'Projects',
|
||||
href: '/projects',
|
||||
icon: <IconFolders className="icon" />,
|
||||
label: 'Auswertung',
|
||||
links: [
|
||||
{ label: 'Dashboard', href: '/dashboard', icon: <IconLayoutDashboard /> },
|
||||
{ label: 'Reports', href: '/reports', icon: <IconReport /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Reports',
|
||||
href: '/reports',
|
||||
icon: <IconFileText className="icon" />,
|
||||
},
|
||||
{
|
||||
label: 'Docs',
|
||||
href: '/docs',
|
||||
icon: <IconBook className="icon" />,
|
||||
label: 'System',
|
||||
links: [
|
||||
{ label: 'Assets', href: '/assets', icon: <IconDeviceDesktop /> },
|
||||
...(isAdmin ? [{ label: 'Admin', href: '/admin', icon: <IconSettings /> }] : []),
|
||||
{ label: 'Docs', href: '/docs', icon: <IconBook /> },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
// Admin link nur wenn isAdmin
|
||||
if (isAdmin) {
|
||||
links.push({
|
||||
label: 'Admin',
|
||||
href: '/admin',
|
||||
icon: <IconSettings className="icon" />,
|
||||
})
|
||||
}
|
||||
const isActive = (href) =>
|
||||
location.pathname === href || location.pathname.startsWith(href + '/')
|
||||
|
||||
return (
|
||||
<Sidebar open={open} setOpen={setOpen}>
|
||||
<SidebarBody className="justify-between gap-10">
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflowX: 'hidden', overflowY: 'auto' }}>
|
||||
<Logo />
|
||||
<div style={{ marginTop: '2rem', display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||
{links.map((link, idx) => (
|
||||
<SidebarLink key={idx} link={link} />
|
||||
<aside className={`side ${collapsed ? 'collapsed' : ''}`}>
|
||||
<div className="side-head">
|
||||
<Link to="/" className="side-logo" title="Webklar">W</Link>
|
||||
<div className="side-brand">
|
||||
Webklar
|
||||
<small>Ticket-Plattform</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="side-nav">
|
||||
{groups.map((group) => (
|
||||
<div className="side-group" key={group.label}>
|
||||
<div className="side-group-label">{group.label}</div>
|
||||
{group.links.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
to={link.href}
|
||||
className={`side-link ${isActive(link.href) ? 'active' : ''}`}
|
||||
title={link.label}
|
||||
>
|
||||
{link.icon}
|
||||
<span className="side-link-label">{link.label}</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{user ? (
|
||||
<>
|
||||
<SidebarLink
|
||||
link={{
|
||||
label: user.name || user.email,
|
||||
href: '#',
|
||||
icon: (
|
||||
<div style={{
|
||||
height: '28px',
|
||||
width: '28px',
|
||||
borderRadius: '50%',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'white',
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold',
|
||||
flexShrink: 0
|
||||
}}>
|
||||
{(user.name || user.email).charAt(0).toUpperCase()}
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<div onClick={handleLogout} style={{ cursor: 'pointer', marginTop: '0.5rem' }}>
|
||||
<SidebarLink
|
||||
link={{
|
||||
label: 'Logout',
|
||||
href: '#',
|
||||
icon: <IconLogout className="icon" />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<SidebarLink
|
||||
link={{
|
||||
label: 'Login',
|
||||
href: '/login',
|
||||
icon: <IconLogout className="icon" />,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</SidebarBody>
|
||||
</Sidebar>
|
||||
)
|
||||
}
|
||||
))}
|
||||
</nav>
|
||||
|
||||
const Logo = () => {
|
||||
return (
|
||||
<Link
|
||||
to="/"
|
||||
style={{
|
||||
position: 'relative',
|
||||
zIndex: 20,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
padding: '0.25rem 0',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 'normal',
|
||||
color: 'var(--text-color, #333)',
|
||||
textDecoration: 'none'
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
height: '20px',
|
||||
width: '24px',
|
||||
flexShrink: 0,
|
||||
borderRadius: '4px 2px 4px 2px',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
|
||||
}} />
|
||||
<motion.span
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
whiteSpace: 'pre',
|
||||
color: 'var(--text-color, #333)'
|
||||
}}
|
||||
>
|
||||
Webklar
|
||||
</motion.span>
|
||||
</Link>
|
||||
<div className="side-foot">
|
||||
{user && (
|
||||
<div className="side-link" title={user.name || user.email} style={{ cursor: 'default' }}>
|
||||
<div className="side-logo" style={{ width: 28, height: 28, fontSize: 12, background: 'linear-gradient(135deg,#667eea,#764ba2)' }}>
|
||||
{(user.name || user.email || '?').charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<span className="side-link-label text-ellipsis">{user.name || user.email}</span>
|
||||
</div>
|
||||
)}
|
||||
<button className="side-toggle" onClick={handleLogout} title="Logout">
|
||||
<IconLogout size={20} />
|
||||
<span className="side-link-label">Logout</span>
|
||||
</button>
|
||||
<button className="side-toggle" onClick={() => setCollapsed((c) => !c)} title="Menue ein-/ausklappen">
|
||||
{collapsed ? <IconLayoutSidebarLeftExpand size={20} /> : <IconLayoutSidebarLeftCollapse size={20} />}
|
||||
<span className="side-link-label">Einklappen</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import { useState } from 'react'
|
||||
import { FaChevronDown, FaPlay, FaStop, FaPlus, FaClockRotateLeft, FaPen } from 'react-icons/fa6'
|
||||
import { formatDistanceToNow, format } from 'date-fns'
|
||||
import { de } from 'date-fns/locale'
|
||||
import StatusDropdown from './StatusDropdown'
|
||||
import PriorityDropdown from './PriorityDropdown'
|
||||
import EditorDropdown from './EditorDropdown'
|
||||
import ResponseDropdown from './ResponseDropdown'
|
||||
import CreateWorksheetModal from './CreateWorksheetModal'
|
||||
import StatusHistoryModal from './StatusHistoryModal'
|
||||
import WorksheetList from './WorksheetList'
|
||||
import WorksheetStats from './WorksheetStats'
|
||||
import TicketAssignedProjectsPanel from './TicketAssignedProjectsPanel'
|
||||
import TicketProjectsModal from './TicketProjectsModal'
|
||||
import InvoicePanel from './InvoicePanel'
|
||||
import DocumentsPanel from './DocumentsPanel'
|
||||
import Tabs from './ui/Tabs'
|
||||
import Badge from './ui/Badge'
|
||||
import { StatusPill, PriorityPill } from './ui/StatusPill'
|
||||
import { useWorksheets } from '../hooks/useWorksheets'
|
||||
|
||||
const TABS = [
|
||||
{ id: 'details', label: 'Details' },
|
||||
{ id: 'worksheets', label: 'Arbeitsblaetter' },
|
||||
{ id: 'invoices', label: 'Rechnungen' },
|
||||
{ id: 'documents', label: 'Dokumente' },
|
||||
{ id: 'project', label: 'Projekt / Preview' },
|
||||
]
|
||||
|
||||
export default function TicketCard({ ticket, onUpdate }) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [activeTab, setActiveTab] = useState('details')
|
||||
const [showCreateWorksheet, setShowCreateWorksheet] = useState(false)
|
||||
const [showHistoryModal, setShowHistoryModal] = useState(false)
|
||||
const [showProjectsModal, setShowProjectsModal] = useState(false)
|
||||
const [projectsRefreshKey, setProjectsRefreshKey] = useState(0)
|
||||
|
||||
const { worksheets, loading: worksheetsLoading, createWorksheet, getTotalTime } = useWorksheets(
|
||||
expanded ? ticket.woid : null
|
||||
)
|
||||
|
||||
const createdAt = new Date(ticket.$createdAt || ticket.createdAt)
|
||||
const elapsed = formatDistanceToNow(createdAt, { locale: de, addSuffix: true })
|
||||
|
||||
const stop = (e) => e.stopPropagation()
|
||||
|
||||
const handleCreateWorksheet = async (worksheetData, currentUser) => {
|
||||
const result = await createWorksheet(worksheetData, currentUser)
|
||||
if (result.success && worksheetData.newStatus !== ticket.status) {
|
||||
await onUpdate(ticket.$id, {
|
||||
status: worksheetData.newStatus,
|
||||
responseLevel: worksheetData.newResponseLevel || ticket.responseLevel,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`tcard ${expanded ? 'open' : ''}`}>
|
||||
<div className="tcard-main" onClick={() => setExpanded((e) => !e)}>
|
||||
<div className="tcard-woid">
|
||||
<span className="id">{ticket.woid || ticket.$id?.slice(-5)}</span>
|
||||
<span className="age">{elapsed}</span>
|
||||
</div>
|
||||
|
||||
<div className="tcard-body">
|
||||
<div className="tcard-row1">
|
||||
<span className="tcard-customer">{ticket.customerName || 'Unbekannt'}</span>
|
||||
{ticket.customerLocation && <span className="tcard-loc">· {ticket.customerLocation}</span>}
|
||||
</div>
|
||||
<div className="tcard-topic">{ticket.topic || ticket.title || 'Kein Betreff'}</div>
|
||||
<div className="tcard-badges">
|
||||
<StatusPill status={ticket.status} />
|
||||
<PriorityPill priority={ticket.priority} />
|
||||
{ticket.type && <Badge tone="muted">{ticket.type}</Badge>}
|
||||
{ticket.systemType && ticket.systemType !== 'n/a' && <Badge tone="muted">{ticket.systemType}</Badge>}
|
||||
{ticket.serviceType && <Badge tone="info">{ticket.serviceType}</Badge>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="tcard-side" onClick={stop}>
|
||||
<div style={{ minWidth: 120 }}>
|
||||
<StatusDropdown value={ticket.status} onChange={(v) => onUpdate(ticket.$id, { status: v })} />
|
||||
</div>
|
||||
<div style={{ minWidth: 110 }}>
|
||||
<PriorityDropdown value={ticket.priority} onChange={(v) => onUpdate(ticket.$id, { priority: v })} />
|
||||
</div>
|
||||
<FaChevronDown className={`chevron ${expanded ? 'up' : ''}`} onClick={() => setExpanded((e) => !e)} style={{ cursor: 'pointer' }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{expanded && (
|
||||
<div className="tcard-detail">
|
||||
<div className="flex gap-3 wrap" style={{ marginBottom: 16, fontSize: 13 }}>
|
||||
<span className="muted"><FaPlay style={{ color: 'var(--ok)' }} /> {ticket.startDate || format(createdAt, 'dd.MM.yyyy')}</span>
|
||||
<span className="muted"><FaStop style={{ color: 'var(--danger)' }} /> {ticket.deadline || '-'}</span>
|
||||
{ticket.requestedBy && <span className="muted">Angefragt von: {ticket.requestedBy}</span>}
|
||||
<span className="grow" />
|
||||
<div style={{ minWidth: 150 }}><EditorDropdown value={ticket.assignedTo} onChange={(v) => onUpdate(ticket.$id, { assignedTo: v })} /></div>
|
||||
<div style={{ minWidth: 150 }}><ResponseDropdown value={ticket.responseLevel} onChange={(v) => onUpdate(ticket.$id, { responseLevel: v })} /></div>
|
||||
</div>
|
||||
|
||||
<Tabs tabs={TABS} active={activeTab} onChange={setActiveTab} />
|
||||
|
||||
<div className="ticket-tab-content">
|
||||
{activeTab === 'details' && (
|
||||
<div className="ui-card pad">
|
||||
<h5 style={{ fontWeight: 700, marginBottom: 12 }}>Ticket-Beschreibung</h5>
|
||||
<p style={{ whiteSpace: 'pre-wrap', color: 'var(--text-muted)', lineHeight: 1.7, margin: 0 }}>
|
||||
{ticket.details || 'Keine Details vorhanden.'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'worksheets' && (
|
||||
<div>
|
||||
<div className="flex gap-2" style={{ marginBottom: 16 }}>
|
||||
<button className="ui-btn ui-btn-primary grow" onClick={() => setShowCreateWorksheet(true)}>
|
||||
<FaPlus /> Arbeitsblatt hinzufuegen
|
||||
</button>
|
||||
<button className="ui-btn" onClick={() => setShowProjectsModal(true)} title="Projekte"><FaPen /></button>
|
||||
<button className="ui-btn" onClick={() => setShowHistoryModal(true)} title="Verlauf"><FaClockRotateLeft /></button>
|
||||
</div>
|
||||
{worksheets.length > 0 && (
|
||||
<div className="ui-card pad" style={{ marginBottom: 16 }}>
|
||||
<WorksheetStats worksheets={worksheets} />
|
||||
</div>
|
||||
)}
|
||||
<WorksheetList worksheets={worksheets} totalTime={getTotalTime()} loading={worksheetsLoading} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'invoices' && <InvoicePanel ticket={ticket} />}
|
||||
{activeTab === 'documents' && <DocumentsPanel ticket={ticket} />}
|
||||
{activeTab === 'project' && (
|
||||
<div>
|
||||
<button className="ui-btn ui-btn-primary" style={{ marginBottom: 12 }} onClick={() => setShowProjectsModal(true)}>
|
||||
<FaPen /> Projekte zuweisen / bearbeiten
|
||||
</button>
|
||||
<TicketAssignedProjectsPanel ticket={ticket} refreshKey={projectsRefreshKey} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CreateWorksheetModal isOpen={showCreateWorksheet} onClose={() => setShowCreateWorksheet(false)} workorder={ticket} onCreate={handleCreateWorksheet} />
|
||||
<StatusHistoryModal isOpen={showHistoryModal} onClose={() => setShowHistoryModal(false)} worksheets={worksheets} ticket={ticket} />
|
||||
<TicketProjectsModal isOpen={showProjectsModal} onClose={() => setShowProjectsModal(false)} ticket={ticket} onUpdated={() => setProjectsRefreshKey((k) => k + 1)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export default function Badge({ tone = 'muted', dot = false, children, className = '', style }) {
|
||||
return (
|
||||
<span className={`badge badge-${tone} ${className}`} style={style}>
|
||||
{dot && <span className="dot" />}
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
const VARIANTS = {
|
||||
default: 'ui-btn',
|
||||
primary: 'ui-btn ui-btn-primary',
|
||||
ghost: 'ui-btn ui-btn-ghost',
|
||||
danger: 'ui-btn ui-btn-danger',
|
||||
}
|
||||
|
||||
export default function Button({
|
||||
variant = 'default',
|
||||
size,
|
||||
as = 'button',
|
||||
className = '',
|
||||
children,
|
||||
...props
|
||||
}) {
|
||||
const cls = `${VARIANTS[variant] || VARIANTS.default}${size === 'sm' ? ' ui-btn-sm' : ''} ${className}`
|
||||
const Comp = as
|
||||
return (
|
||||
<Comp className={cls} {...props}>
|
||||
{children}
|
||||
</Comp>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export default function Card({ title, actions, children, pad = true, className = '', bodyStyle, style }) {
|
||||
if (!title && !actions) {
|
||||
return (
|
||||
<div className={`ui-card ${pad ? 'pad' : ''} ${className}`} style={style}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className={`ui-card ${className}`} style={style}>
|
||||
<div className="ui-card-head">
|
||||
<h3>{title}</h3>
|
||||
{actions && <div className="flex gap-2 items-center">{actions}</div>}
|
||||
</div>
|
||||
<div className="ui-card-body" style={bodyStyle}>{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export default function EmptyState({ icon, title, hint, action }) {
|
||||
return (
|
||||
<div className="empty">
|
||||
{icon && <div className="empty-icon">{icon}</div>}
|
||||
<div style={{ fontWeight: 700, marginBottom: 4 }}>{title}</div>
|
||||
{hint && <div className="faint" style={{ fontSize: 13 }}>{hint}</div>}
|
||||
{action && <div style={{ marginTop: 16 }}>{action}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export default function Field({ label, children, hint }) {
|
||||
return (
|
||||
<div className="field">
|
||||
{label && <label>{label}</label>}
|
||||
{children}
|
||||
{hint && <div className="faint" style={{ fontSize: 12 }}>{hint}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export default function Kpi({ label, value, sub, icon, accent = 'var(--accent)' }) {
|
||||
return (
|
||||
<div className="kpi">
|
||||
<span className="kpi-accent" style={{ background: accent }} />
|
||||
<div className="kpi-label">{icon}{label}</div>
|
||||
<div className="kpi-value">{value}</div>
|
||||
{sub && <div className="kpi-sub">{sub}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export default function PageHeader({ title, subtitle, actions, children }) {
|
||||
return (
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<h1 className="page-title">{title}</h1>
|
||||
{subtitle && <div className="page-subtitle">{subtitle}</div>}
|
||||
{children}
|
||||
</div>
|
||||
{actions && <div className="page-actions">{actions}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
const STATUS_CLASS = {
|
||||
Open: 'status-open',
|
||||
Occupied: 'status-occupied',
|
||||
Assigned: 'status-assigned',
|
||||
Awaiting: 'status-awaiting',
|
||||
'Added Info': 'status-awaiting',
|
||||
Closed: 'status-closed',
|
||||
}
|
||||
|
||||
const PRIORITY = {
|
||||
0: { cls: 'priority-none', label: 'Keine' },
|
||||
1: { cls: 'priority-low', label: 'Niedrig' },
|
||||
2: { cls: 'priority-medium', label: 'Mittel' },
|
||||
3: { cls: 'priority-high', label: 'Hoch' },
|
||||
4: { cls: 'priority-critical', label: 'Kritisch' },
|
||||
}
|
||||
|
||||
const pillBase = {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
padding: '3px 10px',
|
||||
borderRadius: 999,
|
||||
fontSize: 12,
|
||||
fontWeight: 700,
|
||||
}
|
||||
|
||||
export function StatusPill({ status }) {
|
||||
const cls = STATUS_CLASS[status] || 'status-open'
|
||||
return <span className={cls} style={pillBase}>{status || 'Open'}</span>
|
||||
}
|
||||
|
||||
export function PriorityPill({ priority }) {
|
||||
const p = PRIORITY[priority] ?? PRIORITY[1]
|
||||
return <span className={p.cls} style={pillBase}>{p.label}</span>
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export default function Tabs({ tabs, active, onChange }) {
|
||||
return (
|
||||
<div className="ticket-tabs">
|
||||
{tabs.map((t) => (
|
||||
<button
|
||||
key={t.id}
|
||||
type="button"
|
||||
className={`ticket-tab ${active === t.id ? 'ticket-tab-active' : ''}`}
|
||||
onClick={() => onChange(t.id)}
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export { default as PageHeader } from './PageHeader'
|
||||
export { default as Card } from './Card'
|
||||
export { default as Button } from './Button'
|
||||
export { default as Badge } from './Badge'
|
||||
export { StatusPill, PriorityPill } from './StatusPill'
|
||||
export { default as Tabs } from './Tabs'
|
||||
export { default as EmptyState } from './EmptyState'
|
||||
export { default as Field } from './Field'
|
||||
export { default as Kpi } from './Kpi'
|
||||
Reference in new issue
Block a user