Functions Time Eval and PDF
This commit is contained in:
128
src/modules/time/evaluation.service.ts
Normal file
128
src/modules/time/evaluation.service.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import {FastifyInstance} from "fastify";
|
||||||
|
|
||||||
|
|
||||||
|
export async function generateTimesEvaluation(
|
||||||
|
server: FastifyInstance,
|
||||||
|
user_id: string,
|
||||||
|
tenant_id: number,
|
||||||
|
startDateInput: string,
|
||||||
|
endDateInput: string
|
||||||
|
) {
|
||||||
|
const startDate = server.dayjs(startDateInput)
|
||||||
|
const endDate = server.dayjs(endDateInput)
|
||||||
|
|
||||||
|
// 🧾 Profil laden (Arbeitszeiten + Bundesland)
|
||||||
|
const { data: profile, error: profileError } = await server.supabase
|
||||||
|
.from("auth_profiles")
|
||||||
|
.select("*")
|
||||||
|
.eq("user_id", user_id)
|
||||||
|
.eq("tenant_id", tenant_id)
|
||||||
|
.maybeSingle()
|
||||||
|
|
||||||
|
if (profileError || !profile) throw new Error("Profil konnte nicht geladen werden.")
|
||||||
|
|
||||||
|
// 🕒 Arbeitszeiten abrufen
|
||||||
|
const { data: times, error: timeError } = await server.supabase
|
||||||
|
.from("staff_time_entries")
|
||||||
|
.select("*")
|
||||||
|
.eq("tenant_id", tenant_id)
|
||||||
|
.eq("user_id", user_id)
|
||||||
|
.gte("started_at", startDate.toISOString())
|
||||||
|
.lte("started_at", endDate.toISOString())
|
||||||
|
.order("started_at", { ascending: true })
|
||||||
|
|
||||||
|
if (timeError) throw new Error("Fehler beim Laden der Arbeitszeiten: " + timeError.message)
|
||||||
|
|
||||||
|
// 📅 Feiertage aus Tabelle für Bundesland + DE
|
||||||
|
const { data: holidays, error: holidaysError } = await server.supabase
|
||||||
|
.from("holidays")
|
||||||
|
.select("date")
|
||||||
|
.in("state_code", [profile.state_code, "DE"])
|
||||||
|
.gte("date", startDate.format("YYYY-MM-DD"))
|
||||||
|
.lte("date", endDate.format("YYYY-MM-DD"))
|
||||||
|
|
||||||
|
if (holidaysError) throw new Error("Fehler beim Laden der Feiertage: " + holidaysError.message)
|
||||||
|
|
||||||
|
// 🗓️ Sollzeit berechnen
|
||||||
|
let timeSpanWorkingMinutes = 0
|
||||||
|
const totalDays = endDate.add(1, "day").diff(startDate, "days")
|
||||||
|
|
||||||
|
for (let i = 0; i < totalDays; i++) {
|
||||||
|
const date = startDate.add(i, "days")
|
||||||
|
const weekday = date.day()
|
||||||
|
timeSpanWorkingMinutes += (profile.weekly_regular_working_hours?.[weekday] || 0) * 60
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🧮 Eingereicht & genehmigt
|
||||||
|
const calcMinutes = (start: string, end: string | null) =>
|
||||||
|
server.dayjs(end || new Date()).diff(server.dayjs(start), "minutes")
|
||||||
|
|
||||||
|
let sumWorkingMinutesEingereicht = 0
|
||||||
|
let sumWorkingMinutesApproved = 0
|
||||||
|
|
||||||
|
for (const t of times) {
|
||||||
|
const minutes = calcMinutes(t.started_at, t.stopped_at)
|
||||||
|
if(["submitted","approved"].includes(t.state))sumWorkingMinutesEingereicht += minutes
|
||||||
|
if (t.state === "approved") sumWorkingMinutesApproved += minutes
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎉 Feiertagsausgleich
|
||||||
|
let sumWorkingMinutesRecreationDays = 0
|
||||||
|
let sumRecreationDays = 0
|
||||||
|
|
||||||
|
if (profile.recreationDaysCompensation && holidays?.length) {
|
||||||
|
holidays.forEach(({ date }) => {
|
||||||
|
const weekday = server.dayjs(date).day()
|
||||||
|
const hours = profile.weekly_regular_working_hours?.[weekday] || 0
|
||||||
|
sumWorkingMinutesRecreationDays += hours * 60
|
||||||
|
sumRecreationDays++
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🏖️ Urlaub & Krankheit (über Typ)
|
||||||
|
const sumWorkingMinutesVacationDays = times
|
||||||
|
.filter((t) => t.type === "vacation")
|
||||||
|
.reduce((sum, t) => sum + calcMinutes(t.started_at, t.stopped_at), 0)
|
||||||
|
|
||||||
|
const sumWorkingMinutesSickDays = times
|
||||||
|
.filter((t) => t.type === "sick")
|
||||||
|
.reduce((sum, t) => sum + calcMinutes(t.started_at, t.stopped_at), 0)
|
||||||
|
|
||||||
|
const sumVacationDays = times.filter((t) => t.type === "vacation").length
|
||||||
|
const sumSickDays = times.filter((t) => t.type === "sick").length
|
||||||
|
|
||||||
|
// 💰 Salden
|
||||||
|
const saldo =
|
||||||
|
sumWorkingMinutesApproved +
|
||||||
|
sumWorkingMinutesRecreationDays +
|
||||||
|
sumWorkingMinutesVacationDays +
|
||||||
|
sumWorkingMinutesSickDays -
|
||||||
|
timeSpanWorkingMinutes
|
||||||
|
|
||||||
|
const saldoInOfficial =
|
||||||
|
sumWorkingMinutesEingereicht +
|
||||||
|
sumWorkingMinutesRecreationDays +
|
||||||
|
sumWorkingMinutesVacationDays +
|
||||||
|
sumWorkingMinutesSickDays -
|
||||||
|
timeSpanWorkingMinutes
|
||||||
|
|
||||||
|
// 📦 Rückgabe (kompatibel zur alten Struktur)
|
||||||
|
return {
|
||||||
|
user_id,
|
||||||
|
tenant_id,
|
||||||
|
from: startDate.format("YYYY-MM-DD"),
|
||||||
|
to: endDate.format("YYYY-MM-DD"),
|
||||||
|
timeSpanWorkingMinutes,
|
||||||
|
sumWorkingMinutesEingereicht,
|
||||||
|
sumWorkingMinutesApproved,
|
||||||
|
sumWorkingMinutesRecreationDays,
|
||||||
|
sumRecreationDays,
|
||||||
|
sumWorkingMinutesVacationDays,
|
||||||
|
sumVacationDays,
|
||||||
|
sumWorkingMinutesSickDays,
|
||||||
|
sumSickDays,
|
||||||
|
saldo,
|
||||||
|
saldoInOfficial,
|
||||||
|
times
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FastifyInstance } from "fastify";
|
import { FastifyInstance } from "fastify";
|
||||||
import {createInvoicePDF} from "../utils/pdf";
|
import {createInvoicePDF, createTimeSheetPDF} from "../utils/pdf";
|
||||||
import {useNextNumberRangeNumber} from "../utils/functions";
|
import {useNextNumberRangeNumber} from "../utils/functions";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@ import isSameOrAfter from "dayjs/plugin/isSameOrAfter.js"
|
|||||||
import isSameOrBefore from "dayjs/plugin/isSameOrBefore.js"
|
import isSameOrBefore from "dayjs/plugin/isSameOrBefore.js"
|
||||||
import duration from "dayjs/plugin/duration.js";
|
import duration from "dayjs/plugin/duration.js";
|
||||||
import timezone from "dayjs/plugin/timezone.js";
|
import timezone from "dayjs/plugin/timezone.js";
|
||||||
|
import {generateTimesEvaluation} from "../modules/time/evaluation.service";
|
||||||
dayjs.extend(customParseFormat)
|
dayjs.extend(customParseFormat)
|
||||||
dayjs.extend(isoWeek)
|
dayjs.extend(isoWeek)
|
||||||
dayjs.extend(isBetween)
|
dayjs.extend(isBetween)
|
||||||
@@ -19,19 +20,35 @@ dayjs.extend(duration)
|
|||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone)
|
||||||
|
|
||||||
export default async function functionRoutes(server: FastifyInstance) {
|
export default async function functionRoutes(server: FastifyInstance) {
|
||||||
server.post("/functions/createinvoicepdf", async (req, reply) => {
|
server.post("/functions/pdf/:type", async (req, reply) => {
|
||||||
const body = req.body as {
|
const body = req.body as {
|
||||||
invoiceData: any
|
data: any
|
||||||
backgroundPath?: string
|
backgroundPath?: string
|
||||||
}
|
}
|
||||||
|
const {type} = req.params as {type:string}
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pdf = await createInvoicePDF(
|
|
||||||
server,
|
let pdf = null
|
||||||
"base64",
|
|
||||||
body.invoiceData,
|
if(type === "createdDocument") {
|
||||||
body.backgroundPath
|
pdf = await createInvoicePDF(
|
||||||
)
|
server,
|
||||||
|
"base64",
|
||||||
|
body.data,
|
||||||
|
body.backgroundPath
|
||||||
|
)
|
||||||
|
} else if(type === "timesheet") {
|
||||||
|
pdf = await createTimeSheetPDF(
|
||||||
|
server,
|
||||||
|
"base64",
|
||||||
|
body.data,
|
||||||
|
body.backgroundPath
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(pdf)
|
||||||
|
|
||||||
reply.send(pdf) // Fastify wandelt automatisch in JSON
|
reply.send(pdf) // Fastify wandelt automatisch in JSON
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -53,186 +70,83 @@ export default async function functionRoutes(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
server.get("/functions/workingtimeevaluation/:profile_id", async (req, reply) => {
|
/**
|
||||||
|
* @route GET /functions/workingtimeevaluation/:user_id
|
||||||
|
* @query start_date=YYYY-MM-DD
|
||||||
|
* @query end_date=YYYY-MM-DD
|
||||||
|
*/
|
||||||
|
server.get("/functions/timeevaluation/:user_id", async (req, reply) => {
|
||||||
|
const { user_id } = req.params as { user_id: string }
|
||||||
|
const { start_date, end_date } = req.query as { start_date: string; end_date: string }
|
||||||
|
const { tenant_id } = req.user
|
||||||
|
|
||||||
const { profile_id } = req.params as { profile_id: string };
|
// 🔒 Sicherheitscheck: andere User nur bei Berechtigung
|
||||||
const { start_date, end_date } = req.query as { start_date: string, end_date: string };
|
if (user_id !== req.user.user_id && !req.hasPermission("staff.time.read_all")) {
|
||||||
|
return reply.code(403).send({ error: "Not allowed to view other users." })
|
||||||
|
|
||||||
async function generateWorkingTimesEvaluationValues(profile_id,startDateInput,endDateInput) {
|
|
||||||
|
|
||||||
console.log(dayjs(startDateInput))
|
|
||||||
console.log(dayjs(endDateInput))
|
|
||||||
|
|
||||||
let startDate = dayjs(startDateInput)
|
|
||||||
let endDate = dayjs(endDateInput)
|
|
||||||
//console.log(startDate)
|
|
||||||
//console.log(endDate)
|
|
||||||
|
|
||||||
const {data:profile} = await server.supabase.from("auth_profiles").select().eq("old_profile_id",profile_id).single()
|
|
||||||
|
|
||||||
console.log(profile)
|
|
||||||
|
|
||||||
let {data:times,error:timesError} = await server.supabase.from("workingtimes").select().eq("profile", profile.old_profile_id).order("startDate",{ascending: false})
|
|
||||||
const {data:absencerequests, error: timesAbsenceRequestsError} = await server.supabase.from("absencerequests").select().eq("profile",profile.old_profile_id).order("startDate",{ascending: false})
|
|
||||||
|
|
||||||
times = times.filter(i => dayjs(i.startDate).isSameOrAfter(startDate) && dayjs(i.endDate).subtract(1,"day").isSameOrBefore(endDate))
|
|
||||||
|
|
||||||
console.log(times)
|
|
||||||
|
|
||||||
let weekFactor = 4.33
|
|
||||||
//let monthlyWorkingMinutes = profile.weeklyWorkingHours * weekFactor * 60
|
|
||||||
|
|
||||||
//Get Every Day to Calc
|
|
||||||
let days = []
|
|
||||||
let minuteSum = 0
|
|
||||||
let dayCount = dayjs(endDate).add(1,"day").diff(startDate,"days")
|
|
||||||
for (let count = 0; count < dayCount; count++) {
|
|
||||||
let date = dayjs(startDate).add(count,"days")
|
|
||||||
minuteSum += (profile.weekly_regular_working_hours[date.day()] || 0)*60
|
|
||||||
}
|
|
||||||
let timeSpanWorkingMinutes = minuteSum
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let workingMinutesTarget = Math.ceil(Number((Number(dayjs(endDate).add(1,"days").diff(dayjs(startDate),'month',true).toFixed(2)) * 4.33 * profile.weeklyWorkingHours * 60).toFixed(2)))
|
|
||||||
|
|
||||||
//Eingreicht
|
|
||||||
let sumWorkingMinutesEingereicht = 0
|
|
||||||
times.forEach(time => {
|
|
||||||
const minutes = dayjs(time.endDate).diff(dayjs(time.startDate),'minutes')
|
|
||||||
sumWorkingMinutesEingereicht = sumWorkingMinutesEingereicht + minutes
|
|
||||||
})
|
|
||||||
|
|
||||||
//Bestätigt
|
|
||||||
let sumWorkingMinutesApproved = 0
|
|
||||||
times.filter(i => i.approved).forEach(time => {
|
|
||||||
const minutes = dayjs(time.endDate).diff(dayjs(time.startDate),'minutes')
|
|
||||||
sumWorkingMinutesApproved = sumWorkingMinutesApproved + minutes
|
|
||||||
})
|
|
||||||
|
|
||||||
let recreationDays = ["2025-01-01","2025-04-18","2025-04-21","2025-05-01","2025-05-29","2025-06-09","2024-10-03","2024-10-31","2024-12-25","2024-12-26"]
|
|
||||||
|
|
||||||
//Feiertagsausgleich
|
|
||||||
let sumWorkingMinutesRecreationDays = 0
|
|
||||||
let sumRecreationDays = 0
|
|
||||||
if(profile.recreationDaysCompensation) {
|
|
||||||
recreationDays.filter(i => dayjs(i).isSameOrAfter(startDate) && dayjs(i).isSameOrBefore(endDate)).forEach(day => {
|
|
||||||
let compensationTime = profile.weekly_regular_working_hours[dayjs(day).day()] || 0
|
|
||||||
sumWorkingMinutesRecreationDays += compensationTime * 60
|
|
||||||
sumRecreationDays++
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
let isBetween = (date,start,end) => {
|
|
||||||
return dayjs(date).isSameOrAfter(start) && dayjs(date).isSameOrBefore(end)
|
|
||||||
}
|
|
||||||
|
|
||||||
//Urlaubsausgleich
|
|
||||||
let sumWorkingMinutesVacationDays = 0
|
|
||||||
let sumVacationDays = 0
|
|
||||||
|
|
||||||
absencerequests.filter(i => (dayjs(i.startDate).isBetween(dayjs(startDate),dayjs(endDate),"day","[]") || dayjs(i.endDate).isBetween(dayjs(startDate),dayjs(endDate),"day","[]")) && (i.reason === "Urlaub" || i.reason === "Berufsschule") && i.approved === "Genehmigt").forEach(absenceRequest => {
|
|
||||||
let durationInDays = 0
|
|
||||||
|
|
||||||
let durationStartDate = null
|
|
||||||
|
|
||||||
if(isBetween(absenceRequest.startDate,startDate,endDate) && isBetween(absenceRequest.endDate,startDate,endDate)) {
|
|
||||||
//Full in Range
|
|
||||||
durationInDays = dayjs(absenceRequest.endDate).diff(absenceRequest.startDate, "days") + 1
|
|
||||||
durationStartDate = absenceRequest.startDate
|
|
||||||
} else if(isBetween(absenceRequest.startDate,startDate,endDate) && !isBetween(absenceRequest.endDate,startDate,endDate)) {
|
|
||||||
//Start in Range
|
|
||||||
durationInDays = dayjs(endDate).diff(absenceRequest.startDate, "days") + 1
|
|
||||||
durationStartDate = absenceRequest.startDate
|
|
||||||
} else if(!isBetween(absenceRequest.startDate,startDate,endDate) && isBetween(absenceRequest.endDate,startDate,endDate)) {
|
|
||||||
//End in Range
|
|
||||||
durationInDays = dayjs(absenceRequest.endDate).diff(startDate, "days") + 1
|
|
||||||
durationStartDate = startDate
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
let minuteSum = 0
|
|
||||||
|
|
||||||
for (let count = 0; count < durationInDays; count++) {
|
|
||||||
let date = dayjs(durationStartDate).add(count,"days")
|
|
||||||
minuteSum += (profile.weekly_regular_working_hours[date.day()] || 0)*60
|
|
||||||
}
|
|
||||||
sumVacationDays += durationInDays
|
|
||||||
sumWorkingMinutesVacationDays += minuteSum
|
|
||||||
})
|
|
||||||
|
|
||||||
//Krankheitsausgleich
|
|
||||||
let sumWorkingMinutesSickDays = 0
|
|
||||||
let sumSickDays = 0
|
|
||||||
|
|
||||||
absencerequests.filter(i => (dayjs(i.startDate).isBetween(dayjs(startDate),dayjs(endDate)) || dayjs(i.endDate).isBetween(dayjs(startDate),dayjs(endDate)) ) && (i.reason === "Krankheit" || i.reason === "Krankheit ab 1 Tag (mit Attest)" || i.reason === "Krankheit ab 2. Tag (mit Attest)") && i.approved === "Genehmigt").forEach(absenceRequest => {
|
|
||||||
let durationInDays = 0
|
|
||||||
|
|
||||||
let durationStartDate = null
|
|
||||||
|
|
||||||
if(isBetween(absenceRequest.startDate,startDate,endDate) && isBetween(absenceRequest.endDate,startDate,endDate)) {
|
|
||||||
//Full in Range
|
|
||||||
console.log("FULL")
|
|
||||||
durationInDays = dayjs(absenceRequest.endDate).diff(absenceRequest.startDate, "days") + 1
|
|
||||||
durationStartDate = absenceRequest.startDate
|
|
||||||
} else if(isBetween(absenceRequest.startDate,startDate,endDate) && !isBetween(absenceRequest.endDate,startDate,endDate)) {
|
|
||||||
//Start in Range
|
|
||||||
console.log("Start")
|
|
||||||
durationInDays = dayjs(endDate).diff(absenceRequest.startDate, "days") + 1
|
|
||||||
durationStartDate = absenceRequest.startDate
|
|
||||||
} else if(!isBetween(absenceRequest.startDate,startDate,endDate) && isBetween(absenceRequest.endDate,startDate,endDate)) {
|
|
||||||
//End in Range
|
|
||||||
console.log("End")
|
|
||||||
durationInDays = dayjs(absenceRequest.endDate).diff(startDate, "days") + 1
|
|
||||||
durationStartDate = startDate
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
let minuteSum = 0
|
|
||||||
|
|
||||||
for (let count = 0; count < durationInDays; count++) {
|
|
||||||
let date = dayjs(durationStartDate).add(count,"days")
|
|
||||||
minuteSum += (profile.weekly_regular_working_hours[date.day()] || 0)*60
|
|
||||||
}
|
|
||||||
|
|
||||||
sumSickDays += durationInDays
|
|
||||||
sumWorkingMinutesSickDays += minuteSum
|
|
||||||
})
|
|
||||||
|
|
||||||
//Saldo
|
|
||||||
let saldo = (sumWorkingMinutesApproved + sumWorkingMinutesRecreationDays + sumWorkingMinutesVacationDays + sumWorkingMinutesSickDays - timeSpanWorkingMinutes).toFixed(2)
|
|
||||||
let saldoInOfficial = (sumWorkingMinutesEingereicht + sumWorkingMinutesRecreationDays + sumWorkingMinutesVacationDays + sumWorkingMinutesSickDays - timeSpanWorkingMinutes).toFixed(2)
|
|
||||||
|
|
||||||
return {
|
|
||||||
timeSpanWorkingMinutes,
|
|
||||||
workingMinutesTarget,
|
|
||||||
sumWorkingMinutesEingereicht,
|
|
||||||
sumWorkingMinutesApproved,
|
|
||||||
sumWorkingMinutesRecreationDays,
|
|
||||||
sumRecreationDays,
|
|
||||||
sumWorkingMinutesVacationDays,
|
|
||||||
sumVacationDays,
|
|
||||||
sumWorkingMinutesSickDays,
|
|
||||||
sumSickDays,
|
|
||||||
saldo,
|
|
||||||
saldoInOfficial,
|
|
||||||
times
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
reply.send(await generateWorkingTimesEvaluationValues(profile_id,start_date,end_date))
|
const result = await generateTimesEvaluation(server, user_id, tenant_id, start_date, end_date)
|
||||||
} catch(error) {
|
reply.send(result)
|
||||||
console.log(error)
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
reply.code(500).send({ error: error.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
server.get('/functions/check-zip/:zip', async (req, reply) => {
|
||||||
|
const { zip } = req.params as { zip: string }
|
||||||
|
|
||||||
|
if (!zip) {
|
||||||
|
return reply.code(400).send({ error: 'ZIP is required' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data, error } = await server.supabase
|
||||||
|
.from('citys')
|
||||||
|
.select()
|
||||||
|
.eq('zip', zip)
|
||||||
|
.maybeSingle()
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.log(error)
|
||||||
|
return reply.code(500).send({ error: 'Database error' })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return reply.code(404).send({ error: 'ZIP not found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
//districtMap
|
||||||
|
const bundeslaender = [
|
||||||
|
{ code: 'DE-BW', name: 'Baden-Württemberg' },
|
||||||
|
{ code: 'DE-BY', name: 'Bayern' },
|
||||||
|
{ code: 'DE-BE', name: 'Berlin' },
|
||||||
|
{ code: 'DE-BB', name: 'Brandenburg' },
|
||||||
|
{ code: 'DE-HB', name: 'Bremen' },
|
||||||
|
{ code: 'DE-HH', name: 'Hamburg' },
|
||||||
|
{ code: 'DE-HE', name: 'Hessen' },
|
||||||
|
{ code: 'DE-MV', name: 'Mecklenburg-Vorpommern' },
|
||||||
|
{ code: 'DE-NI', name: 'Niedersachsen' },
|
||||||
|
{ code: 'DE-NW', name: 'Nordrhein-Westfalen' },
|
||||||
|
{ code: 'DE-RP', name: 'Rheinland-Pfalz' },
|
||||||
|
{ code: 'DE-SL', name: 'Saarland' },
|
||||||
|
{ code: 'DE-SN', name: 'Sachsen' },
|
||||||
|
{ code: 'DE-ST', name: 'Sachsen-Anhalt' },
|
||||||
|
{ code: 'DE-SH', name: 'Schleswig-Holstein' },
|
||||||
|
{ code: 'DE-TH', name: 'Thüringen' }
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return reply.send({
|
||||||
|
...data,
|
||||||
|
state_code: bundeslaender.find(i => i.name === data.countryName)
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
return reply.code(500).send({ error: 'Internal server error' })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
226
src/utils/pdf.ts
226
src/utils/pdf.ts
@@ -23,13 +23,27 @@ const getCoordinatesForPDFLib = (x:number ,y:number, page:any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const getBackgroundSourceBuffer = async (server:FastifyInstance, path:string) => {
|
||||||
|
|
||||||
|
const {data:backgroundPDFData,error:backgroundPDFError} = await server.supabase.storage.from("files").download(path)
|
||||||
|
|
||||||
|
return backgroundPDFData.arrayBuffer()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDuration = (time) => {
|
||||||
|
const minutes = Math.floor(dayjs(time.stopped_at).diff(dayjs(time.started_at),'minutes',true))
|
||||||
|
const hours = Math.floor(minutes/60)
|
||||||
|
return {
|
||||||
|
//dezimal: dez,
|
||||||
|
hours: hours,
|
||||||
|
minutes: minutes,
|
||||||
|
composed: `${hours}:${String(minutes % 60).padStart(2,"0")} Std`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoiceData, backgroundPath:string) => {
|
export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoiceData, backgroundPath:string) => {
|
||||||
|
|
||||||
console.log(returnMode, invoiceData, backgroundPath)
|
|
||||||
|
|
||||||
const genPDF = async (invoiceData, backgroundSourceBuffer) => {
|
const genPDF = async (invoiceData, backgroundSourceBuffer) => {
|
||||||
const pdfDoc = await PDFDocument.create()
|
const pdfDoc = await PDFDocument.create()
|
||||||
|
|
||||||
@@ -841,14 +855,216 @@ export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoi
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const {data:backgroundPDFData,error:backgroundPDFError} = await server.supabase.storage.from("files").download(backgroundPath)
|
const pdfBytes = await genPDF(invoiceData, await getBackgroundSourceBuffer(server,backgroundPath))
|
||||||
|
|
||||||
const pdfBytes = await genPDF(invoiceData, await backgroundPDFData.arrayBuffer())
|
|
||||||
|
|
||||||
if(returnMode === "base64"){
|
if(returnMode === "base64"){
|
||||||
return {
|
return {
|
||||||
mimeType: 'application/pdf',
|
mimeType: 'application/pdf',
|
||||||
base64: pdfBytes
|
base64: pdfBytes
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const createTimeSheetPDF = async (server: FastifyInstance, returnMode, data, backgroundPath: string) => {
|
||||||
|
|
||||||
|
const genPDF = async (input, backgroundSourceBuffer) => {
|
||||||
|
const pdfDoc = await PDFDocument.create()
|
||||||
|
|
||||||
|
const font = await pdfDoc.embedFont(StandardFonts.Helvetica)
|
||||||
|
const fontBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold)
|
||||||
|
|
||||||
|
let pages = []
|
||||||
|
let pageCounter = 1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const backgroudPdf = await PDFDocument.load(backgroundSourceBuffer)
|
||||||
|
|
||||||
|
const firstPageBackground = await pdfDoc.embedPage(backgroudPdf.getPages()[0])
|
||||||
|
const secondPageBackground = await pdfDoc.embedPage(backgroudPdf.getPages()[backgroudPdf.getPages().length > 1 ? 1 : 0])
|
||||||
|
|
||||||
|
const page1 = pdfDoc.addPage()
|
||||||
|
|
||||||
|
page1.drawPage(firstPageBackground, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
pages.push(page1)
|
||||||
|
|
||||||
|
|
||||||
|
//Falzmarke 1
|
||||||
|
/*pages[pageCounter - 1].drawLine({
|
||||||
|
start: getCoordinatesForPDFLib(0,105,page1),
|
||||||
|
end: getCoordinatesForPDFLib(7,105,page1),
|
||||||
|
thickness: 0.25,
|
||||||
|
color: rgb(0,0,0),
|
||||||
|
opacity: 1
|
||||||
|
})*/
|
||||||
|
|
||||||
|
//Lochmarke
|
||||||
|
/*pages[pageCounter - 1].drawLine({
|
||||||
|
start: getCoordinatesForPDFLib(0,148.5,page1),
|
||||||
|
end: getCoordinatesForPDFLib(7,148.5,page1),
|
||||||
|
thickness: 0.25,
|
||||||
|
color: rgb(0,0,0),
|
||||||
|
opacity: 1
|
||||||
|
})*/
|
||||||
|
|
||||||
|
//Falzmarke 2
|
||||||
|
/*pages[pageCounter - 1].drawLine({
|
||||||
|
start: getCoordinatesForPDFLib(0,210,page1),
|
||||||
|
end: getCoordinatesForPDFLib(7,210,page1),
|
||||||
|
thickness: 0.25,
|
||||||
|
color: rgb(0,0,0),
|
||||||
|
opacity: 1
|
||||||
|
})*/
|
||||||
|
console.log(input)
|
||||||
|
pages[pageCounter - 1].drawText(`Mitarbeiter: ${input.full_name}`,{
|
||||||
|
x: getCoordinatesForPDFLib(20,55,pages[pageCounter -1]).x,
|
||||||
|
y: getCoordinatesForPDFLib(20,55,pages[pageCounter -1]).y,
|
||||||
|
size: 10,
|
||||||
|
})
|
||||||
|
pages[pageCounter - 1].drawText(`Eingereicht: ${Math.floor(input.sumWorkingMinutesEingereicht/60)}:${String(input.sumWorkingMinutesEingereicht % 60).padStart(2,"0")} Std`,{
|
||||||
|
x: getCoordinatesForPDFLib(20,60,pages[pageCounter -1]).x,
|
||||||
|
y: getCoordinatesForPDFLib(20,60,pages[pageCounter -1]).y,
|
||||||
|
size: 10,
|
||||||
|
})
|
||||||
|
pages[pageCounter - 1].drawText(`Genehmigt: ${Math.floor(input.sumWorkingMinutesApproved/60)}:${String(input.sumWorkingMinutesApproved % 60).padStart(2,"0")} Std`,{
|
||||||
|
x: getCoordinatesForPDFLib(20,65,pages[pageCounter -1]).x,
|
||||||
|
y: getCoordinatesForPDFLib(20,65,pages[pageCounter -1]).y,
|
||||||
|
size: 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
pages[pageCounter - 1].drawText(`Feiertagsausgleich: ${Math.floor(input.sumWorkingMinutesRecreationDays/60)}:${String(input.sumWorkingMinutesRecreationDays % 60).padStart(2,"0")} Std`,{
|
||||||
|
x: getCoordinatesForPDFLib(20,70,pages[pageCounter -1]).x,
|
||||||
|
y: getCoordinatesForPDFLib(20,70,pages[pageCounter -1]).y,
|
||||||
|
size: 10,
|
||||||
|
})
|
||||||
|
pages[pageCounter - 1].drawText(`Urlaubsausgleich: ${Math.floor(input.sumWorkingMinutesVacationDays/60)}:${String(input.sumWorkingMinutesVacationDays % 60).padStart(2,"0")} Std`,{
|
||||||
|
x: getCoordinatesForPDFLib(20,75,pages[pageCounter -1]).x,
|
||||||
|
y: getCoordinatesForPDFLib(20,75,pages[pageCounter -1]).y,
|
||||||
|
size: 10,
|
||||||
|
})
|
||||||
|
pages[pageCounter - 1].drawText(`Krankheitsausgleich: ${Math.floor(input.sumWorkingMinutesSickDays/60)}:${String(input.sumWorkingMinutesSickDays % 60).padStart(2,"0")} Std`,{
|
||||||
|
x: getCoordinatesForPDFLib(20,80,pages[pageCounter -1]).x,
|
||||||
|
y: getCoordinatesForPDFLib(20,80,pages[pageCounter -1]).y,
|
||||||
|
size: 10,
|
||||||
|
})
|
||||||
|
pages[pageCounter - 1].drawText(`Soll Stunden: ${Math.floor(input.timeSpanWorkingMinutes/60)}:${Math.floor(Number(String(input.timeSpanWorkingMinutes % 60).padStart(2,"0")))} Std`,{
|
||||||
|
x: getCoordinatesForPDFLib(20,85,pages[pageCounter -1]).x,
|
||||||
|
y: getCoordinatesForPDFLib(20,85,pages[pageCounter -1]).y,
|
||||||
|
size: 10,
|
||||||
|
})
|
||||||
|
pages[pageCounter - 1].drawText(`Inoffizielles Saldo: ${Math.sign(input.saldoInOfficial) === 1 ? "+" : "-"} ${Math.floor(Math.abs(input.saldoInOfficial/60))}:${Math.floor(Number(String(Math.abs(input.saldoInOfficial) % 60).padStart(2,"0")))} Std`,{
|
||||||
|
x: getCoordinatesForPDFLib(20,90,pages[pageCounter -1]).x,
|
||||||
|
y: getCoordinatesForPDFLib(20,90,pages[pageCounter -1]).y,
|
||||||
|
size: 10,
|
||||||
|
})
|
||||||
|
pages[pageCounter - 1].drawText(`Saldo: ${Math.sign(input.saldo) === 1 ? "+" : "-"} ${Math.floor(Math.abs(input.saldo/60))}:${Math.floor(Number(String(Math.abs(input.saldo) % 60).padStart(2,"0")))} Std`,{
|
||||||
|
x: getCoordinatesForPDFLib(20,95,pages[pageCounter -1]).x,
|
||||||
|
y: getCoordinatesForPDFLib(20,95,pages[pageCounter -1]).y,
|
||||||
|
size: 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
pages[pageCounter - 1].drawText(`Start:`,{
|
||||||
|
x: getCoordinatesForPDFLib(20,100,pages[pageCounter -1]).x,
|
||||||
|
y: getCoordinatesForPDFLib(20,100,pages[pageCounter -1]).y,
|
||||||
|
size: 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
pages[pageCounter - 1].drawText(`Ende:`,{
|
||||||
|
x: getCoordinatesForPDFLib(60,100,pages[pageCounter -1]).x,
|
||||||
|
y: getCoordinatesForPDFLib(60,100,pages[pageCounter -1]).y,
|
||||||
|
size: 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
pages[pageCounter - 1].drawText(`Dauer:`,{
|
||||||
|
x: getCoordinatesForPDFLib(100,100,pages[pageCounter -1]).x,
|
||||||
|
y: getCoordinatesForPDFLib(100,100,pages[pageCounter -1]).y,
|
||||||
|
size: 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
let rowHeight = 115
|
||||||
|
|
||||||
|
|
||||||
|
let splitted = []
|
||||||
|
|
||||||
|
let reversedInput = input.times.slice().reverse()
|
||||||
|
|
||||||
|
const splittedLength = Math.floor((reversedInput.length - 25) / 40)
|
||||||
|
|
||||||
|
splitted.push(reversedInput.slice(0,25))
|
||||||
|
|
||||||
|
let lastIndex = 25
|
||||||
|
for (let i = 0; i < splittedLength; ++i ) {
|
||||||
|
splitted.push(reversedInput.slice(lastIndex, lastIndex + (i + 1) * 40))
|
||||||
|
lastIndex = lastIndex + (i + 1) * 40 + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
splitted.push(reversedInput.slice(lastIndex, reversedInput.length))
|
||||||
|
|
||||||
|
|
||||||
|
splitted.forEach((chunk,index) => {
|
||||||
|
if(index > 0) {
|
||||||
|
const page = pdfDoc.addPage()
|
||||||
|
|
||||||
|
page.drawPage(secondPageBackground, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
pages.push(page)
|
||||||
|
pageCounter++
|
||||||
|
rowHeight = 20
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk.forEach(time => {
|
||||||
|
pages[pageCounter - 1].drawText(`${dayjs(time.started_at).format("HH:mm DD.MM.YY")}`,{
|
||||||
|
x: getCoordinatesForPDFLib(20,rowHeight,pages[pageCounter -1]).x,
|
||||||
|
y: getCoordinatesForPDFLib(20,rowHeight,pages[pageCounter -1]).y,
|
||||||
|
size: 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
pages[pageCounter - 1].drawText(`${dayjs(time.stopped_at).format("HH:mm DD.MM.YY")}`,{
|
||||||
|
x: getCoordinatesForPDFLib(60,rowHeight,pages[pageCounter -1]).x,
|
||||||
|
y: getCoordinatesForPDFLib(60,rowHeight,pages[pageCounter -1]).y,
|
||||||
|
size: 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
pages[pageCounter - 1].drawText(`${getDuration(time).composed}`,{
|
||||||
|
x: getCoordinatesForPDFLib(100,rowHeight,pages[pageCounter -1]).x,
|
||||||
|
y: getCoordinatesForPDFLib(100,rowHeight,pages[pageCounter -1]).y,
|
||||||
|
size: 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
rowHeight += 6
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return await pdfDoc.saveAsBase64()
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pdfBytes = await genPDF(data, await getBackgroundSourceBuffer(server,backgroundPath))
|
||||||
|
|
||||||
|
if(returnMode === "base64"){
|
||||||
|
return {
|
||||||
|
mimeType: 'application/pdf',
|
||||||
|
base64: pdfBytes
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return "test"
|
||||||
|
}
|
||||||
|
} catch(error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user