From a964663f514025ca0037d283ce87643ca185e91c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torsten=20L=C3=BCcke?= Date: Thu, 19 Jun 2025 14:18:21 +0200 Subject: [PATCH 1/8] Erweitere die Datenbank --- api/tests/_data/dump.sql | 197 +++++++++++++++++++---------------- resources/sql/postgresql.sql | 66 ++++++------ 2 files changed, 142 insertions(+), 121 deletions(-) diff --git a/api/tests/_data/dump.sql b/api/tests/_data/dump.sql index ffc1978..f841124 100644 --- a/api/tests/_data/dump.sql +++ b/api/tests/_data/dump.sql @@ -22,127 +22,144 @@ DROP TABLE IF EXISTS public."Arbeitszeiten"; -- Name: Arbeitszeiten; Type: TABLE; Schema: public; Owner: torsten -- -CREATE TABLE public."Arbeitszeiten" ( - "Datum" date not null, - "Arbeitszeit" interval(6) null, +CREATE TABLE public."Arbeitszeiten" +( + "Datum" date not null, + "Arbeitszeit" interval(6) null, + "HomeOffice" boolean default FALSE not null, constraint "Arbeitszeiten_pkey" primary key ("Datum") ); -ALTER TABLE public."Arbeitszeiten" OWNER TO bruce; +ALTER TABLE public."Arbeitszeiten" + OWNER TO bruce; -- -- Name: Arbeitszeiten - Jahr; Type: VIEW; Schema: public; Owner: torsten -- CREATE OR REPLACE VIEW public."Arbeitszeiten - Jahr" AS - SELECT (date_part('year'::text, "Arbeitszeiten"."Datum"))::text AS "Jahr", - sum("Arbeitszeiten"."Arbeitszeit") AS "Gesamtarbeitszeit", - count("Arbeitszeiten"."Datum") AS "Arbeitstage", - (sum("Arbeitszeiten"."Arbeitszeit") - ((count("Arbeitszeiten"."Datum"))::double precision * '08:00:00'::interval)) AS "Überstunden" - FROM public."Arbeitszeiten" - GROUP BY (date_part('year'::text, "Arbeitszeiten"."Datum")) - ORDER BY (date_part('year'::text, "Arbeitszeiten"."Datum")); +SELECT (date_part('year'::text, "Arbeitszeiten"."Datum"))::text AS "Jahr", + sum("Arbeitszeiten"."Arbeitszeit")::interval AS "Gesamtarbeitszeit", + count("Arbeitszeiten"."Datum")::bigint AS "Arbeitstage", + (sum("Arbeitszeiten"."Arbeitszeit") - + ((count("Arbeitszeiten"."Datum"))::double precision * '08:00:00'::interval)) AS "Überstunden", + (count(*) filter (where "Arbeitszeiten"."HomeOffice"))::int as "Home-Office-Tage" +FROM public."Arbeitszeiten" +GROUP BY (date_part('year'::text, "Arbeitszeiten"."Datum")) +ORDER BY (date_part('year'::text, "Arbeitszeiten"."Datum")); -ALTER TABLE public."Arbeitszeiten - Jahr" OWNER TO bruce; +ALTER TABLE public."Arbeitszeiten - Jahr" + OWNER TO bruce; -- -- Name: Arbeitszeiten - Monat; Type: VIEW; Schema: public; Owner: torsten -- CREATE OR REPLACE VIEW public."Arbeitszeiten - Monat" AS - SELECT to_char(date_trunc('month'::text, ("Arbeitszeiten"."Datum")::timestamp with time zone), 'TMYYYY TMMonth'::text) AS "Monat", - sum("Arbeitszeiten"."Arbeitszeit") AS "Gesamtarbeitszeit", - count("Arbeitszeiten"."Datum") AS "Arbeitstage", - (sum("Arbeitszeiten"."Arbeitszeit") - ((count("Arbeitszeiten"."Datum"))::double precision * '08:00:00'::interval)) AS "Überstunden" - FROM public."Arbeitszeiten" - GROUP BY (date_trunc('month'::text, ("Arbeitszeiten"."Datum")::timestamp with time zone)) - ORDER BY (date_trunc('month'::text, ("Arbeitszeiten"."Datum")::timestamp with time zone)); +SELECT to_char(date_trunc('month'::text, ("Arbeitszeiten"."Datum")::timestamp with time zone), + 'TMYYYY TMMonth'::text) AS "Monat", + sum("Arbeitszeiten"."Arbeitszeit")::interval AS "Gesamtarbeitszeit", + count("Arbeitszeiten"."Datum")::bigint AS "Arbeitstage", + (sum("Arbeitszeiten"."Arbeitszeit") - + ((count("Arbeitszeiten"."Datum"))::double precision * '08:00:00'::interval)) AS "Überstunden", + (count(*) filter (where "Arbeitszeiten"."HomeOffice"))::int as "Home-Office-Tage" +FROM public."Arbeitszeiten" +GROUP BY (date_trunc('month'::text, ("Arbeitszeiten"."Datum")::timestamp with time zone)) +ORDER BY (date_trunc('month'::text, ("Arbeitszeiten"."Datum")::timestamp with time zone)); -ALTER TABLE public."Arbeitszeiten - Monat" OWNER TO bruce; +ALTER TABLE public."Arbeitszeiten - Monat" + OWNER TO bruce; -- -- Name: Arbeitszeiten - Woche; Type: VIEW; Schema: public; Owner: torsten -- CREATE OR REPLACE VIEW public."Arbeitszeiten - Woche" AS - SELECT to_char(date_trunc('week'::text, ("Arbeitszeiten"."Datum")::timestamp with time zone), 'TMYYYY#WW'::text) AS "Woche", - sum("Arbeitszeiten"."Arbeitszeit") AS "Gesamtarbeitszeit", - count("Arbeitszeiten"."Datum") AS "Arbeitstage", - (sum("Arbeitszeiten"."Arbeitszeit") - ((count("Arbeitszeiten"."Datum"))::double precision * '08:00:00'::interval)) AS "Überstunden" - FROM public."Arbeitszeiten" - GROUP BY (date_trunc('week'::text, ("Arbeitszeiten"."Datum")::timestamp with time zone)) - ORDER BY (date_trunc('week'::text, ("Arbeitszeiten"."Datum")::timestamp with time zone)); +SELECT to_char(date_trunc('week'::text, ("Arbeitszeiten"."Datum")::timestamp with time zone), + 'TMYYYY#WW'::text) AS "Woche", + sum("Arbeitszeiten"."Arbeitszeit")::interval AS "Gesamtarbeitszeit", + count("Arbeitszeiten"."Datum")::bigint AS "Arbeitstage", + (sum("Arbeitszeiten"."Arbeitszeit") - + ((count("Arbeitszeiten"."Datum"))::double precision * '08:00:00'::interval)) AS "Überstunden", + (count(*) filter (where "Arbeitszeiten"."HomeOffice"))::int as "Home-Office-Tage" +FROM public."Arbeitszeiten" +GROUP BY (date_trunc('week'::text, ("Arbeitszeiten"."Datum")::timestamp with time zone)) +ORDER BY (date_trunc('week'::text, ("Arbeitszeiten"."Datum")::timestamp with time zone)); -ALTER TABLE public."Arbeitszeiten - Woche" OWNER TO bruce; +ALTER TABLE public."Arbeitszeiten - Woche" + OWNER TO bruce; -- -- Data for Name: Arbeitszeiten; Type: TABLE DATA; Schema: public; Owner: torsten -- -INSERT INTO public."Arbeitszeiten" ("Datum", "Arbeitszeit") VALUES -('2020-01-07','08:06:00'), -('2020-01-08','08:16:00'), -('2020-01-09','07:49:00'), -('2020-01-10','07:30:00'), -('2020-01-13','08:06:00'), -('2020-01-14','08:04:00'), -('2020-01-15','08:20:00'), -('2020-01-16','07:13:00'), -('2020-01-17','07:57:00'), -('2020-01-20','07:53:00'), -('2020-01-21','07:49:00'), -('2020-01-22','07:56:00'), -('2020-01-23','07:55:00'), -('2020-01-24','07:08:00'), -('2020-01-27','08:23:00'), -('2020-01-28','07:20:00'), -('2020-01-29','08:13:00'), -('2020-01-30','08:43:00'), -('2020-01-31','07:21:00'), -('2020-02-03','07:56:00'), -('2020-02-04','08:05:00'), -('2020-02-05','07:58:00'), -('2020-02-06','08:01:00'), -('2020-02-07','07:59:00'), -('2020-02-10','07:50:00'), -('2020-02-11','08:01:00'), -('2020-02-12','08:14:00'), -('2020-02-13','08:12:00'), -('2020-02-14','07:51:00'), -('2020-02-17','07:59:00'), -('2020-02-18','08:05:00'), -('2020-02-19','07:34:00'), -('2020-02-20','07:33:00'), -('2020-02-21','07:56:00'), -('2020-02-24','08:02:00'), -('2020-02-25','08:13:00'), -('2020-02-26','08:42:00'), -('2020-02-27','07:49:00'), -('2020-02-28','08:16:00'), -('2020-03-02','08:22:00'), -('2020-03-03','08:20:00'), -('2020-03-04','08:12:00'), -('2020-03-05','08:32:00'), -('2020-03-06','04:41:00'), -('2020-03-09','07:05:00'), -('2020-03-10','07:34:00'), -('2020-03-11','07:39:00'), -('2020-03-12','07:50:00'), -('2020-03-13','08:26:00'), -('2020-03-16','07:51:00'), -('2020-03-17','07:50:00'), -('2020-03-18','07:19:00'), -('2020-03-19','07:55:00'), -('2020-03-20','05:43:00'), -('2020-03-23','08:05:00'), -('2020-03-24','08:21:00'), -('2020-03-25','08:10:00'), -('2020-03-26','10:31:00'), -('2020-03-27','04:55:00'), -('2020-03-30','08:21:00'), -('2020-03-31','08:01:00') +INSERT INTO public."Arbeitszeiten" ("Datum", "Arbeitszeit") +VALUES ('2020-01-07', '08:06:00'), + ('2020-01-08', '08:16:00'), + ('2020-01-09', '07:49:00'), + ('2020-01-10', '07:30:00'), + ('2020-01-13', '08:06:00'), + ('2020-01-14', '08:04:00'), + ('2020-01-15', '08:20:00'), + ('2020-01-16', '07:13:00'), + ('2020-01-17', '07:57:00'), + ('2020-01-20', '07:53:00'), + ('2020-01-21', '07:49:00'), + ('2020-01-22', '07:56:00'), + ('2020-01-23', '07:55:00'), + ('2020-01-24', '07:08:00'), + ('2020-01-27', '08:23:00'), + ('2020-01-28', '07:20:00'), + ('2020-01-29', '08:13:00'), + ('2020-01-30', '08:43:00'), + ('2020-01-31', '07:21:00'), + ('2020-02-03', '07:56:00'), + ('2020-02-04', '08:05:00'), + ('2020-02-05', '07:58:00'), + ('2020-02-06', '08:01:00') +; + +INSERT INTO public."Arbeitszeiten" ("Datum", "Arbeitszeit", "HomeOffice") +VALUES ('2020-02-07', '07:59:00', FALSE), + ('2020-02-10', '07:50:00', FALSE), + ('2020-02-11', '08:01:00', FALSE), + ('2020-02-12', '08:14:00', FALSE), + ('2020-02-13', '08:12:00', FALSE), + ('2020-02-14', '07:51:00', FALSE), + ('2020-02-17', '07:59:00', FALSE), + ('2020-02-18', '08:05:00', FALSE), + ('2020-02-19', '07:34:00', FALSE), + ('2020-02-20', '07:33:00', FALSE), + ('2020-02-21', '07:56:00', FALSE), + ('2020-02-24', '08:02:00', FALSE), + ('2020-02-25', '08:13:00', FALSE), + ('2020-02-26', '08:42:00', FALSE), + ('2020-02-27', '07:49:00', TRUE), + ('2020-02-28', '08:16:00', FALSE), + ('2020-03-02', '08:22:00', FALSE), + ('2020-03-03', '08:20:00', FALSE), + ('2020-03-04', '08:12:00', FALSE), + ('2020-03-05', '08:32:00', FALSE), + ('2020-03-06', '04:41:00', FALSE), + ('2020-03-09', '07:05:00', FALSE), + ('2020-03-10', '07:34:00', FALSE), + ('2020-03-11', '07:39:00', TRUE), + ('2020-03-12', '07:50:00', FALSE), + ('2020-03-13', '08:26:00', FALSE), + ('2020-03-16', '07:51:00', FALSE), + ('2020-03-17', '07:50:00', FALSE), + ('2020-03-18', '07:19:00', FALSE), + ('2020-03-19', '07:55:00', FALSE), + ('2020-03-20', '05:43:00', TRUE), + ('2020-03-23', '08:05:00', FALSE), + ('2020-03-24', '08:21:00', FALSE), + ('2020-03-25', '08:10:00', FALSE), + ('2020-03-26', '10:31:00', FALSE), + ('2020-03-27', '04:55:00', FALSE), + ('2020-03-30', '08:21:00', FALSE), + ('2020-03-31', '08:01:00', FALSE) ; \ No newline at end of file diff --git a/resources/sql/postgresql.sql b/resources/sql/postgresql.sql index 2112dbc..41dba69 100644 --- a/resources/sql/postgresql.sql +++ b/resources/sql/postgresql.sql @@ -2,44 +2,48 @@ -- DROP TABLE public."Arbeitszeiten"; create table public."Arbeitszeiten" ( - "Datum" date not null, - "Arbeitszeit" interval(6) null, + "Datum" date not null, + "Arbeitszeit" interval(6) null, + "HomeOffice" boolean default FALSE not null, constraint "Arbeitszeiten_pkey" primary key ("Datum") ); -- # Definition der Ansichten -- ## Wöchentlich -create view "Arbeitszeiten - Woche" ("Woche", "Gesamtarbeitszeit", "Arbeitstage", "Überstunden") as - select - to_char(date_trunc('week'::text, "Arbeitszeiten"."Datum"::timestamp with time zone), - 'TMYYYY#WW'::text) as "Woche", - sum("Arbeitszeiten"."Arbeitszeit") as "Gesamtarbeitszeit", - count("Arbeitszeiten"."Datum") as "Arbeitstage", - sum("Arbeitszeiten"."Arbeitszeit") - count("Arbeitszeiten"."Datum") ::double precision * '08:00:00'::interval as "Überstunden" - from "Arbeitszeiten" - group by (date_trunc('week'::text, "Arbeitszeiten"."Datum"::timestamp with time zone)) - order by (date_trunc('week'::text, "Arbeitszeiten"."Datum"::timestamp with time zone)); +create view "Arbeitszeiten - Woche" ("Woche", "Gesamtarbeitszeit", "Arbeitstage", "Überstunden", "Home-Office-Tage") as +select to_char(date_trunc('week'::text, "Arbeitszeiten"."Datum"::timestamp with time zone), + 'TMYYYY#WW'::text) as "Woche", + sum("Arbeitszeiten"."Arbeitszeit")::interval AS "Gesamtarbeitszeit", + count("Arbeitszeiten"."Datum")::bigint AS "Arbeitstage", + (sum("Arbeitszeiten"."Arbeitszeit") - + ((count("Arbeitszeiten"."Datum"))::double precision * '08:00:00'::interval)) AS "Überstunden", + (count(*) filter (where "Arbeitszeiten"."HomeOffice"))::int as "Home-Office-Tage" +from "Arbeitszeiten" +group by (date_trunc('week'::text, "Arbeitszeiten"."Datum"::timestamp with time zone)) +order by (date_trunc('week'::text, "Arbeitszeiten"."Datum"::timestamp with time zone)); -- ## Monatlich -create view "Arbeitszeiten - Monat" ("Monat", "Gesamtarbeitszeit", "Arbeitstage", "Überstunden") as - select - to_char(date_trunc('month'::text, "Arbeitszeiten"."Datum"::timestamp with time zone), - 'TMYYYY TMMonth'::text) as "Monat", - sum("Arbeitszeiten"."Arbeitszeit") as "Gesamtarbeitszeit", - count("Arbeitszeiten"."Datum") as "Arbeitstage", - sum("Arbeitszeiten"."Arbeitszeit") - count("Arbeitszeiten"."Datum") ::double precision * '08:00:00'::interval as "Überstunden" - from "Arbeitszeiten" - group by (date_trunc('month'::text, "Arbeitszeiten"."Datum"::timestamp with time zone)) - order by (date_trunc('month'::text, "Arbeitszeiten"."Datum"::timestamp with time zone)); +create view "Arbeitszeiten - Monat" ("Monat", "Gesamtarbeitszeit", "Arbeitstage", "Überstunden", "Home-Office-Tage") as +select to_char(date_trunc('month'::text, "Arbeitszeiten"."Datum"::timestamp with time zone), + 'TMYYYY TMMonth'::text) as "Monat", + sum("Arbeitszeiten"."Arbeitszeit")::interval AS "Gesamtarbeitszeit", + count("Arbeitszeiten"."Datum")::bigint AS "Arbeitstage", + (sum("Arbeitszeiten"."Arbeitszeit") - + ((count("Arbeitszeiten"."Datum"))::double precision * '08:00:00'::interval)) AS "Überstunden", + (count(*) filter (where "Arbeitszeiten"."HomeOffice"))::int as "Home-Office-Tage" +from "Arbeitszeiten" +group by (date_trunc('month'::text, "Arbeitszeiten"."Datum"::timestamp with time zone)) +order by (date_trunc('month'::text, "Arbeitszeiten"."Datum"::timestamp with time zone)); -- ## Jährlich -create view "Arbeitszeiten - Jahr"("Jahr", "Gesamtarbeitszeit", "Arbeitstage", "Überstunden") as - select - date_part('year'::text, "Arbeitszeiten"."Datum")::text as "Jahr", - sum("Arbeitszeiten"."Arbeitszeit") as "Gesamtarbeitszeit", - count("Arbeitszeiten"."Datum") as "Arbeitstage", - sum("Arbeitszeiten"."Arbeitszeit") - count("Arbeitszeiten"."Datum") ::double precision * '08:00:00'::interval as "Überstunden" - from "Arbeitszeiten" - group by (date_part('year'::text, "Arbeitszeiten"."Datum")) - order by (date_part('year'::text, "Arbeitszeiten"."Datum")); \ No newline at end of file +create view "Arbeitszeiten - Jahr"("Jahr", "Gesamtarbeitszeit", "Arbeitstage", "Überstunden", "Home-Office-Tage") as +select date_part('year'::text, "Arbeitszeiten"."Datum")::text as "Jahr", + sum("Arbeitszeiten"."Arbeitszeit")::interval AS "Gesamtarbeitszeit", + count("Arbeitszeiten"."Datum")::bigint AS "Arbeitstage", + (sum("Arbeitszeiten"."Arbeitszeit") - + ((count("Arbeitszeiten"."Datum"))::double precision * '08:00:00'::interval)) AS "Überstunden", + (count(*) filter (where "Arbeitszeiten"."HomeOffice"))::int as "Home-Office-Tage" +from "Arbeitszeiten" +group by (date_part('year'::text, "Arbeitszeiten"."Datum")) +order by (date_part('year'::text, "Arbeitszeiten"."Datum")); \ No newline at end of file -- 2.54.0 From cf6eff6ab00c3fde926f58136af031afa8306798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torsten=20L=C3=BCcke?= Date: Thu, 19 Jun 2025 14:18:59 +0200 Subject: [PATCH 2/8] Erweitere die Tests --- api/html/openapi.json | 10 ++- api/tests/_support/Helper/Api.php | 7 +- .../WorkingHours/CreateWorkingHoursCest.php | 3 + .../WorkingHours/UpdateWorkingHoursCest.php | 10 ++- .../Controller/WorkingHoursControllerTest.php | 64 ++++++++++++++++--- api/tests/unit/Models/WorkingHoursTest.php | 46 +++++++++++-- .../unit/Models/WorkingHoursViewTest.php | 20 +++++- .../WorkingHoursRepositoryTest.php | 30 +++++---- .../WorkingHoursViewRepositoryTest.php | 49 ++++++++++---- 9 files changed, 195 insertions(+), 44 deletions(-) diff --git a/api/html/openapi.json b/api/html/openapi.json index a45afac..c88e9c1 100644 --- a/api/html/openapi.json +++ b/api/html/openapi.json @@ -4,7 +4,7 @@ "title": "Zeiterfassung App", "contact": { "name": "Torsten Lücke", - "url": "http://torsten-hettstedt.de", + "url": "https://torsten-hettstedt.de", "email": "arbeit@torsten-hettstedt.de" }, "license": { @@ -188,6 +188,10 @@ "type": "string", "pattern": "[0-5]\\d:[0-5]\\d:[0-5]\\d", "example": "08:01:00" + }, + "isHomeOffice": { + "description": "Wurde an diesem Tag zuhause gearbeitet?", + "type": "boolean" } } }, @@ -217,6 +221,10 @@ "description": "Arbeitstage im Zeitraum", "type": "integer" }, + "homeOfficeDays": { + "description": "Anzahl der Tage, an denen zuhause gearbeitet wurde.", + "type": "number" + }, "overtime": { "description": "Überstunden im Zeitraum", "type": "string", diff --git a/api/tests/_support/Helper/Api.php b/api/tests/_support/Helper/Api.php index 677c203..ec273e2 100644 --- a/api/tests/_support/Helper/Api.php +++ b/api/tests/_support/Helper/Api.php @@ -1,4 +1,5 @@ self::FORMAT_DATE, - 'workingTime' => self::FORMAT_TIME, + 'workingDay' => self::FORMAT_DATE, + 'workingTime' => self::FORMAT_TIME, + 'isHomeOffice' => 'boolean', ]; const WORKING_HOURS_VIEW_JSON_FORMAT = [ @@ -22,6 +24,7 @@ class Api extends Module "periodDesignation" => "string", "totalHours" => self::FORMAT_TIME, "workingDays" => 'integer:>0', + 'homeOfficeDays' => 'number:>=0', "overtime" => self::FORMAT_TIME, ]; diff --git a/api/tests/api/WorkingHours/CreateWorkingHoursCest.php b/api/tests/api/WorkingHours/CreateWorkingHoursCest.php index c2707bc..9305a9b 100644 --- a/api/tests/api/WorkingHours/CreateWorkingHoursCest.php +++ b/api/tests/api/WorkingHours/CreateWorkingHoursCest.php @@ -28,6 +28,7 @@ class CreateWorkingHoursCest $I->sendPost('/working-hours', [ 'workingDay' => '2020-04-01', 'workingTime' => '08:00:00', + 'isHomeOffice' => false, ]); $I->seeResponseCodeIs(HttpCode::CREATED); $I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN); @@ -44,6 +45,7 @@ class CreateWorkingHoursCest $I->sendPost('/working-hours', [ 'workingDay' => '2020-04-01', 'workingTime' => 8.0, + 'isHomeOffice' => false, ]); $I->seeResponseCodeIs(HttpCode::BAD_REQUEST); $I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN); @@ -60,6 +62,7 @@ class CreateWorkingHoursCest $I->sendPost('/working-hours', [ 'workingDay' => '2020-01-15', 'workingTime' => '08:00:00', + 'isHomeOffice' => false, ]); $I->seeResponseCodeIs(HttpCode::CONFLICT); $I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN); diff --git a/api/tests/api/WorkingHours/UpdateWorkingHoursCest.php b/api/tests/api/WorkingHours/UpdateWorkingHoursCest.php index 176eb88..e0a6240 100644 --- a/api/tests/api/WorkingHours/UpdateWorkingHoursCest.php +++ b/api/tests/api/WorkingHours/UpdateWorkingHoursCest.php @@ -1,4 +1,5 @@ -haveHttpHeader('content-type', 'application/json'); $I->haveHttpHeader('Origin', Api::TEST_ORIGIN); $I->sendPut('/working-hours/2020-01-15', [ - 'workingDay' => '2020-01-15', - 'workingTime' => '10:00:00', + 'workingDay' => '2020-01-15', + 'workingTime' => '10:00:00', + 'isHomeOffice' => false, ]); $I->seeResponseCodeIs(HttpCode::OK); $I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN); @@ -43,6 +45,7 @@ class UpdateWorkingHoursCest $I->sendPut('/working-hours/2020-04-15', [ 'workingDay' => '2020-04-15', 'workingTime' => '10:00:00', + 'isHomeOffice' => false, ]); $I->seeResponseCodeIs(HttpCode::NOT_FOUND); $I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN); @@ -58,6 +61,7 @@ class UpdateWorkingHoursCest $I->sendPut('/working-hours/2020-01-15', [ 'workingDay' => '2020-01-15', 'workingTime' => 8.0, + 'isHomeOffice' => false, ]); $I->seeResponseCodeIs(HttpCode::BAD_REQUEST); $I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN); diff --git a/api/tests/unit/Controller/WorkingHoursControllerTest.php b/api/tests/unit/Controller/WorkingHoursControllerTest.php index e6b9903..845f381 100644 --- a/api/tests/unit/Controller/WorkingHoursControllerTest.php +++ b/api/tests/unit/Controller/WorkingHoursControllerTest.php @@ -32,8 +32,9 @@ class WorkingHoursControllerTest extends AbstractControllerTest parent::_before(); $this->request = $this->makeEmpty(Request::class, [ 'getParsedBody' => [ - 'workingDay' => self::EXISTING_DATE, - 'workingTime' => self::EXISTING_INTERVAL, + 'workingDay' => self::EXISTING_DATE, + 'workingTime' => self::EXISTING_INTERVAL, + 'isHomeOfficeDay' => true, ], ]); } @@ -260,6 +261,28 @@ class WorkingHoursControllerTest extends AbstractControllerTest $this->assertNotEmpty($response->getBody()->getContents()); } + /** + * @throws ContainerExceptionInterface + * @throws NotDatabasesException + * @throws NotFoundExceptionInterface + * @throws Exception + */ + public function testUpdateExistRecordWithoutHomeOffice(): void + { + $this->request = $this->makeEmpty(Request::class, [ + 'getParsedBody' => [ + 'workingDay' => self::EXISTING_DATE, + 'workingTime' => self::EXISTING_INTERVAL, + ], + ]); + $controller = new WorkingHoursController($this->container); + $response = $controller->update($this->request, $this->response, [ + 'id' => self::EXISTING_DATE, + ]); + $this->assertEquals(self::FICTIONAL_STATUS_CODE, $response->getStatusCode()); + $this->assertNotEmpty($response->getBody()->getContents()); + } + /** * @throws ContainerExceptionInterface * @throws NotDatabasesException @@ -302,12 +325,13 @@ class WorkingHoursControllerTest extends AbstractControllerTest * @throws Exception */ #[DataProvider('invalidCreatDataProvider')] - public function testCreatInvalidData(string $date, string $time): void + public function testCreatInvalidData(string $date, string $time, bool $isHomeOffice = false): void { $this->request = $this->makeEmpty(Request::class, [ 'getParsedBody' => [ - 'workingDay' => $date, - 'workingTime' => $time, + 'workingDay' => $date, + 'workingTime' => $time, + 'isHomeOfficeDay' => $isHomeOffice, ], ]); $controller = new WorkingHoursController($this->container); @@ -325,8 +349,29 @@ class WorkingHoursControllerTest extends AbstractControllerTest { $this->request = $this->makeEmpty(Request::class, [ 'getParsedBody' => [ - 'workingDay' => self::NEW_DATE, - 'workingTime' => self::NEW_INTERVAL, + 'workingDay' => self::NEW_DATE, + 'workingTime' => self::NEW_INTERVAL, + 'isHomeOfficeDay' => true, + ], + ]); + $controller = new WorkingHoursController($this->container); + $response = $controller->creat($this->request, $this->response, []); + $this->assertEquals(self::FICTIONAL_STATUS_CODE, $response->getStatusCode()); + $this->assertNotEmpty($response->getBody()->getContents()); + } + + /** + * @throws ContainerExceptionInterface + * @throws NotDatabasesException + * @throws NotFoundExceptionInterface + * @throws Exception + */ + public function testCreatNewRecordWithoutHomeOffice(): void + { + $this->request = $this->makeEmpty(Request::class, [ + 'getParsedBody' => [ + 'workingDay' => self::NEW_DATE, + 'workingTime' => self::NEW_INTERVAL, ], ]); $controller = new WorkingHoursController($this->container); @@ -345,8 +390,9 @@ class WorkingHoursControllerTest extends AbstractControllerTest { $this->request = $this->makeEmpty(Request::class, [ 'getParsedBody' => [ - 'workingDay' => self::EXISTING_DATE, - 'workingTime' => self::EXISTING_INTERVAL, + 'workingDay' => self::EXISTING_DATE, + 'workingTime' => self::EXISTING_INTERVAL, + 'isHomeOfficeDay' => true, ], ]); $controller = new WorkingHoursController($this->container); diff --git a/api/tests/unit/Models/WorkingHoursTest.php b/api/tests/unit/Models/WorkingHoursTest.php index b1612ff..e068cc5 100644 --- a/api/tests/unit/Models/WorkingHoursTest.php +++ b/api/tests/unit/Models/WorkingHoursTest.php @@ -17,6 +17,7 @@ class WorkingHoursTest extends Unit $obj = new WorkingHours(); $this->assertNull($obj->getWorkingDay()); $this->assertNull($obj->getWorkingTime()); + $this->assertFalse($obj->isHomeOffice()); $this->expectException(Error::class); $obj->jsonSerialize(); @@ -32,17 +33,21 @@ class WorkingHoursTest extends Unit new DateTime('2020-11-30'), new DateInterval("PT8H"), [ - 'workingDay' => '2020-11-30', - 'workingTime' => '08:00:00', + 'workingDay' => '2020-11-30', + 'workingTime' => '08:00:00', + 'isHomeOffice' => true, ], + true, ], [ new DateTime('2021-05-03'), new DateInterval("PT7H45M"), [ - 'workingDay' => '2021-05-03', - 'workingTime' => '07:45:00', + 'workingDay' => '2021-05-03', + 'workingTime' => '07:45:00', + 'isHomeOffice' => false, ], + false, ], ]; } @@ -51,10 +56,38 @@ class WorkingHoursTest extends Unit * @param DateTime $date * @param DateInterval $interval * @param array $shouldJson + * @param bool $isHomeOffice */ #[DataProvider('workingHoursProvider')] - public function testWorkingHoursWithContent(DateTime $date, DateInterval $interval, array $shouldJson): void + public function testWorkingHoursWithContent(DateTime $date, DateInterval $interval, array $shouldJson, bool $isHomeOffice): void { + $obj = new WorkingHours(); + $obj->setWorkingDay($date)->setWorkingTime($interval)->setIsHomeOffice($isHomeOffice); + + $this->assertNotNull($obj->getWorkingDay()); + $this->assertInstanceOf(DateTime::class, $obj->getWorkingDay()); + $this->assertEquals($date, $obj->getWorkingDay()); + + $this->assertNotNull($obj->getWorkingTime()); + $this->assertInstanceOf(DateInterval::class, $obj->getWorkingTime()); + $this->assertEquals($interval, $obj->getWorkingTime()); + + $this->assertIsBool($obj->isHomeOffice()); + $this->assertEquals($isHomeOffice, $obj->isHomeOffice()); + + $json = $obj->jsonSerialize(); + $this->assertEquals($shouldJson, $json); + } + + /** + * @param DateTime $date + * @param DateInterval $interval + * @param array $shouldJson + */ + #[DataProvider('workingHoursProvider')] + public function testWorkingHoursWithContentWithoutHomeOfficeInformation(DateTime $date, DateInterval $interval, array $shouldJson): void + { + $shouldJson['isHomeOffice'] = false; $obj = new WorkingHours(); $obj->setWorkingDay($date)->setWorkingTime($interval); @@ -66,6 +99,9 @@ class WorkingHoursTest extends Unit $this->assertInstanceOf(DateInterval::class, $obj->getWorkingTime()); $this->assertEquals($interval, $obj->getWorkingTime()); + $this->assertIsBool($obj->isHomeOffice()); + $this->assertFalse($obj->isHomeOffice()); + $json = $obj->jsonSerialize(); $this->assertEquals($shouldJson, $json); } diff --git a/api/tests/unit/Models/WorkingHoursViewTest.php b/api/tests/unit/Models/WorkingHoursViewTest.php index ed79216..547259a 100644 --- a/api/tests/unit/Models/WorkingHoursViewTest.php +++ b/api/tests/unit/Models/WorkingHoursViewTest.php @@ -20,6 +20,7 @@ class WorkingHoursViewTest extends Unit { $date = new DateTime('2020-11-30'); $work_days = 15; + $home_work_days = 12; $total_hours = new DateInterval("PT32H22M"); $overtime = new DateInterval("PT22M"); $absence_time = clone $overtime; @@ -30,12 +31,14 @@ class WorkingHoursViewTest extends Unit PeriodDesignationEnum::WEEKLY, $work_days, $total_hours, + $home_work_days, $overtime, [ 'period' => '2020#49', 'periodDesignation' => 'weekly', 'workingDays' => 15, 'totalHours' => '32:22:00', + 'homeOfficeDays' => $home_work_days, 'overtime' => '00:22:00' ], ], @@ -44,12 +47,14 @@ class WorkingHoursViewTest extends Unit PeriodDesignationEnum::WEEKLY, $work_days, $total_hours, + $home_work_days, $absence_time, [ 'period' => '2020#49', 'periodDesignation' => 'weekly', 'workingDays' => 15, 'totalHours' => '32:22:00', + 'homeOfficeDays' => $home_work_days, 'overtime' => '-00:22:00' ], ], @@ -58,12 +63,14 @@ class WorkingHoursViewTest extends Unit PeriodDesignationEnum::MONTHLY, $work_days, $total_hours, + $home_work_days, $overtime, [ 'period' => '2020-11', 'periodDesignation' => 'monthly', 'workingDays' => 15, 'totalHours' => '32:22:00', + 'homeOfficeDays' => $home_work_days, 'overtime' => '00:22:00' ], ], @@ -72,12 +79,14 @@ class WorkingHoursViewTest extends Unit PeriodDesignationEnum::MONTHLY, $work_days, $total_hours, + $home_work_days, $absence_time, [ 'period' => '2020-11', 'periodDesignation' => 'monthly', 'workingDays' => 15, 'totalHours' => '32:22:00', + 'homeOfficeDays' => $home_work_days, 'overtime' => '-00:22:00' ], ], @@ -86,12 +95,14 @@ class WorkingHoursViewTest extends Unit PeriodDesignationEnum::YEARLY, $work_days, $total_hours, + $home_work_days, $overtime, [ 'period' => '2020', 'periodDesignation' => 'yearly', 'workingDays' => 15, 'totalHours' => '32:22:00', + 'homeOfficeDays' => $home_work_days, 'overtime' => '00:22:00' ], ], @@ -100,12 +111,14 @@ class WorkingHoursViewTest extends Unit PeriodDesignationEnum::YEARLY, $work_days, $total_hours, + $home_work_days, $absence_time, [ 'period' => '2020', 'periodDesignation' => 'yearly', 'workingDays' => 15, 'totalHours' => '32:22:00', + 'homeOfficeDays' => $home_work_days, 'overtime' => '-00:22:00' ], ], @@ -117,6 +130,7 @@ class WorkingHoursViewTest extends Unit * @param PeriodDesignationEnum $periodDesignation * @param int $workingDays * @param DateInterval $totalHours + * @param int $homeWorkDays , * @param DateInterval $overtime * @param array $shouldJson */ @@ -126,10 +140,11 @@ class WorkingHoursViewTest extends Unit PeriodDesignationEnum $periodDesignation, int $workingDays, DateInterval $totalHours, + int $homeWorkDays, DateInterval $overtime, array $shouldJson ): void { - $obj = new WorkingHoursView($period, $periodDesignation, $workingDays, $totalHours, $overtime); + $obj = new WorkingHoursView($period, $periodDesignation, $workingDays, $totalHours, $homeWorkDays, $overtime); $this->assertInstanceOf(DateTimeInterface::class, $obj->getPeriod()); $this->assertEquals($period, $obj->getPeriod()); @@ -142,6 +157,9 @@ class WorkingHoursViewTest extends Unit $this->assertInstanceOf(DateInterval::class, $obj->getTotalHours()); $this->assertEquals($totalHours, $obj->getTotalHours()); + $this->assertIsInt($obj->getHomeOfficeDays()); + $this->assertEquals($homeWorkDays, $obj->getHomeOfficeDays()); + $this->assertInstanceOf(DateInterval::class, $obj->getOvertime()); $this->assertEquals($overtime, $obj->getOvertime()); diff --git a/api/tests/unit/Repositories/WorkingHoursRepositoryTest.php b/api/tests/unit/Repositories/WorkingHoursRepositoryTest.php index 326caff..15ffcbb 100644 --- a/api/tests/unit/Repositories/WorkingHoursRepositoryTest.php +++ b/api/tests/unit/Repositories/WorkingHoursRepositoryTest.php @@ -157,8 +157,9 @@ class WorkingHoursRepositoryTest extends Unit { $repository = new WorkingHoursRepository($this->pdoObject); $model = $this->make(WorkingHours::class, [ - 'workingDay' => new DateTime(self::NEW_DATE), - 'workingTime' => new DateInterval(self::NEW_INTERVAL), + 'workingDay' => new DateTime(self::NEW_DATE), + 'workingTime' => new DateInterval(self::NEW_INTERVAL), + 'isHomeOffice' => false, ]); $repository->insert($model); $model = $repository->findByKey(self::NEW_DATE); @@ -174,8 +175,9 @@ class WorkingHoursRepositoryTest extends Unit { $repository = new WorkingHoursRepository($this->pdoObject); $model = $this->make(WorkingHours::class, [ - 'workingDay' => new DateTime(self::EXISTING_DATE), - 'workingTime' => new DateInterval(self::NEW_INTERVAL), + 'workingDay' => new DateTime(self::EXISTING_DATE), + 'workingTime' => new DateInterval(self::NEW_INTERVAL), + 'isHomeOffice' => false, ]); $this->expectException(RepositoryRecordAlreadyExistException::class); $repository->insert($model); @@ -201,8 +203,9 @@ class WorkingHoursRepositoryTest extends Unit { $repository = new WorkingHoursRepository($this->pdoObject); $model = $this->make(WorkingHours::class, [ - 'workingDay' => new DateTime(self::EXISTING_DATE), - 'workingTime' => new DateInterval(self::EXISTING_INTERVAL), + 'workingDay' => new DateTime(self::EXISTING_DATE), + 'workingTime' => new DateInterval(self::EXISTING_INTERVAL), + 'isHomeOffice' => false, ]); $repository->delete($model); $this->expectException(RepositoryRecordNotFoundException::class); @@ -217,8 +220,9 @@ class WorkingHoursRepositoryTest extends Unit { $repository = new WorkingHoursRepository($this->pdoObject); $model = $this->make(WorkingHours::class, [ - 'workingDay' => new DateTime(self::NEW_DATE), - 'workingTime' => new DateInterval(self::NEW_INTERVAL), + 'workingDay' => new DateTime(self::NEW_DATE), + 'workingTime' => new DateInterval(self::NEW_INTERVAL), + 'isHomeOffice' => false, ]); $this->expectException(RepositoryRecordNotFoundException::class); $repository->delete($model); @@ -244,8 +248,9 @@ class WorkingHoursRepositoryTest extends Unit { $repository = new WorkingHoursRepository($this->pdoObject); $model = $this->make(WorkingHours::class, [ - 'workingDay' => new DateTime(self::EXISTING_DATE), - 'workingTime' => new DateInterval(self::NEW_INTERVAL), + 'workingDay' => new DateTime(self::EXISTING_DATE), + 'workingTime' => new DateInterval(self::NEW_INTERVAL), + 'isHomeOffice' => false, ]); $repository->update($model); $model_db = $repository->findByKey(self::EXISTING_DATE); @@ -259,8 +264,9 @@ class WorkingHoursRepositoryTest extends Unit { $repository = new WorkingHoursRepository($this->pdoObject); $model = $this->make(WorkingHours::class, [ - 'workingDay' => new DateTime(self::NEW_DATE), - 'workingTime' => new DateInterval(self::NEW_INTERVAL), + 'workingDay' => new DateTime(self::NEW_DATE), + 'workingTime' => new DateInterval(self::NEW_INTERVAL), + 'isHomeOffice' => false, ]); $this->expectException(RepositoryRecordNotFoundException::class); $repository->update($model); diff --git a/api/tests/unit/Repositories/WorkingHoursViewRepositoryTest.php b/api/tests/unit/Repositories/WorkingHoursViewRepositoryTest.php index 14ebb54..f5c13fd 100644 --- a/api/tests/unit/Repositories/WorkingHoursViewRepositoryTest.php +++ b/api/tests/unit/Repositories/WorkingHoursViewRepositoryTest.php @@ -6,7 +6,8 @@ use Codeception\Attribute\DataProvider; use Codeception\Test\Unit; use DateTime; use DateTimeZone; -use PDO;use TorstenHettstedt\TimekeepingApi\Models\WorkingHoursView; +use PDO; +use TorstenHettstedt\TimekeepingApi\Models\WorkingHoursView; use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryRecordNotFoundException; use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursWeeklyViewRepository; use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursMonthlyViewRepository; @@ -14,12 +15,14 @@ use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursYearlyViewRepositor class WorkingHoursViewRepositoryTest extends Unit { - protected PDO $pdoObject; + protected PDO $pdoObject; protected function _before(): void { /** @noinspection SpellCheckingInspection */ - $this->pdoObject = new PDO('pgsql:host=psql.torsten-hettstedt.net;port=5432;dbname=testdb;user=bruce;password=mypass'); + $this->pdoObject = new PDO( + 'pgsql:host=psql.torsten-hettstedt.net;port=5432;dbname=testdb;user=bruce;password=mypass' + ); parent::_before(); } @@ -29,7 +32,7 @@ class WorkingHoursViewRepositoryTest extends Unit public function listObjectProvider(): array { return [ - [WorkingHoursYearlyViewRepository::class, 1], + [WorkingHoursYearlyViewRepository::class, 1], [WorkingHoursMonthlyViewRepository::class, 3], [WorkingHoursWeeklyViewRepository::class, 13], ]; @@ -41,9 +44,27 @@ class WorkingHoursViewRepositoryTest extends Unit public function existingObjectProvider(): array { return [ - [WorkingHoursYearlyViewRepository::class, '2020', 61, date_create('2020-01-01', new DateTimeZone('UCT'))], - [WorkingHoursMonthlyViewRepository::class, '2020 February', 20, date_create('2020-02-01', new DateTimeZone('UCT'))], - [WorkingHoursWeeklyViewRepository::class, '2020#03', 5, date_create('2020-01-13', new DateTimeZone('UCT'))], + [ + WorkingHoursYearlyViewRepository::class, + '2020', + 61, + date_create('2020-01-01', new DateTimeZone('UCT')), + 3, + ], + [ + WorkingHoursMonthlyViewRepository::class, + '2020 February', + 20, + date_create('2020-02-01', new DateTimeZone('UCT')), + 1, + ], + [ + WorkingHoursWeeklyViewRepository::class, + '2020#03', + 5, + date_create('2020-01-13', new DateTimeZone('UCT')), + 0, + ], ]; } @@ -53,9 +74,9 @@ class WorkingHoursViewRepositoryTest extends Unit public function notExistingObjectProvider(): array { return [ - [WorkingHoursYearlyViewRepository::class, '2022',], + [WorkingHoursYearlyViewRepository::class, '2022',], [WorkingHoursMonthlyViewRepository::class, '2020 May',], - [WorkingHoursWeeklyViewRepository::class, '2020#30',], + [WorkingHoursWeeklyViewRepository::class, '2020#30',], ]; } @@ -70,13 +91,19 @@ class WorkingHoursViewRepositoryTest extends Unit } #[DataProvider(('existingObjectProvider'))] - public function testExistingFindByKey(string $periodClass, string $search, int $workingDays, DateTime $period): void - { + public function testExistingFindByKey( + string $periodClass, + string $search, + int $workingDays, + DateTime $period, + int $homeOfficeDays + ): void { $repository = new $periodClass($this->pdoObject); $model = $repository->findByKey($search); $this->assertInstanceOf(WorkingHoursView::class, $model); $this->assertEquals($workingDays, $model->getWorkingDays()); $this->assertEquals($period->format('Ymd'), $model->getPeriod()->format('Ymd')); + $this->assertEquals($homeOfficeDays, $model->getHomeOfficeDays()); } #[DataProvider(('notExistingObjectProvider'))] -- 2.54.0 From 532c0d60e8c8b57370a18ee89cf6e9c531c2e921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torsten=20L=C3=BCcke?= Date: Thu, 19 Jun 2025 14:23:00 +0200 Subject: [PATCH 3/8] Erweitere die Klassen --- api/src/Controller/WorkingHoursController.php | 16 +++++++--- api/src/Models/WorkingHours.php | 21 ++++++++++--- api/src/Models/WorkingHoursView.php | 27 ++++++++-------- .../AbstractWorkingHoursViewRepository.php | 4 ++- .../WorkingHoursMonthlyViewRepository.php | 3 +- .../Repositories/WorkingHoursRepository.php | 31 ++++++++++++++----- .../WorkingHoursWeeklyViewRepository.php | 1 + .../WorkingHoursYearlyViewRepository.php | 1 + 8 files changed, 73 insertions(+), 31 deletions(-) diff --git a/api/src/Controller/WorkingHoursController.php b/api/src/Controller/WorkingHoursController.php index 7b14b97..1e50332 100644 --- a/api/src/Controller/WorkingHoursController.php +++ b/api/src/Controller/WorkingHoursController.php @@ -34,7 +34,7 @@ class WorkingHoursController extends AbstractController { $body = $request->getParsedBody(); $repository = new WorkingHoursRepository($this->databases); - $model = $this->buildModel($request, $args['id'], $body['workingTime']); + $model = $this->buildModel($request, $args['id'], $body['workingTime'], $body['isHomeOfficeDay'] ?? false); try { $repository->update($model); } catch (RepositoryRecordNotFoundException $exception) { @@ -120,7 +120,12 @@ class WorkingHoursController extends AbstractController { $body = $request->getParsedBody(); $repository = new WorkingHoursRepository($this->databases); - $model = $this->buildModel($request, $body['workingDay'], $body['workingTime']); + $model = $this->buildModel( + $request, + $body['workingDay'], + $body['workingTime'], + $body['isHomeOfficeDay'] ?? false + ); try { $repository->insert($model); } catch (RepositoryRecordAlreadyExistException $exception) { @@ -143,7 +148,7 @@ class WorkingHoursController extends AbstractController * @return WorkingHours * @throws HttpBadRequestException */ - protected function buildModel(Request $request, string $date, string $time): WorkingHours + protected function buildModel(Request $request, string $date, string $time, bool $isHomeOffice): WorkingHours { try { $workingDay = new DateTime($date); @@ -155,7 +160,10 @@ class WorkingHoursController extends AbstractController } catch (Exception $exception) { throw new HttpBadRequestException($request, 'Der Wert für die Zeit ist falsch', $exception); } - return (new WorkingHours())->setWorkingDay($workingDay)->setWorkingTime($workingTime); + return (new WorkingHours()) + ->setWorkingDay($workingDay) + ->setWorkingTime($workingTime) + ->setIsHomeOffice($isHomeOffice); } } \ No newline at end of file diff --git a/api/src/Models/WorkingHours.php b/api/src/Models/WorkingHours.php index e55e5b4..6e46e60 100644 --- a/api/src/Models/WorkingHours.php +++ b/api/src/Models/WorkingHours.php @@ -10,8 +10,9 @@ use JetBrains\PhpStorm\ArrayShape; class WorkingHours implements ModelInterface { - protected ?DateTime $workingDay = null; + protected ?DateTime $workingDay = null; protected ?DateInterval $workingTime = null; + protected bool $isHomeOffice = false; public function getWorkingDay(): ?DateTime { @@ -35,15 +36,27 @@ class WorkingHours implements ModelInterface return $this; } + public function isHomeOffice(): bool + { + return $this->isHomeOffice; + } + + public function setIsHomeOffice(bool $isHomeOffice): WorkingHours + { + $this->isHomeOffice = $isHomeOffice; + return $this; + } + /** * @return array */ - #[ArrayShape(['workingDay' => "string", 'workingTime' => "string"])] + #[ArrayShape(['workingDay' => "string", 'workingTime' => "string", 'isHomeOffice' => "boolean"])] public function jsonSerialize(): array { return [ - 'workingDay' => $this->getWorkingDay()->format('Y-m-d'), - 'workingTime' => $this->getWorkingTime()->format('%H:%I:%S') + 'workingDay' => $this->getWorkingDay()->format('Y-m-d'), + 'workingTime' => $this->getWorkingTime()->format('%H:%I:%S'), + 'isHomeOffice' => $this->isHomeOffice(), ]; } } \ No newline at end of file diff --git a/api/src/Models/WorkingHoursView.php b/api/src/Models/WorkingHoursView.php index 77bf15d..c8ae87b 100644 --- a/api/src/Models/WorkingHoursView.php +++ b/api/src/Models/WorkingHoursView.php @@ -16,6 +16,7 @@ class WorkingHoursView implements ModelInterface protected PeriodDesignationEnum $periodDesignation, protected int $workingDays, protected DateInterval $totalHours, + private int $homeOfficeDays, protected DateInterval $overtime ) { } @@ -35,6 +36,11 @@ class WorkingHoursView implements ModelInterface return $this->workingDays; } + public function getHomeOfficeDays(): int + { + return $this->homeOfficeDays; + } + public function getTotalHours(): DateInterval { return $this->totalHours; @@ -45,19 +51,13 @@ class WorkingHoursView implements ModelInterface return $this->overtime; } - /** - * @inheritDoc - * - * @return array - */ - #[ArrayShape([ - 'period' => "string", - 'periodDesignation' => "string", - 'totalHours' => "string", - 'workingDays' => "int", - 'overtime' => "string" - ])] - public function jsonSerialize(): array + #[ArrayShape(['period' => "string", + 'periodDesignation' => "string", + 'totalHours' => "string", + 'workingDays' => "int", + 'homeOfficeDays' => "int", + 'overtime' => "string" + ])] public function jsonSerialize(): array { $formatOvertime = (($this->getOvertime()->invert === 1) ? '-' : '') . '%H:%I:%S'; return [ @@ -65,6 +65,7 @@ class WorkingHoursView implements ModelInterface 'periodDesignation' => strtolower($this->getPeriodDesignation()->name), 'workingDays' => $this->getWorkingDays(), 'totalHours' => $this->getTotalHours()->format('%H:%I:%S'), + 'homeOfficeDays' => $this->getHomeOfficeDays(), 'overtime' => $this->getOvertime()->format($formatOvertime) ]; } diff --git a/api/src/Repositories/AbstractWorkingHoursViewRepository.php b/api/src/Repositories/AbstractWorkingHoursViewRepository.php index fbeb182..7ad6843 100644 --- a/api/src/Repositories/AbstractWorkingHoursViewRepository.php +++ b/api/src/Repositories/AbstractWorkingHoursViewRepository.php @@ -49,8 +49,9 @@ abstract class AbstractWorkingHoursViewRepository implements RepositoryReaderInt $model = new WorkingHoursView( $this->buildDateFromPeriod($row['period']), $this->periodDesignation, - (int)$row['workingDays'], + (int) $row['workingDays'], $this->buildDateInterval($row['totalHours']), + (int) $row['homeOfficeDays'], $this->buildDateInterval($row['overtime']) ); $models[] = $model; @@ -82,6 +83,7 @@ abstract class AbstractWorkingHoursViewRepository implements RepositoryReaderInt $this->periodDesignation, (int)$row['workingDays'], $this->buildDateInterval($row['totalHours']), + (int)$row['homeOfficeDays'], $this->buildDateInterval($row['overtime']) ); } diff --git a/api/src/Repositories/WorkingHoursMonthlyViewRepository.php b/api/src/Repositories/WorkingHoursMonthlyViewRepository.php index f20e1eb..6a478ed 100644 --- a/api/src/Repositories/WorkingHoursMonthlyViewRepository.php +++ b/api/src/Repositories/WorkingHoursMonthlyViewRepository.php @@ -12,11 +12,12 @@ use TorstenHettstedt\TimekeepingApi\Models\PeriodDesignationEnum; class WorkingHoursMonthlyViewRepository extends AbstractWorkingHoursViewRepository { - protected const string SQL_SELECT = <<setWorkingDay(new DateTime($row['workingDay'])) - ->setWorkingTime(new DateInterval('P0000-00-00T' . $row['workingTime'])); + ->setWorkingTime(new DateInterval('P0000-00-00T' . $row['workingTime'])) + ->setIsHomeOffice($row['isHomeOfficeDay']); $models[] = $model; } return $models; @@ -130,7 +132,8 @@ SQL; $model = new WorkingHours(); $model ->setWorkingDay(new DateTime($row['workingDay'])) - ->setWorkingTime(new DateInterval('P0000-00-00T' . $row['workingTime'])); + ->setWorkingTime(new DateInterval('P0000-00-00T' . $row['workingTime'])) + ->setIsHomeOffice($row['isHomeOfficeDay']); return $model; } @@ -145,6 +148,7 @@ SQL; if ($model instanceof WorkingHours) { $workingDay = $model->getWorkingDay()->format('Y-m-d'); $workingTime = $model->getWorkingTime()->format('%H:%I:%S'); + $isHomeOffice = $model->isHomeOffice(); } else { throw new InvalidArgumentException('Es wird ein Modell der Klasse "' . WorkingHours::class . '" verlangt.'); } @@ -154,11 +158,15 @@ SQL; throw new RepositoryRecordAlreadyExistException(); } /** @noinspection PhpUnusedLocalVariableInspection */ catch (RepositoryRecordNotFoundException $exception) { - $stmt = $this->database->prepare( - 'insert into public."Arbeitszeiten" ("Datum", "Arbeitszeit") values (?, ?) on conflict do nothing' - ); + $stmt = $this->database->prepare(<<<'SQL' +insert into public."Arbeitszeiten" ("Datum", "Arbeitszeit", "HomeOffice") +values (?, ?, ?) +on conflict do nothing +SQL +); $stmt->bindParam(1, $workingDay); $stmt->bindParam(2, $workingTime); + $stmt->bindParam(3, $isHomeOffice, PDO::PARAM_BOOL); $stmt->execute(); } } @@ -173,12 +181,19 @@ SQL; if ($model instanceof WorkingHours) { $workingDay = $model->getWorkingDay()->format('Y-m-d'); $workingTime = $model->getWorkingTime()->format('%H:%I:%S'); + $isHomeOffice = $model->isHomeOffice(); } else { throw new InvalidArgumentException('Es wird ein Modell der Klasse "' . WorkingHours::class . '" verlangt.'); } - $stmt = $this->database->prepare('update public."Arbeitszeiten" set "Arbeitszeit" = ? where "Datum" = ?'); + $stmt = $this->database->prepare(<<<'SQL' +update public."Arbeitszeiten" +set "Arbeitszeit" = ?, "HomeOffice" = ? +where "Datum" = ? +SQL +); $stmt->bindParam(1, $workingTime); - $stmt->bindParam(2, $workingDay); + $stmt->bindParam(2, $isHomeOffice, PDO::PARAM_BOOL); + $stmt->bindParam(3, $workingDay); $stmt->execute(); if ($stmt->rowCount() < 1) { throw new RepositoryRecordNotFoundException(); diff --git a/api/src/Repositories/WorkingHoursWeeklyViewRepository.php b/api/src/Repositories/WorkingHoursWeeklyViewRepository.php index a0627fc..6a4541a 100644 --- a/api/src/Repositories/WorkingHoursWeeklyViewRepository.php +++ b/api/src/Repositories/WorkingHoursWeeklyViewRepository.php @@ -16,6 +16,7 @@ class WorkingHoursWeeklyViewRepository extends AbstractWorkingHoursViewRepositor "Woche" as "period", "Gesamtarbeitszeit" as "totalHours", "Arbeitstage" as "workingDays", + "Home-Office-Tage" as "homeOfficeDays", "Überstunden" as "overtime" from "Arbeitszeiten - Woche" SQL; diff --git a/api/src/Repositories/WorkingHoursYearlyViewRepository.php b/api/src/Repositories/WorkingHoursYearlyViewRepository.php index 7a01e94..ff2cf03 100644 --- a/api/src/Repositories/WorkingHoursYearlyViewRepository.php +++ b/api/src/Repositories/WorkingHoursYearlyViewRepository.php @@ -16,6 +16,7 @@ class WorkingHoursYearlyViewRepository extends AbstractWorkingHoursViewRepositor "Jahr" as "period", "Gesamtarbeitszeit" as "totalHours", "Arbeitstage" as "workingDays", + "Home-Office-Tage" as "homeOfficeDays", "Überstunden" as "overtime" from "Arbeitszeiten - Jahr" SQL; -- 2.54.0 From 9f53b627c02ed9e4ee7cf78f12a51872e17a55d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torsten=20L=C3=BCcke?= Date: Thu, 19 Jun 2025 14:28:15 +0200 Subject: [PATCH 4/8] Erweitere die Tests --- api/tests/_support/Helper/Api.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/tests/_support/Helper/Api.php b/api/tests/_support/Helper/Api.php index ec273e2..bef9880 100644 --- a/api/tests/_support/Helper/Api.php +++ b/api/tests/_support/Helper/Api.php @@ -24,7 +24,7 @@ class Api extends Module "periodDesignation" => "string", "totalHours" => self::FORMAT_TIME, "workingDays" => 'integer:>0', - 'homeOfficeDays' => 'number:>=0', + 'homeOfficeDays' => 'integer:>=0', "overtime" => self::FORMAT_TIME, ]; -- 2.54.0 From 2dcbb6fdbdb314cb2e4a2eb433dbc6d73497ef6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torsten=20L=C3=BCcke?= Date: Thu, 19 Jun 2025 14:30:23 +0200 Subject: [PATCH 5/8] Erweitere die Tests --- .../api/WorkingHours/CreateWorkingHoursCest.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/api/tests/api/WorkingHours/CreateWorkingHoursCest.php b/api/tests/api/WorkingHours/CreateWorkingHoursCest.php index 9305a9b..ddd1ad9 100644 --- a/api/tests/api/WorkingHours/CreateWorkingHoursCest.php +++ b/api/tests/api/WorkingHours/CreateWorkingHoursCest.php @@ -36,6 +36,21 @@ class CreateWorkingHoursCest $I->seeResponseMatchesJsonType(Api::WORKING_HOURS_JSON_FORMAT); } + public function createWorkingHoursWithNewValidRecordWithoutHomeOffice(ApiTester $I): void + { + $I->haveHttpHeader('accept', 'application/json'); + $I->haveHttpHeader('content-type', 'application/json'); + $I->haveHttpHeader('Origin', Api::TEST_ORIGIN); + $I->sendPost('/working-hours', [ + 'workingDay' => '2020-04-01', + 'workingTime' => '08:00:00', + ]); + $I->seeResponseCodeIs(HttpCode::CREATED); + $I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN); + $I->seeResponseIsJson(); + $I->seeResponseMatchesJsonType(Api::WORKING_HOURS_JSON_FORMAT); + } + // tests public function createWorkingHoursWithNewInvalidRecord(ApiTester $I): void { -- 2.54.0 From 4d91a0443003db1d64c165d4f3f17be17208c8d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torsten=20L=C3=BCcke?= Date: Fri, 20 Jun 2025 12:59:18 +0200 Subject: [PATCH 6/8] Erweitere die Tests --- api/src/Controller/WorkingHoursController.php | 33 +--------------- .../Controller/WorkingHoursControllerTest.php | 38 +++++++++---------- 2 files changed, 20 insertions(+), 51 deletions(-) diff --git a/api/src/Controller/WorkingHoursController.php b/api/src/Controller/WorkingHoursController.php index 1e50332..539da32 100644 --- a/api/src/Controller/WorkingHoursController.php +++ b/api/src/Controller/WorkingHoursController.php @@ -21,12 +21,6 @@ class WorkingHoursController extends AbstractController { /** - * @param Request $request - * @param Response $response - * @param array $args - * - * @return Response - * * @throws HttpBadRequestException * @throws HttpNotFoundException */ @@ -34,7 +28,7 @@ class WorkingHoursController extends AbstractController { $body = $request->getParsedBody(); $repository = new WorkingHoursRepository($this->databases); - $model = $this->buildModel($request, $args['id'], $body['workingTime'], $body['isHomeOfficeDay'] ?? false); + $model = $this->buildModel($request, $args['id'], $body['workingTime'], $body['isHomeOffice'] ?? false); try { $repository->update($model); } catch (RepositoryRecordNotFoundException $exception) { @@ -44,12 +38,6 @@ class WorkingHoursController extends AbstractController } /** - * @param Request $request - * @param Response $response - * @param array $args - * - * @return Response - * * @throws HttpInternalServerErrorException * @throws HttpBadRequestException * @noinspection PhpUnusedParameterInspection @@ -79,12 +67,6 @@ class WorkingHoursController extends AbstractController } /** - * @param Request $request - * @param Response $response - * @param array $args - * - * @return Response - * * @throws HttpNotFoundException * @throws HttpInternalServerErrorException */ @@ -105,12 +87,6 @@ class WorkingHoursController extends AbstractController } /** - * @param Request $request - * @param Response $response - * @param array $args - * - * @return Response - * * @throws HttpBadRequestException * @throws HttpConflictRequestException * @throws HttpInternalServerErrorException @@ -124,7 +100,7 @@ class WorkingHoursController extends AbstractController $request, $body['workingDay'], $body['workingTime'], - $body['isHomeOfficeDay'] ?? false + $body['isHomeOffice'] ?? false ); try { $repository->insert($model); @@ -141,11 +117,6 @@ class WorkingHoursController extends AbstractController } /** - * @param Request $request - * @param string $date - * @param string $time - * - * @return WorkingHours * @throws HttpBadRequestException */ protected function buildModel(Request $request, string $date, string $time, bool $isHomeOffice): WorkingHours diff --git a/api/tests/unit/Controller/WorkingHoursControllerTest.php b/api/tests/unit/Controller/WorkingHoursControllerTest.php index 845f381..3e2919a 100644 --- a/api/tests/unit/Controller/WorkingHoursControllerTest.php +++ b/api/tests/unit/Controller/WorkingHoursControllerTest.php @@ -4,6 +4,7 @@ namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Controller; use Codeception\Attribute\DataProvider; use Exception; +use PHPUnit\Framework\MockObject\Exception as MockException; use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; @@ -25,16 +26,16 @@ class WorkingHoursControllerTest extends AbstractControllerTest /** * @throws Exception - * @throws \PHPUnit\Framework\MockObject\Exception + * @throws MockException */ protected function _before(): void { parent::_before(); $this->request = $this->makeEmpty(Request::class, [ 'getParsedBody' => [ - 'workingDay' => self::EXISTING_DATE, - 'workingTime' => self::EXISTING_INTERVAL, - 'isHomeOfficeDay' => true, + 'workingDay' => self::EXISTING_DATE, + 'workingTime' => self::EXISTING_INTERVAL, + 'isHomeOffice' => true, ], ]); } @@ -271,8 +272,8 @@ class WorkingHoursControllerTest extends AbstractControllerTest { $this->request = $this->makeEmpty(Request::class, [ 'getParsedBody' => [ - 'workingDay' => self::EXISTING_DATE, - 'workingTime' => self::EXISTING_INTERVAL, + 'workingDay' => self::EXISTING_DATE, + 'workingTime' => self::EXISTING_INTERVAL, ], ]); $controller = new WorkingHoursController($this->container); @@ -316,9 +317,6 @@ class WorkingHoursControllerTest extends AbstractControllerTest } /** - * @param string $date - * @param string $time - * * @throws ContainerExceptionInterface * @throws NotDatabasesException * @throws NotFoundExceptionInterface @@ -329,9 +327,9 @@ class WorkingHoursControllerTest extends AbstractControllerTest { $this->request = $this->makeEmpty(Request::class, [ 'getParsedBody' => [ - 'workingDay' => $date, - 'workingTime' => $time, - 'isHomeOfficeDay' => $isHomeOffice, + 'workingDay' => $date, + 'workingTime' => $time, + 'isHomeOffice' => $isHomeOffice, ], ]); $controller = new WorkingHoursController($this->container); @@ -349,9 +347,9 @@ class WorkingHoursControllerTest extends AbstractControllerTest { $this->request = $this->makeEmpty(Request::class, [ 'getParsedBody' => [ - 'workingDay' => self::NEW_DATE, - 'workingTime' => self::NEW_INTERVAL, - 'isHomeOfficeDay' => true, + 'workingDay' => self::NEW_DATE, + 'workingTime' => self::NEW_INTERVAL, + 'isHomeOffice' => true, ], ]); $controller = new WorkingHoursController($this->container); @@ -370,8 +368,8 @@ class WorkingHoursControllerTest extends AbstractControllerTest { $this->request = $this->makeEmpty(Request::class, [ 'getParsedBody' => [ - 'workingDay' => self::NEW_DATE, - 'workingTime' => self::NEW_INTERVAL, + 'workingDay' => self::NEW_DATE, + 'workingTime' => self::NEW_INTERVAL, ], ]); $controller = new WorkingHoursController($this->container); @@ -390,9 +388,9 @@ class WorkingHoursControllerTest extends AbstractControllerTest { $this->request = $this->makeEmpty(Request::class, [ 'getParsedBody' => [ - 'workingDay' => self::EXISTING_DATE, - 'workingTime' => self::EXISTING_INTERVAL, - 'isHomeOfficeDay' => true, + 'workingDay' => self::EXISTING_DATE, + 'workingTime' => self::EXISTING_INTERVAL, + 'isHomeOffice' => true, ], ]); $controller = new WorkingHoursController($this->container); -- 2.54.0 From dbca8ad5708ff537d1994bfe9d7b70dbef88360f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torsten=20L=C3=BCcke?= Date: Fri, 20 Jun 2025 19:43:04 +0200 Subject: [PATCH 7/8] =?UTF-8?q?Eintragung=20von=20HomeOffice=20ist=20?= =?UTF-8?q?=C3=BCber=20die=20Oberfl=C3=A4che=20m=C3=B6glich?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resources/test-data/report-1672928012.CSV | 2 +- ui/src/lib/Models.svelte.js | 3 +- ui/src/lib/Views.svelte | 2 + ui/src/routes/working-hours/+page.server.js | 19 ++++++++-- ui/src/routes/working-hours/+page.svelte | 8 ++-- .../working-hours/[working_day]/+page.svelte | 8 +++- .../working-hours/create-by-day/+page.svelte | 4 ++ .../working-hours/create-by-week/+page.svelte | 38 ++++++++++++++++++- .../working-hours/import-csv/+page.svelte | 18 +++++++-- 9 files changed, 87 insertions(+), 15 deletions(-) diff --git a/resources/test-data/report-1672928012.CSV b/resources/test-data/report-1672928012.CSV index daf9c3a..d13b8a7 100755 --- a/resources/test-data/report-1672928012.CSV +++ b/resources/test-data/report-1672928012.CSV @@ -23,7 +23,7 @@ Arbeit;04:16;2021-07-22 07:55;2021-07-22 12:11; Arbeit;03:11;2021-07-21 13:37;2021-07-21 16:48; Mittagspause ;01:07;2021-07-21 12:29;2021-07-21 13:37; Arbeit;04:58;2021-07-21 07:31;2021-07-21 12:29; -Arbeit;01:17;2021-07-20 18:44;2021-07-20 20:02; +Verkehr;01:17;2021-07-20 18:44;2021-07-20 20:02; Arbeit;03:50;2021-07-20 13:14;2021-07-20 17:04; Mittagspause ;00:41;2021-07-20 12:32;2021-07-20 13:14; Arbeit;04:55;2021-07-20 07:37;2021-07-20 12:32; diff --git a/ui/src/lib/Models.svelte.js b/ui/src/lib/Models.svelte.js index 099129a..f9bc552 100644 --- a/ui/src/lib/Models.svelte.js +++ b/ui/src/lib/Models.svelte.js @@ -1,9 +1,10 @@ export class PeriodRecord { /** - * @param {{ workingDays: Number; totalHours: any; overtime: string; period: string; }} record + * @param {{ workingDays: Number; totalHours: any; overtime: string; period: string; homeOfficeDays: Number }} record */ constructor(record) { this.workingDays = Number(record.workingDays) + this.homeOfficeDays = Number(record.homeOfficeDays) this.totalHours = String(record.totalHours) this.period = String(record.period) this.overtime = String('##:##:##') diff --git a/ui/src/lib/Views.svelte b/ui/src/lib/Views.svelte index 6b4556a..038a7f7 100644 --- a/ui/src/lib/Views.svelte +++ b/ui/src/lib/Views.svelte @@ -43,6 +43,7 @@ Zeitraum Arbeitstage + davon HomeOffice Arbeitsstunden Über- / Unterstunden @@ -52,6 +53,7 @@ {record.period} {record.workingDays} + {record.homeOfficeDays} {record.totalHours} {record.overtime} diff --git a/ui/src/routes/working-hours/+page.server.js b/ui/src/routes/working-hours/+page.server.js index 6656eaa..b0a6eb4 100644 --- a/ui/src/routes/working-hours/+page.server.js +++ b/ui/src/routes/working-hours/+page.server.js @@ -34,6 +34,7 @@ export const actions = { const body_data = { workingDay: data.get('workingDay'), workingTime: data.get('workingTime'), + isHomeOffice: data.get('isHomeOffice'), } await WorkingHoursRepository.update(env.API_URL, data.get('workingDay'), body_data); }, @@ -42,6 +43,7 @@ export const actions = { const body_data = { workingDay: form_data.get('workingDay'), workingTime: form_data.get('workingTime'), + isHomeOffice: form_data.get('isHomeOffice'), } await WorkingHoursRepository.add(env.API_URL, body_data); }, @@ -56,13 +58,19 @@ export const actions = { actualDayObject.setDate(actualDayObject.getDate() + 1) }) for (const pair of form_data.entries()) { - const working_day = day_list[pair[0]] ?? null - if (working_day === null) { + let key = /(?\w+)\[time]/.exec(pair[0]) + if (key === null) { + continue + } + let day = key?.groups?.day ?? '' + const working_day = day_list[day] ?? null + if (working_day === null || pair[1] === '00:00:00' || pair[1] === '00:00') { continue } const body_data = { workingDay: working_day, workingTime: pair[1], + isHomeOffice: form_data.has(day + '[isHomeOffice]') } await WorkingHoursRepository.addOrUpdate(env.API_URL, working_day, body_data); } @@ -70,10 +78,15 @@ export const actions = { 'import-csv': async ({ request }) => { const form_data = await request.formData(); for (const pair of form_data.entries()) { - const working_day = pair[0] + let key = /^(?\d{4}-\d{2}-\d{2})\[time]$/.exec(pair[0]) + if (key === null) { + continue + } + let working_day = key?.groups?.day ?? '' const body_data = { workingDay: working_day, workingTime: pair[1], + isHomeOffice: form_data.has(working_day + '[isHomeOffice]') } await WorkingHoursRepository.addOrUpdate(env.API_URL, working_day, body_data); } diff --git a/ui/src/routes/working-hours/+page.svelte b/ui/src/routes/working-hours/+page.svelte index ee61cef..979449f 100644 --- a/ui/src/routes/working-hours/+page.svelte +++ b/ui/src/routes/working-hours/+page.svelte @@ -4,11 +4,11 @@ import IconifyIcon from "@iconify/svelte"; let { data } = $props(); const TITLE = "Bearbeitung Einträge" - /** @type {{workingDay: String, workingTime: String, }[]|null} **/ + /** @type {{workingDay: String, workingTime: String, isHomeOffice: Boolean, }[]|null} **/ let records = $state([]) const init = async () => await data.promise.then( /** - * @param {{workingDay: String, workingTime: String, }[]} data + * @param {{workingDay: String, workingTime: String, isHomeOffice: Boolean, }[]} data */ data => { records = data @@ -53,14 +53,16 @@ Datum Arbeitsstunden + Zu Hause gearbeitet? Aktionen {#each records as record} - + {record.workingDay} {record.workingTime} + {record.isHomeOffice ? 'ja' : ''}