Arbeitsblätter: Zeit für eigene Git-Pushes nachtragen
70 von 78 Arbeitsblättern sind Git-Pushes, die das Kundenportal mit
employeeId "gitea" anlegt; in employeeName steht der Gitea-Name des
Pushers ("knso", "ANDJ", "JUSN" oder der volle Name). Sie gehören dem
Mitarbeiter mit diesem Kürzel oder Namen - der eigene employees-Eintrag
kommt jetzt aus dem AuthContext.
Bei offenen Pushes heißt der Button "Zeit nachtragen": Beginn ist Pflicht,
das Ende ist der Push. Beim Speichern wird isComment false, damit die Zeit
in der Arbeitszeit zählt (wie früher im Modal der alten Ticketzeile).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
149b38b94b
commit
0c884a57d7
7 files changed
+69
-31
No files matched your search
@@ -22,7 +22,7 @@ import { StatusPill, PriorityPill } from './ui/StatusPill'
|
|||||||
import { useWorksheets } from '../hooks/useWorksheets'
|
import { useWorksheets } from '../hooks/useWorksheets'
|
||||||
import { useAuth } from '../context/AuthContext'
|
import { useAuth } from '../context/AuthContext'
|
||||||
import { databases, DATABASE_ID, COLLECTIONS } from '../lib/appwrite'
|
import { databases, DATABASE_ID, COLLECTIONS } from '../lib/appwrite'
|
||||||
import { serviceTypeLabel } from '../lib/ticketForm'
|
import { serviceTypeLabel, canEditWorksheetTimes } from '../lib/ticketForm'
|
||||||
|
|
||||||
const TABS = [
|
const TABS = [
|
||||||
{ id: 'details', label: 'Details' },
|
{ id: 'details', label: 'Details' },
|
||||||
@@ -33,7 +33,7 @@ const TABS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export default function TicketCard({ ticket, onUpdate }) {
|
export default function TicketCard({ ticket, onUpdate }) {
|
||||||
const { user } = useAuth()
|
const { user, employee } = useAuth()
|
||||||
const [expanded, setExpanded] = useState(false)
|
const [expanded, setExpanded] = useState(false)
|
||||||
const [activeTab, setActiveTab] = useState('details')
|
const [activeTab, setActiveTab] = useState('details')
|
||||||
const [showCreateWorksheet, setShowCreateWorksheet] = useState(false)
|
const [showCreateWorksheet, setShowCreateWorksheet] = useState(false)
|
||||||
@@ -148,7 +148,7 @@ export default function TicketCard({ ticket, onUpdate }) {
|
|||||||
worksheets={worksheets}
|
worksheets={worksheets}
|
||||||
totalTime={getTotalTime()}
|
totalTime={getTotalTime()}
|
||||||
loading={worksheetsLoading}
|
loading={worksheetsLoading}
|
||||||
currentUser={user}
|
canEditTimes={(ws) => canEditWorksheetTimes(ws, user, employee)}
|
||||||
onEditTimes={setEditTimesWorksheet}
|
onEditTimes={setEditTimesWorksheet}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -174,7 +174,7 @@ export default function TicketCard({ ticket, onUpdate }) {
|
|||||||
<WorksheetTimesModal
|
<WorksheetTimesModal
|
||||||
worksheet={editTimesWorksheet}
|
worksheet={editTimesWorksheet}
|
||||||
onClose={() => setEditTimesWorksheet(null)}
|
onClose={() => setEditTimesWorksheet(null)}
|
||||||
onSave={(ws, times) => updateWorksheetTimes(ws, times, user)}
|
onSave={(ws, times) => updateWorksheetTimes(ws, times, user, employee)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<StatusHistoryModal isOpen={showHistoryModal} onClose={() => setShowHistoryModal(false)} worksheets={worksheets} ticket={ticket} />
|
<StatusHistoryModal isOpen={showHistoryModal} onClose={() => setShowHistoryModal(false)} worksheets={worksheets} ticket={ticket} />
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { FaClock, FaUser, FaExchangeAlt, FaComment, FaChevronDown, FaChevronUp, FaPen } from 'react-icons/fa'
|
import { FaClock, FaUser, FaExchangeAlt, FaComment, FaChevronDown, FaChevronUp, FaPen } from 'react-icons/fa'
|
||||||
import { serviceTypeLabel, canEditWorksheetTimes } from '../lib/ticketForm'
|
import { serviceTypeLabel, needsTimeEntry } from '../lib/ticketForm'
|
||||||
|
|
||||||
function needsTimeEntry(ws) {
|
|
||||||
return ws.serviceType === 'GIT' && (!ws.startTime || !Number(ws.totalTime))
|
|
||||||
}
|
|
||||||
|
|
||||||
const headerButtonStyle = {
|
const headerButtonStyle = {
|
||||||
background: 'rgba(255,255,255,0.25)',
|
background: 'rgba(255,255,255,0.25)',
|
||||||
@@ -16,7 +12,7 @@ const headerButtonStyle = {
|
|||||||
marginRight: '8px',
|
marginRight: '8px',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WorksheetList({ worksheets, totalTime, loading, onEditTime, onTogglePlan, currentUser, onEditTimes }) {
|
export default function WorksheetList({ worksheets, totalTime, loading, onEditTime, onTogglePlan, canEditTimes, onEditTimes }) {
|
||||||
const [expandedWorksheets, setExpandedWorksheets] = useState({})
|
const [expandedWorksheets, setExpandedWorksheets] = useState({})
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -176,7 +172,7 @@ export default function WorksheetList({ worksheets, totalTime, loading, onEditTi
|
|||||||
<FaClock className="me-1" /> Zeit nachtragen
|
<FaClock className="me-1" /> Zeit nachtragen
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{onEditTimes && canEditWorksheetTimes(ws, currentUser) && (
|
{onEditTimes && canEditTimes?.(ws) && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-sm"
|
className="btn btn-sm"
|
||||||
@@ -187,7 +183,9 @@ export default function WorksheetList({ worksheets, totalTime, loading, onEditTi
|
|||||||
onEditTimes(ws)
|
onEditTimes(ws)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FaPen className="me-1" /> Zeiten bearbeiten
|
{needsTimeEntry(ws)
|
||||||
|
? <><FaClock className="me-1" /> Zeit nachtragen</>
|
||||||
|
: <><FaPen className="me-1" /> Zeiten bearbeiten</>}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{onTogglePlan && !ws.isComment && Number(ws.totalTime) > 0 && (
|
{onTogglePlan && !ws.isComment && Number(ws.totalTime) > 0 && (
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { FaTimes } from 'react-icons/fa'
|
import { FaTimes } from 'react-icons/fa'
|
||||||
import { isoToGermanDate, timeToWoms, germanDateToIso, womsTimeToInput, minutesBetween } from '../lib/ticketForm'
|
import { isoToGermanDate, timeToWoms, germanDateToIso, womsTimeToInput, minutesBetween, isGitWorksheet, needsTimeEntry } from '../lib/ticketForm'
|
||||||
import { DateTimeInput } from './ui'
|
import { DateTimeInput } from './ui'
|
||||||
|
|
||||||
// Gespeichert ist dd.mm.yyyy / hhmm, die Kalender-Felder brauchen yyyy-mm-dd / hh:mm
|
// Gespeichert ist dd.mm.yyyy / hhmm, die Kalender-Felder brauchen yyyy-mm-dd / hh:mm
|
||||||
const formFromWorksheet = (worksheet) => ({
|
const formFromWorksheet = (worksheet) => ({
|
||||||
startDate: germanDateToIso(worksheet.startDate),
|
startDate: germanDateToIso(worksheet.startDate || worksheet.endDate),
|
||||||
startTime: womsTimeToInput(worksheet.startTime),
|
startTime: womsTimeToInput(worksheet.startTime),
|
||||||
endDate: germanDateToIso(worksheet.endDate || worksheet.startDate),
|
endDate: germanDateToIso(worksheet.endDate || worksheet.startDate),
|
||||||
endTime: womsTimeToInput(worksheet.endTime)
|
endTime: womsTimeToInput(worksheet.endTime)
|
||||||
@@ -13,9 +13,10 @@ const formFromWorksheet = (worksheet) => ({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Beginn, Ende und Arbeitszeit eines Arbeitsblatts nachtraeglich aendern.
|
* Beginn, Ende und Arbeitszeit eines Arbeitsblatts nachtraeglich aendern.
|
||||||
* Wer das darf, entscheidet canEditWorksheetTimes (useWorksheets).
|
* Wer das darf, entscheidet canEditWorksheetTimes (lib/ticketForm).
|
||||||
*/
|
*/
|
||||||
export default function WorksheetTimesModal({ worksheet, onClose, onSave }) {
|
export default function WorksheetTimesModal({ worksheet, onClose, onSave }) {
|
||||||
|
const isGit = isGitWorksheet(worksheet)
|
||||||
const [formData, setFormData] = useState(() => formFromWorksheet(worksheet))
|
const [formData, setFormData] = useState(() => formFromWorksheet(worksheet))
|
||||||
// null = die Arbeitszeit folgt Beginn und Ende. Passte sie schon vorher nicht dazu,
|
// null = die Arbeitszeit folgt Beginn und Ende. Passte sie schon vorher nicht dazu,
|
||||||
// wurde sie von Hand eingetragen (z.B. ohne Pause) und bleibt stehen.
|
// wurde sie von Hand eingetragen (z.B. ohne Pause) und bleibt stehen.
|
||||||
@@ -72,8 +73,11 @@ export default function WorksheetTimesModal({ worksheet, onClose, onSave }) {
|
|||||||
</span>
|
</span>
|
||||||
<div className="overlay-content" style={{ maxWidth: 560 }}>
|
<div className="overlay-content" style={{ maxWidth: 560 }}>
|
||||||
{/* Kurzer Titel: auf dem Handy laeuft ein langer unter das Schliessen-X */}
|
{/* Kurzer Titel: auf dem Handy laeuft ein langer unter das Schliessen-X */}
|
||||||
<h2 className="mb-2">Zeiten bearbeiten</h2>
|
<h2 className="mb-2">{needsTimeEntry(worksheet) ? 'Zeit nachtragen' : 'Zeiten bearbeiten'}</h2>
|
||||||
<p className="text-grey" style={{ fontSize: '14px', marginBottom: '16px' }}>WSID {worksheet.wsid}</p>
|
<p className="text-grey" style={{ fontSize: '14px', marginBottom: '16px' }}>
|
||||||
|
WSID {worksheet.wsid}
|
||||||
|
{isGit && <><br />Das Ende ist der Zeitpunkt des Git-Pushs. Trag ein, wann die Arbeit begonnen hat.</>}
|
||||||
|
</p>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="bg-red text-white p-2 mb-2" style={{ borderRadius: '4px' }}>
|
<div className="bg-red text-white p-2 mb-2" style={{ borderRadius: '4px' }}>
|
||||||
@@ -88,6 +92,7 @@ export default function WorksheetTimesModal({ worksheet, onClose, onSave }) {
|
|||||||
id="worksheet-times-start"
|
id="worksheet-times-start"
|
||||||
timeLabel="Uhrzeit Beginn"
|
timeLabel="Uhrzeit Beginn"
|
||||||
required
|
required
|
||||||
|
timeRequired={isGit}
|
||||||
date={formData.startDate}
|
date={formData.startDate}
|
||||||
time={formData.startTime}
|
time={formData.startTime}
|
||||||
onDateChange={handleStartDateChange}
|
onDateChange={handleStartDateChange}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const openCalendar = (e) => {
|
|||||||
|
|
||||||
/* Datum mit Kalender und Uhrzeit nebeneinander. Werte wie die Browser-Felder:
|
/* Datum mit Kalender und Uhrzeit nebeneinander. Werte wie die Browser-Felder:
|
||||||
Datum "yyyy-mm-dd", Uhrzeit "hh:mm" - umgerechnet wird erst beim Speichern. */
|
Datum "yyyy-mm-dd", Uhrzeit "hh:mm" - umgerechnet wird erst beim Speichern. */
|
||||||
export default function DateTimeInput({ id, date, time, onDateChange, onTimeChange, min, required, timeLabel }) {
|
export default function DateTimeInput({ id, date, time, onDateChange, onTimeChange, min, required, timeRequired, timeLabel }) {
|
||||||
return (
|
return (
|
||||||
<div className="datetime-pair">
|
<div className="datetime-pair">
|
||||||
<input
|
<input
|
||||||
@@ -26,6 +26,7 @@ export default function DateTimeInput({ id, date, time, onDateChange, onTimeChan
|
|||||||
type="time"
|
type="time"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
aria-label={timeLabel}
|
aria-label={timeLabel}
|
||||||
|
required={timeRequired}
|
||||||
value={time}
|
value={time}
|
||||||
onChange={(e) => onTimeChange(e.target.value)}
|
onChange={(e) => onTimeChange(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -70,9 +70,10 @@ async function createSessionWithCleanup(email, password) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hilfsfunktion: Fügt User automatisch zur employees Collection hinzu
|
// Hilfsfunktion: Fügt User automatisch zur employees Collection hinzu und liefert
|
||||||
|
// den Mitarbeiter-Eintrag (Kürzel = Gitea-Name, siehe isWorksheetCreator) oder null
|
||||||
async function ensureEmployeeExists(user) {
|
async function ensureEmployeeExists(user) {
|
||||||
if (!user || DEMO_MODE) return
|
if (!user || DEMO_MODE) return null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await databases.listDocuments(
|
const response = await databases.listDocuments(
|
||||||
@@ -82,7 +83,7 @@ async function ensureEmployeeExists(user) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (response.documents.length === 0) {
|
if (response.documents.length === 0) {
|
||||||
await databases.createDocument(
|
const employee = await databases.createDocument(
|
||||||
DATABASE_ID,
|
DATABASE_ID,
|
||||||
COLLECTIONS.EMPLOYEES,
|
COLLECTIONS.EMPLOYEES,
|
||||||
ID.unique(),
|
ID.unique(),
|
||||||
@@ -94,16 +95,20 @@ async function ensureEmployeeExists(user) {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
console.log('✅ User automatisch zur Mitarbeiter-Liste hinzugefügt')
|
console.log('✅ User automatisch zur Mitarbeiter-Liste hinzugefügt')
|
||||||
|
return employee
|
||||||
}
|
}
|
||||||
|
return response.documents[0]
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code !== 404) {
|
if (error.code !== 404) {
|
||||||
console.warn('Could not add user to employees collection:', error.message)
|
console.warn('Could not add user to employees collection:', error.message)
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AuthProvider({ children }) {
|
export function AuthProvider({ children }) {
|
||||||
const [user, setUser] = useState(null)
|
const [user, setUser] = useState(null)
|
||||||
|
const [employee, setEmployee] = useState(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -129,7 +134,7 @@ export function AuthProvider({ children }) {
|
|||||||
try {
|
try {
|
||||||
const session = await account.get()
|
const session = await account.get()
|
||||||
setUser(session)
|
setUser(session)
|
||||||
await ensureEmployeeExists(session)
|
setEmployee(await ensureEmployeeExists(session))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code !== 401 && error.code !== 404) {
|
if (error.code !== 401 && error.code !== 404) {
|
||||||
console.error('Unexpected error checking user:', error)
|
console.error('Unexpected error checking user:', error)
|
||||||
@@ -160,7 +165,7 @@ export function AuthProvider({ children }) {
|
|||||||
|
|
||||||
const session = await account.get()
|
const session = await account.get()
|
||||||
setUser(session)
|
setUser(session)
|
||||||
await ensureEmployeeExists(session)
|
setEmployee(await ensureEmployeeExists(session))
|
||||||
|
|
||||||
return { success: true }
|
return { success: true }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -202,6 +207,7 @@ export function AuthProvider({ children }) {
|
|||||||
} finally {
|
} finally {
|
||||||
clearAllLocalAppwriteSessions()
|
clearAllLocalAppwriteSessions()
|
||||||
setUser(null)
|
setUser(null)
|
||||||
|
setEmployee(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,6 +215,7 @@ export function AuthProvider({ children }) {
|
|||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
user,
|
user,
|
||||||
|
employee,
|
||||||
loading,
|
loading,
|
||||||
login,
|
login,
|
||||||
logout,
|
logout,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { databases, DATABASE_ID, COLLECTIONS, Query, ID, isDemoMode } from '../lib/appwrite'
|
import { databases, DATABASE_ID, COLLECTIONS, Query, ID, isDemoMode } from '../lib/appwrite'
|
||||||
import { canEditWorksheetTimes } from '../lib/ticketForm'
|
import { canEditWorksheetTimes, isGitWorksheet } from '../lib/ticketForm'
|
||||||
|
|
||||||
const DEMO_MODE = isDemoMode
|
const DEMO_MODE = isDemoMode
|
||||||
|
|
||||||
@@ -391,13 +391,17 @@ export function useWorksheets(woid = null) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Aendert nachtraeglich Beginn, Ende und Arbeitszeit - nur fuer den Ersteller
|
* Aendert nachtraeglich Beginn, Ende und Arbeitszeit - nur fuer den Ersteller
|
||||||
|
* (employee = eigener Eintrag aus "employees", fuer Git-Pushes)
|
||||||
*/
|
*/
|
||||||
const updateWorksheetTimes = async (worksheet, times, currentUser) => {
|
const updateWorksheetTimes = async (worksheet, times, currentUser, employee) => {
|
||||||
if (!canEditWorksheetTimes(worksheet, currentUser)) {
|
if (!canEditWorksheetTimes(worksheet, currentUser, employee)) {
|
||||||
return { success: false, error: 'Nur wer das Arbeitsblatt erstellt hat, kann die Zeiten ändern.' }
|
return { success: false, error: 'Nur wer das Arbeitsblatt erstellt hat, kann die Zeiten ändern.' }
|
||||||
}
|
}
|
||||||
const { startDate, startTime, endDate, endTime, totalTime } = times
|
const { startDate, startTime, endDate, endTime, totalTime } = times
|
||||||
return updateWorksheet(worksheet.$id, { startDate, startTime, endDate, endTime, totalTime })
|
const data = { startDate, startTime, endDate, endTime, totalTime }
|
||||||
|
// Mit nachgetragener Zeit zaehlt ein Git-Push als Arbeitszeit, nicht mehr als Kommentar
|
||||||
|
if (isGitWorksheet(worksheet)) data.isComment = false
|
||||||
|
return updateWorksheet(worksheet.$id, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+27
-4
@@ -23,10 +23,33 @@ export function serviceTypeLabel(value) {
|
|||||||
return SERVICE_TYPE_SHORT[String(value || '').trim().toLowerCase()] || value
|
return SERVICE_TYPE_SHORT[String(value || '').trim().toLowerCase()] || value
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zeiten eines Arbeitsblatts darf nur aendern, wer es angelegt hat.
|
// Git-Pushes legt das Kundenportal als Arbeitsblatt an (employeeId "gitea"): Ende = Push,
|
||||||
// Reine Kommentare haben keine Arbeitszeit und bleiben aussen vor.
|
// der Beginn wird im Ticketsystem nachgetragen
|
||||||
export function canEditWorksheetTimes(worksheet, user) {
|
export function isGitWorksheet(worksheet) {
|
||||||
return Boolean(user?.$id) && worksheet?.employeeId === user.$id && !worksheet.isComment
|
return worksheet?.serviceType === 'GIT'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function needsTimeEntry(worksheet) {
|
||||||
|
return isGitWorksheet(worksheet) && (!worksheet.startTime || !Number(worksheet.totalTime))
|
||||||
|
}
|
||||||
|
|
||||||
|
const sameName = (a, b) => String(a || '').trim().toLowerCase() === String(b || '').trim().toLowerCase()
|
||||||
|
|
||||||
|
/* Wer hat das Arbeitsblatt erstellt? Normale Eintraege tragen die Appwrite-ID des
|
||||||
|
Mitarbeiters. Bei Git-Pushes steht in employeeName der Gitea-Name des Pushers
|
||||||
|
(Benutzername wie "knso" oder voller Name); er gehoert dem Mitarbeiter mit diesem
|
||||||
|
Kuerzel oder Namen. employee ist der eigene Eintrag aus "employees". */
|
||||||
|
export function isWorksheetCreator(worksheet, user, employee) {
|
||||||
|
if (!user?.$id || !worksheet) return false
|
||||||
|
if (!isGitWorksheet(worksheet)) return worksheet.employeeId === user.$id
|
||||||
|
const pusher = String(worksheet.employeeName || '').trim()
|
||||||
|
return Boolean(pusher) && [employee?.shortcode, employee?.displayName, user.name].some((name) => sameName(name, pusher))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zeiten darf nur aendern, wer das Arbeitsblatt erstellt hat. Reine Kommentare haben
|
||||||
|
// keine Arbeitszeit; Git-Pushes sind Kommentare, bis ihre Zeit nachgetragen ist.
|
||||||
|
export function canEditWorksheetTimes(worksheet, user, employee) {
|
||||||
|
return isWorksheetCreator(worksheet, user, employee) && (isGitWorksheet(worksheet) || !worksheet.isComment)
|
||||||
}
|
}
|
||||||
|
|
||||||
const pad = (n) => String(n).padStart(2, '0')
|
const pad = (n) => String(n).padStart(2, '0')
|
||||||
|
|||||||
Reference in new issue
Block a user