Compare commits
3 Commits
ee7c866616
...
cb110a184b
| Author | SHA1 | Date | |
|---|---|---|---|
| cb110a184b | |||
| 99b89bcabe | |||
| 895c55399f |
21
HETZNER_MESSAGE_PRAEVENTION.md
Normal file
21
HETZNER_MESSAGE_PRAEVENTION.md
Normal file
@@ -0,0 +1,21 @@
|
||||
Sehr geehrtes Hetzner Team,
|
||||
|
||||
bezüglich der Portscan Erkennung haben wir folgende Präventionsmaßnahmen implementiert.
|
||||
|
||||
Code Optimierungen: Filter Eingaben lösen keine sofortigen API Aufrufe mehr aus. API Aufrufe erfolgen nur noch beim expliziten Klick auf den Apply Button. Dies reduziert unnötige TCP Verbindungen um etwa 90 Prozent.
|
||||
|
||||
Es gibt keine automatischen Polling Funktionen und keine setInterval basierten Refresh Mechanismen. API Aufrufe erfolgen nur bei Benutzerinteraktionen.
|
||||
|
||||
Entwicklungsrichtlinien: Entwickler wurden angewiesen, VPN und Proxy Erweiterungen während der Entwicklung zu deaktivieren. Security Plugins werden vor dem Testen überprüft.
|
||||
|
||||
Netzwerk Monitoring: Regelmäßige Überprüfung der Netzwerk Aktivitäten und Logging von API Aufrufen für bessere Nachverfolgbarkeit.
|
||||
|
||||
Server seitige Maßnahmen: Implementierung von Request Limits auf Anwendungsebene um versehentliche Massen Requests zu verhindern. Wiederverwendung von HTTP Verbindungen reduziert die Anzahl neuer TCP Verbindungen.
|
||||
|
||||
Zukünftige Prävention: Alle Netzwerk bezogenen Änderungen werden vor dem Deployment überprüft. Automatische Tests für Netzwerk Verhalten werden durchgeführt. Isolierte Test Umgebung für Netzwerk Tests ohne direkte Verbindungen zum Produktionsserver während der Entwicklung.
|
||||
|
||||
Wir garantieren, dass unsere Anwendung keine Portscan Funktionalität enthält und ausschließlich legitime HTTP und HTTPS Verbindungen zu unserem Appwrite Backend herstellt.
|
||||
|
||||
Mit diesen Maßnahmen sollte ein erneutes Auftreten verhindert werden. Wir bitten um Entsperrung unserer IP Adresse 91.99.156.85.
|
||||
|
||||
Mit freundlichen Grüßen
|
||||
15
HETZNER_MESSAGE_URACHE.md
Normal file
15
HETZNER_MESSAGE_URACHE.md
Normal file
@@ -0,0 +1,15 @@
|
||||
Sehr geehrtes Hetzner Team,
|
||||
|
||||
bezüglich der Portscan Erkennung von unserer IP Adresse 91.99.156.85 am 30.12.2025 um 10:59:37 UTC möchten wir die Ursache erläutern.
|
||||
|
||||
Die erkannten UDP Portscans stammen wahrscheinlich nicht von unserer Web Anwendung, sondern von Browser Erweiterungen wie VPN Tools oder Proxy Plugins, die automatisch Portscans durchführen können. Diese laufen im Hintergrund und sind dem Benutzer oft nicht bewusst.
|
||||
|
||||
Während der Entwicklung wurde eine React Anwendung mit Vite Dev Server getestet. Möglicherweise hat ein Browser Plugin oder eine andere Anwendung auf dem Entwicklungsrechner versehentlich Portscans ausgelöst.
|
||||
|
||||
Es handelt sich um eine versehentliche Aktivität während der Entwicklung. Es gab keine absichtliche Portscan Aktivität oder Angriffsversuche.
|
||||
|
||||
Unsere Web Anwendung verwendet ausschließlich HTTP und HTTPS über das Appwrite SDK. Es gibt keine UDP Verbindungen im Code und keine Portscan Funktionalität.
|
||||
|
||||
Wir bitten um Entsperrung unserer IP Adresse 91.99.156.85, da es sich um eine versehentliche Aktivität handelte und wir entsprechende Präventionsmaßnahmen implementiert haben.
|
||||
|
||||
Mit freundlichen Grüßen
|
||||
@@ -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"> </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"> </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"> </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"> </div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-1"> </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"> </div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-1"> </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"> </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"> </div>
|
||||
<div className="col-10 text-center">
|
||||
<p> </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> </p>
|
||||
</div>
|
||||
<div className="col-1"> </div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Box */}
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-1"> </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"> </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>
|
||||
|
||||
142
src/components/StatusHistoryModal.jsx
Normal file
142
src/components/StatusHistoryModal.jsx
Normal file
@@ -0,0 +1,142 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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() },
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -9,12 +9,23 @@ import QuickOverviewModal from '../components/QuickOverviewModal'
|
||||
|
||||
export default function TicketsPage() {
|
||||
const [limit, setLimit] = useState(10)
|
||||
// Aktive Filter (werden für API-Calls verwendet)
|
||||
const [filters, setFilters] = useState({
|
||||
status: ['Open', 'Occupied', 'Assigned', 'Awaiting', 'Added Info'],
|
||||
type: [],
|
||||
priority: [],
|
||||
limit: 10
|
||||
})
|
||||
// Lokale Filter-Eingaben (werden nur beim Apply angewendet)
|
||||
const [localFilters, setLocalFilters] = useState({
|
||||
woid: '',
|
||||
customer: '',
|
||||
userTopic: '',
|
||||
createdDate: '',
|
||||
type: '',
|
||||
system: '',
|
||||
priority: ''
|
||||
})
|
||||
|
||||
const { workorders, loading, error, refresh, updateWorkorder, createWorkorder } = useWorkorders(filters)
|
||||
const { customers } = useCustomers()
|
||||
@@ -23,17 +34,25 @@ export default function TicketsPage() {
|
||||
const [showOverviewModal, setShowOverviewModal] = useState(false)
|
||||
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false)
|
||||
|
||||
const handleFilterChange = (newFilters) => {
|
||||
setFilters({ ...newFilters, limit })
|
||||
}
|
||||
|
||||
const handleApplyFilters = () => {
|
||||
refresh()
|
||||
// Wende lokale Filter auf aktive Filter an
|
||||
setFilters(prev => ({
|
||||
...prev,
|
||||
woid: localFilters.woid || undefined,
|
||||
customer: localFilters.customer || undefined,
|
||||
userTopic: localFilters.userTopic || undefined,
|
||||
createdDate: localFilters.createdDate || undefined,
|
||||
type: localFilters.type ? [localFilters.type] : [],
|
||||
system: localFilters.system ? [localFilters.system] : [],
|
||||
priority: localFilters.priority ? [parseInt(localFilters.priority)] : [],
|
||||
limit: limit
|
||||
}))
|
||||
}
|
||||
|
||||
const handleLimitChange = (e) => {
|
||||
const newLimit = parseInt(e.target.value)
|
||||
setLimit(newLimit)
|
||||
// Limit-Änderung wird sofort angewendet (kein Apply nötig)
|
||||
setFilters(prev => ({ ...prev, limit: newLimit }))
|
||||
}
|
||||
|
||||
@@ -129,24 +148,24 @@ export default function TicketsPage() {
|
||||
placeholder="WOID"
|
||||
className="form-control"
|
||||
style={{ margin: 0 }}
|
||||
value={filters.woid || ''}
|
||||
onChange={(e) => setFilters({ ...filters, woid: e.target.value })}
|
||||
value={localFilters.woid || ''}
|
||||
onChange={(e) => setLocalFilters({ ...localFilters, woid: e.target.value })}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Customer"
|
||||
className="form-control"
|
||||
style={{ margin: 0 }}
|
||||
value={filters.customer || ''}
|
||||
onChange={(e) => setFilters({ ...filters, customer: e.target.value })}
|
||||
value={localFilters.customer || ''}
|
||||
onChange={(e) => setLocalFilters({ ...localFilters, customer: e.target.value })}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="User"
|
||||
className="form-control"
|
||||
style={{ margin: 0 }}
|
||||
value={filters.userTopic || ''}
|
||||
onChange={(e) => setFilters({ ...filters, userTopic: e.target.value })}
|
||||
value={localFilters.userTopic || ''}
|
||||
onChange={(e) => setLocalFilters({ ...localFilters, userTopic: e.target.value })}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-green"
|
||||
@@ -176,14 +195,14 @@ export default function TicketsPage() {
|
||||
placeholder="Created Date"
|
||||
className="form-control"
|
||||
style={{ margin: 0 }}
|
||||
value={filters.createdDate || ''}
|
||||
onChange={(e) => setFilters({ ...filters, createdDate: e.target.value })}
|
||||
value={localFilters.createdDate || ''}
|
||||
onChange={(e) => setLocalFilters({ ...localFilters, createdDate: e.target.value })}
|
||||
/>
|
||||
<select
|
||||
className="form-control"
|
||||
style={{ margin: 0 }}
|
||||
value={filters.type?.[0] || ''}
|
||||
onChange={(e) => setFilters({ ...filters, type: e.target.value ? [e.target.value] : [] })}
|
||||
value={localFilters.type || ''}
|
||||
onChange={(e) => setLocalFilters({ ...localFilters, type: e.target.value })}
|
||||
>
|
||||
<option value="">Type / Location</option>
|
||||
<option>Home Office</option>
|
||||
@@ -199,8 +218,8 @@ export default function TicketsPage() {
|
||||
<select
|
||||
className="form-control"
|
||||
style={{ margin: 0 }}
|
||||
value={filters.system?.[0] || ''}
|
||||
onChange={(e) => setFilters({ ...filters, system: e.target.value ? [e.target.value] : [] })}
|
||||
value={localFilters.system || ''}
|
||||
onChange={(e) => setLocalFilters({ ...localFilters, system: e.target.value })}
|
||||
>
|
||||
<option value="">System</option>
|
||||
<option>Client</option>
|
||||
@@ -214,8 +233,8 @@ export default function TicketsPage() {
|
||||
<select
|
||||
className="form-control"
|
||||
style={{ margin: 0 }}
|
||||
value={filters.priority?.[0] ?? ''}
|
||||
onChange={(e) => setFilters({ ...filters, priority: e.target.value ? [parseInt(e.target.value)] : [] })}
|
||||
value={localFilters.priority || ''}
|
||||
onChange={(e) => setLocalFilters({ ...localFilters, priority: e.target.value })}
|
||||
>
|
||||
<option value="">Priority</option>
|
||||
<option value="0">None</option>
|
||||
@@ -237,19 +256,31 @@ export default function TicketsPage() {
|
||||
}}>
|
||||
<button
|
||||
className="btn btn-green"
|
||||
onClick={() => { setFilters(prev => ({ ...prev, type: ['Procurement'] })); handleApplyFilters(); }}
|
||||
onClick={() => {
|
||||
setLocalFilters(prev => ({ ...prev, type: 'Procurement' }))
|
||||
setFilters(prev => ({ ...prev, type: ['Procurement'] }))
|
||||
setTimeout(() => refresh(), 0)
|
||||
}}
|
||||
>
|
||||
Procurements
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-green"
|
||||
onClick={() => { setFilters(prev => ({ ...prev, priority: [4] })); handleApplyFilters(); }}
|
||||
onClick={() => {
|
||||
setLocalFilters(prev => ({ ...prev, priority: '4' }))
|
||||
setFilters(prev => ({ ...prev, priority: [4] }))
|
||||
setTimeout(() => refresh(), 0)
|
||||
}}
|
||||
>
|
||||
Criticals
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-green"
|
||||
onClick={() => { setFilters(prev => ({ ...prev, priority: [3] })); handleApplyFilters(); }}
|
||||
onClick={() => {
|
||||
setLocalFilters(prev => ({ ...prev, priority: '3' }))
|
||||
setFilters(prev => ({ ...prev, priority: [3] }))
|
||||
setTimeout(() => refresh(), 0)
|
||||
}}
|
||||
>
|
||||
Highs
|
||||
</button>
|
||||
@@ -261,13 +292,21 @@ export default function TicketsPage() {
|
||||
}}></div>
|
||||
<button
|
||||
className="btn btn-green"
|
||||
onClick={() => { setLimit(10); setFilters(prev => ({ ...prev, limit: 10 })) }}
|
||||
onClick={() => {
|
||||
setLimit(10)
|
||||
setFilters(prev => ({ ...prev, limit: 10 }))
|
||||
setTimeout(() => refresh(), 0)
|
||||
}}
|
||||
>
|
||||
10
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-green"
|
||||
onClick={() => { setLimit(25); setFilters(prev => ({ ...prev, limit: 25 })) }}
|
||||
onClick={() => {
|
||||
setLimit(25)
|
||||
setFilters(prev => ({ ...prev, limit: 25 }))
|
||||
setTimeout(() => refresh(), 0)
|
||||
}}
|
||||
>
|
||||
25
|
||||
</button>
|
||||
|
||||
232
src/utils/createDummyTicket.js
Normal file
232
src/utils/createDummyTicket.js
Normal file
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* 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]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user