'use client'
// tdd-guard:skip — Client-Orchestrierung (Dexie/Supabase/DnD-State), Logik-Kerne liegen in lib/ und sind dort getestet

import { useState, useEffect, useCallback, useRef, useMemo } from'react'
import { useLiveQuery } from'dexie-react-hooks'
import { createClient } from'@/lib/supabase/client'
import { logger } from'@/lib/logger'
import { getDB } from'@/lib/offline/db'
import { assignmentRepo, consultantRepo, appointmentTypeRepo } from'@/lib/repositories/planning-repo'
import type { Consultant, AppointmentType, PlanningProject, Assignment, AssignmentStatus } from'@/lib/planning-types'
import type { PlanningPerms } from'@/lib/planning-permissions'
import type { CalendarColors, CustomHoliday } from'@/lib/planning-settings'
import { canEditAssignment, canDeleteAssignment } from'@/lib/planning-permissions'
import { getUserHolidayMap } from'@/lib/holidays/holiday-service'
import { yearRange, formatDateKey, getISOWeek } from'@/lib/planning-config'
import { findConsecutiveSpan, hasOverlapOnDates, datesInRange } from'@/lib/planning/assignment-helpers'
import { exportGridToExcel, triggerPrintGrid } from'./export-grid'
import { useToast } from'@/components/repository/toast'
import PlanningToolbar from'./planning-toolbar'
import PlanningGrid from'./planning-grid'
import CreateAssignmentModal from'./create-assignment-modal'
import EditAssignmentModal from'./edit-assignment-modal'
import AssignmentContextMenu from'./assignment-context-menu'
import BulkActionBar from'./bulk-action-bar'
import BulkContextMenu from'./bulk-context-menu'
import BulkEditModal from'./bulk-edit-modal'
import CopyWeekModal from'./copy-week-modal'

interface Props {
 consultants: Consultant[]
 appointmentTypes: AppointmentType[]
 planningProjects: PlanningProject[]
 initialAssignments: Assignment[]
 initialYear: number
 initialMonth: number
 currentConsultant: Consultant | null
 perms: PlanningPerms
 calendarColors?: CalendarColors
 customHolidays?: CustomHoliday[]
 /** FB-36 (P0 compliance, composition profile): work_mode ("Arbeitsort") capture on/off. */
 workModeCapture: boolean
}

interface ContextMenuState {
 assignment: Assignment
 x: number
 y: number
}

