diff --git a/app/actions/admin.ts b/app/actions/admin.ts index 3bae90a..f29cf9b 100644 --- a/app/actions/admin.ts +++ b/app/actions/admin.ts @@ -23,6 +23,11 @@ export async function getAdminData() { }); } +export async function getDailyLogsDb() { + noStore(); + return await prisma.dailyLog.findMany(); +} + // ========================================== // 2. PERIOD ACTIONS // ========================================== @@ -52,7 +57,7 @@ export async function deletePeriodDb(periodId: string) { where: { id: periodId } }); - return { success: true }; // Returning JSON prevents "Unexpected end of JSON input" + return { success: true }; } // ========================================== @@ -83,7 +88,7 @@ export async function removeYouthDb(youthId: string) { } // ========================================== -// 4. ATTENDANCE ACTIONS +// 4. ATTENDANCE & LOG ACTIONS // ========================================== export async function setAttendanceDb(date: string, youthId: string, shiftId: string, hoursWorked: number, weightedHours: number, status: string, note: string) { @@ -103,7 +108,6 @@ export async function removeAttendanceDb(date: string, youthId: string, shiftId: } export async function bulkSetAttendanceDb(records: { date: string; youthId: string; shiftId: string; hoursWorked: number; weightedHours: number; status: string; note: string }[]) { - // A Prisma transaction runs all these upserts in a single database round-trip! await prisma.$transaction( records.map(record => prisma.attendance.upsert({ @@ -116,6 +120,15 @@ export async function bulkSetAttendanceDb(records: { date: string; youthId: stri return { success: true }; } +export async function setDailyLogDb(date: string, content: string) { + await prisma.dailyLog.upsert({ + where: { date }, + update: { content }, + create: { date, content } + }); + return { success: true }; +} + // ========================================== // 5. AUTHENTICATION ACTIONS // ========================================== @@ -148,6 +161,8 @@ export async function syncOfflineQueueDb(queue: any[]) { await setAttendanceDb(action.payload.date, action.payload.youthId, action.payload.shiftId, action.payload.hoursWorked, action.payload.weightedHours, action.payload.status, action.payload.note); } else if (action.type === 'REMOVE_ATTENDANCE') { await removeAttendanceDb(action.payload.date, action.payload.youthId, action.payload.shiftId); + } else if (action.type === 'SET_DAILY_LOG') { + await setDailyLogDb(action.payload.date, action.payload.content); } } return { success: true }; diff --git a/app/admin/LogTab.tsx b/app/admin/LogTab.tsx new file mode 100644 index 0000000..75e0252 --- /dev/null +++ b/app/admin/LogTab.tsx @@ -0,0 +1,173 @@ +// app/admin/LogTab.tsx +"use client"; + +import { NotebookText, CalendarRange, CheckCircle, ChevronLeft, ChevronRight, Save, AlertTriangle, FileText, Download } from 'lucide-react'; +import React, { useEffect, useState } from 'react'; +import { type Period, toIsoDate } from './adminTypes'; + +interface Props { + periods: Period[]; + dailyLogs: Record; + setDailyLog: (date: string, content: string) => void; + activePeriodId: string; + setActivePeriodId: (id: string) => void; + scheduleData: any[]; +} + +const getDaysInPeriod = (start: string, end: string) => { + const days = []; let curr = new Date(start + 'T12:00:00'); const endDate = new Date(end + 'T12:00:00'); + while (curr <= endDate) { days.push(toIsoDate(curr)); curr.setDate(curr.getDate() + 1); } + return days; +}; + +export const LogTab: React.FC = ({ periods, dailyLogs, setDailyLog, activePeriodId, setActivePeriodId, scheduleData }) => { + const activePeriod = periods.find(p => p.id === activePeriodId); + + const [currentDate, setCurrentDate] = useState(() => { + const today = toIsoDate(new Date()); + if (activePeriod && today >= activePeriod.startDate && today <= activePeriod.endDate) return today; + return activePeriod ? activePeriod.startDate : today; + }); + + const [currentText, setCurrentText] = useState(""); + const [saveStatus, setSaveStatus] = useState<'idle' | 'saved'>('idle'); + + useEffect(() => { + setCurrentText(dailyLogs[currentDate] || ""); + setSaveStatus('idle'); + }, [currentDate]); + + useEffect(() => { + if (saveStatus === 'idle' && currentText === (dailyLogs[currentDate] || "")) { + setCurrentText(dailyLogs[currentDate] || ""); + } + }, [dailyLogs, currentDate]); + + if (!activePeriod) return

Ingen period aktiv.

; + + const handleSave = () => { + setDailyLog(currentDate, currentText); + setSaveStatus('saved'); + setTimeout(() => setSaveStatus('idle'), 2000); + }; + + const changeDate = (days: number) => { + const newDateObj = new Date(currentDate + 'T12:00:00'); newDateObj.setDate(newDateObj.getDate() + days); + const startObj = new Date(activePeriod.startDate + 'T12:00:00'); const endObj = new Date(activePeriod.endDate + 'T12:00:00'); + if (newDateObj >= startObj && newDateObj <= endObj) setCurrentDate(toIsoDate(newDateObj)); + }; + + const dayNameStr = new Date(currentDate + 'T12:00:00').toLocaleDateString('sv-SE', { weekday: 'long' }); + const timelineDays = getDaysInPeriod(activePeriod.startDate, activePeriod.endDate); + const todayIso = toIsoDate(new Date()); + + const exportToCsv = () => { + if (!activePeriod) return; + let csvContent = "Datum;Logg\n"; + const sortedDays = Object.keys(dailyLogs).sort((a, b) => a.localeCompare(b)); + sortedDays.forEach(date => { + if (date >= activePeriod.startDate && date <= activePeriod.endDate) { + const logContent = dailyLogs[date] || ""; + const cleanContent = logContent + .replace(/\n/g, " ") + .replace(/;/g, ",") + .replace(/"/g, '""'); + + csvContent += `${date};"${cleanContent}"\n`; + } + }); + + const blob = new Blob(["\uFEFF" + csvContent], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.setAttribute("href", url); + link.setAttribute("download", `Logg_${activePeriod.name.replace(/ /g, '_')}.csv`); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + + return ( +
+ {/* Datumskrollare likt AttendanceTab */} +
+
+

+ Journalöversikt +

+ +
+
+ {timelineDays.map(day => { + const dayName = new Date(day + 'T12:00:00').toLocaleDateString('sv-SE', { weekday: 'long' }).toLowerCase(); + const daySchedule = scheduleData.find(d => d.day.toLowerCase() === dayName); + const hasWork = daySchedule && ((daySchedule.pilgrimsfalkarna?.time && daySchedule.pilgrimsfalkarna.time !== 'Ledig') || (daySchedule.tumlarna?.time && daySchedule.tumlarna.time !== 'Ledig')); + + const hasLog = !!dailyLogs[day] && dailyLogs[day].trim() !== ""; + const isSelected = day === currentDate; + + let bgClass = "bg-white text-ebony border-slate-teal/10"; + if (!hasWork) bgClass = "bg-slate-teal/5 text-ebony/40 border-transparent"; + else if (hasLog) bgClass = "bg-moss text-white border-moss shadow-inner"; + else if (day <= todayIso) bgClass = "bg-goldenrod text-white border-goldenrod shadow-inner"; + + return ( + + ); + })} +
+
+ +
+ +
+

{dayNameStr}

+

{currentDate}

+
+ +
+ + {/* Skrivyta */} +
+

+
+ +
+ Daglig Journal +

+ +