wsid update
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
141
src/components/StatusHistoryModal.jsx
Normal file
141
src/components/StatusHistoryModal.jsx
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import { FaTimes } from 'react-icons/fa'
|
||||||
|
import { format } from 'date-fns'
|
||||||
|
|
||||||
|
export default function StatusHistoryModal({ isOpen, onClose, worksheets, ticket }) {
|
||||||
|
if (!isOpen) return null
|
||||||
|
|
||||||
|
// Extrahiere Status-Änderungen aus Worksheets
|
||||||
|
const statusHistory = worksheets
|
||||||
|
.filter(ws => ws.oldStatus && ws.newStatus && ws.oldStatus !== ws.newStatus)
|
||||||
|
.map(ws => ({
|
||||||
|
date: ws.startDate,
|
||||||
|
time: ws.startTime,
|
||||||
|
from: ws.oldStatus,
|
||||||
|
to: ws.newStatus,
|
||||||
|
employee: ws.employeeName || ws.employeeShort || 'Unknown',
|
||||||
|
details: ws.details,
|
||||||
|
wsid: ws.wsid
|
||||||
|
}))
|
||||||
|
.sort((a, b) => {
|
||||||
|
// Sortiere nach Datum und Zeit (älteste zuerst)
|
||||||
|
const dateA = `${a.date} ${a.time || '0000'}`
|
||||||
|
const dateB = `${b.date} ${b.time || '0000'}`
|
||||||
|
return dateA.localeCompare(dateB)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Füge aktuellen Status hinzu
|
||||||
|
const currentStatus = ticket?.status || 'Open'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="overlay">
|
||||||
|
<span className="overlay-close" onClick={onClose}>
|
||||||
|
<FaTimes />
|
||||||
|
</span>
|
||||||
|
<div className="overlay-content">
|
||||||
|
<h2 className="mb-2">Status History - WOID {ticket?.woid || ticket?.$id}</h2>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '24px' }}>
|
||||||
|
<div style={{
|
||||||
|
background: 'rgba(45, 55, 72, 0.95)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '16px',
|
||||||
|
border: '1px solid rgba(16, 185, 129, 0.2)',
|
||||||
|
marginBottom: '16px'
|
||||||
|
}}>
|
||||||
|
<strong style={{ color: 'var(--green-primary)' }}>Current Status:</strong>
|
||||||
|
<span style={{
|
||||||
|
marginLeft: '12px',
|
||||||
|
padding: '4px 12px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
background: 'rgba(16, 185, 129, 0.2)',
|
||||||
|
color: 'var(--dark-text)',
|
||||||
|
fontWeight: 'bold'
|
||||||
|
}}>
|
||||||
|
{currentStatus}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{statusHistory.length === 0 ? (
|
||||||
|
<div style={{
|
||||||
|
background: 'rgba(45, 55, 72, 0.95)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '24px',
|
||||||
|
textAlign: 'center',
|
||||||
|
color: '#a0aec0'
|
||||||
|
}}>
|
||||||
|
No status changes recorded yet.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||||
|
{statusHistory.map((entry, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
style={{
|
||||||
|
background: 'rgba(45, 55, 72, 0.95)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '16px',
|
||||||
|
border: '1px solid rgba(16, 185, 129, 0.2)',
|
||||||
|
borderLeft: '4px solid var(--green-primary)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start', marginBottom: '8px' }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '14px', color: '#a0aec0', marginBottom: '4px' }}>
|
||||||
|
{entry.date} {entry.time ? `${entry.time.substring(0, 2)}:${entry.time.substring(2, 4)}` : ''}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||||
|
<span style={{
|
||||||
|
padding: '4px 8px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
background: 'rgba(74, 85, 104, 0.5)',
|
||||||
|
color: 'var(--dark-text)',
|
||||||
|
fontSize: '12px'
|
||||||
|
}}>
|
||||||
|
{entry.from}
|
||||||
|
</span>
|
||||||
|
<span style={{ color: 'var(--green-primary)' }}>→</span>
|
||||||
|
<span style={{
|
||||||
|
padding: '4px 8px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
background: 'rgba(16, 185, 129, 0.3)',
|
||||||
|
color: 'var(--dark-text)',
|
||||||
|
fontSize: '12px',
|
||||||
|
fontWeight: 'bold'
|
||||||
|
}}>
|
||||||
|
{entry.to}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'right' }}>
|
||||||
|
<div style={{ fontSize: '12px', color: '#a0aec0' }}>by</div>
|
||||||
|
<div style={{ fontWeight: 'bold', color: 'var(--dark-text)' }}>{entry.employee}</div>
|
||||||
|
{entry.wsid && (
|
||||||
|
<div style={{ fontSize: '11px', color: '#718096', marginTop: '4px' }}>
|
||||||
|
WSID: {entry.wsid}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{entry.details && (
|
||||||
|
<div style={{
|
||||||
|
marginTop: '12px',
|
||||||
|
padding: '12px',
|
||||||
|
background: 'rgba(31, 41, 55, 0.6)',
|
||||||
|
borderRadius: '6px',
|
||||||
|
fontSize: '13px',
|
||||||
|
color: '#cbd5e0',
|
||||||
|
borderLeft: '3px solid rgba(16, 185, 129, 0.4)'
|
||||||
|
}}>
|
||||||
|
{entry.details}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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) {
|
||||||
|
|||||||
231
src/utils/createDummyTicket.js
Normal file
231
src/utils/createDummyTicket.js
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
/**
|
||||||
|
* Erstellt ein vollständiges Dummy-Ticket mit WOID 10001
|
||||||
|
* Zeigt alle möglichen Funktionen, Felder und Kombinationen
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function createDummyTicket10001() {
|
||||||
|
const now = new Date()
|
||||||
|
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000)
|
||||||
|
const lastWeek = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
|
||||||
|
|
||||||
|
// Haupt-Ticket mit allen Feldern ausgefüllt
|
||||||
|
const dummyTicket = {
|
||||||
|
$id: 'dummy-10001',
|
||||||
|
woid: '10001',
|
||||||
|
topic: 'Kompletter Systemausfall - Server & Netzwerk',
|
||||||
|
status: 'Assigned',
|
||||||
|
priority: 4, // Critical
|
||||||
|
type: 'Emergency Call',
|
||||||
|
systemType: 'Server',
|
||||||
|
responseLevel: '24/7',
|
||||||
|
serviceType: 'On Site',
|
||||||
|
customerId: 'customer-a-id',
|
||||||
|
customerName: 'Kunde A',
|
||||||
|
customerLocation: 'Hauptstraße 123, 12345 Musterstadt',
|
||||||
|
assignedTo: 'user-max-id', // Max Mustermann
|
||||||
|
requestedBy: 'Dr. Anna Schmidt',
|
||||||
|
requestedFor: 'IT-Abteilung Kunde A',
|
||||||
|
startDate: '30.12.2025',
|
||||||
|
startTime: '0800',
|
||||||
|
deadline: '31.12.2025',
|
||||||
|
endTime: '1800',
|
||||||
|
estimate: '480',
|
||||||
|
mailCopyTo: 'admin@kunde-a.de, it@kunde-a.de',
|
||||||
|
sendNotification: true,
|
||||||
|
details: `KRITISCHER SYSTEMAUSFALL - SOFORTIGE BEARBEITUNG ERFORDERLICH
|
||||||
|
|
||||||
|
Problembeschreibung:
|
||||||
|
- Kompletter Serverausfall im Rechenzentrum
|
||||||
|
- Alle Server sind offline (keine Verbindung möglich)
|
||||||
|
- Netzwerk-Infrastruktur betroffen
|
||||||
|
- Keine Backup-Systeme verfügbar
|
||||||
|
|
||||||
|
Betroffene Systeme:
|
||||||
|
- Hauptserver (Windows Server 2022)
|
||||||
|
- Datenbankserver (SQL Server 2019)
|
||||||
|
- Fileserver
|
||||||
|
- Exchange Server
|
||||||
|
- Netzwerk-Switches
|
||||||
|
|
||||||
|
Auswirkungen:
|
||||||
|
- Keine E-Mail-Kommunikation möglich
|
||||||
|
- Alle Anwendungen offline
|
||||||
|
- Kein Zugriff auf Datenbanken
|
||||||
|
- Produktion steht still
|
||||||
|
|
||||||
|
Dringlichkeit: KRITISCH - Produktionsausfall
|
||||||
|
|
||||||
|
Erwartete Bearbeitungszeit: 8 Stunden
|
||||||
|
Benötigte Ressourcen: 2 Techniker, Hardware-Ersatzteile`,
|
||||||
|
approvalStatus: 'approved',
|
||||||
|
createdAt: lastWeek.toISOString(),
|
||||||
|
$createdAt: lastWeek.toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mehrere Worksheets mit verschiedenen Status-Änderungen und Benutzern
|
||||||
|
const dummyWorksheets = [
|
||||||
|
{
|
||||||
|
$id: 'ws-10001-001',
|
||||||
|
wsid: '100001',
|
||||||
|
woid: '10001',
|
||||||
|
workorderId: 'dummy-10001',
|
||||||
|
employeeId: 'user-max-id',
|
||||||
|
employeeName: 'Max Mustermann',
|
||||||
|
employeeShort: 'MM',
|
||||||
|
serviceType: 'Remote',
|
||||||
|
oldStatus: 'Open',
|
||||||
|
newStatus: 'Occupied',
|
||||||
|
oldResponseLevel: '',
|
||||||
|
newResponseLevel: '24/7',
|
||||||
|
totalTime: 30,
|
||||||
|
startDate: '23.12.2025',
|
||||||
|
startTime: '0800',
|
||||||
|
endDate: '23.12.2025',
|
||||||
|
endTime: '0830',
|
||||||
|
details: 'Erste Analyse durchgeführt. Server komplett offline. Keine Remote-Verbindung möglich. Vor-Ort-Einsatz erforderlich.',
|
||||||
|
isComment: false,
|
||||||
|
createdAt: yesterday.toISOString(),
|
||||||
|
$createdAt: yesterday.toISOString()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$id: 'ws-10001-002',
|
||||||
|
wsid: '100002',
|
||||||
|
woid: '10001',
|
||||||
|
workorderId: 'dummy-10001',
|
||||||
|
employeeId: 'user-lisa-id',
|
||||||
|
employeeName: 'Lisa Schneider',
|
||||||
|
employeeShort: 'LS',
|
||||||
|
serviceType: 'On Site',
|
||||||
|
oldStatus: 'Occupied',
|
||||||
|
newStatus: 'Assigned',
|
||||||
|
oldResponseLevel: '24/7',
|
||||||
|
newResponseLevel: '24/7',
|
||||||
|
totalTime: 120,
|
||||||
|
startDate: '23.12.2025',
|
||||||
|
startTime: '1000',
|
||||||
|
endDate: '23.12.2025',
|
||||||
|
endTime: '1200',
|
||||||
|
details: 'Vor-Ort-Einsatz: Hardware-Check durchgeführt. Netzteil des Hauptservers defekt. Ersatzteil bestellt. Notfall-Backup-Server gestartet.',
|
||||||
|
isComment: false,
|
||||||
|
createdAt: yesterday.toISOString(),
|
||||||
|
$createdAt: yesterday.toISOString()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$id: 'ws-10001-003',
|
||||||
|
wsid: '100003',
|
||||||
|
woid: '10001',
|
||||||
|
workorderId: 'dummy-10001',
|
||||||
|
employeeId: 'user-tom-id',
|
||||||
|
employeeName: 'Tom Klein',
|
||||||
|
employeeShort: 'TK',
|
||||||
|
serviceType: 'On Site',
|
||||||
|
oldStatus: 'Assigned',
|
||||||
|
newStatus: 'Assigned',
|
||||||
|
oldResponseLevel: '24/7',
|
||||||
|
newResponseLevel: '24/7',
|
||||||
|
totalTime: 0,
|
||||||
|
startDate: '24.12.2025',
|
||||||
|
startTime: '1400',
|
||||||
|
endDate: '24.12.2025',
|
||||||
|
endTime: '1400',
|
||||||
|
details: 'Warte auf Ersatzteil-Lieferung. Kunde informiert. Backup-System läuft stabil.',
|
||||||
|
isComment: true,
|
||||||
|
createdAt: new Date(yesterday.getTime() - 12 * 60 * 60 * 1000).toISOString(),
|
||||||
|
$createdAt: new Date(yesterday.getTime() - 12 * 60 * 60 * 1000).toISOString()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$id: 'ws-10001-004',
|
||||||
|
wsid: '100004',
|
||||||
|
woid: '10001',
|
||||||
|
workorderId: 'dummy-10001',
|
||||||
|
employeeId: 'user-max-id',
|
||||||
|
employeeName: 'Max Mustermann',
|
||||||
|
employeeShort: 'MM',
|
||||||
|
serviceType: 'On Site',
|
||||||
|
oldStatus: 'Assigned',
|
||||||
|
newStatus: 'In Test',
|
||||||
|
oldResponseLevel: '24/7',
|
||||||
|
newResponseLevel: '24/7',
|
||||||
|
totalTime: 180,
|
||||||
|
startDate: '25.12.2025',
|
||||||
|
startTime: '0900',
|
||||||
|
endDate: '25.12.2025',
|
||||||
|
endTime: '1200',
|
||||||
|
details: 'Ersatzteil eingebaut. Server gestartet. Alle Dienste wiederhergestellt. System-Tests durchgeführt. Datenbank-Verbindungen geprüft.',
|
||||||
|
isComment: false,
|
||||||
|
createdAt: new Date(yesterday.getTime() - 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
$createdAt: new Date(yesterday.getTime() - 24 * 60 * 60 * 1000).toISOString()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$id: 'ws-10001-005',
|
||||||
|
wsid: '100005',
|
||||||
|
woid: '10001',
|
||||||
|
workorderId: 'dummy-10001',
|
||||||
|
employeeId: 'user-lisa-id',
|
||||||
|
employeeName: 'Lisa Schneider',
|
||||||
|
employeeShort: 'LS',
|
||||||
|
serviceType: 'Remote',
|
||||||
|
oldStatus: 'In Test',
|
||||||
|
newStatus: 'Awaiting',
|
||||||
|
oldResponseLevel: '24/7',
|
||||||
|
newResponseLevel: 'Support',
|
||||||
|
totalTime: 45,
|
||||||
|
startDate: '26.12.2025',
|
||||||
|
startTime: '1000',
|
||||||
|
endDate: '26.12.2025',
|
||||||
|
endTime: '1045',
|
||||||
|
details: 'Remote-Monitoring eingerichtet. Warte auf Kunden-Feedback nach 24h Testphase. Alle Systeme laufen stabil.',
|
||||||
|
isComment: false,
|
||||||
|
createdAt: new Date(yesterday.getTime() - 48 * 60 * 60 * 1000).toISOString(),
|
||||||
|
$createdAt: new Date(yesterday.getTime() - 48 * 60 * 60 * 1000).toISOString()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$id: 'ws-10001-006',
|
||||||
|
wsid: '100006',
|
||||||
|
woid: '10001',
|
||||||
|
workorderId: 'dummy-10001',
|
||||||
|
employeeId: 'user-tom-id',
|
||||||
|
employeeName: 'Tom Klein',
|
||||||
|
employeeShort: 'TK',
|
||||||
|
serviceType: 'COMMENT',
|
||||||
|
oldStatus: 'Awaiting',
|
||||||
|
newStatus: 'Closed',
|
||||||
|
oldResponseLevel: 'Support',
|
||||||
|
newResponseLevel: 'Backoffice',
|
||||||
|
totalTime: 0,
|
||||||
|
startDate: '30.12.2025',
|
||||||
|
startTime: '0900',
|
||||||
|
endDate: '30.12.2025',
|
||||||
|
endTime: '0900',
|
||||||
|
details: 'Kunde bestätigt: Alle Systeme funktionieren einwandfrei. Problem vollständig behoben. Ticket kann geschlossen werden.',
|
||||||
|
isComment: true,
|
||||||
|
createdAt: now.toISOString(),
|
||||||
|
$createdAt: now.toISOString()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
ticket: dummyTicket,
|
||||||
|
worksheets: dummyWorksheets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fügt das Dummy-Ticket zu den Demo-Daten hinzu
|
||||||
|
*/
|
||||||
|
export function addDummyTicketToDemo(workorders, worksheets) {
|
||||||
|
const { ticket, worksheets: ticketWorksheets } = createDummyTicket10001()
|
||||||
|
|
||||||
|
// Prüfe ob Ticket bereits existiert
|
||||||
|
const exists = workorders.some(wo => wo.woid === '10001')
|
||||||
|
if (exists) {
|
||||||
|
console.log('Dummy-Ticket 10001 existiert bereits')
|
||||||
|
return { workorders, worksheets }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
workorders: [ticket, ...workorders],
|
||||||
|
worksheets: [...ticketWorksheets, ...worksheets]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user