Auto-saved at 2025-07-30 10:24:37 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
866 lines
38 KiB
TypeScript
866 lines
38 KiB
TypeScript
"use client"
|
|
|
|
import * as React from "react"
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
|
import { Save, Download, Plus } from "lucide-react"
|
|
|
|
interface TimeshiftSpreadsheetProps {
|
|
teamId: string
|
|
teamName: string
|
|
}
|
|
|
|
export function TimeshiftSpreadsheet({ teamId, teamName }: TimeshiftSpreadsheetProps) {
|
|
const spreadsheetRef = React.useRef<HTMLDivElement>(null)
|
|
const jspreadsheetInstance = React.useRef<unknown>(null)
|
|
const isActiveRef = React.useRef<boolean>(true)
|
|
const isMergingRef = React.useRef<boolean>(false)
|
|
const isInitializingRef = React.useRef<boolean>(false)
|
|
const initTimeoutRef = React.useRef<NodeJS.Timeout | null>(null)
|
|
const currentDate = new Date()
|
|
const [selectedMonth, setSelectedMonth] = React.useState<string>((currentDate.getMonth() + 1).toString())
|
|
const [selectedYear, setSelectedYear] = React.useState<string>(currentDate.getFullYear().toString())
|
|
const [dialogOpen, setDialogOpen] = React.useState(false)
|
|
const [forceUpdate, setForceUpdate] = React.useState(0)
|
|
|
|
// Function to generate Excel-style column names (A, B, C, ..., Z, AA, AB, ...)
|
|
const getExcelColumnName = (index: number): string => {
|
|
let result = ''
|
|
while (index >= 0) {
|
|
result = String.fromCharCode(65 + (index % 26)) + result
|
|
index = Math.floor(index / 26) - 1
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Generate monthly schedule data
|
|
const generateMonthlyData = (month: number, year: number) => {
|
|
console.log("Generating data for month:", month, "year:", year)
|
|
// Get first and last day of selected month
|
|
const firstDay = new Date(year, month - 1, 1)
|
|
const lastDay = new Date(year, month, 0)
|
|
console.log("Date range:", firstDay, "to", lastDay)
|
|
|
|
// Get exactly 7 days from previous month
|
|
const prevMonthStart = new Date(firstDay)
|
|
prevMonthStart.setDate(firstDay.getDate() - 7)
|
|
|
|
// End at the last day of the selected month (no next month days)
|
|
const nextMonthEnd = new Date(lastDay)
|
|
|
|
console.log("Full date range:", prevMonthStart.toDateString(), "to", nextMonthEnd.toDateString())
|
|
console.log("Previous month start:", prevMonthStart.toDateString(), "Current month end:", nextMonthEnd.toDateString())
|
|
|
|
// Generate header rows
|
|
const titleRow = [`${String(month).padStart(2, '0')}.${year} v.1`, "Uzávěry ostatní"]
|
|
const dayNameRow = [""]
|
|
const yearRow = ["rok"]
|
|
const monthRow = ["měsíc"]
|
|
const dateRow = ["den"]
|
|
const shiftRow = [`Pohotovost ${teamId.toUpperCase()}`]
|
|
|
|
// Generate columns for each day from prevMonthStart to nextMonthEnd
|
|
const currentDate = new Date(prevMonthStart)
|
|
let dayCount = 0
|
|
const maxDays = 50 // Safety limit to prevent infinite loops
|
|
|
|
console.log("Starting date loop from:", currentDate.toDateString(), "to:", nextMonthEnd.toDateString())
|
|
|
|
while (currentDate <= nextMonthEnd && dayCount < maxDays) {
|
|
const dayName = ["Neděle", "Pondělí", "Úterý", "Středa", "Čtvrtek", "Pátek", "Sobota"][currentDate.getDay()]
|
|
|
|
titleRow.push("", "")
|
|
dayNameRow.push(dayName, "") // Day name in first column, empty in second (for merging)
|
|
yearRow.push(currentDate.getFullYear().toString(), "")
|
|
monthRow.push((currentDate.getMonth() + 1).toString(), "")
|
|
dateRow.push(currentDate.getDate().toString(), "")
|
|
shiftRow.push("den", "noc")
|
|
|
|
currentDate.setDate(currentDate.getDate() + 1)
|
|
dayCount++
|
|
}
|
|
|
|
console.log("Generated", dayCount, "days of data")
|
|
|
|
// Complete employee data from Excel file - cleaned up without empty rows, kontrolní řádek, and X BEZ zkušeností s VN
|
|
const employees = [
|
|
"Pauzer Libor (all in one)",
|
|
"Vörös Pavel (NN)",
|
|
"Janouš Petr (VN)",
|
|
"Dvořák Václav (VN)",
|
|
"Vondrák Pavel (NN)",
|
|
"Čeleda Olda (NN)",
|
|
"Hanzlík Marek (VN)",
|
|
"Kohl David (VN)",
|
|
"Dittrich Vladimír (VN)",
|
|
"Toman Milan (VN)",
|
|
"Glaser Ondřej (NN)",
|
|
"Herbst David (NN)",
|
|
"Ryba Ondřej (NN)",
|
|
"Zábranský Petr (NN)",
|
|
"Žemlička Miroslav (NN)",
|
|
"Teslík Hynek (NN)",
|
|
"Pohotovost IT", // Header - will be made bold
|
|
"Vörös Pavel",
|
|
"Janouš Petr",
|
|
"Glaser Ondřej",
|
|
"Robert Štefan"
|
|
]
|
|
|
|
// Add one more column to the right
|
|
titleRow.push("")
|
|
dayNameRow.push("")
|
|
yearRow.push("")
|
|
monthRow.push("")
|
|
dateRow.push("")
|
|
shiftRow.push("")
|
|
|
|
const employeeRows = employees.map(name => {
|
|
const row = [name]
|
|
// Add empty cells for each day/night pair
|
|
for (let i = 1; i < shiftRow.length; i++) {
|
|
row.push("")
|
|
}
|
|
return row
|
|
})
|
|
|
|
const result = [titleRow, dayNameRow, yearRow, monthRow, dateRow, shiftRow, ...employeeRows]
|
|
console.log("Generated", result.length, "rows with", result[0]?.length, "columns")
|
|
console.log("Title row:", titleRow.slice(0, 10))
|
|
console.log("Day row:", dayNameRow.slice(0, 10))
|
|
return result
|
|
}
|
|
|
|
// Sample data for different teams (fallback)
|
|
const getTeamData = (teamId: string) => {
|
|
const baseData = [
|
|
["Employee", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
|
|
["John Smith", "08:00-16:00", "08:00-16:00", "08:00-16:00", "08:00-16:00", "08:00-16:00", "OFF", "OFF"],
|
|
["Jane Doe", "16:00-00:00", "16:00-00:00", "16:00-00:00", "16:00-00:00", "16:00-00:00", "OFF", "OFF"],
|
|
["Mike Johnson", "00:00-08:00", "00:00-08:00", "00:00-08:00", "00:00-08:00", "00:00-08:00", "OFF", "OFF"],
|
|
["Sarah Wilson", "08:00-16:00", "OFF", "08:00-16:00", "OFF", "08:00-16:00", "08:00-16:00", "08:00-16:00"],
|
|
["", "", "", "", "", "", "", ""],
|
|
["", "", "", "", "", "", "", ""],
|
|
]
|
|
return baseData
|
|
}
|
|
|
|
React.useEffect(() => {
|
|
console.log("useEffect triggered - Month:", selectedMonth, "Year:", selectedYear, "Force:", forceUpdate)
|
|
|
|
// Clear any existing timeout
|
|
if (initTimeoutRef.current) {
|
|
console.log("CLEARING: existing timeout, debouncing useEffect")
|
|
clearTimeout(initTimeoutRef.current)
|
|
}
|
|
|
|
// Prevent multiple simultaneous initializations with immediate return
|
|
if (isInitializingRef.current) {
|
|
console.log("BLOCKED: useEffect - already initializing, skipping completely")
|
|
return
|
|
}
|
|
|
|
console.log("DEBOUNCING: useEffect with 100ms delay")
|
|
|
|
// Debounce the initialization to prevent rapid successive calls
|
|
initTimeoutRef.current = setTimeout(() => {
|
|
console.log("EXECUTING: debounced useEffect after delay")
|
|
|
|
// Double-check we're still not initializing (could have started between timeout set and execution)
|
|
if (isInitializingRef.current) {
|
|
console.log("BLOCKED: Still initializing at timeout execution")
|
|
return
|
|
}
|
|
|
|
isInitializingRef.current = true
|
|
isActiveRef.current = true
|
|
|
|
const loadJSpreadsheet = async () => {
|
|
console.log("Starting loadJSpreadsheet function")
|
|
|
|
// Check if scripts are already loaded
|
|
const windowWithJExcel = window as unknown as { jexcel?: unknown, jspreadsheet?: unknown }
|
|
|
|
if (!windowWithJExcel.jexcel && !windowWithJExcel.jspreadsheet) {
|
|
console.log("Loading jspreadsheet scripts...")
|
|
|
|
// Load CSS if not already loaded
|
|
if (!document.querySelector('link[href*="jexcel.css"]')) {
|
|
const cssLink = document.createElement("link")
|
|
cssLink.rel = "stylesheet"
|
|
cssLink.href = "https://bossanova.uk/jspreadsheet/v4/jexcel.css"
|
|
document.head.appendChild(cssLink)
|
|
}
|
|
|
|
if (!document.querySelector('link[href*="jsuites.css"]')) {
|
|
const jSuiteCssLink = document.createElement("link")
|
|
jSuiteCssLink.rel = "stylesheet"
|
|
jSuiteCssLink.href = "https://jsuites.net/v4/jsuites.css"
|
|
document.head.appendChild(jSuiteCssLink)
|
|
}
|
|
|
|
// Load JavaScript files if not already loaded
|
|
if (!document.querySelector('script[src*="jsuites.js"]')) {
|
|
const jSuitesScript = document.createElement("script")
|
|
jSuitesScript.src = "https://jsuites.net/v4/jsuites.js"
|
|
document.head.appendChild(jSuitesScript)
|
|
}
|
|
|
|
if (!document.querySelector('script[src*="jexcel.js"]')) {
|
|
const jSpreadsheetScript = document.createElement("script")
|
|
jSpreadsheetScript.src = "https://bossanova.uk/jspreadsheet/v4/jexcel.js"
|
|
document.head.appendChild(jSpreadsheetScript)
|
|
|
|
// Wait for scripts to load
|
|
console.log("Waiting for scripts to load...")
|
|
await new Promise((resolve) => {
|
|
jSpreadsheetScript.onload = () => {
|
|
console.log("Scripts loaded successfully")
|
|
setTimeout(resolve, 200) // Longer delay
|
|
}
|
|
jSpreadsheetScript.onerror = () => {
|
|
console.error("Failed to load jspreadsheet script")
|
|
resolve(null)
|
|
}
|
|
})
|
|
}
|
|
} else {
|
|
console.log("Scripts already loaded")
|
|
}
|
|
|
|
// Initialize spreadsheet
|
|
const windowObj = window as unknown as { jexcel?: unknown, jspreadsheet?: unknown }
|
|
const jexcelLib = windowObj.jexcel || windowObj.jspreadsheet
|
|
console.log("Checking for jexcel availability:", !!windowObj.jexcel)
|
|
console.log("Checking for jspreadsheet availability:", !!windowObj.jspreadsheet)
|
|
console.log("Using library:", jexcelLib ? (windowObj.jexcel ? 'jexcel' : 'jspreadsheet') : 'none')
|
|
console.log("Checking spreadsheet ref:", !!spreadsheetRef.current)
|
|
|
|
if (spreadsheetRef.current && jexcelLib) {
|
|
console.log("Initializing spreadsheet...")
|
|
// Clear previous instance and reset any merge states
|
|
if (jspreadsheetInstance.current) {
|
|
console.log("Destroying previous instance")
|
|
try {
|
|
const instance = jspreadsheetInstance.current as any
|
|
// Clear any existing merges before destroying
|
|
if (instance.removeMerge && instance.getMerge) {
|
|
try {
|
|
const merges = instance.getMerge()
|
|
if (merges && Array.isArray(merges)) {
|
|
merges.forEach((merge: any) => {
|
|
try {
|
|
instance.removeMerge(merge.address)
|
|
} catch (e) {
|
|
// Ignore individual merge removal errors
|
|
}
|
|
})
|
|
}
|
|
} catch (e) {
|
|
// Ignore merge clearing errors
|
|
}
|
|
}
|
|
instance.destroy()
|
|
} catch (error) {
|
|
console.error("Error destroying instance:", error)
|
|
}
|
|
jspreadsheetInstance.current = null
|
|
}
|
|
|
|
// Use monthly data if month/year selected, otherwise use basic data
|
|
let data
|
|
try {
|
|
if (selectedMonth && selectedYear) {
|
|
console.log("About to generate monthly data for:", parseInt(selectedMonth), parseInt(selectedYear))
|
|
data = generateMonthlyData(parseInt(selectedMonth), parseInt(selectedYear))
|
|
console.log("Monthly data generated successfully")
|
|
} else {
|
|
console.log("Using basic team data")
|
|
data = getTeamData(teamId)
|
|
}
|
|
} catch (error) {
|
|
console.error("Error generating data:", error)
|
|
data = getTeamData(teamId)
|
|
}
|
|
|
|
console.log("Data generated:", data.length, "rows, first row:", data[0]?.length, "columns")
|
|
console.log("First few rows:", data.slice(0, 3))
|
|
|
|
// Generate column configuration based on data
|
|
const columns = data[0]?.map((_, index) => ({
|
|
type: "text",
|
|
title: getExcelColumnName(index), // A, B, C, ..., Z, AA, AB, ...
|
|
width: index === 0 ? 200 : 25 // First column 200px, all others 25px
|
|
})) || []
|
|
|
|
// Generate styles for day/night shifts based on the Excel pattern
|
|
const styles: Record<string, string> = {}
|
|
if (selectedMonth && selectedYear) {
|
|
// Make first row (row 1) taller - 97px
|
|
for (let col = 0; col < (data[0]?.length || 0); col++) {
|
|
const colLetter = getExcelColumnName(col)
|
|
styles[`${colLetter}1`] = "height: 97px;"
|
|
}
|
|
|
|
// Make day names row (row 2) taller and add top border from column B onwards (except last two columns)
|
|
for (let col = 0; col < (data[0]?.length || 0); col++) {
|
|
const colLetter = getExcelColumnName(col)
|
|
if (col === 0) {
|
|
// First column (A) - just height
|
|
styles[`${colLetter}2`] = "height: 50px;"
|
|
} else if (colLetter === 'BZ' || colLetter === 'CA') {
|
|
// Last two columns (BZ, CA) - height only, no top border
|
|
styles[`${colLetter}2`] = "height: 50px;"
|
|
} else {
|
|
// All other columns (B onwards except BZ, CA) - height + top border
|
|
styles[`${colLetter}2`] = "height: 50px; border-top: 1px solid #000000;"
|
|
}
|
|
}
|
|
|
|
// Style header rows
|
|
for (let col = 1; col < (data[0]?.length || 0); col++) {
|
|
const colLetter = getExcelColumnName(col)
|
|
if (col % 2 === 1) { // Day shifts (odd columns after first)
|
|
styles[`${colLetter}6`] = "background-color: #ffff00; font-weight: normal;" // Yellow for day, not bold
|
|
} else { // Night shifts (even columns after first)
|
|
styles[`${colLetter}6`] = "background-color: #00b0f0; font-weight: normal;" // Blue for night, not bold
|
|
}
|
|
}
|
|
|
|
// Style weekend days (Saturday/Sunday in day name row and date rows below)
|
|
for (let col = 1; col < (data[0]?.length || 0); col += 2) {
|
|
const dayName = data[1]?.[col]
|
|
if (dayName === "Sobota" || dayName === "Neděle") {
|
|
const colLetter = getExcelColumnName(col)
|
|
const nextColLetter = getExcelColumnName(col + 1)
|
|
|
|
// Weekend day name row (row 2) - merged cells with conditional top border
|
|
const hasBorder1 = colLetter !== 'BZ' && colLetter !== 'CA' ? " border-top: 1px solid #000000;" : ""
|
|
const hasBorder2 = nextColLetter !== 'BZ' && nextColLetter !== 'CA' ? " border-top: 1px solid #000000;" : ""
|
|
styles[`${colLetter}2`] = `background-color: #ffd966; height: 50px;${hasBorder1}` // Weekend day name with conditional border
|
|
styles[`${nextColLetter}2`] = `background-color: #ffd966; height: 50px;${hasBorder2}`
|
|
|
|
// Weekend date columns (rows 3, 4, 5 - year, month, day)
|
|
for (let row = 3; row <= 5; row++) {
|
|
styles[`${colLetter}${row}`] = "background-color: #ffd966;" // Weekend date values
|
|
styles[`${nextColLetter}${row}`] = "background-color: #ffd966;" // Weekend date values
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove formatting specifically from BZ6 and CA6 cells
|
|
styles["BZ6"] = "" // Override any formatting for BZ6
|
|
styles["CA6"] = "" // Override any formatting for CA6
|
|
|
|
// Add left border to BZ column from row 2 to the last employee row
|
|
const totalRows = data.length
|
|
const employeeStartRow = 7 // Row 7 is first employee (0-indexed: rows 1-6 are headers, no empty row)
|
|
for (let row = 2; row <= totalRows; row++) {
|
|
styles[`BZ${row}`] = (styles[`BZ${row}`] || "") + " border-left: 1px solid #000000;"
|
|
}
|
|
|
|
// Make "Pohotovost IT" header bold (employee index 16, so row 23)
|
|
styles["A23"] = "font-weight: bold;" // "Pohotovost IT"
|
|
}
|
|
|
|
console.log("Initializing jspreadsheet with config:", {
|
|
dataRows: data.length,
|
|
dataCols: data[0]?.length,
|
|
columns: columns.length,
|
|
stylesCount: Object.keys(styles).length
|
|
})
|
|
|
|
jspreadsheetInstance.current = (jexcelLib as (el: HTMLElement, config: unknown) => unknown)(spreadsheetRef.current, {
|
|
data: data,
|
|
columns: columns,
|
|
minDimensions: [data[0]?.length || 8, Math.max(data.length + 3, 10)],
|
|
allowInsertRow: true,
|
|
allowInsertColumn: false,
|
|
allowDeleteRow: true,
|
|
allowDeleteColumn: false,
|
|
contextMenu: true,
|
|
tableOverflow: true,
|
|
tableWidth: "100%",
|
|
tableHeight: "500px",
|
|
style: styles,
|
|
onchange: (instance: any, cell: HTMLElement, x: number, y: number, value: string) => {
|
|
console.log("onChange triggered:", { x, y, value, isMerging: isMergingRef.current })
|
|
|
|
// Skip onChange events during merging process to prevent infinite loops
|
|
if (isMergingRef.current) {
|
|
console.log("Skipping onChange during merge:", { x, y, value })
|
|
return
|
|
}
|
|
|
|
// Only process if component is still active
|
|
if (!isActiveRef.current || !jspreadsheetInstance.current) return
|
|
|
|
// Apply rotation to first row (y === 0) when text is entered
|
|
if (y === 0 || y === '0') {
|
|
console.log("First row change detected, applying rotation...", { x, y, value })
|
|
|
|
// Use requestAnimationFrame for better DOM timing
|
|
requestAnimationFrame(() => {
|
|
if (!isActiveRef.current || !spreadsheetRef.current) return
|
|
|
|
try {
|
|
const table = spreadsheetRef.current?.querySelector('.jexcel tbody')
|
|
if (table) {
|
|
const rows = table.querySelectorAll('tr')
|
|
if (rows[0]) {
|
|
const cells = rows[0].querySelectorAll('td')
|
|
let targetCell = cells[x + 1] as HTMLElement // +1 because first cell is row number
|
|
console.log("Initial target cell found:", !!targetCell, "Cell display:", targetCell?.style.display, "Target cell:", targetCell)
|
|
|
|
// Handle both hidden and visible cells
|
|
if (targetCell) {
|
|
if (targetCell.style.display === 'none') {
|
|
console.log("🔍 Target cell is hidden, finding visible merged cell...")
|
|
const targetX = parseInt(String(x))
|
|
|
|
// For merged cells, find the visible cell that represents this position
|
|
let foundCell = null
|
|
|
|
// Look for visible cells around this position
|
|
for (let i = 1; i < cells.length; i++) {
|
|
const cell = cells[i] as HTMLElement
|
|
if (cell && cell.style.display !== 'none') {
|
|
const cellX = parseInt(cell.getAttribute('data-x') || '0')
|
|
const cellSpan = cell.colSpan || 1
|
|
|
|
// Check if this visible cell covers our target position
|
|
if (cellX <= targetX && targetX < cellX + cellSpan) {
|
|
foundCell = cell
|
|
console.log("🎯 Found covering visible cell:", foundCell, "covers position", targetX)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if (foundCell) {
|
|
targetCell = foundCell
|
|
} else {
|
|
console.warn("❌ Could not find visible cell for hidden position")
|
|
return // Skip if we can't find a visible cell
|
|
}
|
|
} else {
|
|
console.log("✅ Target cell is visible, using directly")
|
|
}
|
|
}
|
|
|
|
if (targetCell) {
|
|
const displayValue = value !== undefined && value !== null ? String(value).trim() : ''
|
|
console.log("Processing value:", displayValue)
|
|
console.log("Target cell before rotation:", targetCell.outerHTML)
|
|
|
|
if (displayValue) {
|
|
// Force clear any existing content first
|
|
targetCell.innerHTML = ''
|
|
|
|
// Apply rotation with improved styling - using important to override jspreadsheet styles
|
|
const rotatedDiv = document.createElement('div')
|
|
rotatedDiv.style.cssText = `
|
|
transform: rotate(-90deg) !important;
|
|
font-size: 12px !important;
|
|
height: 80px !important;
|
|
width: 20px !important;
|
|
display: flex !important;
|
|
align-items: center !important;
|
|
justify-content: center !important;
|
|
white-space: nowrap !important;
|
|
margin: 0 auto !important;
|
|
transform-origin: center center !important;
|
|
position: relative !important;
|
|
color: #000 !important;
|
|
background: transparent !important;
|
|
`
|
|
rotatedDiv.textContent = displayValue
|
|
targetCell.appendChild(rotatedDiv)
|
|
|
|
console.log("✅ Rotation applied successfully to cell with value:", displayValue)
|
|
console.log("Target cell after rotation:", targetCell.outerHTML)
|
|
} else {
|
|
// Clear cell content but maintain structure
|
|
targetCell.innerHTML = ''
|
|
console.log("✅ Cleared cell content")
|
|
}
|
|
} else {
|
|
console.warn("❌ Target cell not found at index:", x + 1)
|
|
}
|
|
} else {
|
|
console.warn("❌ First row not found")
|
|
}
|
|
} else {
|
|
console.warn("❌ Table not found")
|
|
}
|
|
} catch (error) {
|
|
console.error("❌ Error applying rotation:", error)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
})
|
|
|
|
console.log("jspreadsheet initialized:", !!jspreadsheetInstance.current)
|
|
|
|
// Additional event listener for first row rotation as fallback
|
|
setTimeout(() => {
|
|
if (!isActiveRef.current || !spreadsheetRef.current) return
|
|
|
|
const table = spreadsheetRef.current?.querySelector('.jexcel tbody')
|
|
if (table) {
|
|
// Add input event listeners to first row cells
|
|
const firstRow = table.querySelector('tr:first-child')
|
|
if (firstRow) {
|
|
const cells = firstRow.querySelectorAll('td')
|
|
cells.forEach((cell, index) => {
|
|
if (index > 0) { // Skip row number cell
|
|
// Listen for input events on the cell
|
|
cell.addEventListener('input', (e) => {
|
|
const target = e.target as HTMLElement
|
|
const value = target.textContent || target.innerText || ''
|
|
console.log("🎯 Direct input event on first row cell:", { index, value })
|
|
|
|
if (value.trim()) {
|
|
requestAnimationFrame(() => {
|
|
target.innerHTML = `<div style="
|
|
transform: rotate(-90deg);
|
|
font-size: 12px;
|
|
height: 80px;
|
|
width: 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
white-space: nowrap;
|
|
margin: 0 auto;
|
|
transform-origin: center center;
|
|
">${value.trim()}</div>`
|
|
console.log("🎯 Direct rotation applied via input event")
|
|
})
|
|
}
|
|
})
|
|
|
|
// Also listen for blur events
|
|
cell.addEventListener('blur', (e) => {
|
|
const target = e.target as HTMLElement
|
|
const value = target.textContent || target.innerText || ''
|
|
console.log("🎯 Blur event on first row cell:", { index, value })
|
|
|
|
if (value.trim()) {
|
|
requestAnimationFrame(() => {
|
|
target.innerHTML = `<div style="
|
|
transform: rotate(-90deg);
|
|
font-size: 12px;
|
|
height: 80px;
|
|
width: 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
white-space: nowrap;
|
|
margin: 0 auto;
|
|
transform-origin: center center;
|
|
">${value.trim()}</div>`
|
|
console.log("🎯 Direct rotation applied via blur event")
|
|
})
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}, 500)
|
|
|
|
// DISABLED: Mutation observer for first row rotation
|
|
// This was causing popup issues when switching tabs
|
|
// TODO: Implement a different approach for first row rotation if needed
|
|
|
|
// Apply cell merges for header rows after initialization - with improved error handling
|
|
setTimeout(() => {
|
|
if (!isActiveRef.current || !jspreadsheetInstance.current || !selectedMonth || !selectedYear) return
|
|
|
|
try {
|
|
const instance = jspreadsheetInstance.current as any
|
|
if (!instance.setMerge) return
|
|
|
|
console.log("Starting cell merging process...")
|
|
|
|
// Set merging flag to prevent onChange events during merge process
|
|
isMergingRef.current = true
|
|
|
|
// Clear any existing merges first to start fresh
|
|
if (instance.removeMerge && instance.getMerge) {
|
|
try {
|
|
const existingMerges = instance.getMerge()
|
|
if (existingMerges && Array.isArray(existingMerges)) {
|
|
existingMerges.forEach((merge: any) => {
|
|
try {
|
|
instance.removeMerge(merge.address)
|
|
} catch (e) {
|
|
// Ignore errors - merge might not exist
|
|
}
|
|
})
|
|
}
|
|
} catch (e) {
|
|
// Ignore errors getting existing merges
|
|
}
|
|
}
|
|
|
|
// Now apply new merges
|
|
const totalCols = data[0]?.length || 0
|
|
let mergeCount = 0
|
|
|
|
for (let row = 1; row <= 5; row++) {
|
|
for (let col = 1; col < totalCols; col += 2) {
|
|
// Double check we're still active
|
|
if (!isActiveRef.current || !jspreadsheetInstance.current) {
|
|
console.log("Component became inactive during merging")
|
|
isMergingRef.current = false
|
|
return
|
|
}
|
|
|
|
const colLetter = getExcelColumnName(col)
|
|
|
|
// Skip merging for BZ and CA columns
|
|
if (colLetter === 'BZ' || colLetter === 'CA') {
|
|
continue
|
|
}
|
|
|
|
try {
|
|
const cellAddress = `${colLetter}${row}`
|
|
console.log(`Attempting to merge: ${cellAddress}`)
|
|
instance.setMerge(cellAddress, 2, 1) // 2 columns, 1 row
|
|
mergeCount++
|
|
} catch (error) {
|
|
console.warn(`Failed to merge ${colLetter}${row}:`, error.message)
|
|
// Continue with other merges even if one fails
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(`Successfully applied ${mergeCount} cell merges`)
|
|
|
|
// Clear merging flag after completion
|
|
isMergingRef.current = false
|
|
|
|
} catch (error) {
|
|
console.error("General error during cell merging:", error)
|
|
isMergingRef.current = false
|
|
}
|
|
}, 300) // Increased timeout to ensure spreadsheet is fully ready
|
|
|
|
// Clear initialization flag after successful setup - increased timeout to prevent rapid re-initializations
|
|
setTimeout(() => {
|
|
isInitializingRef.current = false
|
|
initTimeoutRef.current = null
|
|
console.log("Initialization completed, flag and timeout cleared")
|
|
}, 2000) // Extended to 2 seconds to ensure complete initialization
|
|
|
|
// Apply counter-clockwise rotation to data values only (exclude second column which contains field labels)
|
|
setTimeout(() => {
|
|
const table = spreadsheetRef.current?.querySelector('.jexcel tbody')
|
|
if (table) {
|
|
const rows = table.querySelectorAll('tr')
|
|
const totalCells = data[0]?.length || 0
|
|
|
|
// Rotate data values in rows 1, 2, 3, 4, 5, 6 (first row, day names, year, month, day, shifts)
|
|
;[0, 1, 2, 3, 4, 5].forEach(rowIndex => {
|
|
if (rows[rowIndex]) {
|
|
const cells = rows[rowIndex].querySelectorAll('td')
|
|
cells.forEach((cell, cellIndex) => {
|
|
// Skip first two columns (row numbers and field labels)
|
|
if (cellIndex > 1 && cellIndex < totalCells + 1) { // +1 because of row number column
|
|
const originalText = cell.textContent?.trim()
|
|
if (originalText) {
|
|
// Use 12px font for all rotated values (date values, day names, and shifts)
|
|
// For first row (rowIndex 0), ensure width: 100% for better text display
|
|
const extraStyle = rowIndex === 0 ? ' width: 100%;' : ''
|
|
cell.innerHTML = `<div style="transform: rotate(-90deg); font-size: 12px; height: 20px; display: flex; align-items: center; justify-content: center; white-space: nowrap;${extraStyle}">${originalText}</div>`
|
|
}
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
// Make only "Pohotovost TKB" bold, keep other field labels regular and ensure they're not rotated
|
|
;[0, 1, 2, 3, 4, 5].forEach(rowIndex => {
|
|
if (rows[rowIndex]) {
|
|
const cells = rows[rowIndex].querySelectorAll('td')
|
|
if (cells[1]) { // Second column (index 1)
|
|
cells[1].style.transform = 'none'
|
|
// Only make "Pohotovost TKB" bold (row index 5)
|
|
if (rowIndex === 5) {
|
|
cells[1].style.fontWeight = 'bold'
|
|
} else {
|
|
cells[1].style.fontWeight = 'normal'
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}, 200)
|
|
|
|
} else {
|
|
console.log("Cannot initialize - missing ref or jexcel library")
|
|
isInitializingRef.current = false
|
|
}
|
|
}
|
|
|
|
console.log("Calling loadJSpreadsheet...")
|
|
loadJSpreadsheet().catch(error => {
|
|
console.error("Error in loadJSpreadsheet:", error)
|
|
isInitializingRef.current = false
|
|
})
|
|
|
|
}, 100) // 100ms debounce delay
|
|
|
|
return () => {
|
|
// Clear timeout on cleanup
|
|
if (initTimeoutRef.current) {
|
|
clearTimeout(initTimeoutRef.current)
|
|
initTimeoutRef.current = null
|
|
}
|
|
isActiveRef.current = false
|
|
isMergingRef.current = false
|
|
isInitializingRef.current = false
|
|
if (jspreadsheetInstance.current) {
|
|
jspreadsheetInstance.current.destroy()
|
|
jspreadsheetInstance.current = null
|
|
}
|
|
}
|
|
}, [teamId, selectedMonth, selectedYear, forceUpdate])
|
|
|
|
const handleSave = () => {
|
|
if (jspreadsheetInstance.current) {
|
|
const data = jspreadsheetInstance.current.getData()
|
|
console.log("Saving data:", data)
|
|
// Here you would typically save to your backend
|
|
alert("Schedule saved successfully!")
|
|
}
|
|
}
|
|
|
|
const handleExport = () => {
|
|
if (jspreadsheetInstance.current) {
|
|
jspreadsheetInstance.current.download()
|
|
}
|
|
}
|
|
|
|
const handleCreateSchedule = () => {
|
|
setDialogOpen(true)
|
|
}
|
|
|
|
const handleGenerateSchedule = () => {
|
|
console.log("Generate clicked - Month:", selectedMonth, "Year:", selectedYear)
|
|
if (selectedMonth && selectedYear) {
|
|
setDialogOpen(false)
|
|
console.log("Generating schedule for:", selectedMonth, selectedYear)
|
|
// Force useEffect to trigger by updating the forceUpdate counter
|
|
setForceUpdate(prev => prev + 1)
|
|
} else {
|
|
console.log("Month or year not selected")
|
|
alert("Please select both month and year")
|
|
}
|
|
}
|
|
|
|
|
|
return (
|
|
<Card className="w-full">
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<CardTitle className="flex items-center gap-2">{teamName} - Weekly Schedule</CardTitle>
|
|
<CardDescription>Manage work shifts and schedules for your team members</CardDescription>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button onClick={handleCreateSchedule} variant="outline" size="sm">
|
|
<Plus className="size-4 mr-2" />
|
|
Create
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent className="sm:max-w-[425px]">
|
|
<DialogHeader>
|
|
<DialogTitle>Create Monthly Schedule</DialogTitle>
|
|
<DialogDescription>
|
|
Select the month and year for which you want to generate the timetable.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="grid gap-4 py-4">
|
|
<div className="grid grid-cols-4 items-center gap-4">
|
|
<label htmlFor="month" className="text-right">
|
|
Month
|
|
</label>
|
|
<Select value={selectedMonth} onValueChange={setSelectedMonth}>
|
|
<SelectTrigger className="col-span-3">
|
|
<SelectValue placeholder="Select month" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="1">January</SelectItem>
|
|
<SelectItem value="2">February</SelectItem>
|
|
<SelectItem value="3">March</SelectItem>
|
|
<SelectItem value="4">April</SelectItem>
|
|
<SelectItem value="5">May</SelectItem>
|
|
<SelectItem value="6">June</SelectItem>
|
|
<SelectItem value="7">July</SelectItem>
|
|
<SelectItem value="8">August</SelectItem>
|
|
<SelectItem value="9">September</SelectItem>
|
|
<SelectItem value="10">October</SelectItem>
|
|
<SelectItem value="11">November</SelectItem>
|
|
<SelectItem value="12">December</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="grid grid-cols-4 items-center gap-4">
|
|
<label htmlFor="year" className="text-right">
|
|
Year
|
|
</label>
|
|
<Select value={selectedYear} onValueChange={setSelectedYear}>
|
|
<SelectTrigger className="col-span-3">
|
|
<SelectValue placeholder="Select year" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="2024">2024</SelectItem>
|
|
<SelectItem value="2025">2025</SelectItem>
|
|
<SelectItem value="2026">2026</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button type="submit" onClick={handleGenerateSchedule}>Generate Schedule</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
<Button onClick={handleSave} variant="outline" size="sm">
|
|
<Save className="size-4 mr-2" />
|
|
Save
|
|
</Button>
|
|
<Button onClick={handleExport} variant="outline" size="sm">
|
|
<Download className="size-4 mr-2" />
|
|
Export
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="border rounded-lg overflow-hidden">
|
|
<div ref={spreadsheetRef} className="w-full" />
|
|
</div>
|
|
<div className="mt-4 text-sm text-muted-foreground">
|
|
<p>
|
|
<strong>Instructions:</strong>
|
|
</p>
|
|
<ul className="list-disc list-inside mt-2 space-y-1">
|
|
<li>Click on any cell to edit shift times (e.g., "08:00-16:00")</li>
|
|
<li>Use "OFF" for days off</li>
|
|
<li>Right-click for context menu options</li>
|
|
<li>Use the "Add Employee" button to add new team members</li>
|
|
</ul>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|