Arbeitsnachweis: Abo-Stundenkonto nur bei Kunden mit Wartungs-Abo
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b05f49621c
commit
3b6a821687
5 files changed
+78
-5
No files matched your search
@@ -31,7 +31,7 @@ const initialForm = (workorder, newStatus) => ({
|
|||||||
countsToPlan: false
|
countsToPlan: false
|
||||||
})
|
})
|
||||||
|
|
||||||
export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCreate }) {
|
export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCreate, aboHoursAllowed = true }) {
|
||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
|
|
||||||
const isAcquisition = workorder?.type === 'Akquise'
|
const isAcquisition = workorder?.type === 'Akquise'
|
||||||
@@ -109,7 +109,7 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
|||||||
endTime: timeToWoms(formData.endTime),
|
endTime: timeToWoms(formData.endTime),
|
||||||
details: formData.details,
|
details: formData.details,
|
||||||
isComment: formData.isComment,
|
isComment: formData.isComment,
|
||||||
countsToPlan: !formData.isComment && formData.countsToPlan,
|
countsToPlan: aboHoursAllowed && !formData.isComment && formData.countsToPlan,
|
||||||
employeeShort: user?.prefs?.shortCode || '' // Aus User-Preferences
|
employeeShort: user?.prefs?.shortCode || '' // Aus User-Preferences
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,6 +191,7 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{aboHoursAllowed && (
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px' }} title="Zeit wird vom Stundenguthaben des Wartungs-Abos abgezogen und im Kundenportal angezeigt">
|
<label style={{ display: 'flex', alignItems: 'center', gap: '8px' }} title="Zeit wird vom Stundenguthaben des Wartungs-Abos abgezogen und im Kundenportal angezeigt">
|
||||||
<input
|
<input
|
||||||
@@ -202,6 +203,7 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
|||||||
Vom Abo-Stundenkonto abziehen
|
Vom Abo-Stundenkonto abziehen
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col col-6">
|
<div className="col col-6">
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import Tabs from './ui/Tabs'
|
|||||||
import Badge from './ui/Badge'
|
import Badge from './ui/Badge'
|
||||||
import { StatusPill, UrgentPill } from './ui/StatusPill'
|
import { StatusPill, UrgentPill } from './ui/StatusPill'
|
||||||
import { useWorksheets } from '../hooks/useWorksheets'
|
import { useWorksheets } from '../hooks/useWorksheets'
|
||||||
|
import { useCustomerAbo } from '../hooks/useCustomerAbo'
|
||||||
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 { canEditWorksheetTimes } from '../lib/ticketForm'
|
import { canEditWorksheetTimes } from '../lib/ticketForm'
|
||||||
@@ -34,6 +35,11 @@ export default function TicketCard({ ticket, onUpdate }) {
|
|||||||
const [activeTab, setActiveTab] = useState('details')
|
const [activeTab, setActiveTab] = useState('details')
|
||||||
const [showCreateWorksheet, setShowCreateWorksheet] = useState(false)
|
const [showCreateWorksheet, setShowCreateWorksheet] = useState(false)
|
||||||
const [editTimesWorksheet, setEditTimesWorksheet] = useState(null)
|
const [editTimesWorksheet, setEditTimesWorksheet] = useState(null)
|
||||||
|
// Abo-Stundenkonto gibt es nur bei Wartungs-Abo; erst laden, wenn Arbeitsblaetter gebraucht werden
|
||||||
|
const { hoursAllowed: aboHoursAllowed } = useCustomerAbo(
|
||||||
|
ticket.customerId,
|
||||||
|
(expanded && activeTab === 'worksheets') || showCreateWorksheet
|
||||||
|
)
|
||||||
|
|
||||||
const { worksheets, loading: worksheetsLoading, createWorksheet, updateWorksheet, updateWorksheetTimes, getTotalTime } = useWorksheets(
|
const { worksheets, loading: worksheetsLoading, createWorksheet, updateWorksheet, updateWorksheetTimes, getTotalTime } = useWorksheets(
|
||||||
expanded ? ticket.woid : null
|
expanded ? ticket.woid : null
|
||||||
@@ -139,6 +145,7 @@ export default function TicketCard({ ticket, onUpdate }) {
|
|||||||
canEditTimes={(ws) => canEditWorksheetTimes(ws, user, employee)}
|
canEditTimes={(ws) => canEditWorksheetTimes(ws, user, employee)}
|
||||||
onEditTimes={setEditTimesWorksheet}
|
onEditTimes={setEditTimesWorksheet}
|
||||||
onTogglePlan={handleTogglePlan}
|
onTogglePlan={handleTogglePlan}
|
||||||
|
aboHoursAllowed={aboHoursAllowed}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -148,7 +155,7 @@ export default function TicketCard({ ticket, onUpdate }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<CreateWorksheetModal isOpen={showCreateWorksheet} onClose={() => setShowCreateWorksheet(false)} workorder={ticket} onCreate={handleCreateWorksheet} />
|
<CreateWorksheetModal isOpen={showCreateWorksheet} onClose={() => setShowCreateWorksheet(false)} workorder={ticket} onCreate={handleCreateWorksheet} aboHoursAllowed={aboHoursAllowed} />
|
||||||
{editTimesWorksheet && (
|
{editTimesWorksheet && (
|
||||||
<WorksheetTimesModal
|
<WorksheetTimesModal
|
||||||
worksheet={editTimesWorksheet}
|
worksheet={editTimesWorksheet}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const headerButtonStyle = {
|
|||||||
marginRight: '8px',
|
marginRight: '8px',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WorksheetList({ worksheets, totalTime, loading, onTogglePlan, canEditTimes, onEditTimes }) {
|
export default function WorksheetList({ worksheets, totalTime, loading, onTogglePlan, aboHoursAllowed = true, canEditTimes, onEditTimes }) {
|
||||||
const [expandedWorksheets, setExpandedWorksheets] = useState({})
|
const [expandedWorksheets, setExpandedWorksheets] = useState({})
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -177,7 +177,7 @@ export default function WorksheetList({ worksheets, totalTime, loading, onToggle
|
|||||||
: <><FaPen className="me-1" /> Zeiten bearbeiten</>}
|
: <><FaPen className="me-1" /> Zeiten bearbeiten</>}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{onTogglePlan && !ws.isComment && Number(ws.totalTime) > 0 && (
|
{onTogglePlan && !ws.isComment && Number(ws.totalTime) > 0 && (aboHoursAllowed || ws.countsToPlan) && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-sm"
|
className="btn btn-sm"
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { getCustomerAbo } from '../lib/customerAdminApi'
|
||||||
|
|
||||||
|
// Ergebnis je Kunde zwischengespeichert (nur Erfolge), laufende Abfragen werden geteilt
|
||||||
|
const cache = new Map()
|
||||||
|
const inflight = new Map()
|
||||||
|
|
||||||
|
function load(customerId) {
|
||||||
|
if (cache.has(customerId)) return Promise.resolve(cache.get(customerId))
|
||||||
|
if (!inflight.has(customerId)) {
|
||||||
|
const p = getCustomerAbo(customerId)
|
||||||
|
.then((data) => {
|
||||||
|
const abo = { productKey: data?.productKey ?? null, active: Boolean(data?.active) }
|
||||||
|
cache.set(customerId, abo)
|
||||||
|
return abo
|
||||||
|
})
|
||||||
|
.finally(() => inflight.delete(customerId))
|
||||||
|
inflight.set(customerId, p)
|
||||||
|
}
|
||||||
|
return inflight.get(customerId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// { productKey, active, loading, error, hoursAllowed }
|
||||||
|
// hoursAllowed: darf der Schalter "Vom Abo-Stundenkonto abziehen" gezeigt werden?
|
||||||
|
// - nur bei aktivem Wartungs-Abo (Hosting-Abo hat kein Stundenkonto)
|
||||||
|
// - waehrend des Ladens nicht
|
||||||
|
// - bei Fehler der Schnittstelle ja (wie bisher), mit Warnung in der Konsole
|
||||||
|
// - ohne Kunde am Ticket nicht (es gibt kein Konto, das belastet werden koennte)
|
||||||
|
// enabled=false: noch nichts laden (die Karten sind meist zugeklappt)
|
||||||
|
export function useCustomerAbo(customerId, enabled = true) {
|
||||||
|
const [state, setState] = useState(() => ({
|
||||||
|
key: null, abo: null, error: false,
|
||||||
|
}))
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!customerId || !enabled) return undefined
|
||||||
|
let cancelled = false
|
||||||
|
load(customerId)
|
||||||
|
.then((abo) => { if (!cancelled) setState({ key: customerId, abo, error: false }) })
|
||||||
|
.catch((err) => {
|
||||||
|
console.warn('Abo-Status des Kunden konnte nicht geladen werden:', err?.message || err)
|
||||||
|
if (!cancelled) setState({ key: customerId, abo: null, error: true })
|
||||||
|
})
|
||||||
|
return () => { cancelled = true }
|
||||||
|
}, [customerId, enabled])
|
||||||
|
|
||||||
|
const ready = Boolean(customerId) && state.key === customerId
|
||||||
|
const cached = customerId ? cache.get(customerId) : null
|
||||||
|
const abo = ready && state.abo ? state.abo : cached
|
||||||
|
const error = ready && state.error
|
||||||
|
const loading = Boolean(customerId) && enabled && !abo && !error
|
||||||
|
const productKey = abo?.productKey ?? null
|
||||||
|
const active = abo?.active ?? false
|
||||||
|
const hoursAllowed = !customerId || loading
|
||||||
|
? false
|
||||||
|
: error || (productKey === 'wartung' && active)
|
||||||
|
|
||||||
|
return { productKey, active, loading, error, hoursAllowed }
|
||||||
|
}
|
||||||
@@ -34,3 +34,8 @@ export async function updateCustomerWithPortalAccess(customerId, payload) {
|
|||||||
export async function deleteCustomerWithPortalAccess(customerId) {
|
export async function deleteCustomerWithPortalAccess(customerId) {
|
||||||
return adminFetch(`/api/admin/customers/${customerId}`, { method: 'DELETE' })
|
return adminFetch(`/api/admin/customers/${customerId}`, { method: 'DELETE' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wartungs-/Hosting-Abo des Kunden: { productKey: 'wartung' | 'hosting' | null, active }
|
||||||
|
export async function getCustomerAbo(customerId) {
|
||||||
|
return adminFetch(`/api/admin/customers/${encodeURIComponent(customerId)}/abo`, { method: 'GET' })
|
||||||
|
}
|
||||||
Reference in new issue
Block a user