implement Gehaltsaufstellung dyflexis data access

This commit is contained in:
Marcel Arndt 2025-11-18 13:32:36 +01:00
parent 4abe9e44d8
commit 4cf79f3094
18 changed files with 1015 additions and 1107 deletions

View File

@ -46,7 +46,8 @@ func NewServer(cfg *config.Config, logger *slog.Logger) (*Server, error) {
// setupRoutes initializes all routes and handlers // setupRoutes initializes all routes and handlers
func (s *Server) setupRoutes() { func (s *Server) setupRoutes() {
employeeRepo := database.New(s.db) employeeRepo := database.New(s.db)
employeeService := payroll.NewService(employeeRepo, s.logger) zuschlagParser := payroll.NewZuschlagParser(s.logger)
employeeService := payroll.NewService(employeeRepo, s.logger, zuschlagParser)
employeeHandler := payroll.NewEmployeeHandler(*employeeService, s.logger) employeeHandler := payroll.NewEmployeeHandler(*employeeService, s.logger)
s.router.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) { s.router.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
@ -54,8 +55,6 @@ func (s *Server) setupRoutes() {
w.Write([]byte("OK")) w.Write([]byte("OK"))
}) })
s.router.HandleFunc("GET /api/v1/employees", employeeHandler.ListEmployees)
s.router.HandleFunc("GET /api/v1/payroll", employeeHandler.RunPayroll) s.router.HandleFunc("GET /api/v1/payroll", employeeHandler.RunPayroll)
} }

View File

@ -16,6 +16,20 @@ SET xmloption = content;
SET client_min_messages = warning; SET client_min_messages = warning;
SET row_security = off; SET row_security = off;
--
-- Name: public; Type: SCHEMA; Schema: -; Owner: -
--
-- *not* creating schema, since initdb creates it
--
-- Name: SCHEMA public; Type: COMMENT; Schema: -; Owner: -
--
COMMENT ON SCHEMA public IS '';
-- --
-- Name: pgcrypto; Type: EXTENSION; Schema: -; Owner: - -- Name: pgcrypto; Type: EXTENSION; Schema: -; Owner: -
-- --
@ -120,7 +134,7 @@ CREATE TABLE public."Anomaly" (
CREATE TABLE public."AttendanceRegistration" ( CREATE TABLE public."AttendanceRegistration" (
"attendanceRegistrationId" integer NOT NULL, "attendanceRegistrationId" integer NOT NULL,
"employeeId" integer NOT NULL, "employeeId" integer NOT NULL,
"personnelNumber" text NOT NULL, "personnelNumber" text,
"dateTime" timestamp(3) without time zone NOT NULL, "dateTime" timestamp(3) without time zone NOT NULL,
event text NOT NULL event text NOT NULL
); );
@ -151,7 +165,8 @@ CREATE TABLE public."AttendanceRegistrationStaging" (
"attendanceRegistrationId" integer NOT NULL, "attendanceRegistrationId" integer NOT NULL,
"employeeId" integer NOT NULL, "employeeId" integer NOT NULL,
"dateTime" timestamp(3) without time zone NOT NULL, "dateTime" timestamp(3) without time zone NOT NULL,
event text NOT NULL event text NOT NULL,
"personnelNumber" text DEFAULT ''::text
); );
@ -286,6 +301,19 @@ CREATE TABLE public."EmployeeDyflexisStaging" (
); );
--
-- Name: Holiday; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."Holiday" (
id uuid DEFAULT gen_random_uuid() NOT NULL,
date timestamp(3) without time zone NOT NULL,
locked boolean NOT NULL,
"excludedFromPayroll" boolean NOT NULL,
"employeeId" integer NOT NULL
);
-- --
-- Name: Patient; Type: TABLE; Schema: public; Owner: - -- Name: Patient; Type: TABLE; Schema: public; Owner: -
-- --
@ -411,6 +439,22 @@ CREATE TABLE public."PlannedTimeStaging" (
); );
--
-- Name: Sickdays; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."Sickdays" (
id uuid DEFAULT gen_random_uuid() NOT NULL,
start timestamp(3) without time zone NOT NULL,
"end" timestamp(3) without time zone NOT NULL,
hours double precision NOT NULL,
days integer NOT NULL,
note text,
locked boolean NOT NULL,
"employeeId" integer NOT NULL
);
-- --
-- Name: SolutionTarget; Type: TABLE; Schema: public; Owner: - -- Name: SolutionTarget; Type: TABLE; Schema: public; Owner: -
-- --
@ -540,7 +584,10 @@ CREATE TABLE public."Tour" (
"createdAt" timestamp(3) without time zone, "createdAt" timestamp(3) without time zone,
"deletedAt" timestamp(3) without time zone, "deletedAt" timestamp(3) without time zone,
"ticketId" uuid, "ticketId" uuid,
"updatedAt" timestamp(3) without time zone "updatedAt" timestamp(3) without time zone,
"companyName" text,
"overRTW" boolean DEFAULT false NOT NULL,
schwerlast boolean DEFAULT false NOT NULL
); );
@ -616,7 +663,10 @@ CREATE TABLE public."TourSource" (
"fahrerName" text, "fahrerName" text,
fahrer text, fahrer text,
color text, color text,
"lastUpdate" text "lastUpdate" text,
"companyName" text,
"overRTW" text,
schwerlast text
); );
@ -674,7 +724,10 @@ CREATE TABLE public."TourStaging" (
"revenueDeviation" double precision DEFAULT 0 NOT NULL, "revenueDeviation" double precision DEFAULT 0 NOT NULL,
"consumptionCosts" double precision DEFAULT 0 NOT NULL, "consumptionCosts" double precision DEFAULT 0 NOT NULL,
"createdAt" timestamp(3) without time zone, "createdAt" timestamp(3) without time zone,
"updatedAt" timestamp(3) without time zone "updatedAt" timestamp(3) without time zone,
"companyName" text,
"overRTW" boolean DEFAULT false NOT NULL,
schwerlast boolean DEFAULT false NOT NULL
); );
@ -860,6 +913,14 @@ ALTER TABLE ONLY public."Employee"
ADD CONSTRAINT "Employee_pkey" PRIMARY KEY (id); ADD CONSTRAINT "Employee_pkey" PRIMARY KEY (id);
--
-- Name: Holiday Holiday_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public."Holiday"
ADD CONSTRAINT "Holiday_pkey" PRIMARY KEY (id);
-- --
-- Name: PatientSource PatientSource_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- Name: PatientSource PatientSource_pkey; Type: CONSTRAINT; Schema: public; Owner: -
-- --
@ -908,6 +969,14 @@ ALTER TABLE ONLY public."PlannedTime"
ADD CONSTRAINT "PlannedTime_pkey" PRIMARY KEY (id); ADD CONSTRAINT "PlannedTime_pkey" PRIMARY KEY (id);
--
-- Name: Sickdays Sickdays_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public."Sickdays"
ADD CONSTRAINT "Sickdays_pkey" PRIMARY KEY (id);
-- --
-- Name: SolutionTargetStep SolutionTargetStep_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- Name: SolutionTargetStep SolutionTargetStep_pkey; Type: CONSTRAINT; Schema: public; Owner: -
-- --

View File

