implement Gehaltsaufstellung dyflexis data access
This commit is contained in:
parent
4abe9e44d8
commit
4cf79f3094
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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: -
|
||||||
--
|
--
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
AND "userId"::int = @employee_id
|
||||||
|
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 @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
|
||||||
wt.days,
|
week_start::timestamp,
|
||||||
(minutes / 60.0)::float as hours,
|
SUM(worked_hours)::float AS total_worked_hours,
|
||||||
wt.minutes
|
SUM(worked_days)::float AS total_worked_days,
|
||||||
FROM wt;
|
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: ListPlannedTimesForEmployeeInPeriod :many
|
-- 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 = @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
|
SELECT
|
||||||
DISTINCT ON (day)
|
COUNT(CASE WHEN was_punctual THEN 1 END) AS punctual_days
|
||||||
"startDate"::DATE as day,
|
FROM daily_comparison;
|
||||||
"startDate",
|
|
||||||
"endDate",
|
-- name: IsEmployeesDepartmentKTW :one
|
||||||
department,
|
SELECT COALESCE(
|
||||||
pause,
|
(
|
||||||
duration
|
bool_or(department LIKE '%KTW%')
|
||||||
|
AND
|
||||||
|
bool_and(department <> 'Leitstelle')
|
||||||
|
),
|
||||||
|
false
|
||||||
|
)::bool as meets_requirements
|
||||||
FROM "PlannedTime"
|
FROM "PlannedTime"
|
||||||
WHERE
|
WHERE
|
||||||
"userId" = @employee_id AND
|
"userId"::int = @employee_id::bigint
|
||||||
"startDate" > @start_date AND
|
AND "startDate" BETWEEN @start_date AND @end_date
|
||||||
"startDate" < @end_date AND
|
AND department IS NOT NULL;
|
||||||
department <> 'Fortbildung' AND
|
|
||||||
note IS NULL
|
|
||||||
ORDER BY day, "startDate" ASC;
|
|
||||||
|
|
||||||
-- name: ListWorkTimesForEmployeeInPeriod :many
|
-- name: CountVerpflegungspauschale :one
|
||||||
SELECT * FROM "WorkTime"
|
|
||||||
WHERE "userId" = @employee_id AND "startDate" > @start_date AND "startDate" < @end_date;
|
|
||||||
|
|
||||||
-- name: GetSicknessSummaryForEmployeeInPeriod :one
|
|
||||||
WITH pt AS (
|
|
||||||
SELECT
|
|
||||||
COUNT(*) as days,
|
|
||||||
COALESCE(sum(duration), 0)::bigint as minutes
|
|
||||||
FROM "PlannedTime"
|
|
||||||
WHERE "userId" = @employee_id AND "startDate" > @start_date AND "startDate" < @end_date AND note = 'sick'
|
|
||||||
)
|
|
||||||
SELECT
|
SELECT
|
||||||
pt.days,
|
count(*)
|
||||||
(minutes / 60)::float as hours,
|
FROM "WorkTime"
|
||||||
pt.minutes
|
|
||||||
FROM pt;
|
|
||||||
|
|
||||||
-- name: GetEmployeeDyflexis :one
|
|
||||||
SELECT * FROM "DyflexisEmployee"
|
|
||||||
WHERE
|
WHERE
|
||||||
id = $1 AND
|
duration > 480
|
||||||
"contractStart" < CURRENT_DATE AND
|
AND "startDate" BETWEEN @start_date AND @end_date
|
||||||
"contractEnd" > (CURRENT_DATE - INTERVAL '2 months')
|
AND "userId"::int = @employee_id::bigint;
|
||||||
LIMIT 1;
|
|
||||||
|
|
||||||
-- name: FetchDrivenToursInInterval :many
|
|
||||||
SELECT * FROM "Tour"
|
|
||||||
WHERE "check" <> 3
|
|
||||||
AND "deletedAt" IS NULL
|
|
||||||
AND (
|
|
||||||
"patientId" IS NULL
|
|
||||||
OR "patientId" NOT IN (
|
|
||||||
'b7424477-a4b2-42f7-b7e7-a4bef24f6979',
|
|
||||||
'3554e1a9-9c07-45e5-b53c-692a77e4fa9a'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
-- ID's for Pause and Feierabend respectively
|
|
||||||
AND (
|
|
||||||
"carName" IS NULL
|
|
||||||
OR "carName" NOT IN (
|
|
||||||
'Storno',
|
|
||||||
'Abgabe',
|
|
||||||
'Löschen'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
AND "startDate" >= @start_date
|
|
||||||
AND "startDate" <= @end_date
|
|
||||||
AND ("driverId" = @employee_id OR "codriverId" = @employee_id)
|
|
||||||
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;
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
)::bool as meets_requirements
|
||||||
|
FROM "PlannedTime"
|
||||||
WHERE
|
WHERE
|
||||||
TRIM(dy.firstname) = $1 AND
|
"userId"::int = $1::bigint
|
||||||
TRIM(dy.surname) = $2 AND
|
AND "startDate" BETWEEN $2 AND $3
|
||||||
"contractStart" < CURRENT_DATE AND
|
AND department IS NOT NULL
|
||||||
"contractEnd" > (CURRENT_DATE - INTERVAL '2 months')
|
|
||||||
ORDER BY dy."contractEnd" DESC
|
|
||||||
`
|
`
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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%"`
|
||||||
|
}
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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 ---
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
Loading…
Reference in New Issue