wsid update

This commit is contained in:
2025-12-30 20:29:59 +01:00
parent 5717612db5
commit 895c55399f
8 changed files with 1212 additions and 547 deletions

View File

@@ -1,4 +1,5 @@
import { useState, useEffect } from 'react'
import { FaTimes } from 'react-icons/fa'
import { useAuth } from '../context/AuthContext'
const SERVICE_TYPES = ['Remote', 'On Site', 'Off Site', 'COMMENT']
@@ -157,77 +158,26 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
if (!isOpen || !workorder) return null
return (
<div className="overlay" style={{
width: '100%',
background: 'rgba(0,0,0,0.95)'
}}>
<a href="#" className="closebtn" onClick={(e) => { e.preventDefault(); onClose(); }} style={{
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
borderRadius: '50%',
width: '60px',
height: '60px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '2rem',
transition: 'transform 0.2s ease'
}} onMouseEnter={(e) => e.currentTarget.style.transform = 'rotate(90deg)'} onMouseLeave={(e) => e.currentTarget.style.transform = 'rotate(0deg)'}>×</a>
<div className="overlay-content text-white text-left">
<form onSubmit={handleSubmit}>
<div className="container">
<div className="row">
<div className="col-1">&nbsp;</div>
<div className="col-10">
<div className="mb-4 p-4 rounded-3" style={{
background: 'linear-gradient(135deg, #2d3748 0%, #1a202c 100%)',
boxShadow: '0 8px 32px rgba(45, 55, 72, 0.3)'
}}>
<h2 className="mb-0 d-flex align-items-center">
<span className="me-3" style={{
background: 'rgba(16, 185, 129, 0.4)',
borderRadius: '10px',
padding: '10px 15px'
}}>📝</span>
Create New Worksheet
<span className="ms-3 badge" style={{
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
fontSize: '1rem'
}}>WOID {workorder.woid}</span>
</h2>
</div>
</div>
<div className="col-1">&nbsp;</div>
</div>
<div className="overlay">
<span className="overlay-close" onClick={onClose}>
<FaTimes />
</span>
<div className="overlay-content">
<h2 className="mb-2">Create New Worksheet - WOID {workorder.woid}</h2>
{error && (
<div className="bg-red text-white p-2 mb-2" style={{ borderRadius: '4px' }}>
{error}
</div>
{error && (
<div className="container">
<div className="row">
<div className="col-1">&nbsp;</div>
<div className="col-10">
<div className="alert p-4 rounded-3 border-0" style={{
background: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
color: 'white',
boxShadow: '0 4px 16px rgba(239, 68, 68, 0.3)'
}} role="alert">
<strong> {error}</strong>
</div>
</div>
<div className="col-1">&nbsp;</div>
</div>
</div>
)}
<div className="container">
<div className="row">
<div className="col-1">&nbsp;</div>
{/* Linke Spalte */}
<div className="col-5">
<span className="text-left">Service Type</span><br />
<select
className="form-select bg-dark text-white"
)}
<form onSubmit={handleSubmit}>
<div className="row">
<div className="col col-6">
<div className="form-group">
<label className="form-label">Service Type</label>
<select
className="form-control"
value={formData.serviceType}
onChange={(e) => handleChange('serviceType', e.target.value)}
required
@@ -236,11 +186,12 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
<option key={type} value={type}>{type}</option>
))}
</select>
<br /><br />
</div>
<span className="text-left">New Status</span><br />
<select
className="form-select bg-dark text-white"
<div className="form-group">
<label className="form-label">New Status</label>
<select
className="form-control"
value={formData.newStatus}
onChange={(e) => handleChange('newStatus', e.target.value)}
required
@@ -249,42 +200,40 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
<option key={status} value={status}>{status}</option>
))}
</select>
<br /><br />
</div>
<span className="text-left">New Response Level</span><br />
<select
className="form-select bg-dark text-white"
<div className="form-group">
<label className="form-label">New Response Level</label>
<select
className="form-control"
value={formData.newResponseLevel}
onChange={(e) => handleChange('newResponseLevel', e.target.value)}
>
<option value="">Select</option>
<option value="">Select Response Level</option>
{RESPONSE_LEVELS.map(level => (
<option key={level} value={level}>{level}</option>
))}
</select>
<br /><br />
</div>
<div className="form-check">
<input
type="checkbox"
className="form-check-input"
id="isComment"
<div className="form-group">
<label style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<input
type="checkbox"
checked={formData.isComment}
onChange={(e) => handleChange('isComment', e.target.checked)}
/>
<label className="form-check-label" htmlFor="isComment">
Nur Kommentar (keine Arbeitszeit)
</label>
</div>
<br />
Nur Kommentar (keine Arbeitszeit)
</label>
</div>
</div>
{/* Rechte Spalte */}
<div className="col-5">
<span className="text-left">Total Time (Minuten)</span><br />
<input
<div className="col col-6">
<div className="form-group">
<label className="form-label">Total Time (Minuten)</label>
<input
type="number"
className="form-control bg-dark text-white"
className="form-control"
min="0"
step="15"
value={formData.totalTime}
@@ -292,145 +241,83 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
disabled={formData.isComment}
placeholder="0"
/>
<small className="text-muted">
<small style={{ color: '#a0aec0', fontSize: '12px' }}>
{autoCalculate && formData.startTime && formData.endTime
? '✓ Automatisch berechnet'
: 'Manuell eingeben'}
</small>
<br /><br />
</div>
<span className="text-left">Start Date (dd.mm.yyyy)</span><br />
<input
<div className="form-group">
<label className="form-label">Start Date</label>
<input
type="text"
className="form-control bg-dark text-white"
className="form-control"
placeholder="dd.mm.yyyy"
value={formData.startDate}
onChange={(e) => handleChange('startDate', e.target.value)}
pattern="^[0-3][0-9]\.[0-1][0-9]\.[1-2][0-9][0-9][0-9]$"
required
/>
<br /><br />
</div>
<span className="text-left">End Date (dd.mm.yyyy)</span><br />
<input
<div className="form-group">
<label className="form-label">End Date</label>
<input
type="text"
className="form-control bg-dark text-white"
className="form-control"
placeholder="dd.mm.yyyy"
value={formData.endDate}
onChange={(e) => handleChange('endDate', e.target.value)}
pattern="^[0-3][0-9]\.[0-1][0-9]\.[1-2][0-9][0-9][0-9]$"
required
/>
<br /><br />
</div>
<span className="text-left">Start Time (hhmm)</span><br />
<input
<div className="form-group">
<label className="form-label">Start Time</label>
<input
type="text"
className="form-control bg-dark text-white"
className="form-control"
placeholder="hhmm"
value={formData.startTime}
onChange={(e) => handleChange('startTime', e.target.value)}
pattern="[0-2][0-9][0-5][0-9]"
placeholder="1000"
maxLength="4"
/>
<br /><br />
</div>
<span className="text-left">End Time (hhmm)</span><br />
<input
<div className="form-group">
<label className="form-label">End Time</label>
<input
type="text"
className="form-control bg-dark text-white"
className="form-control"
placeholder="hhmm"
value={formData.endTime}
onChange={(e) => handleChange('endTime', e.target.value)}
pattern="[0-2][0-9][0-5][0-9]"
placeholder="1030"
maxLength="4"
/>
<br /><br />
</div>
<div className="col-1">&nbsp;</div>
</div>
</div>
<div className="container">
<div className="row">
<div className="col-1">&nbsp;</div>
<div className="col-10">
<span className="text-left">Action Details</span><br />
<textarea
className="form-control bg-dark text-white"
rows="10"
value={formData.details}
onChange={(e) => handleChange('details', e.target.value)}
placeholder="Beschreibe die durchgeführten Arbeiten..."
required
></textarea>
</div>
<div className="col-1">&nbsp;</div>
</div>
<div className="form-group">
<label className="form-label">Action Details</label>
<textarea
className="form-control"
rows={5}
placeholder="Beschreibe die durchgeführten Arbeiten..."
value={formData.details}
onChange={(e) => handleChange('details', e.target.value)}
required
/>
</div>
<div className="container">
<div className="row">
<div className="col-1">&nbsp;</div>
<div className="col-10 text-center">
<p>&nbsp;</p>
<button
type="submit"
className="btn btn-lg px-5 py-3 border-0"
style={{
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
color: 'white',
fontSize: '1.2rem',
fontWeight: 'bold',
boxShadow: '0 8px 32px rgba(16, 185, 129, 0.4)',
transition: 'transform 0.2s ease, box-shadow 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-2px)'
e.currentTarget.style.boxShadow = '0 12px 40px rgba(16, 185, 129, 0.5)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)'
e.currentTarget.style.boxShadow = '0 8px 32px rgba(16, 185, 129, 0.4)'
}}
disabled={loading}
>
{loading ? '⏳ Erstelle...' : '✨ CREATE NOW'}
</button>
<p>&nbsp;</p>
</div>
<div className="col-1">&nbsp;</div>
</div>
</div>
{/* Info Box */}
<div className="container">
<div className="row">
<div className="col-1">&nbsp;</div>
<div className="col-10">
<div className="p-4 rounded-3 border-0" style={{
background: 'linear-gradient(135deg, #4a5568 0%, #2d3748 100%)',
color: 'white',
boxShadow: '0 4px 16px rgba(74, 85, 104, 0.3)'
}} role="alert">
<strong className="d-block mb-2">📋 Current Work Order</strong>
<div className="d-flex flex-wrap gap-3">
<span className="badge px-3 py-2" style={{
background: 'rgba(16, 185, 129, 0.4)',
fontSize: '0.9rem'
}}>WOID: {workorder.woid}</span>
<span className="badge px-3 py-2" style={{
background: 'rgba(16, 185, 129, 0.4)',
fontSize: '0.9rem'
}}>Status: {workorder.status}</span>
<span className="badge px-3 py-2" style={{
background: 'rgba(16, 185, 129, 0.4)',
fontSize: '0.9rem'
}}>Topic: {workorder.topic}</span>
</div>
</div>
</div>
<div className="col-1">&nbsp;</div>
</div>
<div className="text-center mt-2">
<button
type="submit"
className="btn btn-dark"
disabled={loading}
>
{loading ? 'Creating...' : 'CREATE NOW'}
</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,141 @@
import { FaTimes } from 'react-icons/fa'
import { format } from 'date-fns'
export default function StatusHistoryModal({ isOpen, onClose, worksheets, ticket }) {
if (!isOpen) return null
// Extrahiere Status-Änderungen aus Worksheets
const statusHistory = worksheets
.filter(ws => ws.oldStatus && ws.newStatus && ws.oldStatus !== ws.newStatus)
.map(ws => ({
date: ws.startDate,
time: ws.startTime,
from: ws.oldStatus,
to: ws.newStatus,
employee: ws.employeeName || ws.employeeShort || 'Unknown',
details: ws.details,
wsid: ws.wsid
}))
.sort((a, b) => {
// Sortiere nach Datum und Zeit (älteste zuerst)
const dateA = `${a.date} ${a.time || '0000'}`
const dateB = `${b.date} ${b.time || '0000'}`
return dateA.localeCompare(dateB)
})
// Füge aktuellen Status hinzu
const currentStatus = ticket?.status || 'Open'
return (
<div className="overlay">
<span className="overlay-close" onClick={onClose}>
<FaTimes />
</span>
<div className="overlay-content">
<h2 className="mb-2">Status History - WOID {ticket?.woid || ticket?.$id}</h2>
<div style={{ marginBottom: '24px' }}>
<div style={{
background: 'rgba(45, 55, 72, 0.95)',
borderRadius: '8px',
padding: '16px',
border: '1px solid rgba(16, 185, 129, 0.2)',
marginBottom: '16px'
}}>
<strong style={{ color: 'var(--green-primary)' }}>Current Status:</strong>
<span style={{
marginLeft: '12px',
padding: '4px 12px',
borderRadius: '4px',
background: 'rgba(16, 185, 129, 0.2)',
color: 'var(--dark-text)',
fontWeight: 'bold'
}}>
{currentStatus}
</span>
</div>
</div>
{statusHistory.length === 0 ? (
<div style={{
background: 'rgba(45, 55, 72, 0.95)',
borderRadius: '8px',
padding: '24px',
textAlign: 'center',
color: '#a0aec0'
}}>
No status changes recorded yet.
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{statusHistory.map((entry, index) => (
<div
key={index}
style={{
background: 'rgba(45, 55, 72, 0.95)',
borderRadius: '8px',
padding: '16px',
border: '1px solid rgba(16, 185, 129, 0.2)',
borderLeft: '4px solid var(--green-primary)'
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start', marginBottom: '8px' }}>
<div>
<div style={{ fontSize: '14px', color: '#a0aec0', marginBottom: '4px' }}>
{entry.date} {entry.time ? `${entry.time.substring(0, 2)}:${entry.time.substring(2, 4)}` : ''}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<span style={{
padding: '4px 8px',
borderRadius: '4px',
background: 'rgba(74, 85, 104, 0.5)',
color: 'var(--dark-text)',
fontSize: '12px'
}}>
{entry.from}
</span>
<span style={{ color: 'var(--green-primary)' }}></span>
<span style={{
padding: '4px 8px',
borderRadius: '4px',
background: 'rgba(16, 185, 129, 0.3)',
color: 'var(--dark-text)',
fontSize: '12px',
fontWeight: 'bold'
}}>
{entry.to}
</span>
</div>
</div>
<div style={{ textAlign: 'right' }}>
<div style={{ fontSize: '12px', color: '#a0aec0' }}>by</div>
<div style={{ fontWeight: 'bold', color: 'var(--dark-text)' }}>{entry.employee}</div>
{entry.wsid && (
<div style={{ fontSize: '11px', color: '#718096', marginTop: '4px' }}>
WSID: {entry.wsid}
</div>
)}
</div>
</div>
{entry.details && (
<div style={{
marginTop: '12px',
padding: '12px',
background: 'rgba(31, 41, 55, 0.6)',
borderRadius: '6px',
fontSize: '13px',
color: '#cbd5e0',
borderLeft: '3px solid rgba(16, 185, 129, 0.4)'
}}>
{entry.details}
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
)
}

View File

@@ -1,5 +1,5 @@
import { useState } from 'react'
import { FaLock, FaLockOpen, FaPlay, FaStop, FaTruck, FaSackDollar, FaUserGear, FaPlus } from 'react-icons/fa6'
import { FaLock, FaLockOpen, FaPlay, FaStop, FaTruck, FaSackDollar, FaUserGear, FaPlus, FaClockRotateLeft } from 'react-icons/fa6'
import { formatDistanceToNow, format } from 'date-fns'
import { de } from 'date-fns/locale'
import StatusDropdown from './StatusDropdown'
@@ -7,6 +7,7 @@ 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 { useWorksheets } from '../hooks/useWorksheets'
@@ -46,6 +47,7 @@ export default function TicketRow({ ticket, onUpdate, onExpand }) {
const [expanded, setExpanded] = useState(false)
const [locked, setLocked] = useState(true)
const [showCreateWorksheet, setShowCreateWorksheet] = useState(false)
const [showHistoryModal, setShowHistoryModal] = useState(false)
// Worksheets für dieses Ticket laden (nur wenn expanded)
const {
@@ -185,71 +187,146 @@ export default function TicketRow({ ticket, onUpdate, onExpand }) {
border: '1px solid rgba(16, 185, 129, 0.2)',
borderTop: 'none'
}}>
<div className="card-header d-flex justify-content-between align-items-center" style={{
background: 'linear-gradient(135deg, #2d3748 0%, #1a202c 100%)',
color: 'white',
padding: '1rem 1.5rem',
borderRadius: 0,
borderBottom: '1px solid rgba(16, 185, 129, 0.2)'
}}>
<span className="fs-5 fw-bold">Details - WOID {ticket.woid || ticket.$id}</span>
<button
className="btn btn-sm px-4 py-2 border-0 fw-bold"
style={{
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
color: 'white',
transition: 'all 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-2px)'
e.currentTarget.style.boxShadow = '0 4px 12px rgba(16, 185, 129, 0.4)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)'
e.currentTarget.style.boxShadow = 'none'
}}
onClick={() => setShowCreateWorksheet(true)}
>
<FaPlus className="me-2" /> Add Worksheet
</button>
</div>
<div className="card-body" style={{ borderRadius: '0 0 12px 12px' }}>
<div className="mb-4 p-4 rounded-3 shadow-sm" style={{
background: 'linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%)',
border: '2px solid #10b981'
<div className="card-body" style={{ borderRadius: '0 0 12px 12px', padding: '20px' }}>
{/* Bento Box Layout: 2 Spalten */}
<div style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '20px',
alignItems: 'stretch'
}}>
<h5 className="mb-3" style={{ color: '#1a202c', fontWeight: 'bold' }}>
📋 Ticket-Beschreibung
</h5>
<p style={{
fontSize: '1.1rem',
lineHeight: '1.8',
color: '#1f2937',
whiteSpace: 'pre-wrap',
margin: 0
{/* Linke Spalte: Ticket-Beschreibung (50%) */}
<div style={{
background: 'rgba(45, 55, 72, 0.5)',
borderRadius: '12px',
padding: '20px',
border: '1px solid rgba(16, 185, 129, 0.2)',
display: 'flex',
flexDirection: 'column',
height: '100%'
}}>
{ticket.details || 'Keine Details vorhanden.'}
</p>
<h5 style={{
color: 'var(--dark-text)',
fontWeight: 'bold',
marginBottom: '16px',
fontSize: '18px',
flex: '0 0 auto'
}}>
📋 Ticket-Beschreibung
</h5>
<p style={{
fontSize: '14px',
lineHeight: '1.8',
color: 'rgba(226, 232, 240, 0.8)',
whiteSpace: 'pre-wrap',
margin: 0,
flex: '1 1 auto',
overflowY: 'auto'
}}>
{ticket.details || 'Keine Details vorhanden.'}
</p>
</div>
{/* Rechte Spalte: Statistics, Buttons (50%) */}
<div style={{
display: 'flex',
flexDirection: 'column',
gap: '16px',
height: '100%'
}}>
{/* Button Row: Add Worksheet (100%) + History Icon Button */}
<div style={{
display: 'flex',
gap: '8px',
alignItems: 'stretch'
}}>
{/* Add Worksheet Button - 100% width minus icon button */}
<button
className="btn"
style={{
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
color: 'white',
border: 'none',
padding: '12px 20px',
borderRadius: '8px',
fontWeight: 'bold',
cursor: 'pointer',
transition: 'all 0.2s ease',
flex: 1
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-2px)'
e.currentTarget.style.boxShadow = '0 4px 12px rgba(16, 185, 129, 0.4)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)'
e.currentTarget.style.boxShadow = 'none'
}}
onClick={() => setShowCreateWorksheet(true)}
>
<FaPlus style={{ marginRight: '8px' }} /> Add Worksheet
</button>
{/* History Icon Button - klein, grau, nur Icon */}
<button
style={{
background: '#616161',
color: 'white',
border: 'none',
padding: '12px',
borderRadius: '8px',
cursor: 'pointer',
transition: 'all 0.2s ease',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minWidth: '44px',
width: '44px'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#757575'
e.currentTarget.style.transform = 'translateY(-2px)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#616161'
e.currentTarget.style.transform = 'translateY(0)'
}}
onClick={() => setShowHistoryModal(true)}
title="Status History"
>
<FaClockRotateLeft size={18} />
</button>
</div>
{/* Statistiken */}
{worksheets.length > 0 && (
<div style={{
background: 'rgba(45, 55, 72, 0.5)',
borderRadius: '12px',
padding: '16px',
border: '1px solid rgba(16, 185, 129, 0.2)',
flex: '1 1 auto',
display: 'flex',
flexDirection: 'column',
minHeight: 0
}}>
<WorksheetStats worksheets={worksheets} />
</div>
)}
</div>
</div>
{/* Gesamtarbeitszeit und Worksheet-Liste - 100% Breite unter dem Bento Box */}
<div style={{
marginTop: '20px',
width: '100%'
}}>
<WorksheetList
worksheets={worksheets}
totalTime={getTotalTime()}
loading={worksheetsLoading}
/>
</div>
<hr />
<h5 className="mt-4 mb-3">Worksheets (Arbeitsschritte)</h5>
{/* Statistiken */}
{worksheets.length > 0 && (
<>
<WorksheetStats worksheets={worksheets} />
<hr />
</>
)}
{/* Worksheet-Liste */}
<WorksheetList
worksheets={worksheets}
totalTime={getTotalTime()}
loading={worksheetsLoading}
/>
</div>
</div>
</td>
@@ -263,6 +340,13 @@ export default function TicketRow({ ticket, onUpdate, onExpand }) {
workorder={ticket}
onCreate={handleCreateWorksheet}
/>
<StatusHistoryModal
isOpen={showHistoryModal}
onClose={() => setShowHistoryModal(false)}
worksheets={worksheets}
ticket={ticket}
/>
<tr className="spacer">
<td colSpan={10} style={{ height: '12px', background: 'transparent', border: 'none' }}></td>
</tr>

View File

@@ -1,6 +1,8 @@
import { FaClock, FaUser, FaExchangeAlt, FaComment } from 'react-icons/fa'
import { useState } from 'react'
import { FaClock, FaUser, FaExchangeAlt, FaComment, FaChevronDown, FaChevronUp } from 'react-icons/fa'
export default function WorksheetList({ worksheets, totalTime, loading }) {
const [expandedWorksheets, setExpandedWorksheets] = useState({})
if (loading) {
return (
<div className="text-center p-4">
@@ -34,141 +36,180 @@ export default function WorksheetList({ worksheets, totalTime, loading }) {
return `${date} ${hours}:${mins}`
}
const toggleWorksheet = (wsid) => {
setExpandedWorksheets(prev => ({
...prev,
[wsid]: !prev[wsid]
}))
}
return (
<div className="worksheet-list">
{/* Gesamtzeit-Header */}
<div className="mb-4 p-4 rounded-3 shadow-sm" style={{
background: 'linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%)',
border: 'none'
}}>
<div className="d-flex justify-content-between align-items-center">
<div className="d-flex align-items-center">
<FaClock className="me-3" size={24} style={{ color: '#059669' }} />
<div>
<strong className="fs-5 d-block" style={{ color: '#064e3b' }}>Gesamtarbeitszeit</strong>
<span className="fs-3 fw-bold" style={{ color: '#059669' }}>{formatTime(totalTime)}</span>
</div>
</div>
<div className="text-end">
<span className="badge px-3 py-2" style={{
background: 'rgba(5, 150, 105, 0.2)',
color: '#059669',
fontSize: '1rem'
}}>
{worksheets.filter(ws => !ws.isComment).length} Worksheet(s)
</span>
</div>
</div>
</div>
{/* Worksheet-Einträge */}
<div className="timeline">
{worksheets.map((ws, index) => (
<div key={ws.$id} className="timeline-item mb-4" style={{
animation: `fadeIn 0.5s ease-in-out ${index * 0.1}s backwards`
}}>
<div className="card border-0 shadow-sm overflow-hidden" style={{
borderLeft: ws.isComment ? '4px solid #10b981' : '4px solid #4a5568',
transition: 'transform 0.2s ease, box-shadow 0.2s ease'
}} onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-2px)'
e.currentTarget.style.boxShadow = '0 8px 16px rgba(0,0,0,0.1)'
}} onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)'
e.currentTarget.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)'
{worksheets.map((ws, index) => {
const isExpanded = expandedWorksheets[ws.wsid] || false
return (
<div key={ws.$id} className="timeline-item mb-4" style={{
animation: `fadeIn 0.5s ease-in-out ${index * 0.1}s backwards`
}}>
<div className="card-header d-flex justify-content-between align-items-center py-3" style={{
background: ws.isComment
? 'linear-gradient(135deg, #10b981 0%, #059669 100%)'
: 'linear-gradient(135deg, #4a5568 0%, #2d3748 100%)',
color: 'white',
border: 'none'
<div className="card border-0 shadow-sm overflow-hidden" style={{
borderLeft: ws.isComment ? '4px solid #10b981' : '4px solid #4a5568',
borderRadius: '8px',
transition: 'transform 0.2s ease, box-shadow 0.2s ease'
}} onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-2px)'
e.currentTarget.style.boxShadow = '0 8px 16px rgba(0,0,0,0.1)'
}} onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)'
e.currentTarget.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)'
}}>
<div>
<strong className="fs-6">WSID {ws.wsid}</strong>
{ws.isComment && (
<span className="badge ms-2" style={{
background: 'rgba(255,255,255,0.3)'
}}>
<FaComment className="me-1" /> Kommentar
</span>
)}
</div>
<small style={{ opacity: 0.9 }}>
{formatDateTime(ws.startDate, ws.startTime)}
</small>
</div>
<div className="card-body p-4">
{/* Mitarbeiter & Zeit */}
<div className="row mb-3">
<div className="col-md-6">
<div className="d-flex align-items-center">
<FaUser className="me-2" style={{ color: '#10b981' }} />
<strong>{ws.employeeName}</strong>
{ws.employeeShort && (
{/* Header - Immer sichtbar, klickbar */}
<div
className="card-header d-flex justify-content-between align-items-center py-3"
onClick={() => toggleWorksheet(ws.wsid)}
style={{
background: ws.isComment
? 'linear-gradient(135deg, #10b981 0%, #059669 100%)'
: 'linear-gradient(135deg, #4a5568 0%, #2d3748 100%)',
color: 'white',
border: 'none',
cursor: 'pointer',
userSelect: 'none',
transition: 'background 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = ws.isComment
? 'linear-gradient(135deg, #059669 0%, #047857 100%)'
: 'linear-gradient(135deg, #2d3748 0%, #1a202c 100%)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = ws.isComment
? 'linear-gradient(135deg, #10b981 0%, #059669 100%)'
: 'linear-gradient(135deg, #4a5568 0%, #2d3748 100%)'
}}
>
<div className="d-flex align-items-center gap-3">
{isExpanded ? (
<FaChevronUp style={{ fontSize: '0.9rem', opacity: 0.8 }} />
) : (
<FaChevronDown style={{ fontSize: '0.9rem', opacity: 0.8 }} />
)}
<div>
<strong className="fs-6">WSID {ws.wsid}</strong>
{ws.isComment && (
<span className="badge ms-2" style={{
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
color: 'white'
}}>{ws.employeeShort}</span>
background: 'rgba(255,255,255,0.3)'
}}>
<FaComment className="me-1" /> Kommentar
</span>
)}
</div>
</div>
<div className="col-md-6 text-md-end">
{!ws.isComment && (
<div className="d-flex align-items-center justify-content-md-end">
<FaClock className="me-2" style={{ color: '#10b981' }} />
<strong className="fs-5" style={{ color: '#10b981' }}>{formatTime(ws.totalTime)}</strong>
{/* Collapsed: Mitarbeiter & Zeit im Header */}
{!isExpanded && (
<div className="d-flex align-items-center gap-3 ms-3">
<div className="d-flex align-items-center">
<FaUser style={{ fontSize: '0.9rem', marginRight: '0.5rem' }} />
<span style={{ fontSize: '0.9rem' }}>{ws.employeeName}</span>
</div>
{!ws.isComment && (
<div className="d-flex align-items-center">
<FaClock style={{ fontSize: '0.9rem', marginRight: '0.5rem' }} />
<span style={{ fontSize: '0.9rem' }}>{formatTime(ws.totalTime)}</span>
</div>
)}
<span className="badge" style={{
background: 'rgba(255,255,255,0.2)',
fontSize: '0.8rem'
}}>{ws.serviceType}</span>
</div>
)}
</div>
</div>
{/* Service Type */}
<div className="mb-3">
<span className="badge px-3 py-2" style={{
background: 'linear-gradient(135deg, #4a5568 0%, #2d3748 100%)',
color: 'white',
fontSize: '0.85rem'
}}>{ws.serviceType}</span>
</div>
{/* Status-Änderung */}
{ws.oldStatus !== ws.newStatus && (
<div className="mb-3 p-3 rounded-3" style={{
background: 'linear-gradient(135deg, rgba(16, 185, 129, 0.1) 0%, rgba(5, 150, 105, 0.1) 100%)'
}}>
<FaExchangeAlt className="me-2" style={{ color: '#10b981' }} />
<span className="text-muted">Status:</span>{' '}
<span className="badge" style={{ background: '#6b7280', color: 'white' }}>{ws.oldStatus}</span>
<span className="mx-2" style={{ color: '#10b981' }}></span>
<span className="badge" style={{ background: '#10b981', color: 'white' }}>{ws.newStatus}</span>
</div>
)}
{/* Response Level-Änderung */}
{ws.oldResponseLevel && ws.newResponseLevel && ws.oldResponseLevel !== ws.newResponseLevel && (
<div className="mb-3">
<span className="text-muted">Response Level:</span>{' '}
<span className="badge" style={{ background: '#6b7280', color: 'white' }}>{ws.oldResponseLevel}</span>
<span className="mx-2"></span>
<span className="badge" style={{ background: '#10b981', color: 'white' }}>{ws.newResponseLevel}</span>
</div>
)}
{/* Details */}
<div className="mt-3 p-3 rounded-3" style={{
background: 'rgba(16, 185, 129, 0.05)',
border: '1px solid rgba(16, 185, 129, 0.1)'
}}>
<small className="text-dark" style={{ whiteSpace: 'pre-wrap', lineHeight: '1.6' }}>
{ws.details}
<small style={{ opacity: 0.9 }}>
{formatDateTime(ws.startDate, ws.startTime)}
</small>
</div>
{/* Body - Nur wenn expanded */}
{isExpanded && (
<div
className="card-body p-4"
style={{
animation: 'slideDown 0.3s ease-out'
}}
>
{/* Mitarbeiter & Zeit */}
<div className="row mb-3">
<div className="col-md-6">
<div className="d-flex align-items-center">
<FaUser className="me-2" style={{ color: '#10b981' }} />
<strong>{ws.employeeName}</strong>
{ws.employeeShort && (
<span className="badge ms-2" style={{
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
color: 'white'
}}>{ws.employeeShort}</span>
)}
</div>
</div>
<div className="col-md-6 text-md-end">
{!ws.isComment && (
<div className="d-flex align-items-center justify-content-md-end">
<FaClock className="me-2" style={{ color: '#10b981' }} />
<strong className="fs-5" style={{ color: '#10b981' }}>{formatTime(ws.totalTime)}</strong>
</div>
)}
</div>
</div>
{/* Service Type */}
<div className="mb-3">
<span className="badge px-3 py-2" style={{
background: 'linear-gradient(135deg, #4a5568 0%, #2d3748 100%)',
color: 'white',
fontSize: '0.85rem'
}}>{ws.serviceType}</span>
</div>
{/* Status-Änderung */}
{ws.oldStatus !== ws.newStatus && (
<div className="mb-3 p-3 rounded-3" style={{
background: 'linear-gradient(135deg, rgba(16, 185, 129, 0.1) 0%, rgba(5, 150, 105, 0.1) 100%)'
}}>
<FaExchangeAlt className="me-2" style={{ color: '#10b981' }} />
<span className="text-muted">Status:</span>{' '}
<span className="badge" style={{ background: '#6b7280', color: 'white' }}>{ws.oldStatus}</span>
<span className="mx-2" style={{ color: '#10b981' }}></span>
<span className="badge" style={{ background: '#10b981', color: 'white' }}>{ws.newStatus}</span>
</div>
)}
{/* Response Level-Änderung */}
{ws.oldResponseLevel && ws.newResponseLevel && ws.oldResponseLevel !== ws.newResponseLevel && (
<div className="mb-3">
<span className="text-muted">Response Level:</span>{' '}
<span className="badge" style={{ background: '#6b7280', color: 'white' }}>{ws.oldResponseLevel}</span>
<span className="mx-2"></span>
<span className="badge" style={{ background: '#10b981', color: 'white' }}>{ws.newResponseLevel}</span>
</div>
)}
{/* Details */}
<div className="mt-3 p-3 rounded-3" style={{
background: 'rgba(16, 185, 129, 0.05)',
border: '1px solid rgba(16, 185, 129, 0.1)'
}}>
<small className="text-dark" style={{ whiteSpace: 'pre-wrap', lineHeight: '1.6' }}>
{ws.details}
</small>
</div>
</div>
)}
</div>
</div>
</div>
))}
)
})}
</div>
<style>{`
@@ -182,6 +223,18 @@ export default function WorksheetList({ worksheets, totalTime, loading }) {
transform: translateY(0);
}
}
@keyframes slideDown {
from {
opacity: 0;
max-height: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
max-height: 1000px;
transform: translateY(0);
}
}
`}</style>
</div>
)

View File

@@ -1,4 +1,4 @@
import { FaClock, FaUsers, FaHistory, FaChartLine } from 'react-icons/fa'
import { FaClock, FaUsers, FaChartLine } from 'react-icons/fa'
export default function WorksheetStats({ worksheets }) {
if (!worksheets || worksheets.length === 0) {
@@ -28,16 +28,6 @@ export default function WorksheetStats({ worksheets }) {
return acc
}, {})
// Status-Historie
const statusHistory = worksheets
.filter(ws => ws.oldStatus && ws.newStatus && ws.oldStatus !== ws.newStatus)
.map(ws => ({
date: ws.startDate,
time: ws.startTime,
from: ws.oldStatus,
to: ws.newStatus,
employee: ws.employeeName
}))
// Service Type Verteilung
const byServiceType = worksheets.reduce((acc, ws) => {
@@ -53,159 +43,269 @@ export default function WorksheetStats({ worksheets }) {
return hours > 0 ? `${hours}h ${mins}min` : `${mins}min`
}
const formatTimeShort = (time) => {
if (!time || time.length !== 4) return '-'
return `${time.substring(0, 2)}:${time.substring(2, 4)}`
}
return (
<div className="worksheet-stats mb-4">
<div className="row g-4">
{/* Gesamtübersicht */}
<div className="col-lg-4 col-md-6">
<div className="card h-100 border-0 shadow-sm" style={{
background: 'linear-gradient(135deg, #2d3748 0%, #1a202c 100%)',
color: 'white'
<div className="worksheet-stats" style={{
display: 'flex',
flexDirection: 'column',
gap: '16px',
height: '100%'
}}>
{/* Gesamtübersicht */}
<div style={{
background: 'rgba(45, 55, 72, 0.5)',
borderRadius: '12px',
padding: '16px',
border: '1px solid rgba(16, 185, 129, 0.2)',
flex: '0 0 auto'
}}>
<h6 style={{
color: 'var(--dark-text)',
marginBottom: '12px',
fontSize: '14px',
fontWeight: 'bold',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<FaChartLine size={16} style={{ color: '#10b981' }} />
Gesamtübersicht
</h6>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '8px',
background: 'rgba(26, 32, 44, 0.4)',
borderRadius: '6px'
}}>
<div className="card-body p-4">
<h6 className="card-title mb-3 d-flex align-items-center">
<FaChartLine className="me-2" size={20} style={{ color: '#4ade80' }} />
<strong>Gesamtübersicht</strong>
</h6>
<div className="mt-3">
<div className="d-flex justify-content-between align-items-center mb-3 pb-2" style={{ borderBottom: '1px solid rgba(255,255,255,0.2)' }}>
<span style={{ opacity: 0.9 }}>Worksheets:</span>
<strong className="fs-5">{worksheets.length}</strong>
</div>
<div className="d-flex justify-content-between align-items-center mb-3 pb-2" style={{ borderBottom: '1px solid rgba(255,255,255,0.2)' }}>
<span style={{ opacity: 0.9 }}>Arbeitszeit:</span>
<strong className="fs-5">{formatTime(totalMinutes)}</strong>
</div>
<div className="d-flex justify-content-between align-items-center mb-3 pb-2" style={{ borderBottom: '1px solid rgba(255,255,255,0.2)' }}>
<span style={{ opacity: 0.9 }}>Kommentare:</span>
<strong className="fs-5">{worksheets.filter(ws => ws.isComment).length}</strong>
</div>
<div className="d-flex justify-content-between align-items-center">
<span style={{ opacity: 0.9 }}>Ø pro Worksheet:</span>
<strong className="fs-5">
{formatTime(Math.round(totalMinutes / (worksheets.filter(ws => !ws.isComment).length || 1)))}
</strong>
</div>
</div>
</div>
<span style={{ color: 'rgba(226, 232, 240, 0.8)', fontSize: '12px' }}>Worksheets:</span>
<strong style={{ color: 'var(--dark-text)', fontSize: '16px' }}>{worksheets.length}</strong>
</div>
</div>
{/* Nach Mitarbeiter */}
<div className="col-lg-4 col-md-6">
<div className="card h-100 border-0 shadow-sm" style={{
background: 'linear-gradient(135deg, #22c55e 0%, #10b981 100%)',
color: 'white'
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '8px',
background: 'rgba(26, 32, 44, 0.4)',
borderRadius: '6px'
}}>
<div className="card-body p-4">
<h6 className="card-title mb-3 d-flex align-items-center">
<FaUsers className="me-2" size={20} />
<strong>Nach Mitarbeiter</strong>
</h6>
<div className="mt-3">
{Object.values(byEmployee).map((emp, idx) => (
<div key={idx} className="mb-3 pb-3" style={{ borderBottom: idx < Object.values(byEmployee).length - 1 ? '1px solid rgba(255,255,255,0.2)' : 'none' }}>
<div className="d-flex justify-content-between align-items-center">
<div>
<strong className="d-block">{emp.name}</strong>
{emp.short && (
<span className="badge mt-1" style={{
background: 'rgba(255,255,255,0.25)'
}}>{emp.short}</span>
)}
</div>
<div className="text-end">
<div className="fs-5 fw-bold">{formatTime(emp.time)}</div>
<small style={{ opacity: 0.8 }}>{emp.count} WS</small>
</div>
</div>
</div>
))}
</div>
</div>
<span style={{ color: 'rgba(226, 232, 240, 0.8)', fontSize: '12px' }}>Arbeitszeit:</span>
<strong style={{ color: '#10b981', fontSize: '16px' }}>{formatTime(totalMinutes)}</strong>
</div>
</div>
{/* Service Type Verteilung */}
<div className="col-lg-4 col-md-6">
<div className="card h-100 border-0 shadow-sm" style={{
background: 'linear-gradient(135deg, #4a5568 0%, #2d3748 100%)',
color: 'white'
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '8px',
background: 'rgba(26, 32, 44, 0.4)',
borderRadius: '6px'
}}>
<div className="card-body p-4">
<h6 className="card-title mb-3 d-flex align-items-center">
<FaClock className="me-2" size={20} style={{ color: '#4ade80' }} />
<strong>Service Types</strong>
</h6>
<div className="mt-3">
{Object.entries(byServiceType).map(([type, count], idx) => (
<div key={type} className="d-flex justify-content-between align-items-center mb-3 pb-3" style={{ borderBottom: idx < Object.entries(byServiceType).length - 1 ? '1px solid rgba(255,255,255,0.2)' : 'none' }}>
<span className="badge px-3 py-2" style={{
background: 'rgba(255,255,255,0.25)',
fontSize: '0.9rem'
}}>{type}</span>
<strong className="fs-5">{count}</strong>
</div>
))}
</div>
</div>
<span style={{ color: 'rgba(226, 232, 240, 0.8)', fontSize: '12px' }}>Kommentare:</span>
<strong style={{ color: 'var(--dark-text)', fontSize: '16px' }}>{worksheets.filter(ws => ws.isComment).length}</strong>
</div>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '8px',
background: 'rgba(26, 32, 44, 0.4)',
borderRadius: '6px'
}}>
<span style={{ color: 'rgba(226, 232, 240, 0.8)', fontSize: '12px' }}>Ø pro WS:</span>
<strong style={{ color: 'var(--dark-text)', fontSize: '16px' }}>
{formatTime(Math.round(totalMinutes / (worksheets.filter(ws => !ws.isComment).length || 1)))}
</strong>
</div>
</div>
</div>
{/* Status-Historie */}
{statusHistory.length > 0 && (
<div className="card border-0 shadow-sm mt-3" style={{
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)'
{/* Mitarbeiter Kombiniertes Diagramm */}
<div style={{
background: 'rgba(45, 55, 72, 0.5)',
borderRadius: '12px',
padding: '16px',
border: '1px solid rgba(16, 185, 129, 0.2)',
flex: '1 1 auto',
minHeight: '200px'
}}>
<h6 style={{
color: 'var(--dark-text)',
marginBottom: '16px',
fontSize: '14px',
fontWeight: 'bold',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<div className="card-body p-4">
<h6 className="card-title text-white mb-3 d-flex align-items-center">
<FaHistory className="me-2" size={20} />
<strong>Status-Historie</strong>
</h6>
<div className="table-responsive mt-3">
<table className="table table-sm" style={{ borderColor: 'rgba(255,255,255,0.2)' }}>
<thead>
<tr style={{ color: 'white', borderColor: 'rgba(255,255,255,0.2)' }}>
<th style={{ borderColor: 'rgba(255,255,255,0.2)' }}>Datum</th>
<th style={{ borderColor: 'rgba(255,255,255,0.2)' }}>Zeit</th>
<th style={{ borderColor: 'rgba(255,255,255,0.2)' }}>Von</th>
<th style={{ borderColor: 'rgba(255,255,255,0.2)' }}></th>
<th style={{ borderColor: 'rgba(255,255,255,0.2)' }}>Nach</th>
<th style={{ borderColor: 'rgba(255,255,255,0.2)' }}>Mitarbeiter</th>
</tr>
</thead>
<tbody>
{statusHistory.reverse().map((change, idx) => (
<tr key={idx} style={{ color: 'white', borderColor: 'rgba(255,255,255,0.2)' }}>
<td style={{ borderColor: 'rgba(255,255,255,0.2)' }}>{change.date}</td>
<td style={{ borderColor: 'rgba(255,255,255,0.2)' }}>{formatTimeShort(change.time)}</td>
<td style={{ borderColor: 'rgba(255,255,255,0.2)' }}>
<span className="badge" style={{
background: 'rgba(255,255,255,0.25)'
}}>{change.from}</span>
</td>
<td style={{ borderColor: 'rgba(255,255,255,0.2)' }}></td>
<td style={{ borderColor: 'rgba(255,255,255,0.2)' }}>
<span className="badge" style={{
background: 'rgba(255,255,255,0.4)',
fontWeight: 'bold'
}}>{change.to}</span>
</td>
<td style={{ borderColor: 'rgba(255,255,255,0.2)' }}>{change.employee}</td>
</tr>
))}
</tbody>
</table>
</div>
<FaUsers size={16} style={{ color: '#10b981' }} />
Mitarbeiter-Statistiken
</h6>
{Object.keys(byEmployee).length === 0 ? (
<div style={{ color: '#a0aec0', textAlign: 'center', padding: '20px' }}>
Keine Mitarbeiter-Daten verfügbar
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{(() => {
const employeeArray = Object.values(byEmployee).sort((a, b) => b.time - a.time)
const maxTime = Math.max(...employeeArray.map(e => e.time), 1)
return employeeArray.map((emp, idx) => {
const percentage = maxTime > 0 ? (emp.time / maxTime) * 100 : 0
return (
<div key={idx}>
<div style={{
display: 'flex',
alignItems: 'center',
marginBottom: '6px',
fontSize: '11px'
}}>
<span style={{
color: 'var(--dark-text)',
fontWeight: '500',
display: 'flex',
alignItems: 'center',
gap: '6px',
minWidth: '120px'
}}>
{emp.short && (
<span style={{
background: 'rgba(16, 185, 129, 0.2)',
color: '#10b981',
padding: '2px 6px',
borderRadius: '3px',
fontSize: '9px',
fontWeight: 'bold'
}}>{emp.short}</span>
)}
<span style={{ maxWidth: '100px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{emp.name}
</span>
</span>
</div>
<div style={{
width: '100%',
height: '28px',
background: 'rgba(26, 32, 44, 0.6)',
borderRadius: '6px',
overflow: 'hidden',
position: 'relative',
display: 'flex',
alignItems: 'center'
}}>
{/* WS Anzahl am Anfang */}
<div style={{
position: 'absolute',
left: '8px',
zIndex: 2,
color: 'white',
fontSize: '11px',
fontWeight: 'bold',
display: 'flex',
alignItems: 'center',
gap: '4px'
}}>
<span>WS</span>
<span style={{
background: 'rgba(255, 255, 255, 0.3)',
padding: '2px 6px',
borderRadius: '4px'
}}>{emp.count}</span>
</div>
{/* Balken mit Zeit */}
<div style={{
width: `${percentage}%`,
height: '100%',
background: 'linear-gradient(90deg, #10b981 0%, #059669 100%)',
borderRadius: '6px',
transition: 'width 0.5s ease',
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
paddingRight: '8px',
paddingLeft: '60px',
boxShadow: '0 2px 8px rgba(16, 185, 129, 0.3)',
position: 'relative'
}}>
{/* Zeit am Ende des Balkens */}
<span style={{
color: 'white',
fontSize: '11px',
fontWeight: 'bold',
whiteSpace: 'nowrap'
}}>
{formatTime(emp.time)}
</span>
</div>
{/* Zeit außerhalb des Balkens (falls Balken zu kurz) */}
{percentage < 30 && (
<div style={{
position: 'absolute',
right: '8px',
zIndex: 2,
color: '#10b981',
fontSize: '11px',
fontWeight: 'bold'
}}>
{formatTime(emp.time)}
</div>
)}
</div>
</div>
)
})
})()}
</div>
)}
</div>
{/* Service Type Verteilung */}
<div style={{
background: 'rgba(45, 55, 72, 0.5)',
borderRadius: '12px',
padding: '16px',
border: '1px solid rgba(16, 185, 129, 0.2)',
flex: '0 0 auto'
}}>
<h6 style={{
color: 'var(--dark-text)',
marginBottom: '12px',
fontSize: '14px',
fontWeight: 'bold',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<FaClock size={16} style={{ color: '#10b981' }} />
Service Types
</h6>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{Object.entries(byServiceType).map(([type, count]) => (
<div key={type} style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '8px',
background: 'rgba(26, 32, 44, 0.4)',
borderRadius: '6px'
}}>
<span style={{
color: 'var(--dark-text)',
fontSize: '12px',
fontWeight: '500'
}}>{type}</span>
<strong style={{
color: '#10b981',
fontSize: '14px'
}}>{count}</strong>
</div>
))}
</div>
)}
</div>
</div>
)
}

View File

@@ -4,8 +4,61 @@ import { databases, DATABASE_ID, COLLECTIONS, Query, ID } from '../lib/appwrite'
const DEMO_MODE = !import.meta.env.VITE_APPWRITE_PROJECT_ID
// Demo data for testing without Appwrite
const lastWeek = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
const DEMO_WORKORDERS = [
{ $id: '1', woid: '10001', title: 'Server Wartung', description: 'Monatliche Wartung', status: 'Open', priority: 2, type: 'Maintenance', customerName: 'Kunde A', assignedName: 'Max M.', response: 'Onsite', $createdAt: new Date().toISOString() },
{
$id: 'dummy-10001',
woid: '10001',
topic: 'Kompletter Systemausfall - Server & Netzwerk',
status: 'Assigned',
priority: 4,
type: 'Emergency Call',
systemType: 'Server',
responseLevel: 'Backoffice',
serviceType: 'On Site',
customerName: 'Kunde A',
customerLocation: 'Hauptstraße 123, 12345 Musterstadt',
assignedTo: 'user-max-id',
assignedName: 'Max Mustermann',
requestedBy: 'Dr. Anna Schmidt',
requestedFor: 'IT-Abteilung Kunde A',
startDate: '30.12.2025',
startTime: '0800',
deadline: '31.12.2025',
endTime: '1800',
estimate: '480',
mailCopyTo: 'admin@kunde-a.de, it@kunde-a.de',
sendNotification: true,
details: `KRITISCHER SYSTEMAUSFALL - SOFORTIGE BEARBEITUNG ERFORDERLICH
Problembeschreibung:
- Kompletter Serverausfall im Rechenzentrum
- Alle Server sind offline (keine Verbindung möglich)
- Netzwerk-Infrastruktur betroffen
- Keine Backup-Systeme verfügbar
Betroffene Systeme:
- Hauptserver (Windows Server 2022)
- Datenbankserver (SQL Server 2019)
- Fileserver
- Exchange Server
- Netzwerk-Switches
Auswirkungen:
- Keine E-Mail-Kommunikation möglich
- Alle Anwendungen offline
- Kein Zugriff auf Datenbanken
- Produktion steht still
Dringlichkeit: KRITISCH - Produktionsausfall
Erwartete Bearbeitungszeit: 8 Stunden
Benötigte Ressourcen: 2 Techniker, Hardware-Ersatzteile`,
approvalStatus: 'approved',
$createdAt: lastWeek.toISOString(),
createdAt: lastWeek.toISOString()
},
{ $id: '2', woid: '10002', title: 'Netzwerk Problem', description: 'WLAN funktioniert nicht', status: 'Occupied', priority: 3, type: 'Support', customerName: 'Kunde B', assignedName: 'Lisa S.', response: 'Remote', $createdAt: new Date().toISOString() },
{ $id: '3', woid: '10003', title: 'Software Installation', description: 'Office 365 Setup', status: 'Assigned', priority: 1, type: 'Installation', customerName: 'Kunde C', assignedName: 'Tom K.', response: 'Onsite', $createdAt: new Date().toISOString() },
{ $id: '4', woid: '10004', title: 'Drucker defekt', description: 'Papierstau', status: 'Awaiting', priority: 2, type: 'Hardware', customerName: 'Kunde D', assignedName: '', response: 'Pickup', $createdAt: new Date().toISOString() },

View File

@@ -3,28 +3,144 @@ import { databases, DATABASE_ID, COLLECTIONS, Query, ID } from '../lib/appwrite'
const DEMO_MODE = !import.meta.env.VITE_APPWRITE_PROJECT_ID
// Demo data für Testing
// Demo data für Testing - Vollständiges Dummy-Ticket 10001 mit allen Worksheets
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000)
const twoDaysAgo = new Date(Date.now() - 48 * 60 * 60 * 1000)
const threeDaysAgo = new Date(Date.now() - 72 * 60 * 60 * 1000)
const DEMO_WORKSHEETS = [
{
$id: '1',
$id: 'ws-10001-001',
wsid: '100001',
woid: '10001',
workorderId: '1',
employeeId: 'emp1',
employeeName: 'Max Müller',
employeeShort: 'MAMU',
workorderId: 'dummy-10001',
employeeId: 'user-max-id',
employeeName: 'Max Mustermann',
employeeShort: 'MM',
serviceType: 'Remote',
oldStatus: 'Open',
newStatus: 'Occupied',
oldResponseLevel: '',
newResponseLevel: '24/7',
totalTime: 30,
startDate: '29.12.2025',
startTime: '1000',
endDate: '29.12.2025',
endTime: '1030',
details: 'Router neu gestartet',
startDate: '23.12.2025',
startTime: '0800',
endDate: '23.12.2025',
endTime: '0830',
details: 'Erste Analyse durchgeführt. Server komplett offline. Keine Remote-Verbindung möglich. Vor-Ort-Einsatz erforderlich.',
isComment: false,
$createdAt: new Date().toISOString()
$createdAt: threeDaysAgo.toISOString()
},
{
$id: 'ws-10001-002',
wsid: '100002',
woid: '10001',
workorderId: 'dummy-10001',
employeeId: 'user-lisa-id',
employeeName: 'Lisa Schneider',
employeeShort: 'LS',
serviceType: 'On Site',
oldStatus: 'Occupied',
newStatus: 'Assigned',
oldResponseLevel: '24/7',
newResponseLevel: '24/7',
totalTime: 120,
startDate: '23.12.2025',
startTime: '1000',
endDate: '23.12.2025',
endTime: '1200',
details: 'Vor-Ort-Einsatz: Hardware-Check durchgeführt. Netzteil des Hauptservers defekt. Ersatzteil bestellt. Notfall-Backup-Server gestartet.',
isComment: false,
$createdAt: threeDaysAgo.toISOString()
},
{
$id: 'ws-10001-003',
wsid: '100003',
woid: '10001',
workorderId: 'dummy-10001',
employeeId: 'user-tom-id',
employeeName: 'Tom Klein',
employeeShort: 'TK',
serviceType: 'On Site',
oldStatus: 'Assigned',
newStatus: 'Assigned',
oldResponseLevel: '24/7',
newResponseLevel: '24/7',
totalTime: 0,
startDate: '24.12.2025',
startTime: '1400',
endDate: '24.12.2025',
endTime: '1400',
details: 'Warte auf Ersatzteil-Lieferung. Kunde informiert. Backup-System läuft stabil.',
isComment: true,
$createdAt: twoDaysAgo.toISOString()
},
{
$id: 'ws-10001-004',
wsid: '100004',
woid: '10001',
workorderId: 'dummy-10001',
employeeId: 'user-max-id',
employeeName: 'Max Mustermann',
employeeShort: 'MM',
serviceType: 'On Site',
oldStatus: 'Assigned',
newStatus: 'In Test',
oldResponseLevel: '24/7',
newResponseLevel: '24/7',
totalTime: 180,
startDate: '25.12.2025',
startTime: '0900',
endDate: '25.12.2025',
endTime: '1200',
details: 'Ersatzteil eingebaut. Server gestartet. Alle Dienste wiederhergestellt. System-Tests durchgeführt. Datenbank-Verbindungen geprüft.',
isComment: false,
$createdAt: twoDaysAgo.toISOString()
},
{
$id: 'ws-10001-005',
wsid: '100005',
woid: '10001',
workorderId: 'dummy-10001',
employeeId: 'user-lisa-id',
employeeName: 'Lisa Schneider',
employeeShort: 'LS',
serviceType: 'Remote',
oldStatus: 'In Test',
newStatus: 'Awaiting',
oldResponseLevel: '24/7',
newResponseLevel: 'Support',
totalTime: 45,
startDate: '26.12.2025',
startTime: '1000',
endDate: '26.12.2025',
endTime: '1045',
details: 'Remote-Monitoring eingerichtet. Warte auf Kunden-Feedback nach 24h Testphase. Alle Systeme laufen stabil.',
isComment: false,
$createdAt: yesterday.toISOString()
},
{
$id: 'ws-10001-006',
wsid: '100006',
woid: '10001',
workorderId: 'dummy-10001',
employeeId: 'user-tom-id',
employeeName: 'Tom Klein',
employeeShort: 'TK',
serviceType: 'COMMENT',
oldStatus: 'Awaiting',
newStatus: 'Closed',
oldResponseLevel: 'Support',
newResponseLevel: 'Backoffice',
totalTime: 0,
startDate: '30.12.2025',
startTime: '0900',
endDate: '30.12.2025',
endTime: '0900',
details: 'Kunde bestätigt: Alle Systeme funktionieren einwandfrei. Problem vollständig behoben. Ticket kann geschlossen werden.',
isComment: true,
$createdAt: new Date().toISOString()
}
]
export function useWorksheets(woid = null) {

View File

@@ -0,0 +1,231 @@
/**
* Erstellt ein vollständiges Dummy-Ticket mit WOID 10001
* Zeigt alle möglichen Funktionen, Felder und Kombinationen
*/
export function createDummyTicket10001() {
const now = new Date()
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000)
const lastWeek = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
// Haupt-Ticket mit allen Feldern ausgefüllt
const dummyTicket = {
$id: 'dummy-10001',
woid: '10001',
topic: 'Kompletter Systemausfall - Server & Netzwerk',
status: 'Assigned',
priority: 4, // Critical
type: 'Emergency Call',
systemType: 'Server',
responseLevel: '24/7',
serviceType: 'On Site',
customerId: 'customer-a-id',
customerName: 'Kunde A',
customerLocation: 'Hauptstraße 123, 12345 Musterstadt',
assignedTo: 'user-max-id', // Max Mustermann
requestedBy: 'Dr. Anna Schmidt',
requestedFor: 'IT-Abteilung Kunde A',
startDate: '30.12.2025',
startTime: '0800',
deadline: '31.12.2025',
endTime: '1800',
estimate: '480',
mailCopyTo: 'admin@kunde-a.de, it@kunde-a.de',
sendNotification: true,
details: `KRITISCHER SYSTEMAUSFALL - SOFORTIGE BEARBEITUNG ERFORDERLICH
Problembeschreibung:
- Kompletter Serverausfall im Rechenzentrum
- Alle Server sind offline (keine Verbindung möglich)
- Netzwerk-Infrastruktur betroffen
- Keine Backup-Systeme verfügbar
Betroffene Systeme:
- Hauptserver (Windows Server 2022)
- Datenbankserver (SQL Server 2019)
- Fileserver
- Exchange Server
- Netzwerk-Switches
Auswirkungen:
- Keine E-Mail-Kommunikation möglich
- Alle Anwendungen offline
- Kein Zugriff auf Datenbanken
- Produktion steht still
Dringlichkeit: KRITISCH - Produktionsausfall
Erwartete Bearbeitungszeit: 8 Stunden
Benötigte Ressourcen: 2 Techniker, Hardware-Ersatzteile`,
approvalStatus: 'approved',
createdAt: lastWeek.toISOString(),
$createdAt: lastWeek.toISOString()
}
// Mehrere Worksheets mit verschiedenen Status-Änderungen und Benutzern
const dummyWorksheets = [
{
$id: 'ws-10001-001',
wsid: '100001',
woid: '10001',
workorderId: 'dummy-10001',
employeeId: 'user-max-id',
employeeName: 'Max Mustermann',
employeeShort: 'MM',
serviceType: 'Remote',
oldStatus: 'Open',
newStatus: 'Occupied',
oldResponseLevel: '',
newResponseLevel: '24/7',
totalTime: 30,
startDate: '23.12.2025',
startTime: '0800',
endDate: '23.12.2025',
endTime: '0830',
details: 'Erste Analyse durchgeführt. Server komplett offline. Keine Remote-Verbindung möglich. Vor-Ort-Einsatz erforderlich.',
isComment: false,
createdAt: yesterday.toISOString(),
$createdAt: yesterday.toISOString()
},
{
$id: 'ws-10001-002',
wsid: '100002',
woid: '10001',
workorderId: 'dummy-10001',
employeeId: 'user-lisa-id',
employeeName: 'Lisa Schneider',
employeeShort: 'LS',
serviceType: 'On Site',
oldStatus: 'Occupied',
newStatus: 'Assigned',
oldResponseLevel: '24/7',
newResponseLevel: '24/7',
totalTime: 120,
startDate: '23.12.2025',
startTime: '1000',
endDate: '23.12.2025',
endTime: '1200',
details: 'Vor-Ort-Einsatz: Hardware-Check durchgeführt. Netzteil des Hauptservers defekt. Ersatzteil bestellt. Notfall-Backup-Server gestartet.',
isComment: false,
createdAt: yesterday.toISOString(),
$createdAt: yesterday.toISOString()
},
{
$id: 'ws-10001-003',
wsid: '100003',
woid: '10001',
workorderId: 'dummy-10001',
employeeId: 'user-tom-id',
employeeName: 'Tom Klein',
employeeShort: 'TK',
serviceType: 'On Site',
oldStatus: 'Assigned',
newStatus: 'Assigned',
oldResponseLevel: '24/7',
newResponseLevel: '24/7',
totalTime: 0,
startDate: '24.12.2025',
startTime: '1400',
endDate: '24.12.2025',
endTime: '1400',
details: 'Warte auf Ersatzteil-Lieferung. Kunde informiert. Backup-System läuft stabil.',
isComment: true,
createdAt: new Date(yesterday.getTime() - 12 * 60 * 60 * 1000).toISOString(),
$createdAt: new Date(yesterday.getTime() - 12 * 60 * 60 * 1000).toISOString()
},
{
$id: 'ws-10001-004',
wsid: '100004',
woid: '10001',
workorderId: 'dummy-10001',
employeeId: 'user-max-id',
employeeName: 'Max Mustermann',
employeeShort: 'MM',
serviceType: 'On Site',
oldStatus: 'Assigned',
newStatus: 'In Test',
oldResponseLevel: '24/7',
newResponseLevel: '24/7',
totalTime: 180,
startDate: '25.12.2025',
startTime: '0900',
endDate: '25.12.2025',
endTime: '1200',
details: 'Ersatzteil eingebaut. Server gestartet. Alle Dienste wiederhergestellt. System-Tests durchgeführt. Datenbank-Verbindungen geprüft.',
isComment: false,
createdAt: new Date(yesterday.getTime() - 24 * 60 * 60 * 1000).toISOString(),
$createdAt: new Date(yesterday.getTime() - 24 * 60 * 60 * 1000).toISOString()
},
{
$id: 'ws-10001-005',
wsid: '100005',
woid: '10001',
workorderId: 'dummy-10001',
employeeId: 'user-lisa-id',
employeeName: 'Lisa Schneider',
employeeShort: 'LS',
serviceType: 'Remote',
oldStatus: 'In Test',
newStatus: 'Awaiting',
oldResponseLevel: '24/7',
newResponseLevel: 'Support',
totalTime: 45,
startDate: '26.12.2025',
startTime: '1000',
endDate: '26.12.2025',
endTime: '1045',
details: 'Remote-Monitoring eingerichtet. Warte auf Kunden-Feedback nach 24h Testphase. Alle Systeme laufen stabil.',
isComment: false,
createdAt: new Date(yesterday.getTime() - 48 * 60 * 60 * 1000).toISOString(),
$createdAt: new Date(yesterday.getTime() - 48 * 60 * 60 * 1000).toISOString()
},
{
$id: 'ws-10001-006',
wsid: '100006',
woid: '10001',
workorderId: 'dummy-10001',
employeeId: 'user-tom-id',
employeeName: 'Tom Klein',
employeeShort: 'TK',
serviceType: 'COMMENT',
oldStatus: 'Awaiting',
newStatus: 'Closed',
oldResponseLevel: 'Support',
newResponseLevel: 'Backoffice',
totalTime: 0,
startDate: '30.12.2025',
startTime: '0900',
endDate: '30.12.2025',
endTime: '0900',
details: 'Kunde bestätigt: Alle Systeme funktionieren einwandfrei. Problem vollständig behoben. Ticket kann geschlossen werden.',
isComment: true,
createdAt: now.toISOString(),
$createdAt: now.toISOString()
}
]
return {
ticket: dummyTicket,
worksheets: dummyWorksheets
}
}
/**
* Fügt das Dummy-Ticket zu den Demo-Daten hinzu
*/
export function addDummyTicketToDemo(workorders, worksheets) {
const { ticket, worksheets: ticketWorksheets } = createDummyTicket10001()
// Prüfe ob Ticket bereits existiert
const exists = workorders.some(wo => wo.woid === '10001')
if (exists) {
console.log('Dummy-Ticket 10001 existiert bereits')
return { workorders, worksheets }
}
return {
workorders: [ticket, ...workorders],
worksheets: [...ticketWorksheets, ...worksheets]
}
}