@ -12,90 +12,136 @@ SELECT
e."personTransportCertificate" e."personTransportCertificate"
FROM "DyflexisEmployee" as de FROM "DyflexisEmployee" as de
INNER JOIN "Employee" as e ON de.id::text = e."personnelNumber" INNER JOIN "Employee" as e ON de.id::text = e."personnelNumber"
WHERE de."contractStart" < CURRENT_DATE AND de."contractEnd" > (CURRENT_DATE - INTERVAL '2 months'); WHERE de."contractStart" < CURRENT_DATE AND de."contractEnd" > (CURRENT_DATE - INTERVAL '2 months')
AND de.surname <> 'Shahrasebhi'
ORDER BY de.firstname ASC;
-- name: GetWorkSummaryForEmployeeInPeriod :one -- name: GetWeeklyTimeSummaryForEmployee :many
WITH wt AS ( WITH all_time_entries AS (
SELECT SELECT
COUNT(*) as days, DATE_TRUNC('week', "startDate") AS week_start,
COALESCE(sum(duration), 0)::bigint as minutes SUM("duration" / 60.0) as worked_hours,
count(DISTINCT DATE_TRUNC('day', "startDate"))::float as worked_days,
0::float AS sick_hours,
0::float AS holiday_days
FROM "WorkTime" FROM "WorkTime"
WHERE "userId" = @employee_id AND "startDate" > @start_date AND "startDate" < @end_date WHERE
) "startDate" BETWEEN @start_date AND @end_date
SELECT AND "userId"::int = @employee_id
wt.days, AND "startDate" IS NOT NULL
(minutes / 60.0)::float as hours, GROUP BY
wt.minutes DATE_TRUNC('week', "startDate")
FROM wt;
-- name: ListPlannedTimesForEmployeeInPeriod :many UNION ALL
SELECT
DISTINCT ON (day)
"startDate"::DATE as day,
"startDate",
"endDate",
department,
pause,
duration
FROM "PlannedTime"
WHERE
"userId" = @employee_id AND
"startDate" > @start_date AND
"startDate" < @end_date AND
department <> 'Fortbildung' AND
note IS NULL
ORDER BY day, "startDate" ASC;
-- name: ListWorkTimesForEmployeeInPeriod :many
SELECT * FROM "WorkTime"
WHERE "userId" = @employee_id AND "startDate" > @start_date AND "startDate" < @end_date;
-- name: GetSicknessSummaryForEmployeeInPeriod :one
WITH pt AS (
SELECT SELECT
COUNT(*) as days, DATE_TRUNC('week', "start") AS week_start,
COALESCE(sum(duration), 0)::bigint as minutes 0::float as worked_hours,
FROM "PlannedTime" 0::float as worked_days,
WHERE "userId" = @employee_id AND "startDate" > @start_date AND "startDate" < @end_date AND note = 'sick' SUM("hours") AS sick_hours,
0::float AS holiday_days
FROM "Sickdays"
WHERE
"start" BETWEEN @start_date AND @end_date
AND "employeeId" = @employee_id
AND "start" IS NOT NULL
GROUP BY
DATE_TRUNC('week', "start")
UNION ALL
SELECT
DATE_TRUNC('week', "date") AS week_start,
0::float as worked_hours,
0::float as worked_days,
0::float AS sick_hours,
count(*)::float AS holiday_days
FROM "Holiday"
WHERE
"date" BETWEEN @start_date AND @end_date
AND "employeeId" = @employee_id
AND "date" IS NOT NULL
GROUP BY
DATE_TRUNC('week', "date")
) )
SELECT SELECT
pt.days, week_start::timestamp,
(minutes / 60)::float as hours, SUM(worked_hours)::float AS total_worked_hours,
pt.minutes SUM(worked_days)::float AS total_worked_days,
FROM pt; SUM(sick_hours)::float AS total_sick_hours,
SUM(holiday_days)::float AS total_holiday_days
FROM all_time_entries
GROUP BY
week_start
ORDER BY
week_start;
-- name: GetEmployeeDyflexis :one -- name: EvaluatePunctualityForEmployee :one
SELECT * FROM "DyflexisEmployee" WITH
WHERE planned_start_times AS (
id = $1 AND SELECT
"contractStart" < CURRENT_DATE AND DISTINCT ON ("startDate"::DATE)
"contractEnd" > (CURRENT_DATE - INTERVAL '2 months') "startDate"::DATE AS plan_day,
LIMIT 1; "startDate" AS planned_start_time
FROM "PlannedTime" as p
WHERE
p."userId"::int = @employee_id::bigint
AND p."startDate" BETWEEN @start_date AND @end_date
AND p."startDate" IS NOT NULL
AND p.department <> 'Fortbildung'
AND p.note IS NULL
ORDER BY
"startDate"::DATE, "startDate" ASC
),
actual_start_times AS (
SELECT
DISTINCT ON ("startDate"::DATE)
"startDate"::DATE AS work_day,
"startDate" AS actual_start_time
FROM "WorkTime"
WHERE
"userId"::int = @employee_id::bigint
AND "startDate" BETWEEN @start_date AND @end_date
AND "startDate" IS NOT NULL
ORDER BY
"startDate"::DATE, "startDate" ASC
),
daily_comparison AS (
SELECT
p.plan_day,
p.planned_start_time,
a.actual_start_time,
(a.actual_start_time IS NOT NULL AND a.actual_start_time <= p.planned_start_time) AS was_punctual
FROM planned_start_times p
LEFT JOIN actual_start_times a ON p.plan_day = a.work_day
)
SELECT
COUNT(CASE WHEN was_punctual THEN 1 END) AS punctual_days
FROM daily_comparison;
-- name: FetchDrivenToursInInterval :many -- name: IsEmployeesDepartmentKTW :one
SELECT * FROM "Tour" SELECT COALESCE(
WHERE "check" <> 3 (
AND "deletedAt" IS NULL bool_or(department LIKE '%KTW%')
AND ( AND
"patientId" IS NULL bool_and(department <> 'Leitstelle')
OR "patientId" NOT IN ( ),
'b7424477-a4b2-42f7-b7e7-a4bef24f6979', false
'3554e1a9-9c07-45e5-b53c-692a77e4fa9a' )::bool as meets_requirements
) FROM "PlannedTime"
) WHERE
-- ID's for Pause and Feierabend respectively "userId"::int = @employee_id::bigint
AND ( AND "startDate" BETWEEN @start_date AND @end_date
"carName" IS NULL AND department IS NOT NULL;
OR "carName" NOT IN (
'Storno', -- name: CountVerpflegungspauschale :one
'Abgabe', SELECT
'Löschen' count(*)
) FROM "WorkTime"
) WHERE
AND "startDate" >= @start_date duration > 480
AND "startDate" <= @end_date AND "startDate" BETWEEN @start_date AND @end_date
AND ("driverId" = @employee_id OR "codriverId" = @employee_id) AND "userId"::int = @employee_id::bigint;
ORDER BY "startDate" DESC;
-- name: CountTourengeldAmountInPeriod :one -- name: CountTourengeldAmountInPeriod :one
WITH ToursInPeriod AS ( WITH ToursInPeriod AS (
@ -159,23 +205,13 @@ HighActivityDays AS (
GREATEST(0, tour_amount - 6) AS tourengeld GREATEST(0, tour_amount - 6) AS tourengeld
FROM FROM
DailyCounts DailyCounts
),
TourenGeld AS (
SELECT
SUM(tourengeld)::float as tourengeld
FROM
HighActivityDays
) )
SELECT SELECT
SUM(tourengeld)::float as tourengeld COALESCE(tourengeld, 0.0)
FROM FROM TourenGeld;
HighActivityDays;
-- name: GetEmployeeByName :one
SELECT
dy.id as dyflexis_id,
dl.id as dispolive_id,
dy."contractSalaryHour"
FROM "DyflexisEmployee" as dy
INNER JOIN
"Employee" as dl ON dy.id::text = dl."personnelNumber"
WHERE
TRIM(dy.firstname) = @firstname AND
TRIM(dy.surname) = @lastname AND
"contractStart" < CURRENT_DATE AND
"contractEnd" > (CURRENT_DATE - INTERVAL '2 months')
ORDER BY dy."contractEnd" DESC;

View File

@ -215,11 +215,11 @@ type Anomaly struct {
} }
type AttendanceRegistration struct { type AttendanceRegistration struct {
AttendanceRegistrationId int32 `json:"attendanceRegistrationId"` AttendanceRegistrationId int32 `json:"attendanceRegistrationId"`
EmployeeId int32 `json:"employeeId"` EmployeeId int32 `json:"employeeId"`
PersonnelNumber string `json:"personnelNumber"` PersonnelNumber pgtype.Text `json:"personnelNumber"`
DateTime time.Time `json:"dateTime"` DateTime time.Time `json:"dateTime"`
Event string `json:"event"` Event string `json:"event"`
} }
type AttendanceRegistrationSource struct { type AttendanceRegistrationSource struct {
@ -239,6 +239,7 @@ type AttendanceRegistrationStaging struct {
EmployeeId int32 `json:"employeeId"` EmployeeId int32 `json:"employeeId"`
DateTime time.Time `json:"dateTime"` DateTime time.Time `json:"dateTime"`
Event string `json:"event"` Event string `json:"event"`
PersonnelNumber pgtype.Text `json:"personnelNumber"`
} }
type AvicennaTenant struct { type AvicennaTenant struct {
@ -337,6 +338,14 @@ type EmployeeDyflexisStaging struct {
ContractTypeName pgtype.Text `json:"contractTypeName"` ContractTypeName pgtype.Text `json:"contractTypeName"`
} }
type Holiday struct {
ID pgtype.UUID `json:"id"`
Date time.Time `json:"date"`
Locked bool `json:"locked"`
ExcludedFromPayroll bool `json:"excludedFromPayroll"`
EmployeeId int32 `json:"employeeId"`
}
type Patient struct { type Patient struct {
ID string `json:"id"` ID string `json:"id"`
Surname pgtype.Text `json:"surname"` Surname pgtype.Text `json:"surname"`
@ -443,6 +452,17 @@ type PrismaMigration struct {
AppliedStepsCount int32 `json:"applied_steps_count"` AppliedStepsCount int32 `json:"applied_steps_count"`
} }
type Sickday struct {
ID pgtype.UUID `json:"id"`
Start time.Time `json:"start"`
End time.Time `json:"end"`
Hours float64 `json:"hours"`
Days int32 `json:"days"`
Note pgtype.Text `json:"note"`
Locked bool `json:"locked"`
EmployeeId int32 `json:"employeeId"`
}
type SolutionTarget struct { type SolutionTarget struct {
ID pgtype.UUID `json:"id"` ID pgtype.UUID `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@ -539,6 +559,9 @@ type Tour struct {
DeletedAt *time.Time `json:"deletedAt"` DeletedAt *time.Time `json:"deletedAt"`
TicketId pgtype.UUID `json:"ticketId"` TicketId pgtype.UUID `json:"ticketId"`
UpdatedAt *time.Time `json:"updatedAt"` UpdatedAt *time.Time `json:"updatedAt"`
CompanyName pgtype.Text `json:"companyName"`
OverRTW bool `json:"overRTW"`
Schwerlast bool `json:"schwerlast"`
} }
type TourSource struct { type TourSource struct {
@ -610,6 +633,9 @@ type TourSource struct {
Fahrer pgtype.Text `json:"fahrer"` Fahrer pgtype.Text `json:"fahrer"`
Color pgtype.Text `json:"color"` Color pgtype.Text `json:"color"`
LastUpdate pgtype.Text `json:"lastUpdate"` LastUpdate pgtype.Text `json:"lastUpdate"`
CompanyName pgtype.Text `json:"companyName"`
OverRTW pgtype.Text `json:"overRTW"`
Schwerlast pgtype.Text `json:"schwerlast"`
} }
type TourStaging struct { type TourStaging struct {
@ -663,6 +689,9 @@ type TourStaging struct {
ConsumptionCosts float64 `json:"consumptionCosts"` ConsumptionCosts float64 `json:"consumptionCosts"`
CreatedAt *time.Time `json:"createdAt"` CreatedAt *time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"` UpdatedAt *time.Time `json:"updatedAt"`
CompanyName pgtype.Text `json:"companyName"`
OverRTW bool `json:"overRTW"`
Schwerlast bool `json:"schwerlast"`
} }
type Trace struct { type Trace struct {

View File

@ -74,11 +74,16 @@ HighActivityDays AS (
GREATEST(0, tour_amount - 6) AS tourengeld GREATEST(0, tour_amount - 6) AS tourengeld
FROM FROM
DailyCounts DailyCounts
),
TourenGeld AS (
SELECT
SUM(tourengeld)::float as tourengeld
FROM
HighActivityDays
) )
SELECT SELECT
SUM(tourengeld)::float as tourengeld COALESCE(tourengeld, 0.0)
FROM FROM TourenGeld
HighActivityDays
` `
type CountTourengeldAmountInPeriodParams struct { type CountTourengeldAmountInPeriodParams struct {
@ -94,99 +99,176 @@ func (q *Queries) CountTourengeldAmountInPeriod(ctx context.Context, arg CountTo
return tourengeld, err return tourengeld, err
} }
const fetchDrivenToursInInterval = `-- name: FetchDrivenToursInInterval :many const countVerpflegungspauschale = `-- name: CountVerpflegungspauschale :one
SELECT id, done, direction, "carName", "operationId", "driverId", "driverName", "codriverId", "codriverName", "patientId", "patientName", "patientSurname", "patientStreet", "patientZip", "patientCity", "healthInsurance", "healthInsuranceNumber", type, category, "transportType", "ordinanceType", "rangeEndDate", "startInstitution", "startStreet", "startZip", "startCity", "targetInstitution", "targetStreet", "targetZip", "targetCity", "startDate", "check", "occupiedKm", "totalKm", "startBegin", "startEnd", target, "targetBegin", empty, "hasInfection", "infectionName", "revenueDispoLive", "billDate", "billNumber", revenue, "consumptionCosts", "revenueDeviation", "createdAt", "deletedAt", "ticketId", "updatedAt" FROM "Tour" SELECT
WHERE "check" <> 3 count(*)
AND "deletedAt" IS NULL FROM "WorkTime"
AND ( WHERE
"patientId" IS NULL duration > 480
OR "patientId" NOT IN ( AND "startDate" BETWEEN $1 AND $2
'b7424477-a4b2-42f7-b7e7-a4bef24f6979', AND "userId"::int = $3::bigint
'3554e1a9-9c07-45e5-b53c-692a77e4fa9a'
)
)
AND (
"carName" IS NULL
OR "carName" NOT IN (
'Storno',
'Abgabe',
'Löschen'
)
)
AND "startDate" >= $1
AND "startDate" <= $2
AND ("driverId" = $3 OR "codriverId" = $3)
ORDER BY "startDate" DESC
` `
type FetchDrivenToursInIntervalParams struct { type CountVerpflegungspauschaleParams struct {
StartDate time.Time `json:"start_date"` StartDate *time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"` EndDate *time.Time `json:"end_date"`
EmployeeID pgtype.Text `json:"employee_id"` EmployeeID int64 `json:"employee_id"`
} }
// ID's for Pause and Feierabend respectively func (q *Queries) CountVerpflegungspauschale(ctx context.Context, arg CountVerpflegungspauschaleParams) (int64, error) {
func (q *Queries) FetchDrivenToursInInterval(ctx context.Context, arg FetchDrivenToursInIntervalParams) ([]Tour, error) { row := q.db.QueryRow(ctx, countVerpflegungspauschale, arg.StartDate, arg.EndDate, arg.EmployeeID)
rows, err := q.db.Query(ctx, fetchDrivenToursInInterval, arg.StartDate, arg.EndDate, arg.EmployeeID) var count int64
err := row.Scan(&count)
return count, err
}
const evaluatePunctualityForEmployee = `-- name: EvaluatePunctualityForEmployee :one
WITH
planned_start_times AS (
SELECT
DISTINCT ON ("startDate"::DATE)
"startDate"::DATE AS plan_day,
"startDate" AS planned_start_time
FROM "PlannedTime" as p
WHERE
p."userId"::int = $1::bigint
AND p."startDate" BETWEEN $2 AND $3
AND p."startDate" IS NOT NULL
AND p.department <> 'Fortbildung'
AND p.note IS NULL
ORDER BY
"startDate"::DATE, "startDate" ASC
),
actual_start_times AS (
SELECT
DISTINCT ON ("startDate"::DATE)
"startDate"::DATE AS work_day,
"startDate" AS actual_start_time
FROM "WorkTime"
WHERE
"userId"::int = $1::bigint
AND "startDate" BETWEEN $2 AND $3
AND "startDate" IS NOT NULL
ORDER BY
"startDate"::DATE, "startDate" ASC
),
daily_comparison AS (
SELECT
p.plan_day,
p.planned_start_time,
a.actual_start_time,
(a.actual_start_time IS NOT NULL AND a.actual_start_time <= p.planned_start_time) AS was_punctual
FROM planned_start_times p
LEFT JOIN actual_start_times a ON p.plan_day = a.work_day
)
SELECT
COUNT(CASE WHEN was_punctual THEN 1 END) AS punctual_days
FROM daily_comparison
`
type EvaluatePunctualityForEmployeeParams struct {
EmployeeID int64 `json:"employee_id"`
StartDate *time.Time `json:"start_date"`
EndDate *time.Time `json:"end_date"`
}
func (q *Queries) EvaluatePunctualityForEmployee(ctx context.Context, arg EvaluatePunctualityForEmployeeParams) (int64, error) {
row := q.db.QueryRow(ctx, evaluatePunctualityForEmployee, arg.EmployeeID, arg.StartDate, arg.EndDate)
var punctual_days int64
err := row.Scan(&punctual_days)
return punctual_days, err
}
const getWeeklyTimeSummaryForEmployee = `-- name: GetWeeklyTimeSummaryForEmployee :many
WITH all_time_entries AS (
SELECT
DATE_TRUNC('week', "startDate") AS week_start,
SUM("duration" / 60.0) as worked_hours,
count(DISTINCT DATE_TRUNC('day', "startDate"))::float as worked_days,
0::float AS sick_hours,
0::float AS holiday_days
FROM "WorkTime"
WHERE
"startDate" BETWEEN $1 AND $2
AND "userId"::int = $3
AND "startDate" IS NOT NULL
GROUP BY
DATE_TRUNC('week', "startDate")
UNION ALL
SELECT
DATE_TRUNC('week', "start") AS week_start,
0::float as worked_hours,
0::float as worked_days,
SUM("hours") AS sick_hours,
0::float AS holiday_days
FROM "Sickdays"
WHERE
"start" BETWEEN $1 AND $2
AND "employeeId" = $3
AND "start" IS NOT NULL
GROUP BY
DATE_TRUNC('week', "start")
UNION ALL
SELECT
DATE_TRUNC('week', "date") AS week_start,
0::float as worked_hours,
0::float as worked_days,
0::float AS sick_hours,
count(*)::float AS holiday_days
FROM "Holiday"
WHERE
"date" BETWEEN $1 AND $2
AND "employeeId" = $3
AND "date" IS NOT NULL
GROUP BY
DATE_TRUNC('week', "date")
)
SELECT
week_start::timestamp,
SUM(worked_hours)::float AS total_worked_hours,
SUM(worked_days)::float AS total_worked_days,
SUM(sick_hours)::float AS total_sick_hours,
SUM(holiday_days)::float AS total_holiday_days
FROM all_time_entries
GROUP BY
week_start
ORDER BY
week_start
`
type GetWeeklyTimeSummaryForEmployeeParams struct {
StartDate *time.Time `json:"start_date"`
EndDate *time.Time `json:"end_date"`
EmployeeID string `json:"employee_id"`
}
type GetWeeklyTimeSummaryForEmployeeRow struct {
WeekStart time.Time `json:"week_start"`
TotalWorkedHours float64 `json:"total_worked_hours"`
TotalWorkedDays float64 `json:"total_worked_days"`
TotalSickHours float64 `json:"total_sick_hours"`
TotalHolidayDays float64 `json:"total_holiday_days"`
}
func (q *Queries) GetWeeklyTimeSummaryForEmployee(ctx context.Context, arg GetWeeklyTimeSummaryForEmployeeParams) ([]GetWeeklyTimeSummaryForEmployeeRow, error) {
rows, err := q.db.Query(ctx, getWeeklyTimeSummaryForEmployee, arg.StartDate, arg.EndDate, arg.EmployeeID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var items []Tour var items []GetWeeklyTimeSummaryForEmployeeRow
for rows.Next() { for rows.Next() {
var i Tour var i GetWeeklyTimeSummaryForEmployeeRow
if err := rows.Scan( if err := rows.Scan(
&i.ID, &i.WeekStart,
&i.Done, &i.TotalWorkedHours,
&i.Direction, &i.TotalWorkedDays,
&i.CarName, &i.TotalSickHours,
&i.OperationId, &i.TotalHolidayDays,
&i.DriverId,
&i.DriverName,
&i.CodriverId,
&i.CodriverName,
&i.PatientId,
&i.PatientName,
&i.PatientSurname,
&i.PatientStreet,
&i.PatientZip,
&i.PatientCity,
&i.HealthInsurance,
&i.HealthInsuranceNumber,
&i.Type,
&i.Category,
&i.TransportType,
&i.OrdinanceType,
&i.RangeEndDate,
&i.StartInstitution,
&i.StartStreet,
&i.StartZip,
&i.StartCity,
&i.TargetInstitution,
&i.TargetStreet,
&i.TargetZip,
&i.TargetCity,
&i.StartDate,
&i.Check,
&i.OccupiedKm,
&i.TotalKm,
&i.StartBegin,
&i.StartEnd,
&i.Target,
&i.TargetBegin,
&i.Empty,
&i.HasInfection,
&i.InfectionName,
&i.RevenueDispoLive,
&i.BillDate,
&i.BillNumber,
&i.Revenue,
&i.ConsumptionCosts,
&i.RevenueDeviation,
&i.CreatedAt,
&i.DeletedAt,
&i.TicketId,
&i.UpdatedAt,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -198,132 +280,33 @@ func (q *Queries) FetchDrivenToursInInterval(ctx context.Context, arg FetchDrive
return items, nil return items, nil
} }
const getEmployeeByName = `-- name: GetEmployeeByName :one const isEmployeesDepartmentKTW = `-- name: IsEmployeesDepartmentKTW :one
SELECT SELECT COALESCE(
dy.id as dyflexis_id, (
dl.id as dispolive_id, bool_or(department LIKE '%KTW%')
dy."contractSalaryHour" AND
FROM "DyflexisEmployee" as dy bool_and(department <> 'Leitstelle')
INNER JOIN ),
"Employee" as dl ON dy.id::text = dl."personnelNumber" false
WHERE )::bool as meets_requirements
TRIM(dy.firstname) = $1 AND FROM "PlannedTime"
TRIM(dy.surname) = $2 AND WHERE
"contractStart" < CURRENT_DATE AND "userId"::int = $1::bigint
"contractEnd" > (CURRENT_DATE - INTERVAL '2 months') AND "startDate" BETWEEN $2 AND $3
ORDER BY dy."contractEnd" DESC AND department IS NOT NULL
` `
type GetEmployeeByNameParams struct { type IsEmployeesDepartmentKTWParams struct {
Firstname string `json:"firstname"` EmployeeID int64 `json:"employee_id"`
Lastname string `json:"lastname"`
}
type GetEmployeeByNameRow struct {
DyflexisID int32 `json:"dyflexis_id"`
DispoliveID string `json:"dispolive_id"`
ContractSalaryHour float64 `json:"contractSalaryHour"`
}
func (q *Queries) GetEmployeeByName(ctx context.Context, arg GetEmployeeByNameParams) (GetEmployeeByNameRow, error) {
row := q.db.QueryRow(ctx, getEmployeeByName, arg.Firstname, arg.Lastname)
var i GetEmployeeByNameRow
err := row.Scan(&i.DyflexisID, &i.DispoliveID, &i.ContractSalaryHour)
return i, err
}
const getEmployeeDyflexis = `-- name: GetEmployeeDyflexis :one
SELECT id, firstname, surname, "contractStart", "contractEnd", "contractHoursWeek", "contractSalaryHour", "contractTypeName", "internalId" FROM "DyflexisEmployee"
WHERE
id = $1 AND
"contractStart" < CURRENT_DATE AND
"contractEnd" > (CURRENT_DATE - INTERVAL '2 months')
LIMIT 1
`
func (q *Queries) GetEmployeeDyflexis(ctx context.Context, id int32) (DyflexisEmployee, error) {
row := q.db.QueryRow(ctx, getEmployeeDyflexis, id)
var i DyflexisEmployee
err := row.Scan(
&i.ID,
&i.Firstname,
&i.Surname,
&i.ContractStart,
&i.ContractEnd,
&i.ContractHoursWeek,
&i.ContractSalaryHour,
&i.ContractTypeName,
&i.InternalId,
)
return i, err
}
const getSicknessSummaryForEmployeeInPeriod = `-- name: GetSicknessSummaryForEmployeeInPeriod :one
WITH pt AS (
SELECT
COUNT(*) as days,
COALESCE(sum(duration), 0)::bigint as minutes
FROM "PlannedTime"
WHERE "userId" = $1 AND "startDate" > $2 AND "startDate" < $3 AND note = 'sick'
)
SELECT
pt.days,
(minutes / 60)::float as hours,
pt.minutes
FROM pt
`
type GetSicknessSummaryForEmployeeInPeriodParams struct {
EmployeeID string `json:"employee_id"`
StartDate *time.Time `json:"start_date"` StartDate *time.Time `json:"start_date"`
EndDate *time.Time `json:"end_date"` EndDate *time.Time `json:"end_date"`
} }
type GetSicknessSummaryForEmployeeInPeriodRow struct { func (q *Queries) IsEmployeesDepartmentKTW(ctx context.Context, arg IsEmployeesDepartmentKTWParams) (bool, error) {
Days int64 `json:"days"` row := q.db.QueryRow(ctx, isEmployeesDepartmentKTW, arg.EmployeeID, arg.StartDate, arg.EndDate)
Hours float64 `json:"hours"` var meets_requirements bool
Minutes int64 `json:"minutes"` err := row.Scan(&meets_requirements)
} return meets_requirements, err
func (q *Queries) GetSicknessSummaryForEmployeeInPeriod(ctx context.Context, arg GetSicknessSummaryForEmployeeInPeriodParams) (GetSicknessSummaryForEmployeeInPeriodRow, error) {
row := q.db.QueryRow(ctx, getSicknessSummaryForEmployeeInPeriod, arg.EmployeeID, arg.StartDate, arg.EndDate)
var i GetSicknessSummaryForEmployeeInPeriodRow
err := row.Scan(&i.Days, &i.Hours, &i.Minutes)
return i, err
}
const getWorkSummaryForEmployeeInPeriod = `-- name: GetWorkSummaryForEmployeeInPeriod :one
WITH wt AS (
SELECT
COUNT(*) as days,
COALESCE(sum(duration), 0)::bigint as minutes
FROM "WorkTime"
WHERE "userId" = $1 AND "startDate" > $2 AND "startDate" < $3
)
SELECT
wt.days,
(minutes / 60.0)::float as hours,
wt.minutes
FROM wt
`
type GetWorkSummaryForEmployeeInPeriodParams struct {
EmployeeID string `json:"employee_id"`
StartDate *time.Time `json:"start_date"`
EndDate *time.Time `json:"end_date"`
}
type GetWorkSummaryForEmployeeInPeriodRow struct {
Days int64 `json:"days"`
Hours float64 `json:"hours"`
Minutes int64 `json:"minutes"`
}
func (q *Queries) GetWorkSummaryForEmployeeInPeriod(ctx context.Context, arg GetWorkSummaryForEmployeeInPeriodParams) (GetWorkSummaryForEmployeeInPeriodRow, error) {
row := q.db.QueryRow(ctx, getWorkSummaryForEmployeeInPeriod, arg.EmployeeID, arg.StartDate, arg.EndDate)
var i GetWorkSummaryForEmployeeInPeriodRow
err := row.Scan(&i.Days, &i.Hours, &i.Minutes)
return i, err
} }
const listActiveEmployees = `-- name: ListActiveEmployees :many const listActiveEmployees = `-- name: ListActiveEmployees :many
@ -341,6 +324,8 @@ SELECT
FROM "DyflexisEmployee" as de FROM "DyflexisEmployee" as de
INNER JOIN "Employee" as e ON de.id::text = e."personnelNumber" INNER JOIN "Employee" as e ON de.id::text = e."personnelNumber"
WHERE de."contractStart" < CURRENT_DATE AND de."contractEnd" > (CURRENT_DATE - INTERVAL '2 months') WHERE de."contractStart" < CURRENT_DATE AND de."contractEnd" > (CURRENT_DATE - INTERVAL '2 months')
AND de.surname <> 'Shahrasebhi'
ORDER BY de.firstname ASC
` `
type ListActiveEmployeesRow struct { type ListActiveEmployeesRow struct {
@ -386,105 +371,3 @@ func (q *Queries) ListActiveEmployees(ctx context.Context) ([]ListActiveEmployee
} }
return items, nil return items, nil
} }
const listPlannedTimesForEmployeeInPeriod = `-- name: ListPlannedTimesForEmployeeInPeriod :many
SELECT
DISTINCT ON (day)
"startDate"::DATE as day,
"startDate",
"endDate",
department,
pause,
duration
FROM "PlannedTime"
WHERE
"userId" = $1 AND
"startDate" > $2 AND
"startDate" < $3 AND
department <> 'Fortbildung' AND
note IS NULL
ORDER BY "day"
`
type ListPlannedTimesForEmployeeInPeriodParams struct {
EmployeeID string `json:"employee_id"`
StartDate *time.Time `json:"start_date"`
EndDate *time.Time `json:"end_date"`
}
type ListPlannedTimesForEmployeeInPeriodRow struct {
Day pgtype.Date `json:"day"`
StartDate *time.Time `json:"startDate"`
EndDate *time.Time `json:"endDate"`
Department pgtype.Text `json:"department"`
Pause int32 `json:"pause"`
Duration int32 `json:"duration"`
}
func (q *Queries) ListPlannedTimesForEmployeeInPeriod(ctx context.Context, arg ListPlannedTimesForEmployeeInPeriodParams) ([]ListPlannedTimesForEmployeeInPeriodRow, error) {
rows, err := q.db.Query(ctx, listPlannedTimesForEmployeeInPeriod, arg.EmployeeID, arg.StartDate, arg.EndDate)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListPlannedTimesForEmployeeInPeriodRow
for rows.Next() {
var i ListPlannedTimesForEmployeeInPeriodRow
if err := rows.Scan(
&i.Day,
&i.StartDate,
&i.EndDate,
&i.Department,
&i.Pause,
&i.Duration,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listWorkTimesForEmployeeInPeriod = `-- name: ListWorkTimesForEmployeeInPeriod :many
SELECT id, "userId", firstname, surname, "startDate", "endDate", department, pause, duration FROM "WorkTime"
WHERE "userId" = $1 AND "startDate" > $2 AND "startDate" < $3
`
type ListWorkTimesForEmployeeInPeriodParams struct {
EmployeeID string `json:"employee_id"`
StartDate *time.Time `json:"start_date"`
EndDate *time.Time `json:"end_date"`
}
func (q *Queries) ListWorkTimesForEmployeeInPeriod(ctx context.Context, arg ListWorkTimesForEmployeeInPeriodParams) ([]WorkTime, error) {
rows, err := q.db.Query(ctx, listWorkTimesForEmployeeInPeriod, arg.EmployeeID, arg.StartDate, arg.EndDate)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkTime
for rows.Next() {
var i WorkTime
if err := rows.Scan(
&i.ID,
&i.UserId,
&i.Firstname,
&i.Surname,
&i.StartDate,
&i.EndDate,
&i.Department,
&i.Pause,
&i.Duration,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View File

@ -0,0 +1,70 @@
package domain
import (
"time"
)
type PayrollInput struct {
Firstname string
Surname string
// Vertragliche Basis
HourlyRate float64
ContractHours int64
DepartmentIsKtw bool
// Erfasste Zeiten
WorkedHours float64
WorkedDays int64
SickDays int64
HolidayDays int64
TotalHours float64
OvertimeHours float64
// Zulagen und Boni
ZulageTourengeld float64
ZulagePassengerTransport int64
ZulagePuenktlichkeit int64
Verpflegungspauschale int64
// Zuschlaege
ZuschlagNacht25 float64
ZuschlagNacht40 float64
ZuschlagSamstag float64
ZuschlagSonntag float64
ZuschlagVerpflegung float64
ZuschlagFeiertag125 float64
ZuschlagFeiertag150 float64
}
type WeeklyTimeData struct {
WeekStart time.Time
WorkedHours float64
WorkedDays float64
SickHours float64
HolidayDays float64
}
type PayrollStatement struct {
Nachname string `json:"nachname" csv:"Nachname"`
Vorname string `json:"vorname" csv:"Vorname"`
Verpflegungspauschale float64 `json:"verpflegungspauschale" csv:"Verpflegungspauschale"`
VertragStundenlohn float64 `json:"stundenlohn" csv:"Vertr. Stundenlohn"`
Vertragsstunden float64 `json:"vertragsstunden" csv:"Arbeitsstunden lt. Vertrag"`
GearbeiteteStunden float64 `json:"gearbeiteteStunden" csv:"Gearbeitet Stunden"`
GearbeiteteTage float64 `json:"gearbeiteteTage" csv:"Gearbeitet Tage"`
PuenktlichkeitspraemieAnzahl float64 `json:"puenktlichkeitspraemieAnzahl" csv:"Pünktlichkeitsprämie anzahl"`
Krankheitstage float64 `json:"krankheitstage" csv:"Krankheitstage x 8 Stunden"`
Ueberstunden float64 `json:"ueberstunden" csv:"davon Überstunden"`
Tourengeld float64 `json:"tourengeld" csv:"Tourengeld Anzahl"`
TourengeldBonus float64 `json:"tourengeldBonus" csv:"Tourengeld-Bonus (Platz 1-3, EUR)"`
GesundheitsbonusAbzug string `json:"gesundheitsbonusAbzug" csv:"Gesundheitsbonus abzuziehen? (ja/nein)"`
Urlaubsgeld float64 `json:"urlaubsgeld" csv:"Urlaubsgeld (44 EUR pro tag)"`
PScheinZulage float64 `json:"pScheinZulage" csv:"P-schein Zulage"`
Wochenendzuschlag15 float64 `json:"wochenendzuschlag15" csv:"Wochenendzuschlag 15%"`
Sonntagszuschlag50 float64 `json:"sonntagszuschlag50" csv:"Sonntagszuschlag 50%"`
Feiertagszuschlag125 float64 `json:"feiertagszuschlag125" csv:"Feiertagszuschlag 125%"`
Feiertagszuschlag150 float64 `json:"feiertagszuschlag150" csv:"Feiertagszuschlag 150%"`
Nachtzuschlag25 float64 `json:"nachtzuschlag25" csv:"Nachtzuschlag 25%"`
Nachtzuschlag40 float64 `json:"nachtzuschlag40" csv:"Nachtzuschlag 40%"`
}

View File

@ -1,122 +0,0 @@
package excelread
import (
"business-engine/internal/database"
"fmt"
"os"
"slices"
"strings"
"time"
"github.com/gocarina/gocsv"
)
func sortSliceByDate[T any](slice []T, getDate func(T) time.Time) {
compare := func(a, b T) int {
aDate := getDate(a)
bDate := getDate(b)
if aDate.Before(bDate) {
return -1
}
if aDate.After(bDate) {
return 1
}
return 0
}
slices.SortFunc(slice, compare)
}
// Zu kalkulieren
// - Puenktlichkeitspraemie
// - Tourengeld Anzahl
// - Tourengeld Bonus
func GeneratePayrollStatement(data EmployeeDataRow, input PayrollInputData) PayrollStatement {
isKTW := slices.IndexFunc(input.Plannedtimes, func(pt database.ListPlannedTimesForEmployeeInPeriodRow) bool {
return strings.Contains(pt.Department.String, "KTW")
}) >= 0 && slices.IndexFunc(input.Plannedtimes, func(pt database.ListPlannedTimesForEmployeeInPeriodRow) bool {
return strings.Contains(pt.Department.String, "Leitstelle")
}) == -1
statement := PayrollStatement{
Nachname: data.Nachname,
Vorname: data.Vorname,
Verpflegungspauschale: data.AuszahlenVerpflegungspauschaleTage,
VertragStundenlohn: input.ContractSalaryHour,
Vertragsstunden: data.IstSollVertragsStunden,
GearbeiteteStunden: data.GearbeiteteStunden,
GearbeiteteTage: data.GearbeiteteTage,
Krankheitstage: data.Krankheitstage,
Ueberstunden: data.IstSollPlusMinusStunden,
Wochenendzuschlag15: data.AuszahlenZuschlag15Stunden,
Sonntagszuschlag50: data.AuszahlenZuschlag50Stunden,
Nachtzuschlag25: data.AuszahlenZuschlag25Stunden,
Nachtzuschlag40: data.AuszahlenZuschlag40Stunden,
}
if data.Krankheitstage > 0 || !isKTW {
statement.GesundheitsbonusAbzug = "ja"
}
if isKTW && data.GenommeneUrlaubstage > 0 {
statement.Urlaubsgeld = data.GenommeneUrlaubstage
}
if isKTW && data.AuszahlenAnzahlPSchein > 0 {
statement.PScheinZulage = data.AuszahlenAnzahlPSchein
}
if isKTW && input.Tourengeld > 0 {
statement.Tourengeld = float64(input.Tourengeld)
}
if isKTW && len(input.Worktimes) > 0 && len(input.Plannedtimes) > 0 {
ptGetStartDate := func(pt database.ListPlannedTimesForEmployeeInPeriodRow) time.Time {
return *pt.StartDate
}
wtGetStartDate := func(pt database.WorkTime) time.Time {
return *pt.StartDate
}
sortSliceByDate(input.Plannedtimes, ptGetStartDate)
sortSliceByDate(input.Worktimes, wtGetStartDate)
var employeeOnTime []EmployeeOnTime
for _, pt := range input.Plannedtimes {
for _, wt := range input.Worktimes {
if isSameDay(ptGetStartDate(pt), wtGetStartDate(wt)) && (wtGetStartDate(wt).Before(ptGetStartDate(pt)) || wtGetStartDate(wt) == ptGetStartDate(pt)) {
statement.PuenktlichkeitspraemieAnzahl++
employeeOnTime = append(employeeOnTime, EmployeeOnTime{
EmployeeId: fmt.Sprintf("%s %s", data.Vorname, data.Nachname),
WorkTime: *wt.StartDate,
PlannedTime: *pt.StartDate,
IsOnTime: true,
})
}
}
}
if data.Vorname == "David" && data.Nachname == "Hastedt" {
GenerateEmployeesOnTime(employeeOnTime)
}
}
return statement
}
func GenerateEmployeesOnTime(employeesontime []EmployeeOnTime) error {
file, err := os.Create("employees_on_time.csv")
if err != nil {
return err
}
defer file.Close()
if err := gocsv.MarshalFile(&employeesontime, file); err != nil {
return err
}
return nil
}
// isSameDay prüft, ob zwei Zeitpunkte am selben Kalendertag liegen.
func isSameDay(t1, t2 time.Time) bool {
return t1.Year() == t2.Year() && t1.Month() == t2.Month() && t1.Day() == t2.Day()
}

View File

@ -1,210 +0,0 @@
package excelread
import (
"encoding/csv"
"fmt"
"log"
"os"
"strconv"
"strings"
)
// mapRowToStruct konvertiert eine einzelne CSV-Zeile (als []string)
// in die EmployeeDataRow-Struct.
func mapRowToStruct(row []string) EmployeeDataRow {
data := EmployeeDataRow{}
data.Personalnummer = parseInt(row[0])
data.Nachname = strings.TrimSpace(row[1])
data.Vorname = strings.TrimSpace(row[2])
data.Vertragsart = strings.TrimSpace(row[3])
data.HatPersonenbefoerderungsschein = strings.TrimSpace(row[4])
data.HatRechtAufZuschlaege = strings.TrimSpace(row[5])
data.Vertragsstart = strings.TrimSpace(row[6])
data.VertragstageProWoche = parseFloat(row[7])
data.VertragsstundenProWoche = parseFloat(row[8])
data.VertragsstundenMax = strings.TrimSpace(row[9])
data.UrlaubstageProJahr = parseFloat(row[10])
data.GearbeiteteTage = parseFloat(row[11])
data.GearbeiteteStunden = parseFloat(row[12])
data.GenommeneUrlaubstage = parseFloat(row[13])
data.GenommeneUrlaubsstunden = parseFloat(row[14])
data.Krankheitstage = parseFloat(row[15])
data.Krankheitsstunden = parseFloat(row[16])
// data.KrankengeldTage = parseFloat(row[17])
// data.KrankengeldStunden = parseFloat(row[18])
// data.UnbezahlterUrlaubTage = parseFloat(row[19])
// data.UnbezahlterUrlaubStunden = parseFloat(row[20])
data.IstSollGesamtTage = parseFloat(row[17])
data.IstSollGesamtStunden = parseFloat(row[18])
data.IstSollVertragsTage = parseFloat(row[19])
data.IstSollVertragsStunden = parseFloat(row[20])
data.IstSollPlusMinusStunden = parseFloat(row[21])
data.BerechnungMehrstunden = parseFloat(row[22])
data.BerechnungUeberstunden = parseFloat(row[23])
data.BerechnungAnzahlPSchein = parseFloat(row[24])
data.BerechnungKrankOhneAUTage = parseFloat(row[25])
data.BerechnungKrankOhneAUStunden = parseFloat(row[26])
data.BerechnungNachtzuschlag25Tage = parseFloat(row[27])
data.BerechnungNachtzuschlag25Stunden = parseFloat(row[28])
data.BerechnungNachtzuschlag40Tage = parseFloat(row[29])
data.BerechnungNachtzuschlag40Stunden = parseFloat(row[30])
data.BerechnungSonntagszuschlag50Tage = parseFloat(row[31])
data.BerechnungSonntagszuschlag50Stunden = parseFloat(row[32])
data.BerechnungVerpflegungspauschale14Tage = parseFloat(row[33])
data.BerechnungSamstagszuschlag15Tage = parseFloat(row[34])
data.BerechnungSamstagszuschlag15Stunden = parseFloat(row[35])
data.BerechnungZeitausgleichStart = parseFloat(row[36])
data.BerechnungZeitausgleichPlusMinus = parseFloat(row[37])
data.BerechnungZeitausgleichAenderungen = parseFloat(row[38])
data.BerechnungZeitausgleichAuszahlen = parseFloat(row[39])
data.BerechnungZeitausgleichJetzt = parseFloat(row[40])
data.SaldenUrlaubStart = parseFloat(row[41])
data.SaldenUrlaubAbbau = parseFloat(row[42])
data.SaldenUrlaubKorrektur = parseFloat(row[43])
data.SaldenUrlaubJetzt = parseFloat(row[44])
data.SaldenMehrstunden = parseFloat(row[45])
data.SaldenUeberstunden = parseFloat(row[46])
data.AuszahlenFahrtkostenKm = parseFloat(row[47])
data.AuszahlenVerpflegungspauschaleTage = parseFloat(row[48])
data.AuszahlenAnzahlPSchein = parseFloat(row[49])
data.AuszahlenZuschlag25Stunden = parseFloat(row[50])
data.AuszahlenZuschlag40Stunden = parseFloat(row[51])
data.AuszahlenZuschlag50Stunden = parseFloat(row[52])
data.AuszahlenZuschlag14Stunden = parseFloat(row[53])
data.AuszahlenZuschlag15Stunden = parseFloat(row[54])
return data
}
// map from: July 2025
// func mapRowToStruct(row []string) EmployeeDataRow {
// data := EmployeeDataRow{}
// data.Personalnummer = parseInt(row[0])
// data.Nachname = strings.TrimSpace(row[1])
// data.Vorname = strings.TrimSpace(row[2])
// data.Vertragsart = strings.TrimSpace(row[3])
// data.HatPersonenbefoerderungsschein = strings.TrimSpace(row[4])
// data.HatRechtAufZuschlaege = strings.TrimSpace(row[5])
// data.Vertragsstart = strings.TrimSpace(row[6])
// data.VertragstageProWoche = parseFloat(row[7])
// data.VertragsstundenProWoche = parseFloat(row[8])
// data.VertragsstundenMax = strings.TrimSpace(row[9])
// data.UrlaubstageProJahr = parseFloat(row[10])
// data.GearbeiteteTage = parseFloat(row[11])
// data.GearbeiteteStunden = parseFloat(row[12])
// data.GenommeneUrlaubstage = parseFloat(row[13])
// data.GenommeneUrlaubsstunden = parseFloat(row[14])
// data.Krankheitstage = parseFloat(row[15])
// data.Krankheitsstunden = parseFloat(row[16])
// data.KrankengeldTage = parseFloat(row[17])
// data.KrankengeldStunden = parseFloat(row[18])
// data.UnbezahlterUrlaubTage = parseFloat(row[19])
// data.UnbezahlterUrlaubStunden = parseFloat(row[20])
// data.IstSollGesamtTage = parseFloat(row[21])
// data.IstSollGesamtStunden = parseFloat(row[22])
// data.IstSollVertragsTage = parseFloat(row[23])
// data.IstSollVertragsStunden = parseFloat(row[24])
// data.IstSollPlusMinusStunden = parseFloat(row[25])
// data.BerechnungMehrstunden = parseFloat(row[26])
// data.BerechnungUeberstunden = parseFloat(row[27])
// data.BerechnungAnzahlPSchein = parseFloat(row[28])
// data.BerechnungKrankOhneAUTage = parseFloat(row[29])
// data.BerechnungKrankOhneAUStunden = parseFloat(row[30])
// data.BerechnungNachtzuschlag25Tage = parseFloat(row[31])
// data.BerechnungNachtzuschlag25Stunden = parseFloat(row[32])
// data.BerechnungNachtzuschlag40Tage = parseFloat(row[33])
// data.BerechnungNachtzuschlag40Stunden = parseFloat(row[34])
// data.BerechnungSonntagszuschlag50Tage = parseFloat(row[35])
// data.BerechnungSonntagszuschlag50Stunden = parseFloat(row[36])
// data.BerechnungVerpflegungspauschale14Tage = parseFloat(row[37])
// data.BerechnungSamstagszuschlag15Tage = parseFloat(row[38])
// data.BerechnungSamstagszuschlag15Stunden = parseFloat(row[39])
// data.BerechnungZeitausgleichStart = parseFloat(row[40])
// data.BerechnungZeitausgleichPlusMinus = parseFloat(row[41])
// data.BerechnungZeitausgleichAenderungen = parseFloat(row[42])
// data.BerechnungZeitausgleichAuszahlen = parseFloat(row[43])
// data.BerechnungZeitausgleichJetzt = parseFloat(row[44])
// data.SaldenUrlaubStart = parseFloat(row[45])
// data.SaldenUrlaubAbbau = parseFloat(row[46])
// data.SaldenUrlaubJetzt = parseFloat(row[47])
// data.SaldenMehrstunden = parseFloat(row[48])
// data.SaldenUeberstunden = parseFloat(row[49])
// data.AuszahlenFahrtkostenKm = parseFloat(row[50])
// data.AuszahlenVerpflegungspauschaleTage = parseFloat(row[51])
// data.AuszahlenAnzahlPSchein = parseFloat(row[52])
// data.AuszahlenZuschlag25Stunden = parseFloat(row[53])
// data.AuszahlenZuschlag40Stunden = parseFloat(row[54])
// data.AuszahlenZuschlag50Stunden = parseFloat(row[55])
// data.AuszahlenZuschlag14Stunden = parseFloat(row[56])
// data.AuszahlenZuschlag15Stunden = parseFloat(row[57])
// return data
// }
func ReadEmployeeData() []EmployeeDataRow {
content, err := os.ReadFile("daten.csv")
if err != nil {
log.Fatalf("Fehler beim Lesen der Datei: %v", err)
}
reader := csv.NewReader(strings.NewReader(string(content)))
reader.Comma = ';'
reader.FieldsPerRecord = -1
records, err := reader.ReadAll()
if err != nil {
log.Fatalf("Fehler beim Parsen der CSV: %v", err)
}
if len(records) < 5 {
log.Fatal("Die CSV-Datei hat nicht genügend Zeilen.")
}
dataRows := records[3 : len(records)-1]
var employees []EmployeeDataRow
for i, row := range dataRows {
fmt.Printf("%v", len(row))
if row[1] != "Shahrasebi" {
employee := mapRowToStruct(row)
employees = append(employees, employee)
fmt.Printf("Zeile %d geladen: %s, %s\n", i+4, employee.Nachname, employee.Vorname)
}
}
fmt.Printf("\n--- Insgesamt %d Mitarbeiterdatensätze erfolgreich geladen. ---\n", len(employees))
return employees
}
// Helfer-Funktion, um Strings mit Komma als Dezimaltrennzeichen in float64 umzuwandeln.
func parseFloat(s string) float64 {
s = strings.TrimSpace(s)
if s == "" {
return 0.0
}
// Ersetze das Komma durch einen Punkt für die Standard-Konvertierung
s = strings.Replace(s, ",", ".", 1)
val, err := strconv.ParseFloat(s, 64)
if err != nil {
return 0.0 // Bei Fehler einfach 0 zurückgeben
}
return val
}
// Helfer-Funktion, um einen String in einen Integer umzuwandeln.
func parseInt(s string) int {
s = strings.TrimSpace(s)
if s == "" {
return 0
}
val, err := strconv.Atoi(s)
if err != nil {
return 0
}
return val
}

View File

@ -1,131 +0,0 @@
package excelread
import (
"business-engine/internal/database"
"fmt"
"time"
)
// EmployeeDataRow repräsentiert eine einzelne, vollständige Datenzeile aus der CSV.
type EmployeeDataRow struct {
Personalnummer int `json:"personalnummer"`
Nachname string `json:"nachname"`
Vorname string `json:"vorname"`
Vertragsart string `json:"vertragsart"`
HatPersonenbefoerderungsschein string `json:"hatPersonenbefoerderungsschein"`
HatRechtAufZuschlaege string `json:"hatRechtAufZuschlaege"`
Vertragsstart string `json:"vertragsstart"`
VertragstageProWoche float64 `json:"vertragstageProWoche"`
VertragsstundenProWoche float64 `json:"vertragsstundenProWoche"`
VertragsstundenMax string `json:"vertragsstundenMax"`
UrlaubstageProJahr float64 `json:"urlaubstageProJahr"`
GearbeiteteTage float64 `json:"gearbeiteteTage"`
GearbeiteteStunden float64 `json:"gearbeiteteStunden"`
GenommeneUrlaubstage float64 `json:"genommeneUrlaubstage"`
GenommeneUrlaubsstunden float64 `json:"genommeneUrlaubsstunden"`
Krankheitstage float64 `json:"krankheitstage"`
Krankheitsstunden float64 `json:"krankheitsstunden"`
KrankengeldTage float64 `json:"krankengeldTage"`
KrankengeldStunden float64 `json:"krankengeldStunden"`
UnbezahlterUrlaubTage float64 `json:"unbezahlterUrlaubTage"`
UnbezahlterUrlaubStunden float64 `json:"unbezahlterUrlaubStunden"`
EinbehaltungGesamtTage float64 `json:"einbehaltungGesamtTage"`
EinbehaltungGesamtStunden float64 `json:"einbehaltungGesamtStunden"`
IstSollGesamtTage float64 `json:"istSollGesamtTage"`
IstSollGesamtStunden float64 `json:"istSollGesamtStunden"`
IstSollVertragsTage float64 `json:"istSollVertragsTage"`
IstSollVertragsStunden float64 `json:"istSollVertragsStunden"`
IstSollPlusMinusStunden float64 `json:"istSollPlusMinusStunden"`
BerechnungMehrstunden float64 `json:"berechnungMehrstunden"`
BerechnungUeberstunden float64 `json:"berechnungUeberstunden"`
BerechnungAnzahlPSchein float64 `json:"berechnungAnzahlPSchein"`
BerechnungKrankOhneAUTage float64 `json:"berechnungKrankOhneAUTage"`
BerechnungKrankOhneAUStunden float64 `json:"berechnungKrankOhneAUStunden"`
BerechnungNachtzuschlag25Tage float64 `json:"berechnungNachtzuschlag25Tage"`
BerechnungNachtzuschlag25Stunden float64 `json:"berechnungNachtzuschlag25Stunden"`
BerechnungNachtzuschlag40Tage float64 `json:"berechnungNachtzuschlag40Tage"`
BerechnungNachtzuschlag40Stunden float64 `json:"berechnungNachtzuschlag40Stunden"`
BerechnungSonntagszuschlag50Tage float64 `json:"berechnungSonntagszuschlag50Tage"`
BerechnungSonntagszuschlag50Stunden float64 `json:"berechnungSonntagszuschlag50Stunden"`
BerechnungVerpflegungspauschale14Tage float64 `json:"berechnungVerpflegungspauschale14Tage"`
BerechnungSamstagszuschlag15Tage float64 `json:"berechnungSamstagszuschlag15Tage"`
BerechnungSamstagszuschlag15Stunden float64 `json:"berechnungSamstagszuschlag15Stunden"`
BerechnungZeitausgleichStart float64 `json:"berechnungZeitausgleichStart"`
BerechnungZeitausgleichPlusMinus float64 `json:"berechnungZeitausgleichPlusMinus"`
BerechnungZeitausgleichAenderungen float64 `json:"berechnungZeitausgleichAenderungen"`
BerechnungZeitausgleichAuszahlen float64 `json:"berechnungZeitausgleichAuszahlen"`
BerechnungZeitausgleichJetzt float64 `json:"berechnungZeitausgleichJetzt"`
SaldenUrlaubStart float64 `json:"saldenUrlaubStart"`
SaldenUrlaubAbbau float64 `json:"saldenUrlaubAbbau"`
SaldenUrlaubKorrektur float64 `json:"saldenUrlaubKorrektur"`
SaldenUrlaubJetzt float64 `json:"saldenUrlaubJetzt"`
SaldenMehrstunden float64 `json:"saldenMehrstunden"`
SaldenUeberstunden float64 `json:"saldenUeberstunden"`
AuszahlenFahrtkostenKm float64 `json:"auszahlenFahrtkostenKm"`
AuszahlenVerpflegungspauschaleTage float64 `json:"auszahlenVerpflegungspauschaleTage"`
AuszahlenAnzahlPSchein float64 `json:"auszahlenAnzahlPSchein"`
AuszahlenZuschlag25Stunden float64 `json:"auszahlenZuschlag25Stunden"`
AuszahlenZuschlag40Stunden float64 `json:"auszahlenZuschlag40Stunden"`
AuszahlenZuschlag50Stunden float64 `json:"auszahlenZuschlag50Stunden"`
AuszahlenZuschlag14Stunden float64 `json:"auszahlenZuschlag14Stunden"`
AuszahlenZuschlag15Stunden float64 `json:"auszahlenZuschlag15Stunden"`
}
type PayrollInputData struct {
ContractSalaryHour float64
Tourengeld float64
Worktimes []database.WorkTime
Plannedtimes []database.ListPlannedTimesForEmployeeInPeriodRow
}
func NewPayrollInputData(salaryHour float64, tourengeld float64, worktimes []database.WorkTime, plannedtimes []database.ListPlannedTimesForEmployeeInPeriodRow) PayrollInputData {
return PayrollInputData{
ContractSalaryHour: salaryHour,
Tourengeld: tourengeld,
Worktimes: worktimes,
Plannedtimes: plannedtimes,
}
}
type PayrollStatement struct {
Nachname string `json:"nachname" csv:"Nachname"`
Vorname string `json:"vorname" csv:"Vorname"`
Verpflegungspauschale float64 `json:"verpflegungspauschale" csv:"Verpflegungspauschale"`
VertragStundenlohn float64 `json:"stundenlohn" csv:"Vertr. Stundenlohn"`
Vertragsstunden float64 `json:"vertragsstunden" csv:"Arbeitsstunden lt. Vertrag"`
GearbeiteteStunden float64 `json:"gearbeiteteStunden" csv:"Gearbeitet Stunden"`
GearbeiteteTage float64 `json:"gearbeiteteTage" csv:"Gearbeitet Tage"`
PuenktlichkeitspraemieAnzahl float64 `json:"puenktlichkeitspraemieAnzahl" csv:"Pünktlichkeitsprämie anzahl"`
Krankheitstage float64 `json:"krankheitstage" csv:"Krankheitstage x 8 Stunden"`
Ueberstunden float64 `json:"ueberstunden" csv:"davon Überstunden"`
Tourengeld float64 `json:"tourengeld" csv:"Tourengeld Anzahl"`
TourengeldBonus float64 `json:"tourengeldBonus" csv:"Tourengeld-Bonus (Platz 1-3, EUR)"`
GesundheitsbonusAbzug string `json:"gesundheitsbonusAbzug" csv:"Gesundheitsbonus abzuziehen? (ja/nein)"`
Urlaubsgeld float64 `json:"urlaubsgeld" csv:"Urlaubsgeld (44 EUR pro tag)"`
PScheinZulage float64 `json:"pScheinZulage" csv:"P-schein Zulage"`
Wochenendzuschlag15 float64 `json:"wochenendzuschlag15" csv:"Wochenendzuschlag 15%"`
Sonntagszuschlag50 float64 `json:"sonntagszuschlag50" csv:"Sonntagszuschlag 50%"`
Feiertagszuschlag125 float64 `json:"feiertagszuschlag125" csv:"Feiertagszuschlag 125%"`
Nachtzuschlag25 float64 `json:"nachtzuschlag25" csv:"Nachtzuschlag 25%"`
Nachtzuschlag40 float64 `json:"nachtzuschlag40" csv:"Nachtzuschlag 40%"`
}
type EmployeeOnTime struct {
EmployeeId string `csv:"EmployeeId"`
WorkTime time.Time `csv:"Arbeitszeit"`
PlannedTime time.Time `csv:"Schichtplanung"`
IsOnTime bool `csv:"Pünktlich?"`
}
func NewEmployeeOnTime(employeeId string, worktime, plannedtime time.Time) EmployeeOnTime {
loc, err := time.LoadLocation("Europe/Berlin")
if err != nil {
fmt.Println("Fehler beim Laden der Zeitzone:", err)
}
return EmployeeOnTime{
EmployeeId: employeeId,
WorkTime: worktime.In(loc),
PlannedTime: plannedtime.In(loc),
IsOnTime: worktime.Before(plannedtime) || worktime == plannedtime,
}
}

View File

@ -1,7 +1,7 @@
package gencsv package gencsv
import ( import (
excelread "business-engine/internal/excel_read" "business-engine/internal/domain"
"encoding/csv" "encoding/csv"
"io" "io"
"os" "os"
@ -9,7 +9,7 @@ import (
"github.com/gocarina/gocsv" "github.com/gocarina/gocsv"
) )
func GeneratePayrollStatementCSV(stmts []excelread.PayrollStatement) error { func GeneratePayrollStatementCSV(stmts []domain.PayrollStatement) error {
file, err := os.Create("payroll_statement.csv") file, err := os.Create("payroll_statement.csv")
if err != nil { if err != nil {
return err return err
@ -27,16 +27,3 @@ func GeneratePayrollStatementCSV(stmts []excelread.PayrollStatement) error {
} }
return nil return nil
} }
func GenerateEmployeesOnTime(employeesontime []excelread.EmployeeOnTime) error {
file, err := os.Create("employees_on_time.csv")
if err != nil {
return err
}
defer file.Close()
if err := gocsv.MarshalFile(&employeesontime, file); err != nil {
return err
}
return nil
}

View File

@ -1,7 +1,7 @@
package genpdf package genpdf
import ( import (
excelread "business-engine/internal/excel_read" domain "business-engine/internal/domain"
"fmt" "fmt"
"slices" "slices"
"time" "time"
@ -35,7 +35,7 @@ func (item LineItem) Total() float64 {
return item.Anzahl * item.Betrag return item.Anzahl * item.Betrag
} }
func GenerateSalaryStatement(stmt excelread.PayrollStatement, filename string, month time.Time) error { func GenerateSalaryStatement(stmt domain.PayrollStatement, filename string, month time.Time) error {
pdf := fpdf.New("P", "mm", "A4", "") pdf := fpdf.New("P", "mm", "A4", "")
pdf.AddUTF8Font("DejaVu", "", "DejaVuSans.ttf") pdf.AddUTF8Font("DejaVu", "", "DejaVuSans.ttf")
pdf.AddUTF8Font("DejaVu", "B", "DejaVuSans-Bold.ttf") pdf.AddUTF8Font("DejaVu", "B", "DejaVuSans-Bold.ttf")
@ -86,7 +86,7 @@ func GenerateSalaryStatement(stmt excelread.PayrollStatement, filename string, m
return nil return nil
} }
func genLineItemsFromStatement(stmt excelread.PayrollStatement) []LineItem { func genLineItemsFromStatement(stmt domain.PayrollStatement) []LineItem {
var items []LineItem var items []LineItem
// --- Gehalt & Stunden --- // --- Gehalt & Stunden ---

View File

@ -1,14 +0,0 @@
package payroll
// SalaryComponent is a single position
type SalaryComponent struct {
Name string
Type string
Amount int64
Value int64
Details string
}
type Rule interface {
Execute(data PayrollInput) []SalaryComponent
}

View File

@ -1,7 +1,7 @@
package payroll package payroll
import ( import (
excelread "business-engine/internal/excel_read" "business-engine/internal/domain"
"business-engine/internal/gencsv" "business-engine/internal/gencsv"
"business-engine/internal/genpdf" "business-engine/internal/genpdf"
"encoding/json" "encoding/json"
@ -24,38 +24,40 @@ func NewEmployeeHandler(service Service, logger *slog.Logger) *EmployeeHandler {
} }
} }
func (h *EmployeeHandler) ListEmployees(w http.ResponseWriter, r *http.Request) { func (h *EmployeeHandler) RunPayroll(w http.ResponseWriter, r *http.Request) {
month := time.Date(2025, time.September, 1, 0, 0, 0, 0, time.UTC) month := time.Date(2025, time.November, 1, 0, 0, 0, 0, time.UTC)
employees, err := h.service.GenerateMonthlyRun(r.Context(), month)
employees, err := h.service.FetchActiveEmployees(r.Context())
if err != nil { if err != nil {
h.logger.Error("failed to get employees", "error", err) h.logger.Error("failed to fetch active employees", "error", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError) http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return return
} }
h.logger.Info(fmt.Sprintf("found %d active employees", len(employees)))
w.Header().Set("Content-Type", "application/json") allZuschlaege, err := h.service.zuschlagParser.ParseFile("daten.csv")
if err := json.NewEncoder(w).Encode(employees); err != nil { if err != nil {
h.logger.Error("failed to encode response", "error", err) h.logger.Error("failed to load data from csv export", "error", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError) http.Error(w, "Internal Server Error", http.StatusInternalServerError)
} }
}
func (h *EmployeeHandler) RunPayroll(w http.ResponseWriter, r *http.Request) { var data []domain.PayrollInput
month := time.Date(2025, time.September, 1, 0, 0, 0, 0, time.UTC) var payrollStatements []domain.PayrollStatement
employeeData := excelread.ReadEmployeeData() for _, employee := range employees {
h.logger.Info("loading payroll input for employee", "name", fmt.Sprintf("%s %s", employee.Firstname, employee.Surname))
var payrollStatements []excelread.PayrollStatement input, err := h.service.LoadPayrollInput(r.Context(), month.AddDate(0, -1, 0), employee, allZuschlaege)
for _, employeeRow := range employeeData {
input, err := h.service.FetchPayrollInput(r.Context(), month, employeeRow.Vorname, employeeRow.Nachname)
if err != nil { if err != nil {
h.logger.Error("could not fetch payroll input", "error", err) h.logger.Error("failed to fetch payroll input", "error", err)
// http.Error(w, "Internal Server Error", http.StatusInternalServerError) http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
} }
statement := excelread.GeneratePayrollStatement(employeeRow, input)
statement := h.service.GeneratePayrollStatement(input)
payrollStatements = append(payrollStatements, statement) payrollStatements = append(payrollStatements, statement)
data = append(data, input)
} }
slices.SortFunc(payrollStatements, func(a, b excelread.PayrollStatement) int { slices.SortFunc(payrollStatements, func(a, b domain.PayrollStatement) int {
if a.Tourengeld < b.Tourengeld { if a.Tourengeld < b.Tourengeld {
return 1 return 1
} }
@ -70,7 +72,7 @@ func (h *EmployeeHandler) RunPayroll(w http.ResponseWriter, r *http.Request) {
payrollStatements[2].TourengeldBonus = 300 payrollStatements[2].TourengeldBonus = 300
h.logger.Info("generating csv salary statements") h.logger.Info("generating csv salary statements")
err := gencsv.GeneratePayrollStatementCSV(payrollStatements) err = gencsv.GeneratePayrollStatementCSV(payrollStatements)
if err != nil { if err != nil {
h.logger.Error("could not generate payroll statement csv", "error", err) h.logger.Error("could not generate payroll statement csv", "error", err)
} }
@ -83,8 +85,57 @@ func (h *EmployeeHandler) RunPayroll(w http.ResponseWriter, r *http.Request) {
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(payrollStatements); err != nil { if err := json.NewEncoder(w).Encode(data); err != nil {
h.logger.Error("failed to encode response", "error", err) h.logger.Error("failed to encode response", "error", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError) http.Error(w, "Internal Server Error", http.StatusInternalServerError)
} }
} }
// func (h *EmployeeHandler) ReadEmployeeData(w http.ResponseWriter, r *http.Request) {
// month := time.Date(2025, time.September, 1, 0, 0, 0, 0, time.UTC)
// employeeData := excelread.ReadEmployeeData()
// var payrollStatements []excelread.PayrollStatement
// for _, employeeRow := range employeeData {
// input, err := h.service.FetchPayrollInput(r.Context(), month, employeeRow.Vorname, employeeRow.Nachname)
// if err != nil {
// h.logger.Error("could not fetch payroll input", "error", err)
// // http.Error(w, "Internal Server Error", http.StatusInternalServerError)
// }
// statement := excelread.GeneratePayrollStatement(employeeRow, input)
// payrollStatements = append(payrollStatements, statement)
// }
// slices.SortFunc(payrollStatements, func(a, b excelread.PayrollStatement) int {
// if a.Tourengeld < b.Tourengeld {
// return 1
// }
// if a.Tourengeld > b.Tourengeld {
// return -1
// }
// return 0
// })
// payrollStatements[0].TourengeldBonus = 500
// payrollStatements[1].TourengeldBonus = 400
// payrollStatements[2].TourengeldBonus = 300
// h.logger.Info("generating csv salary statements")
// err := gencsv.GeneratePayrollStatementCSV(payrollStatements)
// if err != nil {
// h.logger.Error("could not generate payroll statement csv", "error", err)
// }
// h.logger.Info("generating pdf salary statements")
// for _, stmt := range payrollStatements {
// err := genpdf.GenerateSalaryStatement(stmt, fmt.Sprintf("%s_%s.pdf", month.Format("2006-01"), fmt.Sprintf("%s %s", stmt.Vorname, stmt.Nachname)), month)
// if err != nil {
// h.logger.Error("could not generate salary statement pdf", "error", err)
// }
// }
// w.Header().Set("Content-Type", "application/json")
// if err := json.NewEncoder(w).Encode(payrollStatements); err != nil {
// h.logger.Error("failed to encode response", "error", err)
// http.Error(w, "Internal Server Error", http.StatusInternalServerError)
// }
// }

View File

@ -1,29 +0,0 @@
package payroll
import (
"business-engine/internal/database"
)
type PayrollInput struct {
database.ListActiveEmployeesRow
WorkSummary struct {
Days int64
Hours float64
Minutes int64
}
SicknessSummary struct {
Days int64
Hours float64
Minutes int64
}
WorkTime []database.WorkTime
}
func NewPayrollInput(employee database.ListActiveEmployeesRow) PayrollInput {
return PayrollInput{
ListActiveEmployeesRow: employee,
}
}

View File

@ -0,0 +1,185 @@
package payroll
import (
"encoding/csv"
"fmt"
"io"
"log/slog"
"os"
"strconv"
"strings"
)
type ZuschlagData struct {
Nacht25 float64
Nacht40 float64
Sonntag50 float64
Samstag15 float64
Feiertag125 float64
Feiertag150 float64
Verpflegung14 float64
}
type ZuschlagParser struct {
logger *slog.Logger
knownIdentifiers map[string]string
knownColumns map[string]string
}
func NewZuschlagParser(logger *slog.Logger) *ZuschlagParser {
return &ZuschlagParser{
logger: logger,
knownIdentifiers: map[string]string{
"Nachname": "Lastname",
"Vorname": "Firstname",
},
knownColumns: map[string]string{
"25%": "Nacht25",
"40%": "Nacht40",
"50%": "Sonntag50",
"14%": "Verpflegung14",
"15%": "Samstag15",
"125%": "Feiertag125",
"150%": "Feiertag150",
},
}
}
func (p *ZuschlagParser) ParseFile(filepath string) (map[string]ZuschlagData, error) {
file, err := os.Open(filepath)
if err != nil {
return nil, fmt.Errorf("zuschlag parser: failed to open file: %w", err)
}
defer file.Close()
reader := csv.NewReader(file)
reader.Comma = ','
reader.LazyQuotes = true
if _, err := reader.Read(); err != nil {
return nil, fmt.Errorf("zuschlag parser: an error occured while reading header 1: %w", err)
}
headerRow, err := reader.Read()
if err != nil {
return nil, fmt.Errorf("zuschlag parser: fehler beim lesen von header 2 (zuschlag row): %w", err)
}
identifierHeaderRow, err := reader.Read()
if err != nil {
return nil, fmt.Errorf("zuschlag parser: fehler beim lesen von header 3 (identifier row): %w", err)
}
identifierIndex := p.buildIndex(identifierHeaderRow, p.knownIdentifiers)
zuschlagIndex := p.buildIndex(headerRow, p.knownColumns)
runtimeIndex := make(map[string]int)
for k, v := range identifierIndex {
runtimeIndex[k] = v
}
for k, v := range zuschlagIndex {
runtimeIndex[k] = v
}
results := make(map[string]ZuschlagData)
for {
row, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
p.logger.Warn("zuschlag parser: zeile übersprungen (lesefehler)", "error", err)
continue
}
compoundKey, data, err := p.extractRowData(row, runtimeIndex)
if err != nil {
p.logger.Warn("zuschlag parser: zeile übersprungen (extraktionsfehler)", "error", err)
continue
}
results[compoundKey] = data
}
p.logger.Info("Zuschlag-Datei erfolgreich geparst", "records", len(results))
return results, nil
}
// buildAndValidateIndex erstellt die Index-Map und validiert Pflichtfelder.
// (map[internerName]csvSpaltenIndex)
func (p *ZuschlagParser) buildIndex(headerRow []string, knownMap map[string]string) map[string]int {
zuschlagIndex := make(map[string]int)
for i, colNameFromFile := range headerRow {
trimmedName := strings.TrimSpace(colNameFromFile)
if internalName, ok := knownMap[trimmedName]; ok {
zuschlagIndex[internalName] = i
p.logger.Debug("zuschlag parser: index gemappt", "header_name", trimmedName, "internal_name", internalName, "index", i)
}
}
return zuschlagIndex
}
// extractRowData liest eine einzelne Datenzeile basierend auf dem Index.
func (p *ZuschlagParser) extractRowData(row []string, index map[string]int) (string, ZuschlagData, error) {
var data ZuschlagData
lastname := row[index["Lastname"]]
firstname := row[index["Firstname"]]
if lastname == "" || firstname == "" {
return "", data, fmt.Errorf("leerer name in zeile (lastname: '%s', firstname: '%s')", lastname, firstname)
}
compoundKey := fmt.Sprintf("%s,%s", lastname, firstname)
for internalName, colIdx := range index {
if internalName == "Lastname" || internalName == "Firstname" {
continue
}
if colIdx >= len(row) {
p.logger.Warn("zuschlag parser: zeile kürzer als header-index", "key", compoundKey, "missing_index", colIdx)
continue
}
valueStr := row[colIdx]
valueFloat := p.parseGermanFloat(valueStr)
switch internalName {
case "Nacht25":
data.Nacht25 = valueFloat
case "Nacht40":
data.Nacht40 = valueFloat
case "Sonntag50":
data.Sonntag50 = valueFloat
case "Verpflegung14":
data.Verpflegung14 = valueFloat
case "Samstag15":
data.Samstag15 = valueFloat
case "Feiertag125":
data.Feiertag125 = valueFloat
case "Feiertag150":
data.Feiertag150 = valueFloat
}
}
return compoundKey, data, nil
}
// parseGermanFloat bereinigt und parst deutsche Währungs-/Zahlenformate.
func (p *ZuschlagParser) parseGermanFloat(valueStr string) float64 {
valueStr = strings.ReplaceAll(valueStr, ".", ",")
valueStr = strings.TrimSpace(valueStr)
valueStr = strings.ReplaceAll(valueStr, ",", ".")
if valueStr == "" {
return 0.0
}
f, err := strconv.ParseFloat(valueStr, 64)
if err != nil {
p.logger.Warn("zuschlag parser: float-parse-fehler", "input", valueStr, "error", err)
return 0.0
}
return f
}

View File

@ -1,21 +0,0 @@
package payroll
type BaseSalaryRule struct{}
func NewBaseSalaryRule() *BaseSalaryRule {
return &BaseSalaryRule{}
}
func (r *BaseSalaryRule) Execute(data PayrollInput) []SalaryComponent {
stundenlohnCent := data.ContractSalaryHour * 100
monatsGehaltCent := stundenlohnCent * data.ContractHoursWeek * 4.33
return []SalaryComponent{
{
Name: "Grundgehalt",
Type: "Grundgehalt",
Amount: int64(monatsGehaltCent),
},
}
}

View File

@ -2,7 +2,7 @@ package payroll
import ( import (
"business-engine/internal/database" "business-engine/internal/database"
excelread "business-engine/internal/excel_read" "business-engine/internal/domain"
"context" "context"
"fmt" "fmt"
"log/slog" "log/slog"
@ -13,165 +13,280 @@ import (
type Repository interface { type Repository interface {
ListActiveEmployees(ctx context.Context) ([]database.ListActiveEmployeesRow, error) ListActiveEmployees(ctx context.Context) ([]database.ListActiveEmployeesRow, error)
GetWorkSummaryForEmployeeInPeriod(ctx context.Context, args database.GetWorkSummaryForEmployeeInPeriodParams) (database.GetWorkSummaryForEmployeeInPeriodRow, error)
GetSicknessSummaryForEmployeeInPeriod(ctx context.Context, args database.GetSicknessSummaryForEmployeeInPeriodParams) (database.GetSicknessSummaryForEmployeeInPeriodRow, error)
ListWorkTimesForEmployeeInPeriod(ctx context.Context, args database.ListWorkTimesForEmployeeInPeriodParams) ([]database.WorkTime, error)
ListPlannedTimesForEmployeeInPeriod(ctx context.Context, args database.ListPlannedTimesForEmployeeInPeriodParams) ([]database.ListPlannedTimesForEmployeeInPeriodRow, error)
FetchDrivenToursInInterval(ctx context.Context, args database.FetchDrivenToursInIntervalParams) ([]database.Tour, error)
CountTourengeldAmountInPeriod(ctx context.Context, args database.CountTourengeldAmountInPeriodParams) (float64, error) CountTourengeldAmountInPeriod(ctx context.Context, args database.CountTourengeldAmountInPeriodParams) (float64, error)
GetEmployeeByName(ctx context.Context, args database.GetEmployeeByNameParams) (database.GetEmployeeByNameRow, error) GetWeeklyTimeSummaryForEmployee(ctx context.Context, args database.GetWeeklyTimeSummaryForEmployeeParams) ([]database.GetWeeklyTimeSummaryForEmployeeRow, error)
} EvaluatePunctualityForEmployee(ctx context.Context, args database.EvaluatePunctualityForEmployeeParams) (int64, error)
IsEmployeesDepartmentKTW(ctx context.Context, args database.IsEmployeesDepartmentKTWParams) (bool, error)
type Engine interface { CountVerpflegungspauschale(ctx context.Context, args database.CountVerpflegungspauschaleParams) (int64, error)
Calculate() // payroll input -> payroll statement
} }
type Service struct { type Service struct {
repo Repository repo Repository
engine Engine logger *slog.Logger
logger *slog.Logger zuschlagParser *ZuschlagParser
} }
func NewService(repo Repository, logger *slog.Logger) *Service { func NewService(repo Repository, logger *slog.Logger, zParser *ZuschlagParser) *Service {
return &Service{ return &Service{
repo: repo, repo: repo,
logger: logger, logger: logger,
zuschlagParser: zParser,
} }
} }
func (s *Service) FetchPayrollInput(ctx context.Context, month time.Time, employeeFirstname, employeeLastname string) (excelread.PayrollInputData, error) { func (s *Service) FetchActiveEmployees(ctx context.Context) ([]database.ListActiveEmployeesRow, error) {
s.logger.Info("fetching payroll input", "month", month) s.logger.Info("fetching active employees")
employeeIDs, err := s.repo.GetEmployeeByName(ctx, database.GetEmployeeByNameParams{ employees, err := s.repo.ListActiveEmployees(ctx)
Firstname: employeeFirstname, if err != nil {
Lastname: employeeLastname, s.logger.Error("could not fetch list of active employees", "error", err)
return nil, err
}
return employees, nil
}
func (s *Service) GeneratePayrollStatement(input domain.PayrollInput) domain.PayrollStatement {
statement := domain.PayrollStatement{
Nachname: input.Surname,
Vorname: input.Firstname,
VertragStundenlohn: input.HourlyRate,
Vertragsstunden: float64(input.ContractHours) * 4.333,
GearbeiteteStunden: input.WorkedHours,
GearbeiteteTage: float64(input.WorkedDays),
Krankheitstage: float64(input.SickDays),
Ueberstunden: input.OvertimeHours,
Verpflegungspauschale: float64(input.Verpflegungspauschale),
Wochenendzuschlag15: input.ZuschlagSamstag,
Sonntagszuschlag50: input.ZuschlagSonntag,
Nachtzuschlag25: input.ZuschlagNacht25,
Nachtzuschlag40: input.ZuschlagNacht40,
Feiertagszuschlag125: input.ZuschlagFeiertag125,
Feiertagszuschlag150: input.ZuschlagFeiertag150,
}
if input.SickDays > 0 || !input.DepartmentIsKtw {
statement.GesundheitsbonusAbzug = "ja"
}
if input.DepartmentIsKtw {
statement.Urlaubsgeld = float64(input.HolidayDays)
statement.PScheinZulage = float64(input.ZulagePassengerTransport)
statement.PuenktlichkeitspraemieAnzahl = float64(input.ZulagePuenktlichkeit)
statement.Tourengeld = input.ZulageTourengeld
}
return statement
}
func (s *Service) LoadPayrollInput(ctx context.Context, month time.Time, employee database.ListActiveEmployeesRow, zuschlaege map[string]ZuschlagData) (domain.PayrollInput, error) {
weeklyTimeData, err := s.getWeeklyTimeData(ctx, month, int64(employee.ID))
if err != nil {
s.logger.Error("failed to sum worked hours for employee", "employeeId", employee.ID, "error", err)
return domain.PayrollInput{}, fmt.Errorf("failed to get time data for employee %d: %w", employee.ID, err)
}
metrics := s.calculatePayrollMetrics(weeklyTimeData, employee)
tourengeld, err := s.fetchTourengeld(ctx, month, employee.DlId)
if err != nil {
s.logger.Error("could not fetch Tourengeld for employee", "employeeId", employee.ID, "error", err)
return domain.PayrollInput{}, err
}
punctuality, err := s.getPunctualityData(ctx, month, int64(employee.ID))
if err != nil {
s.logger.Error("could not fetch punctuality for employee", "employeeId", employee.ID, "error", err)
return domain.PayrollInput{}, err
}
verpflegungsPauschale, err := s.countVerpflegungspauschale(ctx, month, int64(employee.ID))
if err != nil {
s.logger.Error("could not count verpflegungspauschale for employee", "employeeId", employee.ID, "error", err)
return domain.PayrollInput{}, err
}
isKtw, err := s.isEmployeesDepartmentKTW(ctx, month, int64(employee.ID))
if err != nil {
s.logger.Error("could not load employee department", "error", err)
return domain.PayrollInput{}, err
}
s.logger.Debug("fetched department", "isKtw", isKtw)
input := domain.PayrollInput{
Firstname: employee.Firstname,
Surname: employee.Surname,
HourlyRate: employee.ContractSalaryHour,
ContractHours: int64(employee.ContractHoursWeek),
DepartmentIsKtw: isKtw,
WorkedHours: metrics.WorkedHours,
WorkedDays: int64(metrics.WorkedDays),
SickDays: int64(metrics.SickHours / 8),
HolidayDays: int64(metrics.HolidayDays),
OvertimeHours: metrics.OvertimeHours,
TotalHours: metrics.TotalHours,
ZulageTourengeld: tourengeld,
ZulagePuenktlichkeit: punctuality,
Verpflegungspauschale: verpflegungsPauschale,
}
if isKtw && employee.PersonTransportCertificate != nil {
if employee.PersonTransportCertificate.After(month) {
input.ZulagePassengerTransport = int64(metrics.WorkedDays)
}
}
compoundKey := fmt.Sprintf("%s,%s", employee.Surname, employee.Firstname)
if zuschlagData, ok := zuschlaege[compoundKey]; ok {
input.ZuschlagNacht25 = zuschlagData.Nacht25
input.ZuschlagNacht40 = zuschlagData.Nacht40
input.ZuschlagSamstag = zuschlagData.Samstag15
input.ZuschlagSonntag = zuschlagData.Sonntag50
input.ZuschlagFeiertag125 = zuschlagData.Feiertag125
input.ZuschlagFeiertag150 = zuschlagData.Feiertag150
input.ZuschlagVerpflegung = zuschlagData.Verpflegung14
} else {
s.logger.Warn("Keine Zuschlag-Daten für Mitarbeiter gefunden", "key", compoundKey)
// (Kein Fehler, der Mitarbeiter hatte vielleicht keine Zuschläge)
}
return input, nil
}
func (s *Service) countVerpflegungspauschale(ctx context.Context, month time.Time, employeeId int64) (int64, error) {
startOfMonth := beginningOfMonth(month)
endOfMonth := endOfMonth(month)
s.logger.Info("counting VerpflegungsPauschale for employee", "employeeId", employeeId, "startDate", startOfMonth, "endDate", endOfMonth)
verpflegungsPauschale, err := s.repo.CountVerpflegungspauschale(ctx, database.CountVerpflegungspauschaleParams{
StartDate: &startOfMonth,
EndDate: &endOfMonth,
EmployeeID: employeeId,
}) })
if err != nil { if err != nil {
s.logger.Error("could not find employee with that name", "firstname", employeeFirstname, "lastname", employeeLastname, "error", err) return 0, fmt.Errorf("failed to count VerpflegungsPauschale: %w", err)
return excelread.PayrollInputData{}, err
} }
s.logger.Debug("got employee data", "employee", employeeIDs) return verpflegungsPauschale, nil
}
startOfMonth := BeginningOfMonth(month.AddDate(0, -1, 0)) func (s *Service) isEmployeesDepartmentKTW(ctx context.Context, month time.Time, employeeId int64) (bool, error) {
endOfMonth := EndOfMonth(month.AddDate(0, -1, 0)) startOfMonth := beginningOfMonth(month)
s.logger.Info("fetching data for interval", "startDate", startOfMonth, "endDate", endOfMonth) endOfMonth := endOfMonth(month)
s.logger.Info("fetching department for employee", "employeeId", employeeId, "startDate", startOfMonth, "endDate", endOfMonth)
isKtw, err := s.repo.IsEmployeesDepartmentKTW(ctx, database.IsEmployeesDepartmentKTWParams{
StartDate: &startOfMonth,
EndDate: &endOfMonth,
EmployeeID: employeeId,
})
if err != nil {
return false, fmt.Errorf("failed to fetch department for employee %d: %w", employeeId, err)
}
return isKtw, nil
}
func (s *Service) getPunctualityData(ctx context.Context, month time.Time, employeeId int64) (int64, error) {
startOfMonth := beginningOfMonth(month)
endOfMonth := endOfMonth(month)
s.logger.Info("fetching punctuality for employee", "employeeId", employeeId, "startDate", startOfMonth, "endDate", endOfMonth)
punctuality, err := s.repo.EvaluatePunctualityForEmployee(ctx, database.EvaluatePunctualityForEmployeeParams{
StartDate: &startOfMonth,
EndDate: &endOfMonth,
EmployeeID: employeeId,
})
if err != nil {
return 0, fmt.Errorf("failed to fetch punctuality evaluation for employee %d: %w", employeeId, err)
}
return punctuality, nil
}
type payrollMetrics struct {
WorkedHours float64
WorkedDays float64
SickHours float64
HolidayDays float64
OvertimeHours float64
TotalHours float64
}
func (s *Service) calculatePayrollMetrics(weeklyTimeData []domain.WeeklyTimeData, employee database.ListActiveEmployeesRow) payrollMetrics {
var m payrollMetrics
employeeDailyContractHours := employee.ContractHoursWeek / 5
for _, week := range weeklyTimeData {
m.WorkedHours += week.WorkedHours
m.WorkedDays += week.WorkedDays
m.SickHours += week.SickHours
m.HolidayDays += week.HolidayDays
}
m.TotalHours = m.WorkedHours + m.SickHours + (m.HolidayDays * employeeDailyContractHours)
m.OvertimeHours = m.TotalHours - (employee.ContractHoursWeek * 4.3333)
return m
}
func (s *Service) getWeeklyTimeData(ctx context.Context, month time.Time, employeeId int64) ([]domain.WeeklyTimeData, error) {
startOfMonth := beginningOfMonth(month)
endOfMonth := endOfMonth(month)
s.logger.Info("fetching weekly summary for employee", "employeeId", employeeId, "startDate", startOfMonth, "endDate", endOfMonth)
timeSummary, err := s.repo.GetWeeklyTimeSummaryForEmployee(ctx, database.GetWeeklyTimeSummaryForEmployeeParams{
StartDate: &startOfMonth,
EndDate: &endOfMonth,
EmployeeID: fmt.Sprintf("%d", employeeId),
})
if err != nil {
return nil, fmt.Errorf("failed to fetch weekly time summary for employee %d. %w", employeeId, err)
}
var weeklyTimeData []domain.WeeklyTimeData
for _, week := range timeSummary {
wtd := domain.WeeklyTimeData{
WeekStart: week.WeekStart,
WorkedHours: week.TotalWorkedHours,
WorkedDays: week.TotalWorkedDays,
SickHours: week.TotalSickHours,
HolidayDays: week.TotalHolidayDays,
}
weeklyTimeData = append(weeklyTimeData, wtd)
}
return weeklyTimeData, nil
}
func (s *Service) fetchTourengeld(ctx context.Context, month time.Time, dlEmployeeId string) (float64, error) {
startOfMonth := beginningOfMonth(month)
endOfMonth := endOfMonth(month)
tourengeld, err := s.repo.CountTourengeldAmountInPeriod(ctx, database.CountTourengeldAmountInPeriodParams{ tourengeld, err := s.repo.CountTourengeldAmountInPeriod(ctx, database.CountTourengeldAmountInPeriodParams{
StartDate: startOfMonth, StartDate: startOfMonth,
EndDate: endOfMonth, EndDate: endOfMonth,
EmployeeID: pgtype.Text{ EmployeeID: pgtype.Text{
String: employeeIDs.DispoliveID, String: dlEmployeeId,
Valid: true, Valid: true,
}, },
}) })
if err != nil { if err != nil {
s.logger.Error("could not count tourengeld for employee", "employeeId", employeeIDs.DyflexisID, "error", err) return 0.0, fmt.Errorf("failed to count tourengeld %w", err)
} }
return tourengeld, nil
plannedtimes, err := s.repo.ListPlannedTimesForEmployeeInPeriod(ctx, database.ListPlannedTimesForEmployeeInPeriodParams{
EmployeeID: fmt.Sprintf("%d", employeeIDs.DyflexisID),
StartDate: &startOfMonth,
EndDate: &endOfMonth,
})
if err != nil {
s.logger.Error("could not fetch plannedtimes for employee", "employeeId", employeeIDs.DyflexisID, "error", err)
}
worktimes, err := s.repo.ListWorkTimesForEmployeeInPeriod(ctx, database.ListWorkTimesForEmployeeInPeriodParams{
EmployeeID: fmt.Sprintf("%d", employeeIDs.DyflexisID),
StartDate: &startOfMonth,
EndDate: &endOfMonth,
})
if err != nil {
s.logger.Error("could not fetch worktimes for employee", "employeeId", employeeIDs.DyflexisID, "error", err)
}
s.logger.Info("Fetched payroll input", "employee", fmt.Sprintf("%s %s", employeeFirstname, employeeLastname), "tourengeld", tourengeld, "workdays", len(worktimes))
return excelread.NewPayrollInputData(employeeIDs.ContractSalaryHour, tourengeld, worktimes, plannedtimes), nil
} }
func (s *Service) GenerateMonthlyRun(ctx context.Context, month time.Time) ([]PayrollInput, error) { func beginningOfMonth(date time.Time) time.Time {
s.logger.Info("starting monthly payroll run", "month", month)
employees, err := s.repo.ListActiveEmployees(ctx)
if err != nil {
s.logger.Error("could not get active employees", "error", err)
return []PayrollInput{}, err
}
s.logger.Info("found active employees for payroll run", "count", len(employees))
var inputs []PayrollInput
for _, employee := range employees {
log := s.logger.With("employee_id", employee.ID)
log.Info("processing employee")
payrollInput := NewPayrollInput(employee)
startOfMonth := BeginningOfMonth(month.AddDate(0, -1, 0))
endOfMonth := EndOfMonth(month.AddDate(0, -1, 0))
workSummary, err := s.repo.GetWorkSummaryForEmployeeInPeriod(ctx, database.GetWorkSummaryForEmployeeInPeriodParams{
EmployeeID: fmt.Sprintf("%d", employee.ID),
StartDate: &startOfMonth,
EndDate: &endOfMonth,
})
if err != nil {
s.logger.Error("could not get work summary for employee", "error", err)
return []PayrollInput{}, err
}
payrollInput.WorkSummary = struct {
Days int64
Hours float64
Minutes int64
}(workSummary)
sicknessSummary, err := s.repo.GetSicknessSummaryForEmployeeInPeriod(ctx, database.GetSicknessSummaryForEmployeeInPeriodParams{
EmployeeID: fmt.Sprintf("%d", employee.ID),
StartDate: &startOfMonth,
EndDate: &endOfMonth,
})
if err != nil {
s.logger.Error("could not get work summary for employee", "error", err)
return []PayrollInput{}, err
}
payrollInput.SicknessSummary = struct {
Days int64
Hours float64
Minutes int64
}(sicknessSummary)
worktimes, err := s.repo.ListWorkTimesForEmployeeInPeriod(ctx, database.ListWorkTimesForEmployeeInPeriodParams{
EmployeeID: fmt.Sprintf("%d", employee.ID),
StartDate: &startOfMonth,
EndDate: &endOfMonth,
})
if err != nil {
s.logger.Error("could not get worktimes for employee", "error", err)
return []PayrollInput{}, nil
}
payrollInput.WorkTime = worktimes
inputs = append(inputs, payrollInput)
}
return inputs, nil
}
func BeginningOfMonth(date time.Time) time.Time {
return date.AddDate(0, 0, -date.Day()+1) return date.AddDate(0, 0, -date.Day()+1)
} }
func EndOfMonth(date time.Time) time.Time { func endOfMonth(date time.Time) time.Time {
eom := date.AddDate(0, 1, -date.Day()) eom := date.AddDate(0, 1, -date.Day())
return time.Date( return time.Date(
eom.Year(), eom.Year(),
eom.Month(), eom.Month(),
eom.Day(), eom.Day(),
23, 59, 59, 999, 23, 59, 59, 999999999,
eom.Location(), eom.Location(),
) )
} }

View File

@ -23,4 +23,15 @@ sql:
- db_type: "pg_catalog.timestamptz" - db_type: "pg_catalog.timestamptz"
nullable: true nullable: true
go_type: go_type:
type: "*time.Time" type: "time.Time"
pointer: true
- db_type: bool
go_type:
import: ""
type: bool
- db_type: bool
go_type:
import: ""
type: bool
pointer: true
nullable: true