Compare commits
6 Commits
5717612db5
...
test
| Author | SHA1 | Date | |
|---|---|---|---|
| 5fbb2fb4b5 | |||
| a4c64b5398 | |||
| cb110a184b | |||
| 99b89bcabe | |||
| 895c55399f | |||
| ee7c866616 |
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 { useState, useEffect } from 'react'
|
||||||
|
import { FaTimes } from 'react-icons/fa'
|
||||||
import { useAuth } from '../context/AuthContext'
|
import { useAuth } from '../context/AuthContext'
|
||||||
|
|
||||||
const SERVICE_TYPES = ['Remote', 'On Site', 'Off Site', 'COMMENT']
|
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
|
if (!isOpen || !workorder) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overlay" style={{
|
<div className="overlay">
|
||||||
width: '100%',
|
<span className="overlay-close" onClick={onClose}>
|
||||||
background: 'rgba(0,0,0,0.95)'
|
<FaTimes />
|
||||||
}}>
|
</span>
|
||||||
<a href="#" className="closebtn" onClick={(e) => { e.preventDefault(); onClose(); }} style={{
|
<div className="overlay-content">
|
||||||
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
<h2 className="mb-2">Create New Worksheet - WOID {workorder.woid}</h2>
|
||||||
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>
|
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="container">
|
<div className="bg-red text-white p-2 mb-2" style={{ borderRadius: '4px' }}>
|
||||||
<div className="row">
|
{error}
|
||||||
<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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="container">
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-1"> </div>
|
<div className="col col-6">
|
||||||
|
<div className="form-group">
|
||||||
{/* Linke Spalte */}
|
<label className="form-label">Service Type</label>
|
||||||
<div className="col-5">
|
|
||||||
<span className="text-left">Service Type</span><br />
|
|
||||||
<select
|
<select
|
||||||
className="form-select bg-dark text-white"
|
className="form-control"
|
||||||
value={formData.serviceType}
|
value={formData.serviceType}
|
||||||
onChange={(e) => handleChange('serviceType', e.target.value)}
|
onChange={(e) => handleChange('serviceType', e.target.value)}
|
||||||
required
|
required
|
||||||
@@ -236,11 +186,12 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
|||||||
<option key={type} value={type}>{type}</option>
|
<option key={type} value={type}>{type}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<br /><br />
|
</div>
|
||||||
|
|
||||||
<span className="text-left">New Status</span><br />
|
<div className="form-group">
|
||||||
|
<label className="form-label">New Status</label>
|
||||||
<select
|
<select
|
||||||
className="form-select bg-dark text-white"
|
className="form-control"
|
||||||
value={formData.newStatus}
|
value={formData.newStatus}
|
||||||
onChange={(e) => handleChange('newStatus', e.target.value)}
|
onChange={(e) => handleChange('newStatus', e.target.value)}
|
||||||
required
|
required
|
||||||
@@ -249,42 +200,40 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
|||||||
<option key={status} value={status}>{status}</option>
|
<option key={status} value={status}>{status}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<br /><br />
|
</div>
|
||||||
|
|
||||||
<span className="text-left">New Response Level</span><br />
|
<div className="form-group">
|
||||||
|
<label className="form-label">New Response Level</label>
|
||||||
<select
|
<select
|
||||||
className="form-select bg-dark text-white"
|
className="form-control"
|
||||||
value={formData.newResponseLevel}
|
value={formData.newResponseLevel}
|
||||||
onChange={(e) => handleChange('newResponseLevel', e.target.value)}
|
onChange={(e) => handleChange('newResponseLevel', e.target.value)}
|
||||||
>
|
>
|
||||||
<option value="">Select</option>
|
<option value="">Select Response Level</option>
|
||||||
{RESPONSE_LEVELS.map(level => (
|
{RESPONSE_LEVELS.map(level => (
|
||||||
<option key={level} value={level}>{level}</option>
|
<option key={level} value={level}>{level}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<br /><br />
|
</div>
|
||||||
|
|
||||||
<div className="form-check">
|
<div className="form-group">
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="form-check-input"
|
|
||||||
id="isComment"
|
|
||||||
checked={formData.isComment}
|
checked={formData.isComment}
|
||||||
onChange={(e) => handleChange('isComment', e.target.checked)}
|
onChange={(e) => handleChange('isComment', e.target.checked)}
|
||||||
/>
|
/>
|
||||||
<label className="form-check-label" htmlFor="isComment">
|
|
||||||
Nur Kommentar (keine Arbeitszeit)
|
Nur Kommentar (keine Arbeitszeit)
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Rechte Spalte */}
|
<div className="col col-6">
|
||||||
<div className="col-5">
|
<div className="form-group">
|
||||||
<span className="text-left">Total Time (Minuten)</span><br />
|
<label className="form-label">Total Time (Minuten)</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="form-control bg-dark text-white"
|
className="form-control"
|
||||||
min="0"
|
min="0"
|
||||||
step="15"
|
step="15"
|
||||||
value={formData.totalTime}
|
value={formData.totalTime}
|
||||||
@@ -292,145 +241,83 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
|||||||
disabled={formData.isComment}
|
disabled={formData.isComment}
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
/>
|
/>
|
||||||
<small className="text-muted">
|
<small style={{ color: '#a0aec0', fontSize: '12px' }}>
|
||||||
{autoCalculate && formData.startTime && formData.endTime
|
{autoCalculate && formData.startTime && formData.endTime
|
||||||
? '✓ Automatisch berechnet'
|
? '✓ Automatisch berechnet'
|
||||||
: 'Manuell eingeben'}
|
: 'Manuell eingeben'}
|
||||||
</small>
|
</small>
|
||||||
<br /><br />
|
</div>
|
||||||
|
|
||||||
<span className="text-left">Start Date (dd.mm.yyyy)</span><br />
|
<div className="form-group">
|
||||||
|
<label className="form-label">Start Date</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control bg-dark text-white"
|
className="form-control"
|
||||||
|
placeholder="dd.mm.yyyy"
|
||||||
value={formData.startDate}
|
value={formData.startDate}
|
||||||
onChange={(e) => handleChange('startDate', e.target.value)}
|
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
|
required
|
||||||
/>
|
/>
|
||||||
<br /><br />
|
</div>
|
||||||
|
|
||||||
<span className="text-left">End Date (dd.mm.yyyy)</span><br />
|
<div className="form-group">
|
||||||
|
<label className="form-label">End Date</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control bg-dark text-white"
|
className="form-control"
|
||||||
|
placeholder="dd.mm.yyyy"
|
||||||
value={formData.endDate}
|
value={formData.endDate}
|
||||||
onChange={(e) => handleChange('endDate', e.target.value)}
|
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
|
required
|
||||||
/>
|
/>
|
||||||
<br /><br />
|
</div>
|
||||||
|
|
||||||
<span className="text-left">Start Time (hhmm)</span><br />
|
<div className="form-group">
|
||||||
|
<label className="form-label">Start Time</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control bg-dark text-white"
|
className="form-control"
|
||||||
|
placeholder="hhmm"
|
||||||
value={formData.startTime}
|
value={formData.startTime}
|
||||||
onChange={(e) => handleChange('startTime', e.target.value)}
|
onChange={(e) => handleChange('startTime', e.target.value)}
|
||||||
pattern="[0-2][0-9][0-5][0-9]"
|
|
||||||
placeholder="1000"
|
|
||||||
maxLength="4"
|
maxLength="4"
|
||||||
/>
|
/>
|
||||||
<br /><br />
|
</div>
|
||||||
|
|
||||||
<span className="text-left">End Time (hhmm)</span><br />
|
<div className="form-group">
|
||||||
|
<label className="form-label">End Time</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control bg-dark text-white"
|
className="form-control"
|
||||||
|
placeholder="hhmm"
|
||||||
value={formData.endTime}
|
value={formData.endTime}
|
||||||
onChange={(e) => handleChange('endTime', e.target.value)}
|
onChange={(e) => handleChange('endTime', e.target.value)}
|
||||||
pattern="[0-2][0-9][0-5][0-9]"
|
|
||||||
placeholder="1030"
|
|
||||||
maxLength="4"
|
maxLength="4"
|
||||||
/>
|
/>
|
||||||
<br /><br />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-1"> </div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="container">
|
<div className="form-group">
|
||||||
<div className="row">
|
<label className="form-label">Action Details</label>
|
||||||
<div className="col-1"> </div>
|
|
||||||
<div className="col-10">
|
|
||||||
<span className="text-left">Action Details</span><br />
|
|
||||||
<textarea
|
<textarea
|
||||||
className="form-control bg-dark text-white"
|
className="form-control"
|
||||||
rows="10"
|
rows={5}
|
||||||
|
placeholder="Beschreibe die durchgeführten Arbeiten..."
|
||||||
value={formData.details}
|
value={formData.details}
|
||||||
onChange={(e) => handleChange('details', e.target.value)}
|
onChange={(e) => handleChange('details', e.target.value)}
|
||||||
placeholder="Beschreibe die durchgeführten Arbeiten..."
|
|
||||||
required
|
required
|
||||||
></textarea>
|
/>
|
||||||
</div>
|
|
||||||
<div className="col-1"> </div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="container">
|
<div className="text-center mt-2">
|
||||||
<div className="row">
|
|
||||||
<div className="col-1"> </div>
|
|
||||||
<div className="col-10 text-center">
|
|
||||||
<p> </p>
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btn btn-lg px-5 py-3 border-0"
|
className="btn btn-dark"
|
||||||
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}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{loading ? '⏳ Erstelle...' : '✨ CREATE NOW'}
|
{loading ? 'Creating...' : 'CREATE NOW'}
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
143
src/components/StatusHistoryModal.jsx
Normal file
143
src/components/StatusHistoryModal.jsx
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
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 { 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 { formatDistanceToNow, format } from 'date-fns'
|
||||||
import { de } from 'date-fns/locale'
|
import { de } from 'date-fns/locale'
|
||||||
import StatusDropdown from './StatusDropdown'
|
import StatusDropdown from './StatusDropdown'
|
||||||
@@ -7,6 +7,7 @@ import PriorityDropdown from './PriorityDropdown'
|
|||||||
import EditorDropdown from './EditorDropdown'
|
import EditorDropdown from './EditorDropdown'
|
||||||
import ResponseDropdown from './ResponseDropdown'
|
import ResponseDropdown from './ResponseDropdown'
|
||||||
import CreateWorksheetModal from './CreateWorksheetModal'
|
import CreateWorksheetModal from './CreateWorksheetModal'
|
||||||
|
import StatusHistoryModal from './StatusHistoryModal'
|
||||||
import WorksheetList from './WorksheetList'
|
import WorksheetList from './WorksheetList'
|
||||||
import WorksheetStats from './WorksheetStats'
|
import WorksheetStats from './WorksheetStats'
|
||||||
import { useWorksheets } from '../hooks/useWorksheets'
|
import { useWorksheets } from '../hooks/useWorksheets'
|
||||||
@@ -46,6 +47,7 @@ export default function TicketRow({ ticket, onUpdate, onExpand }) {
|
|||||||
const [expanded, setExpanded] = useState(false)
|
const [expanded, setExpanded] = useState(false)
|
||||||
const [locked, setLocked] = useState(true)
|
const [locked, setLocked] = useState(true)
|
||||||
const [showCreateWorksheet, setShowCreateWorksheet] = useState(false)
|
const [showCreateWorksheet, setShowCreateWorksheet] = useState(false)
|
||||||
|
const [showHistoryModal, setShowHistoryModal] = useState(false)
|
||||||
|
|
||||||
// Worksheets für dieses Ticket laden (nur wenn expanded)
|
// Worksheets für dieses Ticket laden (nur wenn expanded)
|
||||||
const {
|
const {
|
||||||
@@ -185,20 +187,72 @@ export default function TicketRow({ ticket, onUpdate, onExpand }) {
|
|||||||
border: '1px solid rgba(16, 185, 129, 0.2)',
|
border: '1px solid rgba(16, 185, 129, 0.2)',
|
||||||
borderTop: 'none'
|
borderTop: 'none'
|
||||||
}}>
|
}}>
|
||||||
<div className="card-header d-flex justify-content-between align-items-center" style={{
|
<div className="card-body" style={{ borderRadius: '0 0 12px 12px', padding: '20px' }}>
|
||||||
background: 'linear-gradient(135deg, #2d3748 0%, #1a202c 100%)',
|
{/* Bento Box Layout: 2 Spalten */}
|
||||||
color: 'white',
|
<div style={{
|
||||||
padding: '1rem 1.5rem',
|
display: 'grid',
|
||||||
borderRadius: 0,
|
gridTemplateColumns: '1fr 1fr',
|
||||||
borderBottom: '1px solid rgba(16, 185, 129, 0.2)'
|
gap: '20px',
|
||||||
|
alignItems: 'stretch'
|
||||||
}}>
|
}}>
|
||||||
<span className="fs-5 fw-bold">Details - WOID {ticket.woid || ticket.$id}</span>
|
{/* 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%'
|
||||||
|
}}>
|
||||||
|
<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
|
<button
|
||||||
className="btn btn-sm px-4 py-2 border-0 fw-bold"
|
className="btn"
|
||||||
style={{
|
style={{
|
||||||
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
transition: 'all 0.2s ease'
|
border: 'none',
|
||||||
|
padding: '12px 20px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
flex: 1
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.transform = 'translateY(-2px)'
|
e.currentTarget.style.transform = 'translateY(-2px)'
|
||||||
@@ -210,41 +264,63 @@ export default function TicketRow({ ticket, onUpdate, onExpand }) {
|
|||||||
}}
|
}}
|
||||||
onClick={() => setShowCreateWorksheet(true)}
|
onClick={() => setShowCreateWorksheet(true)}
|
||||||
>
|
>
|
||||||
<FaPlus className="me-2" /> Add Worksheet
|
<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>
|
</button>
|
||||||
</div>
|
</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'
|
|
||||||
}}>
|
|
||||||
<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
|
|
||||||
}}>
|
|
||||||
{ticket.details || 'Keine Details vorhanden.'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<h5 className="mt-4 mb-3">Worksheets (Arbeitsschritte)</h5>
|
|
||||||
|
|
||||||
{/* Statistiken */}
|
{/* Statistiken */}
|
||||||
{worksheets.length > 0 && (
|
{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} />
|
<WorksheetStats worksheets={worksheets} />
|
||||||
<hr />
|
</div>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Worksheet-Liste */}
|
{/* Gesamtarbeitszeit und Worksheet-Liste - 100% Breite unter dem Bento Box */}
|
||||||
|
<div style={{
|
||||||
|
marginTop: '20px',
|
||||||
|
width: '100%'
|
||||||
|
}}>
|
||||||
<WorksheetList
|
<WorksheetList
|
||||||
worksheets={worksheets}
|
worksheets={worksheets}
|
||||||
totalTime={getTotalTime()}
|
totalTime={getTotalTime()}
|
||||||
@@ -252,6 +328,7 @@ export default function TicketRow({ ticket, onUpdate, onExpand }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</>
|
</>
|
||||||
@@ -263,6 +340,13 @@ export default function TicketRow({ ticket, onUpdate, onExpand }) {
|
|||||||
workorder={ticket}
|
workorder={ticket}
|
||||||
onCreate={handleCreateWorksheet}
|
onCreate={handleCreateWorksheet}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<StatusHistoryModal
|
||||||
|
isOpen={showHistoryModal}
|
||||||
|
onClose={() => setShowHistoryModal(false)}
|
||||||
|
worksheets={worksheets}
|
||||||
|
ticket={ticket}
|
||||||
|
/>
|
||||||
<tr className="spacer">
|
<tr className="spacer">
|
||||||
<td colSpan={10} style={{ height: '12px', background: 'transparent', border: 'none' }}></td>
|
<td colSpan={10} style={{ height: '12px', background: 'transparent', border: 'none' }}></td>
|
||||||
</tr>
|
</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 }) {
|
export default function WorksheetList({ worksheets, totalTime, loading }) {
|
||||||
|
const [expandedWorksheets, setExpandedWorksheets] = useState({})
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center p-4">
|
<div className="text-center p-4">
|
||||||
@@ -34,41 +36,27 @@ export default function WorksheetList({ worksheets, totalTime, loading }) {
|
|||||||
return `${date} ${hours}:${mins}`
|
return `${date} ${hours}:${mins}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toggleWorksheet = (wsid) => {
|
||||||
|
setExpandedWorksheets(prev => ({
|
||||||
|
...prev,
|
||||||
|
[wsid]: !prev[wsid]
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="worksheet-list">
|
<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 */}
|
{/* Worksheet-Einträge */}
|
||||||
<div className="timeline">
|
<div className="timeline">
|
||||||
{worksheets.map((ws, index) => (
|
{worksheets.map((ws, index) => {
|
||||||
|
const isExpanded = expandedWorksheets[ws.wsid] || false
|
||||||
|
|
||||||
|
return (
|
||||||
<div key={ws.$id} className="timeline-item mb-4" style={{
|
<div key={ws.$id} className="timeline-item mb-4" style={{
|
||||||
animation: `fadeIn 0.5s ease-in-out ${index * 0.1}s backwards`
|
animation: `fadeIn 0.5s ease-in-out ${index * 0.1}s backwards`
|
||||||
}}>
|
}}>
|
||||||
<div className="card border-0 shadow-sm overflow-hidden" style={{
|
<div className="card border-0 shadow-sm overflow-hidden" style={{
|
||||||
borderLeft: ws.isComment ? '4px solid #10b981' : '4px solid #4a5568',
|
borderLeft: ws.isComment ? '4px solid #10b981' : '4px solid #4a5568',
|
||||||
|
borderRadius: '8px',
|
||||||
transition: 'transform 0.2s ease, box-shadow 0.2s ease'
|
transition: 'transform 0.2s ease, box-shadow 0.2s ease'
|
||||||
}} onMouseEnter={(e) => {
|
}} onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.transform = 'translateY(-2px)'
|
e.currentTarget.style.transform = 'translateY(-2px)'
|
||||||
@@ -77,13 +65,37 @@ export default function WorksheetList({ worksheets, totalTime, loading }) {
|
|||||||
e.currentTarget.style.transform = 'translateY(0)'
|
e.currentTarget.style.transform = 'translateY(0)'
|
||||||
e.currentTarget.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)'
|
e.currentTarget.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)'
|
||||||
}}>
|
}}>
|
||||||
<div className="card-header d-flex justify-content-between align-items-center py-3" style={{
|
{/* 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
|
background: ws.isComment
|
||||||
? 'linear-gradient(135deg, #10b981 0%, #059669 100%)'
|
? 'linear-gradient(135deg, #10b981 0%, #059669 100%)'
|
||||||
: 'linear-gradient(135deg, #4a5568 0%, #2d3748 100%)',
|
: 'linear-gradient(135deg, #4a5568 0%, #2d3748 100%)',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
border: 'none'
|
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>
|
<div>
|
||||||
<strong className="fs-6">WSID {ws.wsid}</strong>
|
<strong className="fs-6">WSID {ws.wsid}</strong>
|
||||||
{ws.isComment && (
|
{ws.isComment && (
|
||||||
@@ -94,12 +106,39 @@ export default function WorksheetList({ worksheets, totalTime, loading }) {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{/* 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>
|
||||||
<small style={{ opacity: 0.9 }}>
|
<small style={{ opacity: 0.9 }}>
|
||||||
{formatDateTime(ws.startDate, ws.startTime)}
|
{formatDateTime(ws.startDate, ws.startTime)}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card-body p-4">
|
{/* Body - Nur wenn expanded */}
|
||||||
|
{isExpanded && (
|
||||||
|
<div
|
||||||
|
className="card-body p-4"
|
||||||
|
style={{
|
||||||
|
animation: 'slideDown 0.3s ease-out'
|
||||||
|
}}
|
||||||
|
>
|
||||||
{/* Mitarbeiter & Zeit */}
|
{/* Mitarbeiter & Zeit */}
|
||||||
<div className="row mb-3">
|
<div className="row mb-3">
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
@@ -166,9 +205,11 @@ export default function WorksheetList({ worksheets, totalTime, loading }) {
|
|||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>{`
|
<style>{`
|
||||||
@@ -182,6 +223,18 @@ export default function WorksheetList({ worksheets, totalTime, loading }) {
|
|||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@keyframes slideDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
max-height: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
max-height: 1000px;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
</div>
|
</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 }) {
|
export default function WorksheetStats({ worksheets }) {
|
||||||
if (!worksheets || worksheets.length === 0) {
|
if (!worksheets || worksheets.length === 0) {
|
||||||
@@ -28,16 +28,6 @@ export default function WorksheetStats({ worksheets }) {
|
|||||||
return acc
|
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
|
// Service Type Verteilung
|
||||||
const byServiceType = worksheets.reduce((acc, ws) => {
|
const byServiceType = worksheets.reduce((acc, ws) => {
|
||||||
@@ -53,160 +43,270 @@ export default function WorksheetStats({ worksheets }) {
|
|||||||
return hours > 0 ? `${hours}h ${mins}min` : `${mins}min`
|
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 (
|
return (
|
||||||
<div className="worksheet-stats mb-4">
|
<div className="worksheet-stats" style={{
|
||||||
<div className="row g-4">
|
display: 'flex',
|
||||||
{/* Gesamtübersicht */}
|
flexDirection: 'column',
|
||||||
<div className="col-lg-4 col-md-6">
|
gap: '16px',
|
||||||
<div className="card h-100 border-0 shadow-sm" style={{
|
height: '100%'
|
||||||
background: 'linear-gradient(135deg, #2d3748 0%, #1a202c 100%)',
|
|
||||||
color: 'white'
|
|
||||||
}}>
|
}}>
|
||||||
<div className="card-body p-4">
|
{/* Gesamtübersicht */}
|
||||||
<h6 className="card-title mb-3 d-flex align-items-center">
|
<div style={{
|
||||||
<FaChartLine className="me-2" size={20} style={{ color: '#4ade80' }} />
|
background: 'rgba(45, 55, 72, 0.5)',
|
||||||
<strong>Gesamtübersicht</strong>
|
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>
|
</h6>
|
||||||
<div className="mt-3">
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
|
||||||
<div className="d-flex justify-content-between align-items-center mb-3 pb-2" style={{ borderBottom: '1px solid rgba(255,255,255,0.2)' }}>
|
<div style={{
|
||||||
<span style={{ opacity: 0.9 }}>Worksheets:</span>
|
display: 'flex',
|
||||||
<strong className="fs-5">{worksheets.length}</strong>
|
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' }}>Worksheets:</span>
|
||||||
|
<strong style={{ color: 'var(--dark-text)', fontSize: '16px' }}>{worksheets.length}</strong>
|
||||||
</div>
|
</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)' }}>
|
<div style={{
|
||||||
<span style={{ opacity: 0.9 }}>Arbeitszeit:</span>
|
display: 'flex',
|
||||||
<strong className="fs-5">{formatTime(totalMinutes)}</strong>
|
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' }}>Arbeitszeit:</span>
|
||||||
|
<strong style={{ color: '#10b981', fontSize: '16px' }}>{formatTime(totalMinutes)}</strong>
|
||||||
</div>
|
</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)' }}>
|
<div style={{
|
||||||
<span style={{ opacity: 0.9 }}>Kommentare:</span>
|
display: 'flex',
|
||||||
<strong className="fs-5">{worksheets.filter(ws => ws.isComment).length}</strong>
|
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' }}>Kommentare:</span>
|
||||||
|
<strong style={{ color: 'var(--dark-text)', fontSize: '16px' }}>{worksheets.filter(ws => ws.isComment).length}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="d-flex justify-content-between align-items-center">
|
<div style={{
|
||||||
<span style={{ opacity: 0.9 }}>Ø pro Worksheet:</span>
|
display: 'flex',
|
||||||
<strong className="fs-5">
|
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)))}
|
{formatTime(Math.round(totalMinutes / (worksheets.filter(ws => !ws.isComment).length || 1)))}
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Nach Mitarbeiter */}
|
{/* Mitarbeiter Kombiniertes Diagramm */}
|
||||||
<div className="col-lg-4 col-md-6">
|
<div style={{
|
||||||
<div className="card h-100 border-0 shadow-sm" style={{
|
background: 'rgba(45, 55, 72, 0.5)',
|
||||||
background: 'linear-gradient(135deg, #22c55e 0%, #10b981 100%)',
|
borderRadius: '12px',
|
||||||
color: 'white'
|
padding: '16px',
|
||||||
|
border: '1px solid rgba(16, 185, 129, 0.2)',
|
||||||
|
flex: '1 1 auto',
|
||||||
|
minHeight: '200px'
|
||||||
}}>
|
}}>
|
||||||
<div className="card-body p-4">
|
<h6 style={{
|
||||||
<h6 className="card-title mb-3 d-flex align-items-center">
|
color: 'var(--dark-text)',
|
||||||
<FaUsers className="me-2" size={20} />
|
marginBottom: '16px',
|
||||||
<strong>Nach Mitarbeiter</strong>
|
fontSize: '14px',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '8px'
|
||||||
|
}}>
|
||||||
|
<FaUsers size={16} style={{ color: '#10b981' }} />
|
||||||
|
Mitarbeiter-Statistiken
|
||||||
</h6>
|
</h6>
|
||||||
<div className="mt-3">
|
|
||||||
{Object.values(byEmployee).map((emp, idx) => (
|
{Object.keys(byEmployee).length === 0 ? (
|
||||||
<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 style={{ color: '#a0aec0', textAlign: 'center', padding: '20px' }}>
|
||||||
<div className="d-flex justify-content-between align-items-center">
|
Keine Mitarbeiter-Daten verfügbar
|
||||||
<div>
|
</div>
|
||||||
<strong className="d-block">{emp.name}</strong>
|
) : (
|
||||||
|
<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 && (
|
{emp.short && (
|
||||||
<span className="badge mt-1" style={{
|
<span style={{
|
||||||
background: 'rgba(255,255,255,0.25)'
|
background: 'rgba(16, 185, 129, 0.2)',
|
||||||
|
color: '#10b981',
|
||||||
|
padding: '2px 6px',
|
||||||
|
borderRadius: '3px',
|
||||||
|
fontSize: '9px',
|
||||||
|
fontWeight: 'bold'
|
||||||
}}>{emp.short}</span>
|
}}>{emp.short}</span>
|
||||||
)}
|
)}
|
||||||
|
<span style={{ maxWidth: '100px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||||
|
{emp.name}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-end">
|
<div style={{
|
||||||
<div className="fs-5 fw-bold">{formatTime(emp.time)}</div>
|
width: '100%',
|
||||||
<small style={{ opacity: 0.8 }}>{emp.count} WS</small>
|
height: '28px',
|
||||||
</div>
|
background: 'rgba(26, 32, 44, 0.6)',
|
||||||
</div>
|
borderRadius: '6px',
|
||||||
</div>
|
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>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Service Type Verteilung */}
|
{/* Service Type Verteilung */}
|
||||||
<div className="col-lg-4 col-md-6">
|
<div style={{
|
||||||
<div className="card h-100 border-0 shadow-sm" style={{
|
background: 'rgba(45, 55, 72, 0.5)',
|
||||||
background: 'linear-gradient(135deg, #4a5568 0%, #2d3748 100%)',
|
borderRadius: '12px',
|
||||||
color: 'white'
|
padding: '16px',
|
||||||
|
border: '1px solid rgba(16, 185, 129, 0.2)',
|
||||||
|
flex: '0 0 auto'
|
||||||
}}>
|
}}>
|
||||||
<div className="card-body p-4">
|
<h6 style={{
|
||||||
<h6 className="card-title mb-3 d-flex align-items-center">
|
color: 'var(--dark-text)',
|
||||||
<FaClock className="me-2" size={20} style={{ color: '#4ade80' }} />
|
marginBottom: '12px',
|
||||||
<strong>Service Types</strong>
|
fontSize: '14px',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '8px'
|
||||||
|
}}>
|
||||||
|
<FaClock size={16} style={{ color: '#10b981' }} />
|
||||||
|
Service Types
|
||||||
</h6>
|
</h6>
|
||||||
<div className="mt-3">
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||||
{Object.entries(byServiceType).map(([type, count], idx) => (
|
{Object.entries(byServiceType).map(([type, count]) => (
|
||||||
<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' }}>
|
<div key={type} style={{
|
||||||
<span className="badge px-3 py-2" style={{
|
display: 'flex',
|
||||||
background: 'rgba(255,255,255,0.25)',
|
justifyContent: 'space-between',
|
||||||
fontSize: '0.9rem'
|
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>
|
}}>{type}</span>
|
||||||
<strong className="fs-5">{count}</strong>
|
<strong style={{
|
||||||
|
color: '#10b981',
|
||||||
|
fontSize: '14px'
|
||||||
|
}}>{count}</strong>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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%)'
|
|
||||||
}}>
|
|
||||||
<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>
|
|
||||||
</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
|
const DEMO_MODE = !import.meta.env.VITE_APPWRITE_PROJECT_ID
|
||||||
|
|
||||||
// Demo data for testing without Appwrite
|
// Demo data for testing without Appwrite
|
||||||
|
const lastWeek = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
|
||||||
|
|
||||||
const DEMO_WORKORDERS = [
|
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: '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: '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() },
|
{ $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
|
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 = [
|
const DEMO_WORKSHEETS = [
|
||||||
{
|
{
|
||||||
$id: '1',
|
$id: 'ws-10001-001',
|
||||||
wsid: '100001',
|
wsid: '100001',
|
||||||
woid: '10001',
|
woid: '10001',
|
||||||
workorderId: '1',
|
workorderId: 'dummy-10001',
|
||||||
employeeId: 'emp1',
|
employeeId: 'user-max-id',
|
||||||
employeeName: 'Max Müller',
|
employeeName: 'Max Mustermann',
|
||||||
employeeShort: 'MAMU',
|
employeeShort: 'MM',
|
||||||
serviceType: 'Remote',
|
serviceType: 'Remote',
|
||||||
oldStatus: 'Open',
|
oldStatus: 'Open',
|
||||||
newStatus: 'Occupied',
|
newStatus: 'Occupied',
|
||||||
|
oldResponseLevel: '',
|
||||||
|
newResponseLevel: '24/7',
|
||||||
totalTime: 30,
|
totalTime: 30,
|
||||||
startDate: '29.12.2025',
|
startDate: '23.12.2025',
|
||||||
startTime: '1000',
|
startTime: '0800',
|
||||||
endDate: '29.12.2025',
|
endDate: '23.12.2025',
|
||||||
endTime: '1030',
|
endTime: '0830',
|
||||||
details: 'Router neu gestartet',
|
details: 'Erste Analyse durchgeführt. Server komplett offline. Keine Remote-Verbindung möglich. Vor-Ort-Einsatz erforderlich.',
|
||||||
isComment: false,
|
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) {
|
export function useWorksheets(woid = null) {
|
||||||
|
|||||||
@@ -9,12 +9,23 @@ import QuickOverviewModal from '../components/QuickOverviewModal'
|
|||||||
|
|
||||||
export default function TicketsPage() {
|
export default function TicketsPage() {
|
||||||
const [limit, setLimit] = useState(10)
|
const [limit, setLimit] = useState(10)
|
||||||
|
// Aktive Filter (werden für API-Calls verwendet)
|
||||||
const [filters, setFilters] = useState({
|
const [filters, setFilters] = useState({
|
||||||
status: ['Open', 'Occupied', 'Assigned', 'Awaiting', 'Added Info'],
|
status: ['Open', 'Occupied', 'Assigned', 'Awaiting', 'Added Info'],
|
||||||
type: [],
|
type: [],
|
||||||
priority: [],
|
priority: [],
|
||||||
limit: 10
|
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 { workorders, loading, error, refresh, updateWorkorder, createWorkorder } = useWorkorders(filters)
|
||||||
const { customers } = useCustomers()
|
const { customers } = useCustomers()
|
||||||
@@ -23,17 +34,25 @@ export default function TicketsPage() {
|
|||||||
const [showOverviewModal, setShowOverviewModal] = useState(false)
|
const [showOverviewModal, setShowOverviewModal] = useState(false)
|
||||||
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false)
|
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false)
|
||||||
|
|
||||||
const handleFilterChange = (newFilters) => {
|
|
||||||
setFilters({ ...newFilters, limit })
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleApplyFilters = () => {
|
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 handleLimitChange = (e) => {
|
||||||
const newLimit = parseInt(e.target.value)
|
const newLimit = parseInt(e.target.value)
|
||||||
setLimit(newLimit)
|
setLimit(newLimit)
|
||||||
|
// Limit-Änderung wird sofort angewendet (kein Apply nötig)
|
||||||
setFilters(prev => ({ ...prev, limit: newLimit }))
|
setFilters(prev => ({ ...prev, limit: newLimit }))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,24 +148,24 @@ export default function TicketsPage() {
|
|||||||
placeholder="WOID"
|
placeholder="WOID"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
style={{ margin: 0 }}
|
style={{ margin: 0 }}
|
||||||
value={filters.woid || ''}
|
value={localFilters.woid || ''}
|
||||||
onChange={(e) => setFilters({ ...filters, woid: e.target.value })}
|
onChange={(e) => setLocalFilters({ ...localFilters, woid: e.target.value })}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Customer"
|
placeholder="Customer"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
style={{ margin: 0 }}
|
style={{ margin: 0 }}
|
||||||
value={filters.customer || ''}
|
value={localFilters.customer || ''}
|
||||||
onChange={(e) => setFilters({ ...filters, customer: e.target.value })}
|
onChange={(e) => setLocalFilters({ ...localFilters, customer: e.target.value })}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="User"
|
placeholder="User"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
style={{ margin: 0 }}
|
style={{ margin: 0 }}
|
||||||
value={filters.userTopic || ''}
|
value={localFilters.userTopic || ''}
|
||||||
onChange={(e) => setFilters({ ...filters, userTopic: e.target.value })}
|
onChange={(e) => setLocalFilters({ ...localFilters, userTopic: e.target.value })}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
className="btn btn-green"
|
className="btn btn-green"
|
||||||
@@ -176,14 +195,14 @@ export default function TicketsPage() {
|
|||||||
placeholder="Created Date"
|
placeholder="Created Date"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
style={{ margin: 0 }}
|
style={{ margin: 0 }}
|
||||||
value={filters.createdDate || ''}
|
value={localFilters.createdDate || ''}
|
||||||
onChange={(e) => setFilters({ ...filters, createdDate: e.target.value })}
|
onChange={(e) => setLocalFilters({ ...localFilters, createdDate: e.target.value })}
|
||||||
/>
|
/>
|
||||||
<select
|
<select
|
||||||
className="form-control"
|
className="form-control"
|
||||||
style={{ margin: 0 }}
|
style={{ margin: 0 }}
|
||||||
value={filters.type?.[0] || ''}
|
value={localFilters.type || ''}
|
||||||
onChange={(e) => setFilters({ ...filters, type: e.target.value ? [e.target.value] : [] })}
|
onChange={(e) => setLocalFilters({ ...localFilters, type: e.target.value })}
|
||||||
>
|
>
|
||||||
<option value="">Type / Location</option>
|
<option value="">Type / Location</option>
|
||||||
<option>Home Office</option>
|
<option>Home Office</option>
|
||||||
@@ -199,8 +218,8 @@ export default function TicketsPage() {
|
|||||||
<select
|
<select
|
||||||
className="form-control"
|
className="form-control"
|
||||||
style={{ margin: 0 }}
|
style={{ margin: 0 }}
|
||||||
value={filters.system?.[0] || ''}
|
value={localFilters.system || ''}
|
||||||
onChange={(e) => setFilters({ ...filters, system: e.target.value ? [e.target.value] : [] })}
|
onChange={(e) => setLocalFilters({ ...localFilters, system: e.target.value })}
|
||||||
>
|
>
|
||||||
<option value="">System</option>
|
<option value="">System</option>
|
||||||
<option>Client</option>
|
<option>Client</option>
|
||||||
@@ -214,8 +233,8 @@ export default function TicketsPage() {
|
|||||||
<select
|
<select
|
||||||
className="form-control"
|
className="form-control"
|
||||||
style={{ margin: 0 }}
|
style={{ margin: 0 }}
|
||||||
value={filters.priority?.[0] ?? ''}
|
value={localFilters.priority || ''}
|
||||||
onChange={(e) => setFilters({ ...filters, priority: e.target.value ? [parseInt(e.target.value)] : [] })}
|
onChange={(e) => setLocalFilters({ ...localFilters, priority: e.target.value })}
|
||||||
>
|
>
|
||||||
<option value="">Priority</option>
|
<option value="">Priority</option>
|
||||||
<option value="0">None</option>
|
<option value="0">None</option>
|
||||||
@@ -237,19 +256,31 @@ export default function TicketsPage() {
|
|||||||
}}>
|
}}>
|
||||||
<button
|
<button
|
||||||
className="btn btn-green"
|
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
|
Procurements
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="btn btn-green"
|
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
|
Criticals
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="btn btn-green"
|
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
|
Highs
|
||||||
</button>
|
</button>
|
||||||
@@ -261,13 +292,21 @@ export default function TicketsPage() {
|
|||||||
}}></div>
|
}}></div>
|
||||||
<button
|
<button
|
||||||
className="btn btn-green"
|
className="btn btn-green"
|
||||||
onClick={() => { setLimit(10); setFilters(prev => ({ ...prev, limit: 10 })) }}
|
onClick={() => {
|
||||||
|
setLimit(10)
|
||||||
|
setFilters(prev => ({ ...prev, limit: 10 }))
|
||||||
|
setTimeout(() => refresh(), 0)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
10
|
10
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="btn btn-green"
|
className="btn btn-green"
|
||||||
onClick={() => { setLimit(25); setFilters(prev => ({ ...prev, limit: 25 })) }}
|
onClick={() => {
|
||||||
|
setLimit(25)
|
||||||
|
setFilters(prev => ({ ...prev, limit: 25 }))
|
||||||
|
setTimeout(() => refresh(), 0)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
25
|
25
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
233
src/utils/createDummyTicket.js
Normal file
233
src/utils/createDummyTicket.js
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
/**
|
||||||
|
* 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