diff --git a/src/components/EditWorksheetTimeModal.jsx b/src/components/EditWorksheetTimeModal.jsx deleted file mode 100644 index a4cd04f..0000000 --- a/src/components/EditWorksheetTimeModal.jsx +++ /dev/null @@ -1,142 +0,0 @@ -import { useEffect, useMemo, useState } from 'react' -import { FaClock } from 'react-icons/fa' - -/** Minuten zwischen hhmm-Zeiten, Übernacht (+24h) wie useWorksheets.calculateTime. */ -function calculateMinutes(startTime, endTime) { - if (!startTime || !endTime) return null - const sh = parseInt(startTime.substring(0, 2), 10) - const sm = parseInt(startTime.substring(2, 4), 10) - const eh = parseInt(endTime.substring(0, 2), 10) - const em = parseInt(endTime.substring(2, 4), 10) - if ([sh, sm, eh, em].some(Number.isNaN)) return null - let diff = eh * 60 + em - (sh * 60 + sm) - if (diff < 0) diff += 24 * 60 - return diff -} - -function formatMinutes(minutes) { - if (minutes === null || minutes === undefined) return '–' - const h = Math.floor(minutes / 60) - const m = minutes % 60 - return h > 0 ? `${h}h ${m}min` : `${m}min` -} - -/** - * Startzeit fuer Git-Push-Worksheets nachtragen: Die Endzeit wurde beim Push - * automatisch gesetzt, hier wird der Arbeitsbeginn erfasst. Beim Speichern - * wird totalTime berechnet und isComment auf false gesetzt, damit der Eintrag - * in der Arbeitszeit-Statistik zaehlt. - */ -export default function EditWorksheetTimeModal({ isOpen, onClose, worksheet, onSave }) { - const [startDate, setStartDate] = useState('') - const [startTime, setStartTime] = useState('') - const [saving, setSaving] = useState(false) - const [error, setError] = useState('') - - useEffect(() => { - if (isOpen && worksheet) { - setStartDate(worksheet.startDate || worksheet.endDate || '') - setStartTime(worksheet.startTime || '') - setError('') - } - }, [isOpen, worksheet]) - - const totalTime = useMemo( - () => calculateMinutes(startTime, worksheet?.endTime), - [startTime, worksheet] - ) - - if (!isOpen || !worksheet) return null - - const handleSubmit = async (e) => { - e.preventDefault() - if (!/^\d{2}\.\d{2}\.\d{4}$/.test(startDate)) { - setError('Startdatum bitte als TT.MM.JJJJ angeben.') - return - } - if (!/^\d{4}$/.test(startTime)) { - setError('Startzeit bitte als HHMM angeben (z.B. 0930).') - return - } - if (totalTime === null) { - setError('Zeit konnte nicht berechnet werden.') - return - } - setSaving(true) - setError('') - const result = await onSave(worksheet, { - startDate, - startTime, - totalTime, - isComment: false, - }) - setSaving(false) - if (result?.success) { - onClose() - } else { - setError(result?.error || 'Speichern fehlgeschlagen') - } - } - - return ( -
- - × - -
-

- - Zeit nachtragen – WSID {worksheet.wsid} -

-

- Die Endzeit wurde beim Git-Push automatisch gesetzt - ({worksheet.endDate} {worksheet.endTime ? `${worksheet.endTime.substring(0, 2)}:${worksheet.endTime.substring(2, 4)}` : ''}). - Trage hier ein, wann die Arbeit begonnen hat. -

- - {error && ( -
- {error} -
- )} - -
-
- - setStartDate(e.target.value)} - placeholder="11.07.2026" - required - /> -
-
- - setStartTime(e.target.value.replace(/[^0-9]/g, '').slice(0, 4))} - placeholder="0930" - maxLength={4} - required - /> -
-
- -
- {formatMinutes(totalTime)} -
-
-
- -
-
-
-
- ) -} diff --git a/src/components/ModernSidebar.css b/src/components/ModernSidebar.css deleted file mode 100644 index 62a64be..0000000 --- a/src/components/ModernSidebar.css +++ /dev/null @@ -1,138 +0,0 @@ -/* Modern Sidebar Styles */ - -.modern-sidebar-desktop { - height: 100vh; - padding: 1rem; - display: none; - flex-direction: column; - background: var(--sidebar-bg, #f5f5f5); - border-right: 1px solid var(--border-color, #e0e0e0); - flex-shrink: 0; - overflow: hidden; - transition: width 0.3s ease; -} - -@media (min-width: 768px) { - .modern-sidebar-desktop { - display: flex; - } -} - -/* Mobile Sidebar */ -.modern-sidebar-mobile-header { - height: 60px; - padding: 1rem; - display: flex; - align-items: center; - justify-content: space-between; - background: var(--sidebar-bg, #f5f5f5); - border-bottom: 1px solid var(--border-color, #e0e0e0); - width: 100%; -} - -@media (min-width: 768px) { - .modern-sidebar-mobile-header { - display: none; - } -} - -.modern-sidebar-mobile-toggle { - display: flex; - justify-content: flex-end; - width: 100%; - cursor: pointer; - color: var(--text-color, #333); -} - -.modern-sidebar-mobile-toggle svg { - width: 24px; - height: 24px; -} - -.modern-sidebar-mobile-menu { - position: fixed; - height: 100vh; - width: 100%; - inset: 0; - background: var(--sidebar-bg, #fff); - padding: 2.5rem; - z-index: 100; - display: flex; - flex-direction: column; - justify-content: space-between; -} - -.modern-sidebar-mobile-close { - position: absolute; - right: 2.5rem; - top: 2.5rem; - z-index: 50; - cursor: pointer; - color: var(--text-color, #333); -} - -.modern-sidebar-mobile-close svg { - width: 24px; - height: 24px; -} - -/* Sidebar Link */ -.modern-sidebar-link { - display: flex; - align-items: center; - justify-content: flex-start; - gap: 0.5rem; - padding: 0.75rem 0.5rem; - color: var(--text-color, #333); - text-decoration: none; - border-radius: 6px; - transition: all 0.15s ease; -} - -.modern-sidebar-link:hover { - background: var(--hover-bg, rgba(0, 0, 0, 0.05)); - transform: translateX(2px); -} - -.modern-sidebar-link-icon { - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; -} - -.modern-sidebar-link-icon svg { - width: 20px; - height: 20px; -} - -.modern-sidebar-link-label { - font-size: 0.875rem; - white-space: pre; - margin: 0; - padding: 0; -} - -/* Dark mode support */ -@media (prefers-color-scheme: dark) { - .modern-sidebar-desktop, - .modern-sidebar-mobile-header, - .modern-sidebar-mobile-menu { - background: var(--dark-sidebar-bg, #1a1a1a); - border-color: var(--dark-border-color, #333); - } - - .modern-sidebar-link { - color: var(--dark-text-color, #e0e0e0); - } - - .modern-sidebar-link:hover { - background: var(--dark-hover-bg, rgba(255, 255, 255, 0.1)); - } - - .modern-sidebar-mobile-toggle, - .modern-sidebar-mobile-close { - color: var(--dark-text-color, #e0e0e0); - } -} - diff --git a/src/components/ModernSidebar.jsx b/src/components/ModernSidebar.jsx deleted file mode 100644 index 7317360..0000000 --- a/src/components/ModernSidebar.jsx +++ /dev/null @@ -1,116 +0,0 @@ -import { useState, createContext, useContext } from 'react' -import { motion, AnimatePresence } from 'motion/react' -import { IconMenu2, IconX } from '@tabler/icons-react' -import { cn } from '../lib/utils' -import './ModernSidebar.css' - -const SidebarContext = createContext(undefined) - -export const useSidebar = () => { - const context = useContext(SidebarContext) - if (!context) { - throw new Error('useSidebar must be used within a SidebarProvider') - } - return context -} - -export const SidebarProvider = ({ children, open: openProp, setOpen: setOpenProp, animate = true }) => { - const [openState, setOpenState] = useState(false) - const open = openProp !== undefined ? openProp : openState - const setOpen = setOpenProp !== undefined ? setOpenProp : setOpenState - - return ( - - {children} - - ) -} - -export const Sidebar = ({ children, open, setOpen, animate }) => { - return ( - - {children} - - ) -} - -export const SidebarBody = ({ className, children, ...props }) => { - return ( - <> - {children} - {children} - - ) -} - -export const DesktopSidebar = ({ className, children, ...props }) => { - const { open, setOpen, animate } = useSidebar() - - return ( - setOpen(true)} - onMouseLeave={() => setOpen(false)} - {...props} - > - {children} - - ) -} - -export const MobileSidebar = ({ className, children }) => { - const { open, setOpen } = useSidebar() - - return ( - <> -
-
- setOpen(!open)} /> -
- - {open && ( - -
setOpen(!open)}> - -
- {children} -
- )} -
-
- - ) -} - -export const SidebarLink = ({ link, className, ...props }) => { - const { open, animate } = useSidebar() - - return ( - - {link.icon} - - {link.label} - - - ) -} - diff --git a/src/components/PixelBlast.css b/src/components/PixelBlast.css deleted file mode 100644 index 1fe556b..0000000 --- a/src/components/PixelBlast.css +++ /dev/null @@ -1,7 +0,0 @@ -.pixel-blast-container { - width: 100%; - height: 100%; - position: relative; - overflow: hidden; -} - diff --git a/src/components/PixelBlast.jsx b/src/components/PixelBlast.jsx deleted file mode 100644 index 08709f3..0000000 --- a/src/components/PixelBlast.jsx +++ /dev/null @@ -1,674 +0,0 @@ -import { useEffect, useRef } from 'react'; -import * as THREE from 'three'; -import { EffectComposer, EffectPass, RenderPass, Effect } from 'postprocessing'; -import './PixelBlast.css'; - -const createTouchTexture = () => { - const size = 64; - const canvas = document.createElement('canvas'); - canvas.width = size; - canvas.height = size; - const ctx = canvas.getContext('2d'); - if (!ctx) throw new Error('2D context not available'); - - ctx.fillStyle = 'black'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - - const texture = new THREE.Texture(canvas); - texture.minFilter = THREE.LinearFilter; - texture.magFilter = THREE.LinearFilter; - texture.generateMipmaps = false; - - const trail = []; - let last = null; - const maxAge = 64; - let radius = 0.1 * size; - const speed = 1 / maxAge; - - const clear = () => { - ctx.fillStyle = 'black'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - }; - - const drawPoint = p => { - const pos = { x: p.x * size, y: (1 - p.y) * size }; - let intensity = 1; - - const easeOutSine = t => Math.sin((t * Math.PI) / 2); - const easeOutQuad = t => -t * (t - 2); - - if (p.age < maxAge * 0.3) intensity = easeOutSine(p.age / (maxAge * 0.3)); - else intensity = easeOutQuad(1 - (p.age - maxAge * 0.3) / (maxAge * 0.7)) || 0; - - intensity *= p.force; - - const color = `${((p.vx + 1) / 2) * 255}, ${((p.vy + 1) / 2) * 255}, ${intensity * 255}`; - const offset = size * 5; - ctx.shadowOffsetX = offset; - ctx.shadowOffsetY = offset; - ctx.shadowBlur = radius; - ctx.shadowColor = `rgba(${color},${0.22 * intensity})`; - - ctx.beginPath(); - ctx.fillStyle = 'rgba(255,0,0,1)'; - ctx.arc(pos.x - offset, pos.y - offset, radius, 0, Math.PI * 2); - ctx.fill(); - }; - - const addTouch = norm => { - let force = 0; - let vx = 0; - let vy = 0; - - if (last) { - const dx = norm.x - last.x; - const dy = norm.y - last.y; - if (dx === 0 && dy === 0) return; - - const dd = dx * dx + dy * dy; - const d = Math.sqrt(dd); - vx = dx / (d || 1); - vy = dy / (d || 1); - force = Math.min(dd * 10000, 1); - } - - last = { x: norm.x, y: norm.y }; - trail.push({ x: norm.x, y: norm.y, age: 0, force, vx, vy }); - }; - - const update = () => { - clear(); - for (let i = trail.length - 1; i >= 0; i--) { - const point = trail[i]; - const f = point.force * speed * (1 - point.age / maxAge); - point.x += point.vx * f; - point.y += point.vy * f; - point.age++; - if (point.age > maxAge) trail.splice(i, 1); - } - for (let i = 0; i < trail.length; i++) drawPoint(trail[i]); - texture.needsUpdate = true; - }; - - return { - canvas, - texture, - addTouch, - update, - set radiusScale(v) { - radius = 0.1 * size * v; - }, - get radiusScale() { - return radius / (0.1 * size); - }, - size - }; -}; - -const createLiquidEffect = (texture, opts) => { - const fragment = ` - uniform sampler2D uTexture; - uniform float uStrength; - uniform float uTime; - uniform float uFreq; - - void mainUv(inout vec2 uv) { - vec4 tex = texture2D(uTexture, uv); - float vx = tex.r * 2.0 - 1.0; - float vy = tex.g * 2.0 - 1.0; - float intensity = tex.b; - float wave = 0.5 + 0.5 * sin(uTime * uFreq + intensity * 6.2831853); - float amt = uStrength * intensity * wave; - uv += vec2(vx, vy) * amt; - } - `; - - return new Effect('LiquidEffect', fragment, { - uniforms: new Map([ - ['uTexture', new THREE.Uniform(texture)], - ['uStrength', new THREE.Uniform(opts?.strength ?? 0.025)], - ['uTime', new THREE.Uniform(0)], - ['uFreq', new THREE.Uniform(opts?.freq ?? 4.5)] - ]) - }); -}; - -const SHAPE_MAP = { - square: 0, - circle: 1, - triangle: 2, - diamond: 3 -}; - -const VERTEX_SRC = ` -void main() { - gl_Position = vec4(position, 1.0); -} -`; - -const FRAGMENT_SRC = ` -precision highp float; - -uniform vec3 uColor; -uniform vec2 uResolution; -uniform float uTime; -uniform float uPixelSize; -uniform float uScale; -uniform float uDensity; -uniform float uPixelJitter; -uniform int uEnableRipples; -uniform float uRippleSpeed; -uniform float uRippleThickness; -uniform float uRippleIntensity; -uniform float uEdgeFade; -uniform int uShapeType; - -const int SHAPE_SQUARE = 0; -const int SHAPE_CIRCLE = 1; -const int SHAPE_TRIANGLE = 2; -const int SHAPE_DIAMOND = 3; - -const int MAX_CLICKS = 10; -uniform vec2 uClickPos [MAX_CLICKS]; -uniform float uClickTimes[MAX_CLICKS]; - -out vec4 fragColor; - -float Bayer2(vec2 a) { - a = floor(a); - return fract(a.x / 2. + a.y * a.y * .75); -} - -#define Bayer4(a) (Bayer2(.5*(a))*0.25 + Bayer2(a)) -#define Bayer8(a) (Bayer4(.5*(a))*0.25 + Bayer2(a)) - -#define FBM_OCTAVES 5 -#define FBM_LACUNARITY 1.25 -#define FBM_GAIN 1.0 - -float hash11(float n){ return fract(sin(n)*43758.5453); } - -float vnoise(vec3 p){ - vec3 ip = floor(p); - vec3 fp = fract(p); - float n000 = hash11(dot(ip + vec3(0.0,0.0,0.0), vec3(1.0,57.0,113.0))); - float n100 = hash11(dot(ip + vec3(1.0,0.0,0.0), vec3(1.0,57.0,113.0))); - float n010 = hash11(dot(ip + vec3(0.0,1.0,0.0), vec3(1.0,57.0,113.0))); - float n110 = hash11(dot(ip + vec3(1.0,1.0,0.0), vec3(1.0,57.0,113.0))); - float n001 = hash11(dot(ip + vec3(0.0,0.0,1.0), vec3(1.0,57.0,113.0))); - float n101 = hash11(dot(ip + vec3(1.0,0.0,1.0), vec3(1.0,57.0,113.0))); - float n011 = hash11(dot(ip + vec3(0.0,1.0,1.0), vec3(1.0,57.0,113.0))); - float n111 = hash11(dot(ip + vec3(1.0,1.0,1.0), vec3(1.0,57.0,113.0))); - - vec3 w = fp*fp*fp*(fp*(fp*6.0-15.0)+10.0); - - float x00 = mix(n000, n100, w.x); - float x10 = mix(n010, n110, w.x); - float x01 = mix(n001, n101, w.x); - float x11 = mix(n011, n111, w.x); - - float y0 = mix(x00, x10, w.y); - float y1 = mix(x01, x11, w.y); - - return mix(y0, y1, w.z) * 2.0 - 1.0; -} - -float fbm2(vec2 uv, float t){ - vec3 p = vec3(uv * uScale, t); - float amp = 1.0; - float freq = 1.0; - float sum = 1.0; - - for (int i = 0; i < FBM_OCTAVES; ++i){ - sum += amp * vnoise(p * freq); - freq *= FBM_LACUNARITY; - amp *= FBM_GAIN; - } - - return sum * 0.5 + 0.5; -} - -float maskCircle(vec2 p, float cov){ - float r = sqrt(cov) * .25; - float d = length(p - 0.5) - r; - float aa = 0.5 * fwidth(d); - return cov * (1.0 - smoothstep(-aa, aa, d * 2.0)); -} - -float maskTriangle(vec2 p, vec2 id, float cov){ - bool flip = mod(id.x + id.y, 2.0) > 0.5; - if (flip) p.x = 1.0 - p.x; - - float r = sqrt(cov); - float d = p.y - r*(1.0 - p.x); - float aa = fwidth(d); - - return cov * clamp(0.5 - d/aa, 0.0, 1.0); -} - -float maskDiamond(vec2 p, float cov){ - float r = sqrt(cov) * 0.564; - return step(abs(p.x - 0.49) + abs(p.y - 0.49), r); -} - -void main(){ - float pixelSize = uPixelSize; - - vec2 fragCoord = gl_FragCoord.xy - uResolution * .5; - float aspectRatio = uResolution.x / uResolution.y; - - vec2 pixelId = floor(fragCoord / pixelSize); - vec2 pixelUV = fract(fragCoord / pixelSize); - - float cellPixelSize = 8.0 * pixelSize; - vec2 cellId = floor(fragCoord / cellPixelSize); - vec2 cellCoord = cellId * cellPixelSize; - - vec2 uv = cellCoord / uResolution * vec2(aspectRatio, 1.0); - - float base = fbm2(uv, uTime * 0.05); - base = base * 0.5 - 0.65; - - float feed = base + (uDensity - 0.5) * 0.3; - - float speed = uRippleSpeed; - float thickness = uRippleThickness; - const float dampT = 1.0; - const float dampR = 10.0; - - if (uEnableRipples == 1) { - for (int i = 0; i < MAX_CLICKS; ++i){ - vec2 pos = uClickPos[i]; - if (pos.x < 0.0) continue; - - float cellPixelSize = 8.0 * pixelSize; - vec2 cuv = (((pos - uResolution * .5 - cellPixelSize * .5) / (uResolution))) * vec2(aspectRatio, 1.0); - - float t = max(uTime - uClickTimes[i], 0.0); - float r = distance(uv, cuv); - - float waveR = speed * t; - float ring = exp(-pow((r - waveR) / thickness, 2.0)); - float atten = exp(-dampT * t) * exp(-dampR * r); - - feed = max(feed, ring * atten * uRippleIntensity); - } - } - - float bayer = Bayer8(fragCoord / uPixelSize) - 0.5; - float bw = step(0.5, feed + bayer); - - float h = fract(sin(dot(floor(fragCoord / uPixelSize), vec2(127.1, 311.7))) * 43758.5453); - float jitterScale = 1.0 + (h - 0.5) * uPixelJitter; - float coverage = bw * jitterScale; - - float M; - if (uShapeType == SHAPE_CIRCLE) M = maskCircle (pixelUV, coverage); - else if (uShapeType == SHAPE_TRIANGLE) M = maskTriangle(pixelUV, pixelId, coverage); - else if (uShapeType == SHAPE_DIAMOND) M = maskDiamond(pixelUV, coverage); - else M = coverage; - - if (uEdgeFade > 0.0) { - vec2 norm = gl_FragCoord.xy / uResolution; - float edge = min(min(norm.x, norm.y), min(1.0 - norm.x, 1.0 - norm.y)); - float fade = smoothstep(0.0, uEdgeFade, edge); - M *= fade; - } - - vec3 color = uColor; - fragColor = vec4(color, M); -} -`; - -const MAX_CLICKS = 10; - -const PixelBlast = ({ - variant = 'square', - pixelSize = 3, - color = '#10b981', - className, - style, - antialias = true, - patternScale = 2, - patternDensity = 1, - liquid = false, - liquidStrength = 0.1, - liquidRadius = 1, - pixelSizeJitter = 0, - enableRipples = true, - rippleIntensityScale = 1, - rippleThickness = 0.1, - rippleSpeed = 0.3, - liquidWobbleSpeed = 4.5, - autoPauseOffscreen = true, - speed = 0.5, - transparent = true, - edgeFade = 0.5, - noiseAmount = 0 -}) => { - const containerRef = useRef(null); - const visibilityRef = useRef({ visible: true }); - const speedRef = useRef(speed); - const threeRef = useRef(null); - const prevConfigRef = useRef(null); - - useEffect(() => { - const container = containerRef.current; - if (!container) return; - - speedRef.current = speed; - - const needsReinitKeys = ['antialias', 'liquid', 'noiseAmount']; - const cfg = { antialias, liquid, noiseAmount }; - let mustReinit = false; - - if (!threeRef.current) mustReinit = true; - else if (prevConfigRef.current) { - for (const k of needsReinitKeys) - if (prevConfigRef.current[k] !== cfg[k]) { - mustReinit = true; - break; - } - } - - if (mustReinit) { - if (threeRef.current) { - const t = threeRef.current; - t.resizeObserver?.disconnect(); - cancelAnimationFrame(t.raf); - t.quad?.geometry.dispose(); - t.material.dispose(); - t.composer?.dispose(); - t.renderer.dispose(); - if (t.renderer.domElement.parentElement === container) container.removeChild(t.renderer.domElement); - threeRef.current = null; - } - - const canvas = document.createElement('canvas'); - const renderer = new THREE.WebGLRenderer({ - canvas, - antialias, - alpha: true, - powerPreference: 'high-performance' - }); - - renderer.domElement.style.width = '100%'; - renderer.domElement.style.height = '100%'; - renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); - container.appendChild(renderer.domElement); - - if (transparent) renderer.setClearAlpha(0); - else renderer.setClearColor(0x000000, 1); - - const uniforms = { - uResolution: { value: new THREE.Vector2(0, 0) }, - uTime: { value: 0 }, - uColor: { value: new THREE.Color(color) }, - uClickPos: { - value: Array.from({ length: MAX_CLICKS }, () => new THREE.Vector2(-1, -1)) - }, - uClickTimes: { value: new Float32Array(MAX_CLICKS) }, - uShapeType: { value: SHAPE_MAP[variant] ?? 0 }, - uPixelSize: { value: pixelSize * renderer.getPixelRatio() }, - uScale: { value: patternScale }, - uDensity: { value: patternDensity }, - uPixelJitter: { value: pixelSizeJitter }, - uEnableRipples: { value: enableRipples ? 1 : 0 }, - uRippleSpeed: { value: rippleSpeed }, - uRippleThickness: { value: rippleThickness }, - uRippleIntensity: { value: rippleIntensityScale }, - uEdgeFade: { value: edgeFade } - }; - - const scene = new THREE.Scene(); - const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); - - const material = new THREE.ShaderMaterial({ - vertexShader: VERTEX_SRC, - fragmentShader: FRAGMENT_SRC, - uniforms, - transparent: true, - depthTest: false, - depthWrite: false, - glslVersion: THREE.GLSL3 - }); - - const quadGeom = new THREE.PlaneGeometry(2, 2); - const quad = new THREE.Mesh(quadGeom, material); - scene.add(quad); - - const clock = new THREE.Clock(); - - const setSize = () => { - const w = container.clientWidth || 1; - const h = container.clientHeight || 1; - renderer.setSize(w, h, false); - uniforms.uResolution.value.set(renderer.domElement.width, renderer.domElement.height); - if (threeRef.current?.composer) - threeRef.current.composer.setSize(renderer.domElement.width, renderer.domElement.height); - uniforms.uPixelSize.value = pixelSize * renderer.getPixelRatio(); - }; - - setSize(); - - const ro = new ResizeObserver(setSize); - ro.observe(container); - - const randomFloat = () => { - if (typeof window !== 'undefined' && window.crypto?.getRandomValues) { - const u32 = new Uint32Array(1); - window.crypto.getRandomValues(u32); - return u32[0] / 0xffffffff; - } - return Math.random(); - }; - - const timeOffset = randomFloat() * 1000; - - let composer; - let touch; - let liquidEffect; - - if (liquid) { - touch = createTouchTexture(); - touch.radiusScale = liquidRadius; - - composer = new EffectComposer(renderer); - const renderPass = new RenderPass(scene, camera); - - liquidEffect = createLiquidEffect(touch.texture, { - strength: liquidStrength, - freq: liquidWobbleSpeed - }); - - const effectPass = new EffectPass(camera, liquidEffect); - effectPass.renderToScreen = true; - - composer.addPass(renderPass); - composer.addPass(effectPass); - } - - if (noiseAmount > 0) { - if (!composer) { - composer = new EffectComposer(renderer); - composer.addPass(new RenderPass(scene, camera)); - } - - const noiseEffect = new Effect( - 'NoiseEffect', - `uniform float uTime; uniform float uAmount; float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1,311.7))) * 43758.5453);} void mainUv(inout vec2 uv){} void mainImage(const in vec4 inputColor,const in vec2 uv,out vec4 outputColor){ float n=hash(floor(uv*vec2(1920.0,1080.0))+floor(uTime*60.0)); float g=(n-0.5)*uAmount; outputColor=inputColor+vec4(vec3(g),0.0);} `, - { - uniforms: new Map([ - ['uTime', new THREE.Uniform(0)], - ['uAmount', new THREE.Uniform(noiseAmount)] - ]) - } - ); - - const noisePass = new EffectPass(camera, noiseEffect); - noisePass.renderToScreen = true; - - if (composer && composer.passes.length > 0) composer.passes.forEach(p => (p.renderToScreen = false)); - composer.addPass(noisePass); - } - - if (composer) composer.setSize(renderer.domElement.width, renderer.domElement.height); - - const mapToPixels = e => { - const rect = renderer.domElement.getBoundingClientRect(); - const scaleX = renderer.domElement.width / rect.width; - const scaleY = renderer.domElement.height / rect.height; - const fx = (e.clientX - rect.left) * scaleX; - const fy = (rect.height - (e.clientY - rect.top)) * scaleY; - return { - fx, - fy, - w: renderer.domElement.width, - h: renderer.domElement.height - }; - }; - - const onPointerDown = e => { - const { fx, fy } = mapToPixels(e); - const ix = threeRef.current?.clickIx ?? 0; - uniforms.uClickPos.value[ix].set(fx, fy); - uniforms.uClickTimes.value[ix] = uniforms.uTime.value; - if (threeRef.current) threeRef.current.clickIx = (ix + 1) % MAX_CLICKS; - }; - - const onPointerMove = e => { - if (!touch) return; - const { fx, fy, w, h } = mapToPixels(e); - touch.addTouch({ x: fx / w, y: fy / h }); - }; - - renderer.domElement.addEventListener('pointerdown', onPointerDown, { - passive: true - }); - renderer.domElement.addEventListener('pointermove', onPointerMove, { - passive: true - }); - - let raf = 0; - - const animate = () => { - if (autoPauseOffscreen && !visibilityRef.current.visible) { - raf = requestAnimationFrame(animate); - return; - } - - uniforms.uTime.value = timeOffset + clock.getElapsedTime() * speedRef.current; - if (liquidEffect) liquidEffect.uniforms.get('uTime').value = uniforms.uTime.value; - - if (composer) { - if (touch) touch.update(); - composer.passes.forEach(p => { - const effs = p.effects; - if (effs) - effs.forEach(eff => { - const u = eff.uniforms?.get('uTime'); - if (u) u.value = uniforms.uTime.value; - }); - }); - composer.render(); - } else renderer.render(scene, camera); - - raf = requestAnimationFrame(animate); - }; - - raf = requestAnimationFrame(animate); - - threeRef.current = { - renderer, - scene, - camera, - material, - clock, - clickIx: 0, - uniforms, - resizeObserver: ro, - raf, - quad, - timeOffset, - composer, - touch, - liquidEffect - }; - } else { - const t = threeRef.current; - t.uniforms.uShapeType.value = SHAPE_MAP[variant] ?? 0; - t.uniforms.uPixelSize.value = pixelSize * t.renderer.getPixelRatio(); - t.uniforms.uColor.value.set(color); - t.uniforms.uScale.value = patternScale; - t.uniforms.uDensity.value = patternDensity; - t.uniforms.uPixelJitter.value = pixelSizeJitter; - t.uniforms.uEnableRipples.value = enableRipples ? 1 : 0; - t.uniforms.uRippleIntensity.value = rippleIntensityScale; - t.uniforms.uRippleThickness.value = rippleThickness; - t.uniforms.uRippleSpeed.value = rippleSpeed; - t.uniforms.uEdgeFade.value = edgeFade; - - if (transparent) t.renderer.setClearAlpha(0); - else t.renderer.setClearColor(0x000000, 1); - - if (t.liquidEffect) { - const uStrength = t.liquidEffect.uniforms.get('uStrength'); - if (uStrength) uStrength.value = liquidStrength; - const uFreq = t.liquidEffect.uniforms.get('uFreq'); - if (uFreq) uFreq.value = liquidWobbleSpeed; - } - - if (t.touch) t.touch.radiusScale = liquidRadius; - } - - prevConfigRef.current = cfg; - - return () => { - if (threeRef.current && mustReinit) return; - if (!threeRef.current) return; - - const t = threeRef.current; - t.resizeObserver?.disconnect(); - cancelAnimationFrame(t.raf); - t.quad?.geometry.dispose(); - t.material.dispose(); - t.composer?.dispose(); - t.renderer.dispose(); - if (t.renderer.domElement.parentElement === container) container.removeChild(t.renderer.domElement); - threeRef.current = null; - }; - }, [ - antialias, - liquid, - noiseAmount, - pixelSize, - patternScale, - patternDensity, - enableRipples, - rippleIntensityScale, - rippleThickness, - rippleSpeed, - pixelSizeJitter, - edgeFade, - transparent, - liquidStrength, - liquidRadius, - liquidWobbleSpeed, - autoPauseOffscreen, - variant, - color, - speed - ]); - - return ( -
- ); -}; - -export default PixelBlast; - diff --git a/src/components/ResponseDropdown.jsx b/src/components/ResponseDropdown.jsx deleted file mode 100644 index 83c8e06..0000000 --- a/src/components/ResponseDropdown.jsx +++ /dev/null @@ -1,32 +0,0 @@ -const RESPONSE_LEVELS = [ - { value: 'KEY USER', label: 'Key User' }, - { value: 'Helpdesk', label: '1st Level' }, - { value: 'Support', label: '2nd Level' }, - { value: 'Admin', label: '3rd Level' }, - { value: 'FS/FE', label: 'FS/FE' }, - { value: '24/7', label: '24/7' }, - { value: 'TECH MGMT', label: 'Tech Mgmt' }, - { value: 'Backoffice', label: 'Backoffice' }, - { value: 'BUSI MGMT', label: 'Busi Mgmt' } -] - -export default function ResponseDropdown({ value, onChange }) { - return ( -
- -
- {RESPONSE_LEVELS.map(level => ( - onChange(level.value)} - > - {level.label} - - ))} -
-
- ) -} diff --git a/src/components/TicketFilters.jsx b/src/components/TicketFilters.jsx deleted file mode 100644 index 71eaeb8..0000000 --- a/src/components/TicketFilters.jsx +++ /dev/null @@ -1,177 +0,0 @@ -import { useState } from 'react' -import { FaCaretDown } from 'react-icons/fa6' - -const TICKET_TYPES = [ - 'Home Office', 'Holidays', 'Trip', 'Supportrequest', 'Change Request', - 'Maintenance', 'Project', 'Controlling', 'Development', 'Documentation', - 'Meeting/Conference', 'IT Management', 'IT Security', 'Procurement', - 'Rollout', 'Emergency Call', 'Other Services' -] - -const SYSTEMS = [ - 'Account View', 'Client', 'Cofano', 'Credentials', 'Diamant', 'Docuware', - 'EDI', 'eMail', 'Employee', 'Invoice', 'LBase', 'Medical Office', 'Network', - 'O365', 'PDF Viewer', 'Printer', 'Reports', 'Server', 'Time Tracking', - 'TK', 'TOS', 'Vivendi NG', 'VGM', '(W)LAN', '(W)WAN', 'WOMS', 'n/a' -] - -const STATUSES = [ - 'Open', 'Occupied', 'Assigned', 'Awaiting', 'Added Info', - 'In Test', 'Halted', 'Aborted', 'Cancelled', 'Closed' -] - -const PRIORITIES = [ - { value: 0, label: 'None' }, - { value: 1, label: 'Low' }, - { value: 2, label: 'Medium' }, - { value: 3, label: 'High' }, - { value: 4, label: 'Critical' } -] - -export default function TicketFilters({ filters, onChange, onApply }) { - const [localFilters, setLocalFilters] = useState(filters) - - const handleCheckboxChange = (category, value) => { - const current = localFilters[category] || [] - const updated = current.includes(value) - ? current.filter(v => v !== value) - : [...current, value] - setLocalFilters({ ...localFilters, [category]: updated }) - } - - const handleInputChange = (field, value) => { - setLocalFilters({ ...localFilters, [field]: value }) - } - - const handleApply = () => { - onChange(localFilters) - onApply() - } - - return ( - - - handleInputChange('woid', e.target.value)} - /> - - - handleInputChange('createdDate', e.target.value)} - /> - - -
- -
- {TICKET_TYPES.map(type => ( - - ))} -
-
- - -
- -
- {SYSTEMS.map(system => ( - - ))} -
-
- - - handleInputChange('customer', e.target.value)} - /> - - - handleInputChange('userTopic', e.target.value)} - /> - - -
- -
- {STATUSES.map(status => ( - - ))} -
-
- - -
- -
- {PRIORITIES.map(p => ( - - ))} -
-
- - Approval - - - - - ) -} diff --git a/src/components/TicketRow.jsx b/src/components/TicketRow.jsx deleted file mode 100644 index 0ecb06d..0000000 --- a/src/components/TicketRow.jsx +++ /dev/null @@ -1,308 +0,0 @@ -import { useState } from 'react' -import { FaLock, FaLockOpen, FaPlay, FaStop, FaTruck, FaSackDollar, FaUserGear, FaPlus, FaClockRotateLeft, FaPen } from 'react-icons/fa6' -import { formatDistanceToNow, format } from 'date-fns' -import { de } from 'date-fns/locale' -import StatusDropdown from './StatusDropdown' -import PriorityDropdown from './PriorityDropdown' -import EditorDropdown from './EditorDropdown' -import ResponseDropdown from './ResponseDropdown' -import CreateWorksheetModal from './CreateWorksheetModal' -import EditWorksheetTimeModal from './EditWorksheetTimeModal' -import StatusHistoryModal from './StatusHistoryModal' -import WorksheetList from './WorksheetList' -import WorksheetStats from './WorksheetStats' -import TicketAssignedProjectsPanel from './TicketAssignedProjectsPanel' -import TicketProjectsModal from './TicketProjectsModal' -import InvoicePanel from './InvoicePanel' -import DocumentsPanel from './DocumentsPanel' -import { useWorksheets } from '../hooks/useWorksheets' - -const PRIORITY_CLASSES = { - 0: 'priority-none', - 1: 'priority-low', - 2: 'priority-medium', - 3: 'priority-high', - 4: 'priority-critical' -} - -const STATUS_CLASSES = { - 'Open': 'status-open', - 'Occupied': 'status-occupied', - 'Assigned': 'status-assigned', - 'Awaiting': 'status-awaiting', - 'Closed': 'status-closed' -} - -const APPROVAL_ICONS = { - 'pending': FaUserGear, - 'approved': FaSackDollar, - 'quote': FaSackDollar, - 'shipping': FaTruck -} - -const TABS = [ - { id: 'details', label: 'Details' }, - { id: 'worksheets', label: 'Arbeitsblaetter' }, - { id: 'invoices', label: 'Rechnungen' }, - { id: 'documents', label: 'Dokumente' }, - { id: 'project', label: 'Projekt / Preview' }, -] - -export default function TicketRow({ ticket, onUpdate, onExpand }) { - const [expanded, setExpanded] = useState(false) - const [locked, setLocked] = useState(true) - const [activeTab, setActiveTab] = useState('details') - const [showCreateWorksheet, setShowCreateWorksheet] = useState(false) - const [editTimeWorksheet, setEditTimeWorksheet] = useState(null) - const [showHistoryModal, setShowHistoryModal] = useState(false) - const [showProjectsModal, setShowProjectsModal] = useState(false) - const [projectsRefreshKey, setProjectsRefreshKey] = useState(0) - - const { - worksheets, - loading: worksheetsLoading, - createWorksheet, - updateWorksheet, - getTotalTime - } = useWorksheets(expanded ? ticket.woid : null) - - // Startzeit fuer Git-Push-Worksheets nachtragen (Endzeit kam vom Push) - const handleSaveWorksheetTime = async (ws, data) => updateWorksheet(ws.$id, data) - // Wartungs-Abo: Arbeitszeit aufs Stundenkonto des Kunden buchen (Kundenportal zeigt den Stand) - const handleTogglePlan = async (ws) => updateWorksheet(ws.$id, { countsToPlan: !ws.countsToPlan }) - - const createdAt = new Date(ticket.$createdAt || ticket.createdAt) - const elapsed = formatDistanceToNow(createdAt, { locale: de }) - - const handleStatusChange = (newStatus) => onUpdate(ticket.$id, { status: newStatus }) - const handlePriorityChange = (newPriority) => onUpdate(ticket.$id, { priority: newPriority }) - const handleEditorChange = (newEditor) => onUpdate(ticket.$id, { assignedTo: newEditor }) - const handleResponseChange = (newResponse) => onUpdate(ticket.$id, { responseLevel: newResponse }) - - const toggleLock = () => { - if (locked) { - setExpanded(true) - onExpand?.(ticket.$id) - } else { - setExpanded(false) - } - setLocked(!locked) - } - - const handleCreateWorksheet = async (worksheetData, currentUser) => { - const result = await createWorksheet(worksheetData, currentUser) - if (result.success && worksheetData.newStatus !== ticket.status) { - await onUpdate(ticket.$id, { - status: worksheetData.newStatus, - responseLevel: worksheetData.newResponseLevel || ticket.responseLevel - }) - } - return result - } - - const ApprovalIcon = APPROVAL_ICONS[ticket.approvalStatus] || FaUserGear - - return ( - <> - - -
WOID: {ticket.woid || ticket.$id?.slice(-5)}
-
{elapsed}
- - - {format(createdAt, 'dd.MM.yyyy')} - - - {ticket.type} -
- {ticket.serviceType || 'Remote'} -
- {ticket.startDate || format(createdAt, 'dd.MM.yyyy')} -
- {ticket.deadline || '-'} - - - {ticket.systemType || 'n/a'} - - - {ticket.customerName || 'Unknown'} -
- {ticket.customerLocation || ''} - - - {ticket.requestedBy || '-'} -
- {ticket.requestedFor && <>Requested for: {ticket.requestedFor}
} - {ticket.topic} - - - - - - - - - - - - {locked ? : } - - - - - - - - - - - {expanded && ( - - -
-
- - {/* Tab-Leiste */} -
- {TABS.map((t) => ( - - ))} -
- -
- {activeTab === 'details' && ( -
-
- Ticket-Beschreibung -
-

- {ticket.details || 'Keine Details vorhanden.'} -

-
- )} - - {activeTab === 'worksheets' && ( -
-
- - - -
- - {worksheets.length > 0 && ( -
- -
- )} - - -
- )} - - {activeTab === 'invoices' && } - - {activeTab === 'documents' && } - - {activeTab === 'project' && ( -
- - -
- )} -
-
-
- - - )} - - setShowCreateWorksheet(false)} - workorder={ticket} - onCreate={handleCreateWorksheet} - /> - - setEditTimeWorksheet(null)} - worksheet={editTimeWorksheet} - onSave={handleSaveWorksheetTime} - /> - - setShowHistoryModal(false)} - worksheets={worksheets} - ticket={ticket} - /> - - setShowProjectsModal(false)} - ticket={ticket} - onUpdated={() => setProjectsRefreshKey((k) => k + 1)} - /> - - - - - ) -} diff --git a/src/components/WebpageProjectPanel.jsx b/src/components/WebpageProjectPanel.jsx deleted file mode 100644 index 5a23c05..0000000 --- a/src/components/WebpageProjectPanel.jsx +++ /dev/null @@ -1,109 +0,0 @@ -import { useState, useEffect, useCallback } from 'react' -import { FaPlus, FaTrash } from 'react-icons/fa' -import PreviewLinkButton from './PreviewLinkButton' -import { useWebsiteProjects } from '../hooks/useWebsiteProjects' -import { createProjectFromTemplate } from '../lib/projectAdminApi' -import { WEBPAGE_TICKET_TYPE } from '../lib/appwrite' - -function slugify(v) { - return String(v || '').toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 100) -} - -export default function WebpageProjectPanel({ ticket }) { - const { fetchAllProjects, fetchByTicketId, getAvailableProjects, assignProjects, unassignProject } = useWebsiteProjects() - const [assigned, setAssigned] = useState([]) - const [available, setAvailable] = useState([]) - const [selectedIds, setSelectedIds] = useState([]) - const [loading, setLoading] = useState(true) - const [actionLoading, setActionLoading] = useState(false) - const [error, setError] = useState('') - const [createForm, setCreateForm] = useState({ displayName: '', subdomain: '', repoName: '' }) - - const loadProjects = useCallback(async () => { - if (!ticket?.$id || ticket.type !== WEBPAGE_TICKET_TYPE) return - setLoading(true) - setError('') - try { - const [all, mine] = await Promise.all([fetchAllProjects(), fetchByTicketId(ticket.$id)]) - setAssigned(mine) - const ids = new Set(mine.map((p) => p.$id)) - setAvailable(getAvailableProjects(all, ticket.customerId).filter((p) => !ids.has(p.$id))) - } catch (err) { - setError(err.message) - } finally { - setLoading(false) - } - }, [ticket, fetchAllProjects, fetchByTicketId, getAvailableProjects]) - - useEffect(() => { loadProjects() }, [loadProjects]) - if (ticket?.type !== WEBPAGE_TICKET_TYPE) return null - - const handleAssign = async () => { - if (!selectedIds.length || !ticket.customerId) return - setActionLoading(true) - const r = await assignProjects(selectedIds, { customerId: ticket.customerId, ticketId: ticket.$id }) - setActionLoading(false) - if (r.success) { setSelectedIds([]); await loadProjects() } else setError(r.error) - } - - const handleCreate = async (e) => { - e.preventDefault() - if (!ticket.customerId) { setError('Kein Kunde am Ticket.'); return } - setActionLoading(true) - setError('') - try { - await createProjectFromTemplate({ - repoName: createForm.repoName || createForm.subdomain, - displayName: createForm.displayName, - subdomain: slugify(createForm.subdomain), - customerId: ticket.customerId, - ticketId: ticket.$id, - }) - setCreateForm({ displayName: '', subdomain: '', repoName: '' }) - await loadProjects() - } catch (err) { - setError(err.message) - } finally { - setActionLoading(false) - } - } - - return ( -
-
Website-Projekte
- {error &&
{error}
} - {loading ?

Laden...

: ( - <> -
-
Zugewiesen
- {assigned.length === 0 ?

Keine Projekte.

: assigned.map((p) => ( -
- {p.projectName} ({p.subdomain}) - - -
- ))} -
-
-
Verf�gbar zuweisen
- {available.map((p) => ( - - ))} - -
-
-
Neues Projekt
-
- setCreateForm((f) => ({ ...f, displayName: e.target.value }))} required /> - setCreateForm((f) => ({ ...f, subdomain: e.target.value, repoName: f.repoName || slugify(e.target.value) }))} required /> - -
-
- - )} -
- ) -} diff --git a/src/components/WorksheetList.jsx b/src/components/WorksheetList.jsx index 0f5a31f..6d589db 100644 --- a/src/components/WorksheetList.jsx +++ b/src/components/WorksheetList.jsx @@ -23,7 +23,7 @@ const headerButtonStyle = { marginRight: '8px', } -export default function WorksheetList({ worksheets, totalTime, loading, onEditTime, onTogglePlan, canEditTimes, onEditTimes }) { +export default function WorksheetList({ worksheets, totalTime, loading, onTogglePlan, canEditTimes, onEditTimes }) { const [expandedWorksheets, setExpandedWorksheets] = useState({}) if (loading) { return ( @@ -162,19 +162,6 @@ export default function WorksheetList({ worksheets, totalTime, loading, onEditTi )}
- {needsTimeEntry(ws) && onEditTime && ( - - )} {onEditTimes && canEditTimes?.(ws) && (