export default function PlanningClient({
 consultants, appointmentTypes, planningProjects,
 initialAssignments, initialYear, initialMonth,
 currentConsultant, perms,
 calendarColors, customHolidays,
 workModeCapture,
}: Props) {
 // ── State ──────────────────────────────────────────────────────────────────
 const [year, setYear] = useState(initialYear)
 const [teamFilter, setTeamFilter] = useState('all')
 const [consultantFilter, setConsultantFilter] = useState('all')
 const [typeFilter, setTypeFilter] = useState('all')
 const [projectSearch, setProjectSearch] = useState('')
 const [travelFilter, setTravelFilter] = useState(false)
 const [assignments, setAssignments] = useState<Assignment[]>(initialAssignments)
 const [loading, setLoading] = useState(false)
 const [mounted, setMounted] = useState(false)
 const [dbHolidays, setDbHolidays] = useState<Map<string, string> | undefined>(undefined)

 // Modals
 const [createModal, setCreateModal] = useState<{ consultantId: string; date: string; endDate: string } | null>(null)
 const [editModal, setEditModal] = useState<Assignment | null>(null)
 const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null)
 const [copyWeekModal, setCopyWeekModal] = useState(false)
 const [bulkContextMenu, setBulkContextMenu] = useState<{ x: number; y: number } | null>(null)
 const [bulkEditModal, setBulkEditModal] = useState(false)

 // Bulk selection
 const [selectionMode, setSelectionMode] = useState(false)
 const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())

 // Undo — stores the previous assignment data after a move or resize
 const [undoStack, setUndoStack] = useState<Assignment[] | null>(null)
 const undoTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)

 const isFirstRender = useRef(true)

 // Tracks the"focus day"for keyboard day/week navigation on desktop.
 // Initialized to the middle of the initial month to avoid month-boundary edge cases.
 const focusDayRef = useRef(new Date(initialYear, initialMonth - 1, 15))

 // ── Mobile detection + week navigation ────────────────────────────────────
 const [isMobile, setIsMobile] = useState(false)
 const [weekOffset, setWeekOffset] = useState(0) // 0 = current week

 useEffect(() => {
 const check = () => setIsMobile(window.innerWidth < 640)
 check()
 window.addEventListener('resize', check)
 return () => window.removeEventListener('resize', check)
 }, [])

 const supabase = useMemo(() => createClient(), [])

 const { toast } = useToast()

 // ── Seed local stores from server-fetched props ───────────────────────────
 useEffect(() => {
 setMounted(true)
 assignmentRepo.seedFromServer(initialAssignments).catch(() => {})
 consultantRepo.seedFromServer(consultants).catch(() => {})
 appointmentTypeRepo.seedFromServer(appointmentTypes).catch(() => {})
 }, []) // eslint-disable-line react-hooks/exhaustive-deps

 // ── DB holidays (user preference-driven, re-fetched per year) ────────────
 useEffect(() => {
 if (!currentConsultant?.auth_user_id) return
 getUserHolidayMap(supabase, currentConsultant.auth_user_id, year)
 .then(map => setDbHolidays(map.size > 0 ? map : undefined))
 .catch(() => setDbHolidays(undefined))
 }, [year, currentConsultant?.auth_user_id, supabase])

 // ── Reactive assignments from IndexedDB ───────────────────────────────────
 // On mobile show a 7-day week; on desktop show the full month
 const mobileDays = useMemo<Date[] | undefined>(() => {
 if (!isMobile) return undefined
 const today = new Date()
 const dow = today.getDay() === 0 ? 7 : today.getDay() // Mon=1 … Sun=7
 const monday = new Date(today)
 monday.setDate(today.getDate() - (dow - 1) + weekOffset * 7)
 monday.setHours(0, 0, 0, 0)
 return Array.from({ length: 14 }, (_, i) => {
 const d = new Date(monday)
 d.setDate(monday.getDate() + i)
 return d
 })
 }, [isMobile, weekOffset])

 // Sync year when mobile week crosses a year boundary
 useEffect(() => {
 if (!mobileDays) return
 const thu = mobileDays[3]
 const y = thu.getFullYear()
 if (y !== year) setYear(y)
 // eslint-disable-next-line react-hooks/exhaustive-deps
 }, [mobileDays])

 // Fetch range — 2-week slice on mobile, full year on desktop
 const { rangeFirst, rangeLast } = useMemo(() => {
 if (mobileDays && mobileDays.length > 0) {
 return {
 rangeFirst: formatDateKey(mobileDays[0]),
 rangeLast: formatDateKey(mobileDays[mobileDays.length - 1]),
 }
 }
 const { first, last } = yearRange(year)
 return { rangeFirst: first, rangeLast: last }
 }, [mobileDays, year])

 const localAssignments = useLiveQuery<Assignment[]>(
 async () => {
 if (!mounted) return []
 const rows = await getDB().assignments.where('date').between(rangeFirst, rangeLast, true, true).toArray()
 return rows as unknown as Assignment[]
 },
 [mounted, rangeFirst, rangeLast],
 )

 // Use local store when available, fall back to React state during initial load
 const effectiveAssignments = (localAssignments ?? assignments) as Assignment[]

 // Sync status map for grid badges
 const syncStatusMap = useMemo(() => {
 const map: Record<string, string> = {}
 for (const a of effectiveAssignments) {
 const status = (a as Assignment & { _syncStatus?: string })._syncStatus
 if (status && status !=='synced') map[a.id] = status
 }
 return map
 }, [effectiveAssignments])

 // ── Data fetching ──────────────────────────────────────────────────────────
 const fetchAssignments = useCallback(async () => {
 // Capture scrollLeft BEFORE the loading flicker so we can restore it
 // after React reconciles the new data. Otherwise the calendar visibly
 // jumps back to January (scrollLeft = 0) on every mutation.
 //
 // We restore through multiple checkpoints because something in the
 // grid render pipeline (likely a temporary content-width shrink while
 // tiles re-mount) clamps scrollLeft on at least one of the early
 // frames. rAF×2 gets the first pass; the 80ms / 250ms setTimeouts
 // catch the late re-layout that happened after Manu's previous test.
 const scrollLeft = gridContainerRef.current?.scrollLeft ?? null
 setLoading(true)
 const { data, error } = await supabase
 .from('assignments')
 .select('*')
 .gte('date', rangeFirst)
 .lte('date', rangeLast)
 if (error) logger.error('planning.assignments.fetch_failed', error)
 const fetched = (data as Assignment[]) ?? []
 setAssignments(fetched)
 assignmentRepo.seedFromServer(fetched).catch(() => {})
 setLoading(false)
 if (scrollLeft != null && scrollLeft > 0) {
 const restore = () => {
 if (gridContainerRef.current && gridContainerRef.current.scrollLeft !== scrollLeft) {
 gridContainerRef.current.scrollLeft = scrollLeft
 }
 }
 requestAnimationFrame(() => requestAnimationFrame(restore))
 setTimeout(restore, 80)
 setTimeout(restore, 250)
 }
 // eslint-disable-next-line react-hooks/exhaustive-deps
 }, [rangeFirst, rangeLast])

 useEffect(() => {
 if (isFirstRender.current) { isFirstRender.current = false; return }
 fetchAssignments()
 // eslint-disable-next-line react-hooks/exhaustive-deps
 }, [rangeFirst, rangeLast])

 // ── Scroll container ref + scroll-to-date ─────────────────────────────────
 const gridContainerRef = useRef<HTMLDivElement>(null)
 const scrollAfterYearChangeRef = useRef<Date | null>(null)

 // Stable: no reactive deps (takes year explicitly to avoid stale closures)
 const scrollToDate = useCallback((date: Date, targetYear: number) => {
 const container = gridContainerRef.current
 if (!container || date.getFullYear() !== targetYear) return
 const yearStart = new Date(targetYear, 0, 1)
 const dayIndex = Math.round((date.getTime() - yearStart.getTime()) / 86400000)
 const NAME_COL = 160
 const DAY_COL  = 40
 const targetX  = NAME_COL + dayIndex * DAY_COL
 container.scrollTo({ left: Math.max(0, targetX - container.clientWidth / 2), behavior: 'smooth' })
 }, [])

 // After year changes and grid re-renders, execute pending scroll
 useEffect(() => {
 if (!scrollAfterYearChangeRef.current) return
 const target = scrollAfterYearChangeRef.current
 scrollAfterYearChangeRef.current = null
 const t = setTimeout(() => scrollToDate(target, year), 80)
 return () => clearTimeout(t)
 }, [year, scrollToDate])

 // On initial mount: scroll to today
 useEffect(() => {
 const today = new Date()
 if (today.getFullYear() !== year) return
 const t = setTimeout(() => scrollToDate(today, year), 120)
 return () => clearTimeout(t)
 // eslint-disable-next-line react-hooks/exhaustive-deps
 }, [])

 // ── Navigation ────────────────────────────────────────────────────────────
 const prevYear = useCallback(() => setYear((y) => y - 1), [])
 const nextYear = useCallback(() => setYear((y) => y + 1), [])

 const goToday = useCallback(() => {
 setWeekOffset(0)
 const today = new Date()
 focusDayRef.current = today
 const todayYear = today.getFullYear()
 if (todayYear !== year) {
 scrollAfterYearChangeRef.current = today
 setYear(todayYear)
 } else {
 scrollToDate(today, todayYear)
 }
 }, [year, scrollToDate])

 const prevWeek = useCallback(() => setWeekOffset((w) => w - 1), [])
 const nextWeek = useCallback(() => setWeekOffset((w) => w + 1), [])

 // Mobile: dispatch week navigation; desktop: dispatch year navigation
 const handlePrev = isMobile ? prevWeek : prevYear
 const handleNext = isMobile ? nextWeek : nextYear

 // Day-granular keyboard navigation (desktop only): navigate by ±delta days
 const shiftByDays = useCallback((delta: number) => {
 const next = new Date(focusDayRef.current)
 next.setDate(next.getDate() + delta)
 focusDayRef.current = next
 const nextYear = next.getFullYear()
 if (nextYear !== year) {
 scrollAfterYearChangeRef.current = next
 setYear(nextYear)
 } else {
 scrollToDate(next, nextYear)
 }
 }, [year, scrollToDate])

 // Mobile week label: "KW 14–15 · 31.03 – 13.04.2025" (2-week view)
 const mobileWeekLabel = useMemo<string | undefined>(() => {
 if (!isMobile || !mobileDays || mobileDays.length < 14) return undefined
 const kw1 = getISOWeek(mobileDays[0])
 const kw2 = getISOWeek(mobileDays[13])
 const fmt = (d: Date) =>
 `${String(d.getDate()).padStart(2,'0')}.${String(d.getMonth() + 1).padStart(2,'0')}.`
 const kwLabel = kw1 === kw2 ? `KW ${kw1}` : `KW ${kw1}–${kw2}`
 return `${kwLabel} · ${fmt(mobileDays[0])} – ${fmt(mobileDays[13])}${mobileDays[13].getFullYear()}`
 }, [isMobile, mobileDays])

 // ── Swipe navigation (mobile) ─────────────────────────────────────────────
 const touchStartRef = useRef<{ x: number; y: number } | null>(null)

 const handleTouchStart = useCallback((e: React.TouchEvent) => {
 touchStartRef.current = { x: e.touches[0].clientX, y: e.touches[0].clientY }
 }, [])

 const handleTouchEnd = useCallback((e: React.TouchEvent) => {
 if (!touchStartRef.current || !isMobile) return
 const dx = e.changedTouches[0].clientX - touchStartRef.current.x
 const dy = e.changedTouches[0].clientY - touchStartRef.current.y
 touchStartRef.current = null
 // Only register horizontal swipes (wider than tall, and > 50px)
 if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 50) {
 if (dx < 0) nextWeek()
 else prevWeek()
 }
 }, [isMobile, nextWeek, prevWeek])

 // ── Selection helpers ──────────────────────────────────────────────────────
 // Clear selection on month change; use functional updater to skip re-render
 // when the Set is already empty (avoids unnecessary PlanningGrid re-renders)
 useEffect(() => {
 setSelectedIds((prev) => (prev.size === 0 ? prev : new Set()))
 setSelectedWeeks((prev) => (prev.size === 0 ? prev : new Set()))
 }, [year])

 const handleToggleSelectionMode = useCallback(() => {
 setSelectionMode((prev) => {
 if (prev) setSelectedIds(new Set()) // clear when turning off
 return !prev
 })
 }, [])

 // ── Global keyboard handler ────────────────────────────────────────────────
 useEffect(() => {
 function onKey(e: KeyboardEvent) {
 const noModal = !createModal && !editModal && !contextMenu && !copyWeekModal
 if (noModal) {
 if (isMobile) {
 // Mobile: left/right arrow = prev/next week
 if (e.key ==='ArrowLeft') { e.preventDefault(); prevWeek() }
 if (e.key ==='ArrowRight') { e.preventDefault(); nextWeek() }
 } else {
 // Desktop: arrow keys = continuous day/week scrolling
 if (e.key ==='ArrowLeft') { e.preventDefault(); shiftByDays(-1) }
 if (e.key ==='ArrowRight') { e.preventDefault(); shiftByDays(1) }
 if (e.key ==='ArrowUp') { e.preventDefault(); shiftByDays(-7) }
 if (e.key ==='ArrowDown') { e.preventDefault(); shiftByDays(7) }
 }
 if (e.key ==='Escape'&& selectionMode) setSelectionMode(false)
 // Delete/Backspace: delete a single selected assignment
 if ((e.key ==='Delete'|| e.key ==='Backspace') && selectedIds.size === 1) {
 const [id] = Array.from(selectedIds)
 const found = effectiveAssignments.find((a) => a.id === id)
 if (found) {
 handleDelete(found)
 setSelectedIds(new Set())
 setSelectionMode(false)
 }
 }
 }
 if (e.key ==='Escape'&& contextMenu) setContextMenu(null)
 }
 document.addEventListener('keydown', onKey)
 return () => document.removeEventListener('keydown', onKey)
 // eslint-disable-next-line react-hooks/exhaustive-deps
 }, [createModal, editModal, contextMenu, copyWeekModal, selectionMode, selectedIds, effectiveAssignments, isMobile, prevWeek, nextWeek, shiftByDays])

 // ── Mutation handlers ──────────────────────────────────────────────────────
 const handleStatusChange = useCallback(async (assignment: Assignment, status: AssignmentStatus) => {
 if (!canEditAssignment(perms, assignment)) return
 await assignmentRepo.update(assignment.id, { status, updated_at: new Date().toISOString() }).catch((err) => {
 logger.error('planning.assignment.status_update_failed', err, { assignment_id: assignment.id, status })
 })
 }, [perms])

 const handleDuplicate = useCallback(async (assignment: Assignment) => {
 if (!perms.canCreate) return
 const { data: { user } } = await supabase.auth.getUser()
 const { error } = await supabase.from('assignments').insert({
 date: assignment.date,
 consultant_id: assignment.consultant_id,
 appointment_type_id: assignment.appointment_type_id,
 project_id: assignment.project_id,
 supplier_name: assignment.supplier_name,
 supplier_id: assignment.supplier_id,
 location_label: assignment.location_label,
 description: assignment.description,
 // FB-36: capture disabled -> omit work_mode entirely so the DB default
 // ('onsite', NOT NULL) applies, instead of copying the source
 // assignment's stored value into a row the user never had a chance to
 // set it on.
 ...(workModeCapture ? { work_mode: assignment.work_mode } : {}),
 status: assignment.status,
 is_all_day: assignment.is_all_day,
 start_time: assignment.start_time,
 end_time: assignment.end_time,
 source:'duplicated',
 created_by: user?.id ?? null,
 updated_by: user?.id ?? null,
 })
 if (error) logger.error('planning.assignment.duplicate_failed', error, { source_id: assignment.id })
 else fetchAssignments()
 }, [perms.canCreate, fetchAssignments, workModeCapture])

 const handleDelete = useCallback(async (assignment: Assignment) => {
 if (!canDeleteAssignment(perms, assignment)) return
 // Right-click delete on a multi-day block should remove the whole
 // span, not just the anchor day — otherwise the user sees the
 // surrounding days remain and reads it as "nothing happened".
 const span = findConsecutiveSpan(effectiveAssignments, assignment)
 const ids = span.map((a) => a.id)
 try {
 const { error: sbErr } = await supabase.from('assignments').delete().in('id', ids)
 if (sbErr) {
 logger.error('planning.assignment.delete_failed', sbErr, { assignment_ids: ids })
 setOverlapToast(`Löschen fehlgeschlagen: ${sbErr.message}`)
 setTimeout(() => setOverlapToast(null), 5000)
 fetchAssignments()
 return
 }
 // Mirror the deletion into the local Dexie store so the UI updates
 // immediately even before the next fetch resolves.
 await Promise.all(ids.map((id) => assignmentRepo.remove(id).catch(() => {})))
 fetchAssignments()
 } catch (err) {
 logger.error('planning.assignment.delete_failed', err as Error, { assignment_ids: ids })
 const msg = err instanceof Error ? err.message :'Unbekannter Fehler'
 setOverlapToast(`Löschen fehlgeschlagen: ${msg}`)
 setTimeout(() => setOverlapToast(null), 5000)
 }
 }, [perms, fetchAssignments, effectiveAssignments])

 // TODO [O1]: Route through offline queue — see docs/TECHNICAL_DEBT.md
 const handleTileMove = useCallback(async (
 movedAssignments: Assignment[],
 newStartDate: string,
 newConsultantId: string,
 ) => {
 if (!movedAssignments.length) return
 const a0 = movedAssignments[0]
 if (!canEditAssignment(perms, a0)) return

 const offsetDays = Math.round(
 (new Date(newStartDate +'T00:00:00').getTime() - new Date(a0.date +'T00:00:00').getTime()) / 86400000,
 )
 if (offsetDays === 0 && a0.consultant_id === newConsultantId) return

 // Conflict detection — same pattern as handleSpanResize
 const newDates = movedAssignments.map((a) => {
 const d = new Date(a.date +'T00:00:00')
 d.setDate(d.getDate() + offsetDays)
 return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`
 })
 const excludeIds = new Set(movedAssignments.map((a) => a.id))
 if (hasOverlapOnDates(effectiveAssignments, newConsultantId, newDates, excludeIds)) {
 toast({ message:'Terminkonflikt — Eintrag wurde nicht verschoben.', type:'error'})
 return
 }

 // Save previous state for undo
 const previousState = movedAssignments.map((a) => ({ ...a }))

 const results = await Promise.all(movedAssignments.map((a, i) => {
 return supabase.from('assignments').update({
 date: newDates[i],
 consultant_id: newConsultantId,
 updated_at: new Date().toISOString(),
 }).eq('id', a.id)
 }))
 const moveError = results.find((r) => r.error)?.error
 if (moveError) {
 logger.error('planning.assignment.move_failed', moveError)
 setOverlapToast(`Verschieben fehlgeschlagen: ${moveError.message}`)
 setTimeout(() => setOverlapToast(null), 5000)
 }
 fetchAssignments()

 // Undo toast
 if (undoTimerRef.current) clearTimeout(undoTimerRef.current)
 setUndoStack(previousState)
 undoTimerRef.current = setTimeout(() => setUndoStack(null), 5000)
 }, [perms, effectiveAssignments, fetchAssignments, toast])

 const handleBulkDelete = useCallback(async () => {
 const ids = Array.from(selectedIds)
 if (ids.length === 0) return
 const { error } = await supabase.from('assignments').delete().in('id', ids)
 if (error) {
 logger.error('planning.assignments.bulk_delete_failed', error, { count: ids.length })
 setOverlapToast(`Löschen fehlgeschlagen: ${error.message}`)
 setTimeout(() => setOverlapToast(null), 5000)
 return
 }
 setSelectedIds(new Set())
 fetchAssignments()
 }, [selectedIds, fetchAssignments])

 const handleBulkStatusChange = useCallback(async (status: AssignmentStatus) => {
 const ids = Array.from(selectedIds)
 if (ids.length === 0) return
 const { error } = await supabase
 .from('assignments')
 .update({ status, updated_at: new Date().toISOString() })
 .in('id', ids)
 if (error) logger.error('planning.assignments.bulk_status_failed', error, { count: ids.length, status })
 else { setSelectedIds(new Set()); fetchAssignments() }
 }, [selectedIds, fetchAssignments])

 // ── Stable grid callbacks (so React.memo on PlanningGrid actually works) ───
 const handleCellClick = useCallback((consultantId: string, startDate: string, endDate: string) => {
 if (!perms.canCreate) return
 setCreateModal({ consultantId, date: startDate, endDate })
 }, [perms.canCreate])

 const handleCardClick = useCallback((a: Assignment) => {
 if (!canEditAssignment(perms, a)) return
 setEditModal(a)
 }, [perms])

 const handleCardRightClick = useCallback((a: Assignment, x: number, y: number) => {
 setContextMenu({ assignment: a, x, y })
 }, [])

 const handleToggleSelect = useCallback((id: string) => {
 setSelectedIds((prev) => {
 const next = new Set(prev)
 if (next.has(id)) next.delete(id)
 else next.add(id)
 return next
 })
 }, [])

 const handleContextMenuClose = useCallback(() => setContextMenu(null), [])

 // ── Ctrl-click: enable selection mode + toggle this card ───────────────────
 const handleCtrlClickCard = useCallback((id: string) => {
 setSelectionMode(true)
 setSelectedIds((prev) => {
 const next = new Set(prev)
 if (next.has(id)) next.delete(id)
 else next.add(id)
 return next
 })
 }, [])

 // ── Select-range: drag in selection mode selects all assignments in range ──
 const handleSelectRange = useCallback((consultantId: string, startDate: string, endDate: string) => {
 setSelectionMode(true)
 setSelectedIds((prev) => {
 const next = new Set(prev)
 for (const a of effectiveAssignments) {
 if (a.consultant_id === consultantId && a.date >= startDate && a.date <= endDate) {
 next.add(a.id)
 }
 }
 return next
 })
 }, [effectiveAssignments])

 // ── Bulk context menu / bulk edit ──────────────────────────────────────────
 const handleBulkRightClick = useCallback((x: number, y: number) => setBulkContextMenu({ x, y }), [])
 const handleBulkContextClose = useCallback(() => setBulkContextMenu(null), [])
 const handleBulkEditOpen = useCallback(() => setBulkEditModal(true), [])
 const handleBulkEditClose = useCallback(() => setBulkEditModal(false), [])
 const handleBulkEditSaved = useCallback(() => {
 setBulkEditModal(false)
 setSelectedIds(new Set())
 fetchAssignments()
 }, [fetchAssignments])

 // TODO [O1]: Route through offline queue — see docs/TECHNICAL_DEBT.md
 const handleSpanResize = useCallback(async (
 _consultantId: string,
 originalAssignments: Assignment[],
 newStart: string,
 newEnd: string,
 ) => {
 if (originalAssignments.length === 0) return
 const base = originalAssignments[0]

 // ── Overlap check (2.10) ───────────────────────────────────────────────────
 const newDates = datesInRange(newStart, newEnd)
 const excludeIds = new Set(originalAssignments.map((a) => a.id))
 if (hasOverlapOnDates(effectiveAssignments, base.consultant_id, newDates, excludeIds)) {
 setOverlapToast('Überlappung erkannt — Größe wurde zurückgesetzt.')
 setTimeout(() => setOverlapToast(null), 3000)
 return
 }

 const idsToDelete = originalAssignments
 .filter((a) => a.date < newStart || a.date > newEnd)
 .map((a) => a.id)

 const origDates = new Set(originalAssignments.map((a) => a.date))
 const datesToInsert = newDates.filter((d) => !origDates.has(d))

 if (idsToDelete.length > 0) {
 const { error: delErr } = await supabase.from('assignments').delete().in('id', idsToDelete)
 if (delErr) {
 logger.error('planning.assignment.resize_delete_failed', delErr, { count: idsToDelete.length })
 setOverlapToast(`Größe konnte nicht angepasst werden: ${delErr.message}`)
 setTimeout(() => setOverlapToast(null), 5000)
 fetchAssignments()
 return
 }
 }
 if (datesToInsert.length > 0) {
 // Note: assignments has `created_by` / `updated_by`, NOT `user_id`.
 // The Assignment type carries `user_id` as a legacy alias, but the
 // physical column was renamed; including it in the insert blew up
 // the resize flow with "Could not find the 'user_id' column".
 const rows = datesToInsert.map((date) => ({
 consultant_id: base.consultant_id,
 appointment_type_id: base.appointment_type_id,
 project_id: base.project_id,
 supplier_name: base.supplier_name,
 supplier_id: base.supplier_id,
 location_label: base.location_label,
 description: base.description,
 // FB-36: currently unconditional — this handler is dead code (not wired
 // to any grid callback, see the lint no-unused-vars finding on
 // `handleSpanResize` above). If it is ever reactivated, gate this the
 // same way as handleDuplicate/handleBulkCopy: omit work_mode from the
 // insert payload when workModeCapture is false, don't just copy it.
 work_mode: base.work_mode,
 status: base.status,
 is_all_day: base.is_all_day,
 start_time: base.start_time,
 end_time: base.end_time,
 created_by: base.created_by,
 requires_travel: base.requires_travel,
 is_demo: base.is_demo,
 source:'manual'as const,
 date,
 updated_at: new Date().toISOString(),
 }))
 const { error: insErr } = await supabase.from('assignments').insert(rows)
 if (insErr) {
 logger.error('planning.assignment.resize_insert_failed', insErr, { count: datesToInsert.length })
 setOverlapToast(`Größe konnte nicht erweitert werden: ${insErr.message}`)
 setTimeout(() => setOverlapToast(null), 5000)
 fetchAssignments()
 return
 }
 }

 // Undo toast for resize
 if (undoTimerRef.current) clearTimeout(undoTimerRef.current)
 setUndoStack(originalAssignments.map((a) => ({ ...a })))
 undoTimerRef.current = setTimeout(() => setUndoStack(null), 5000)

 fetchAssignments()
 }, [effectiveAssignments, fetchAssignments])

 // ── Undo handler ──────────────────────────────────────────────────────────
 const handleUndo = useCallback(async () => {
 if (!undoStack || undoStack.length === 0) return
 // Restore each assignment to its previous state
 const results = await Promise.all(undoStack.map((a) =>
 supabase.from('assignments').update({
 date: a.date,
 consultant_id: a.consultant_id,
 updated_at: new Date().toISOString(),
 }).eq('id', a.id),
 ))
 for (const { error } of results) {
 if (error) logger.error('planning.assignment.undo_failed', error)
 }
 setUndoStack(null)
 if (undoTimerRef.current) clearTimeout(undoTimerRef.current)
 fetchAssignments()
 }, [undoStack, fetchAssignments])

 const handleBulkCopy = useCallback(async () => {
 const ids = Array.from(selectedIds)
 if (ids.length === 0) return
 const { data: { user } } = await supabase.auth.getUser()
 const toCopy = effectiveAssignments.filter((a) => ids.includes(a.id))
 const rows = toCopy.map((a) => ({
 date: a.date,
 consultant_id: a.consultant_id,
 appointment_type_id: a.appointment_type_id,
 project_id: a.project_id,
 supplier_name: a.supplier_name,
 supplier_id: a.supplier_id,
 location_label: a.location_label,
 description: a.description,
 // FB-36: capture disabled -> omit work_mode entirely so the DB default
 // ('onsite', NOT NULL) applies, instead of copying the source
 // assignment's stored value into a row the user never had a chance to
 // set it on.
 ...(workModeCapture ? { work_mode: a.work_mode } : {}),
 status: a.status,
 is_all_day: a.is_all_day,
 start_time: a.start_time,
 end_time: a.end_time,
 source:'duplicated',
 created_by: user?.id ?? null,
 updated_by: user?.id ?? null,
 }))
 const { error } = await supabase.from('assignments').insert(rows)
 if (error) logger.error('planning.assignments.bulk_copy_failed', error, { count: rows.length })
 else { setSelectedIds(new Set()); fetchAssignments() }
 }, [selectedIds, effectiveAssignments, fetchAssignments, workModeCapture])

 // ── Filters ────────────────────────────────────────────────────────────────
 const teamCodes = useMemo(
 () => Array.from(new Set(consultants.map((c) => c.team_code))).sort(),
 [consultants],
 )

 const filteredConsultants = useMemo(
 () => consultants.filter((c) => {
 if (teamFilter !=='all'&& c.team_code !== teamFilter) return false
 if (consultantFilter !=='all'&& c.id !== consultantFilter) return false
 return true
 }),
 [consultants, teamFilter, consultantFilter],
 )

 // Current user always at the top of the list
 const sortedConsultants = useMemo(() => {
 if (!currentConsultant) return filteredConsultants
 const myId = currentConsultant.id
 return [
 ...filteredConsultants.filter((c) => c.id === myId),
 ...filteredConsultants.filter((c) => c.id !== myId),
 ]
 }, [filteredConsultants, currentConsultant])

 // Row highlighting (consultant multi-select)
 const [highlightedIds, setHighlightedIds] = useState<Set<string>>(new Set())

 const handleToggleHighlight = useCallback((id: string) => {
 setHighlightedIds((prev) => {
 const next = new Set(prev)
 if (next.has(id)) next.delete(id)
 else next.add(id)
 return next
 })
 }, [])

 const handleClearHighlights = useCallback(() => setHighlightedIds(new Set()), [])

 // KW (calendar week) column highlight
 const [selectedWeeks, setSelectedWeeks] = useState<Set<number>>(new Set())

 const handleToggleWeek = useCallback((week: number) => {
 setSelectedWeeks((prev) => {
 const next = new Set(prev)
 if (next.has(week)) next.delete(week)
 else next.add(week)
 return next
 })
 }, [])

 const handleClearWeeks = useCallback(() => setSelectedWeeks(new Set()), [])

 // Overlap toast (2.10)
 const [overlapToast, setOverlapToast] = useState<string | null>(null)

 const projectLookup = useMemo(
 () => new Map(planningProjects.map((p) => [p.id, p])),
 [planningProjects],
 )

 // ── Export callbacks (after filters so closures capture current values) ────
 const filteredAssignments = useMemo(
 () => effectiveAssignments.filter((a) => {
 if (typeFilter !=='all'&& a.appointment_type_id !== typeFilter) return false
 if (projectSearch) {
 const q = projectSearch.toLowerCase()
 const proj = a.project_id ? projectLookup.get(a.project_id) : undefined
 const hit =
 proj?.code.toLowerCase().includes(q) ||
 proj?.name.toLowerCase().includes(q) ||
 a.supplier_name?.toLowerCase().includes(q) ||
 a.location_label?.toLowerCase().includes(q)
 if (!hit) return false
 }
 if (travelFilter && !a.requires_travel) return false
 return true
 }),
 [effectiveAssignments, typeFilter, projectSearch, travelFilter, projectLookup],
 )


 const handleExcelExport = useCallback(() => {
 const month = focusDayRef.current.getMonth() + 1
 exportGridToExcel({ year, month, consultants: filteredConsultants, assignments: filteredAssignments, appointmentTypes, planningProjects })
 }, [year, filteredConsultants, filteredAssignments, appointmentTypes, planningProjects])

 const handlePrintExport = useCallback(() => {
 triggerPrintGrid('planning-grid-print', `Einsatzplanung ${year}`)
 }, [year])

 // Month quick-nav: scroll to the 1st of the given month (0-indexed)
 const handleScrollToMonth = useCallback((m: number) => {
 scrollToDate(new Date(year, m, 1), year)
 }, [year, scrollToDate])

 // ── Stable modal/toolbar callbacks (prevent inline-arrow re-creation on every render) ──
 // Without these, modals'useCallback([onClose]) + useEffect([handleKeyDown]) chains
 // tear down and re-add document event listeners on every parent re-render.
 const handleCreateModalClose = useCallback(() => setCreateModal(null), [])
 const handleCreateModalCreated = useCallback(() => { setCreateModal(null); fetchAssignments() }, [fetchAssignments])
 const handleEditModalClose = useCallback(() => setEditModal(null), [])
 const handleEditModalSaved = useCallback(() => { setEditModal(null); fetchAssignments() }, [fetchAssignments])
 const handleEditModalDeleted = useCallback(() => { setEditModal(null); fetchAssignments() }, [fetchAssignments])
 const handleCreateClick = useCallback(() => {
 const today = new Date().toISOString().slice(0, 10)
 const first = filteredConsultants[0]
 if (first) setCreateModal({ consultantId: first.id, date: today, endDate: today })
 }, [filteredConsultants])
 const handleCopyWeekOpen = useCallback(() => setCopyWeekModal(true), [])
 const handleCopyWeekClose = useCallback(() => setCopyWeekModal(false), [])
 const handleCopyWeekCopied = useCallback(() => { setCopyWeekModal(false); fetchAssignments() }, [fetchAssignments])
 const handleToggleTravelFilter = useCallback(() => setTravelFilter((v) => !v), [])
 const handleClearSelection = useCallback(() => { setSelectedIds(new Set()); setSelectionMode(false) }, [])

 return (
 <div className="flex-1 flex flex-col min-h-0 bg-muted">

 <PlanningToolbar
 year={year}
 teamFilter={teamFilter} consultantFilter={consultantFilter}
 typeFilter={typeFilter} projectSearch={projectSearch}
 teamCodes={teamCodes} consultants={consultants}
 appointmentTypes={appointmentTypes}
 selectionMode={selectionMode}
 travelFilter={travelFilter}
 canCreate={perms.canCreate}
 mobileWeekLabel={mobileWeekLabel}
 onYearChange={setYear}
 onScrollToMonth={handleScrollToMonth}
 onTeamChange={setTeamFilter} onConsultantChange={setConsultantFilter}
 onTypeChange={setTypeFilter} onProjectSearchChange={setProjectSearch}
 onPrev={handlePrev} onNext={handleNext}
 onToday={goToday}
 onCreateClick={handleCreateClick}
 onToggleSelectionMode={handleToggleSelectionMode}
 onCopyWeekClick={handleCopyWeekOpen}
 onToggleTravelFilter={handleToggleTravelFilter}
 onExcelExport={handleExcelExport}
 onPrintExport={handlePrintExport}
 />

 <BulkActionBar
 selectedCount={selectedIds.size}
 onClearSelection={handleClearSelection}
 onBulkDelete={handleBulkDelete}
 onBulkStatusChange={handleBulkStatusChange}
 />

 {/* Legend — top bar, first 5 appointment types only */}
 <div className="shrink-0 bg-card border-b border-muted px-4 py-2 flex items-center gap-x-4 gap-y-1 flex-wrap">
 <span className="text-[11px] font-semibold text-foreground">Auftragsart:</span>
 {appointmentTypes.slice(0, 5).map((t) => (
 <span key={t.id} className="flex items-center gap-1.5 text-[11px] text-[#555555]">
 <span className="w-2 h-2 rounded-full shrink-0"style={{ backgroundColor: t.color }} />
 {t.label}
 </span>
 ))}
 <div className="ml-auto flex items-center gap-3">
 {selectedWeeks.size > 0 && (
 <button
 onClick={handleClearWeeks}
 className="flex items-center gap-1 text-[11px] text-muted-foreground hover:text-foreground font-medium"
 >
 ✕ KW-Auswahl ({selectedWeeks.size})
 </button>
 )}
 {highlightedIds.size > 0 && (
 <button
 onClick={handleClearHighlights}
 className="flex items-center gap-1 text-[11px] text-primary hover:text-primary-dark font-medium"
 >
 ✕ Auswahl aufheben ({highlightedIds.size})
 </button>
 )}
 </div>
 </div>

 {/* Overlap toast */}
 {overlapToast && (
 <div className="mx-4 mt-2 shrink-0 flex items-center gap-2 px-3 py-2 bg-status-red-soft border border-status-red-strong rounded text-[12px] text-destructive">
 ⚠ {overlapToast}
 </div>
 )}

 {/* Undo toast — appears after move or resize */}
 {undoStack && (
 <div className="mx-4 mt-2 shrink-0 flex items-center gap-3 px-3 py-2 bg-secondary border border-secondary rounded text-[12px] text-primary-dark">
 <span>Eintrag verschoben</span>
 <button
 onClick={handleUndo}
 className="font-semibold underline hover:no-underline"
 >
 Rückgängig
 </button>
 </div>
 )}

 {/* Loading indicator — kept OUTSIDE the scroll container so the grid
     never unmounts during a refetch (otherwise the scrollLeft snaps back
     to 0 on remount, making the whole calendar jump to January). */}
 {loading && (
 <div className="mx-4 mt-2 shrink-0 inline-flex items-center gap-2 self-start px-3 py-1 bg-secondary border border-secondary rounded text-[11px] text-primary-dark">
 <span className="w-2 h-2 rounded-full bg-primary animate-pulse" />
 Daten werden geladen…
 </div>
 )}

 <div
 ref={gridContainerRef}
 id="planning-grid-print"
 className="flex-1 pb-4 overflow-auto"
 onTouchStart={handleTouchStart}
 onTouchEnd={handleTouchEnd}
 >
 <PlanningGrid
 year={year}
 daysOverride={mobileDays}
 consultants={sortedConsultants}
 assignments={filteredAssignments}
 appointmentTypes={appointmentTypes}
 planningProjects={planningProjects}
 selectionMode={selectionMode}
 selectedIds={selectedIds}
 syncStatusMap={syncStatusMap}
 highlightedIds={highlightedIds}
 currentConsultantId={currentConsultant?.id}
 selectedWeeks={selectedWeeks}
 onCellClick={handleCellClick}
 onCardClick={handleCardClick}
 onCardRightClick={handleCardRightClick}
 onBulkRightClick={handleBulkRightClick}
 onTileMove={handleTileMove}
 onToggleSelect={handleToggleSelect}
 onCtrlClickCard={handleCtrlClickCard}
 onSelectRange={handleSelectRange}
 onToggleHighlight={handleToggleHighlight}
 onToggleWeek={handleToggleWeek}
 calendarColors={calendarColors}
 customHolidays={customHolidays}
 dbHolidays={dbHolidays}
 />
 </div>


 {/* Modals */}
 {createModal && perms.canCreate && (
 <CreateAssignmentModal
 consultantId={createModal.consultantId}
 date={createModal.date}
 endDate={createModal.endDate}
 consultants={consultants}
 appointmentTypes={appointmentTypes}
 planningProjects={planningProjects}
 existingAssignments={assignments}
 onClose={handleCreateModalClose}
 onCreated={handleCreateModalCreated}
 workModeCapture={workModeCapture}
 />
 )}

 {editModal && canEditAssignment(perms, editModal) && (
 <EditAssignmentModal
 assignment={editModal}
 consultants={consultants}
 appointmentTypes={appointmentTypes}
 planningProjects={planningProjects}
 onClose={handleEditModalClose}
 onSaved={handleEditModalSaved}
 onDeleted={handleEditModalDeleted}
 workModeCapture={workModeCapture}
 />
 )}

 {contextMenu && (
 <AssignmentContextMenu
 assignment={contextMenu.assignment}
 x={contextMenu.x}
 y={contextMenu.y}
 onClose={handleContextMenuClose}
 onEdit={() => {
 if (canEditAssignment(perms, contextMenu.assignment)) setEditModal(contextMenu.assignment)
 handleContextMenuClose()
 }}
 onDuplicate={() => { handleDuplicate(contextMenu.assignment); handleContextMenuClose() }}
 onStatusChange={(status) => { handleStatusChange(contextMenu.assignment, status); handleContextMenuClose() }}
 onDelete={() => { handleDelete(contextMenu.assignment); handleContextMenuClose() }}
 />
 )}

 {copyWeekModal && (
 <CopyWeekModal
 consultants={consultants}
 assignments={assignments}
 currentYear={year}
 currentMonth={focusDayRef.current.getMonth() + 1}
 onClose={handleCopyWeekClose}
 onCopied={handleCopyWeekCopied}
 workModeCapture={workModeCapture}
 />
 )}

 {bulkContextMenu && (
 <BulkContextMenu
 count={selectedIds.size}
 x={bulkContextMenu.x}
 y={bulkContextMenu.y}
 onClose={handleBulkContextClose}
 onBulkEdit={handleBulkEditOpen}
 onBulkCopy={handleBulkCopy}
 onBulkDelete={handleBulkDelete}
 />
 )}

 {bulkEditModal && (
 <BulkEditModal
 selectedIds={selectedIds}
 appointmentTypes={appointmentTypes}
 onClose={handleBulkEditClose}
 onSaved={handleBulkEditSaved}
 workModeCapture={workModeCapture}
 />
 )}
 </div>
 )
}
