15 Commits
Author SHA1 Message Date
TorstenHettstedt dcce40113d Merge pull request 'pagination-table' (#33) from pagination-table into master
Reviewed-on: #33
2025-06-27 12:36:05 +02:00
TorstenHettstedt 0e1244c02f Bestimmung der Zeilenanzahl hinzufügen 2025-06-27 12:23:26 +02:00
TorstenHettstedt b4d30066b9 Pagination bei der Tabelle der Einträge 2025-06-27 12:20:21 +02:00
TorstenHettstedt ab9bccf532 Vereinfache den Aufruf der Anzeige der Arbeitszeitdaten 2025-06-27 11:49:17 +02:00
TorstenHettstedt 4acf1275a0 Erstelle eine Pagination-Komponente für *Svelte* 2025-06-27 11:49:17 +02:00
TorstenHettstedt 6fb8f87c1c Merge pull request 'home-office' (#32) from home-office into master
Reviewed-on: #32
2025-06-27 11:43:57 +02:00
TorstenHettstedt 1d53db71e7 Warum war es wieder weg 2025-06-27 11:42:54 +02:00
TorstenHettstedt dbca8ad570 Eintragung von HomeOffice ist über die Oberfläche möglich 2025-06-20 19:43:04 +02:00
TorstenHettstedt 4d91a04430 Erweitere die Tests 2025-06-20 12:59:18 +02:00
TorstenHettstedt 2dcbb6fdbd Erweitere die Tests 2025-06-19 14:30:23 +02:00
TorstenHettstedt 9f53b627c0 Erweitere die Tests 2025-06-19 14:28:15 +02:00
TorstenHettstedt 532c0d60e8 Erweitere die Klassen 2025-06-19 14:23:00 +02:00
TorstenHettstedt cf6eff6ab0 Erweitere die Tests 2025-06-19 14:19:09 +02:00
TorstenHettstedt a964663f51 Erweitere die Datenbank 2025-06-19 14:18:21 +02:00
TorstenHettstedt a9c455381d Merge pull request 'refactor/update' (#30) from refactor/update into master
Reviewed-on: #30
2025-06-16 15:48:33 +02:00
32 changed files with 616 additions and 257 deletions
+9 -1
View File
@@ -4,7 +4,7 @@
"title": "Zeiterfassung App", "title": "Zeiterfassung App",
"contact": { "contact": {
"name": "Torsten Lücke", "name": "Torsten Lücke",
"url": "http://torsten-hettstedt.de", "url": "https://torsten-hettstedt.de",
"email": "arbeit@torsten-hettstedt.de" "email": "arbeit@torsten-hettstedt.de"
}, },
"license": { "license": {
@@ -188,6 +188,10 @@
"type": "string", "type": "string",
"pattern": "[0-5]\\d:[0-5]\\d:[0-5]\\d", "pattern": "[0-5]\\d:[0-5]\\d:[0-5]\\d",
"example": "08:01:00" "example": "08:01:00"
},
"isHomeOffice": {
"description": "Wurde an diesem Tag zuhause gearbeitet?",
"type": "boolean"
} }
} }
}, },
@@ -217,6 +221,10 @@
"description": "Arbeitstage im Zeitraum", "description": "Arbeitstage im Zeitraum",
"type": "integer" "type": "integer"
}, },
"homeOfficeDays": {
"description": "Anzahl der Tage, an denen zuhause gearbeitet wurde.",
"type": "number"
},
"overtime": { "overtime": {
"description": "Überstunden im Zeitraum", "description": "Überstunden im Zeitraum",
"type": "string", "type": "string",
+12 -33
View File
@@ -21,12 +21,6 @@ class WorkingHoursController extends AbstractController
{ {
/** /**
* @param Request $request
* @param Response $response
* @param array<string, mixed> $args
*
* @return Response
*
* @throws HttpBadRequestException * @throws HttpBadRequestException
* @throws HttpNotFoundException * @throws HttpNotFoundException
*/ */
@@ -34,7 +28,7 @@ class WorkingHoursController extends AbstractController
{ {
$body = $request->getParsedBody(); $body = $request->getParsedBody();
$repository = new WorkingHoursRepository($this->databases); $repository = new WorkingHoursRepository($this->databases);
$model = $this->buildModel($request, $args['id'], $body['workingTime']); $model = $this->buildModel($request, $args['id'], $body['workingTime'], $body['isHomeOffice'] ?? false);
try { try {
$repository->update($model); $repository->update($model);
} catch (RepositoryRecordNotFoundException $exception) { } catch (RepositoryRecordNotFoundException $exception) {
@@ -44,12 +38,6 @@ class WorkingHoursController extends AbstractController
} }
/** /**
* @param Request $request
* @param Response $response
* @param array<string, mixed> $args
*
* @return Response
*
* @throws HttpInternalServerErrorException * @throws HttpInternalServerErrorException
* @throws HttpBadRequestException * @throws HttpBadRequestException
* @noinspection PhpUnusedParameterInspection * @noinspection PhpUnusedParameterInspection
@@ -79,12 +67,6 @@ class WorkingHoursController extends AbstractController
} }
/** /**
* @param Request $request
* @param Response $response
* @param array<string, mixed> $args
*
* @return Response
*
* @throws HttpNotFoundException * @throws HttpNotFoundException
* @throws HttpInternalServerErrorException * @throws HttpInternalServerErrorException
*/ */
@@ -105,12 +87,6 @@ class WorkingHoursController extends AbstractController
} }
/** /**
* @param Request $request
* @param Response $response
* @param array<string, mixed> $args
*
* @return Response
*
* @throws HttpBadRequestException * @throws HttpBadRequestException
* @throws HttpConflictRequestException * @throws HttpConflictRequestException
* @throws HttpInternalServerErrorException * @throws HttpInternalServerErrorException
@@ -120,7 +96,12 @@ class WorkingHoursController extends AbstractController
{ {
$body = $request->getParsedBody(); $body = $request->getParsedBody();
$repository = new WorkingHoursRepository($this->databases); $repository = new WorkingHoursRepository($this->databases);
$model = $this->buildModel($request, $body['workingDay'], $body['workingTime']); $model = $this->buildModel(
$request,
$body['workingDay'],
$body['workingTime'],
$body['isHomeOffice'] ?? false
);
try { try {
$repository->insert($model); $repository->insert($model);
} catch (RepositoryRecordAlreadyExistException $exception) { } catch (RepositoryRecordAlreadyExistException $exception) {
@@ -136,14 +117,9 @@ class WorkingHoursController extends AbstractController
} }
/** /**
* @param Request $request
* @param string $date
* @param string $time
*
* @return WorkingHours
* @throws HttpBadRequestException * @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 { try {
$workingDay = new DateTime($date); $workingDay = new DateTime($date);
@@ -155,7 +131,10 @@ class WorkingHoursController extends AbstractController
} catch (Exception $exception) { } catch (Exception $exception) {
throw new HttpBadRequestException($request, 'Der Wert für die Zeit ist falsch', $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);
} }
} }
+15 -2
View File
@@ -12,6 +12,7 @@ class WorkingHours implements ModelInterface
{ {
protected ?DateTime $workingDay = null; protected ?DateTime $workingDay = null;
protected ?DateInterval $workingTime = null; protected ?DateInterval $workingTime = null;
protected bool $isHomeOffice = false;
public function getWorkingDay(): ?DateTime public function getWorkingDay(): ?DateTime
{ {
@@ -35,15 +36,27 @@ class WorkingHours implements ModelInterface
return $this; return $this;
} }
public function isHomeOffice(): bool
{
return $this->isHomeOffice;
}
public function setIsHomeOffice(bool $isHomeOffice): WorkingHours
{
$this->isHomeOffice = $isHomeOffice;
return $this;
}
/** /**
* @return array<string, string> * @return array<string, string>
*/ */
#[ArrayShape(['workingDay' => "string", 'workingTime' => "string"])] #[ArrayShape(['workingDay' => "string", 'workingTime' => "string", 'isHomeOffice' => "boolean"])]
public function jsonSerialize(): array public function jsonSerialize(): array
{ {
return [ return [
'workingDay' => $this->getWorkingDay()->format('Y-m-d'), 'workingDay' => $this->getWorkingDay()->format('Y-m-d'),
'workingTime' => $this->getWorkingTime()->format('%H:%I:%S') 'workingTime' => $this->getWorkingTime()->format('%H:%I:%S'),
'isHomeOffice' => $this->isHomeOffice(),
]; ];
} }
} }
+10 -9
View File
@@ -16,6 +16,7 @@ class WorkingHoursView implements ModelInterface
protected PeriodDesignationEnum $periodDesignation, protected PeriodDesignationEnum $periodDesignation,
protected int $workingDays, protected int $workingDays,
protected DateInterval $totalHours, protected DateInterval $totalHours,
private int $homeOfficeDays,
protected DateInterval $overtime protected DateInterval $overtime
) { ) {
} }
@@ -35,6 +36,11 @@ class WorkingHoursView implements ModelInterface
return $this->workingDays; return $this->workingDays;
} }
public function getHomeOfficeDays(): int
{
return $this->homeOfficeDays;
}
public function getTotalHours(): DateInterval public function getTotalHours(): DateInterval
{ {
return $this->totalHours; return $this->totalHours;
@@ -45,19 +51,13 @@ class WorkingHoursView implements ModelInterface
return $this->overtime; return $this->overtime;
} }
/** #[ArrayShape(['period' => "string",
* @inheritDoc
*
* @return array<string, mixed>
*/
#[ArrayShape([
'period' => "string",
'periodDesignation' => "string", 'periodDesignation' => "string",
'totalHours' => "string", 'totalHours' => "string",
'workingDays' => "int", 'workingDays' => "int",
'homeOfficeDays' => "int",
'overtime' => "string" 'overtime' => "string"
])] ])] public function jsonSerialize(): array
public function jsonSerialize(): array
{ {
$formatOvertime = (($this->getOvertime()->invert === 1) ? '-' : '') . '%H:%I:%S'; $formatOvertime = (($this->getOvertime()->invert === 1) ? '-' : '') . '%H:%I:%S';
return [ return [
@@ -65,6 +65,7 @@ class WorkingHoursView implements ModelInterface
'periodDesignation' => strtolower($this->getPeriodDesignation()->name), 'periodDesignation' => strtolower($this->getPeriodDesignation()->name),
'workingDays' => $this->getWorkingDays(), 'workingDays' => $this->getWorkingDays(),
'totalHours' => $this->getTotalHours()->format('%H:%I:%S'), 'totalHours' => $this->getTotalHours()->format('%H:%I:%S'),
'homeOfficeDays' => $this->getHomeOfficeDays(),
'overtime' => $this->getOvertime()->format($formatOvertime) 'overtime' => $this->getOvertime()->format($formatOvertime)
]; ];
} }
@@ -17,8 +17,8 @@ use TorstenHettstedt\TimekeepingApi\Models\WorkingHoursView;
abstract class AbstractWorkingHoursViewRepository implements RepositoryReaderInterface abstract class AbstractWorkingHoursViewRepository implements RepositoryReaderInterface
{ {
protected const SQL_SELECT = ''; protected const string SQL_SELECT = '';
protected const SQL_WHERE = ''; protected const string SQL_WHERE = '';
protected PeriodDesignationEnum $periodDesignation; protected PeriodDesignationEnum $periodDesignation;
@@ -49,8 +49,9 @@ abstract class AbstractWorkingHoursViewRepository implements RepositoryReaderInt
$model = new WorkingHoursView( $model = new WorkingHoursView(
$this->buildDateFromPeriod($row['period']), $this->buildDateFromPeriod($row['period']),
$this->periodDesignation, $this->periodDesignation,
(int)$row['workingDays'], (int) $row['workingDays'],
$this->buildDateInterval($row['totalHours']), $this->buildDateInterval($row['totalHours']),
(int) $row['homeOfficeDays'],
$this->buildDateInterval($row['overtime']) $this->buildDateInterval($row['overtime'])
); );
$models[] = $model; $models[] = $model;
@@ -82,6 +83,7 @@ abstract class AbstractWorkingHoursViewRepository implements RepositoryReaderInt
$this->periodDesignation, $this->periodDesignation,
(int)$row['workingDays'], (int)$row['workingDays'],
$this->buildDateInterval($row['totalHours']), $this->buildDateInterval($row['totalHours']),
(int)$row['homeOfficeDays'],
$this->buildDateInterval($row['overtime']) $this->buildDateInterval($row['overtime'])
); );
} }
@@ -17,6 +17,7 @@ class WorkingHoursMonthlyViewRepository extends AbstractWorkingHoursViewReposito
"Monat" as "period", "Monat" as "period",
"Gesamtarbeitszeit" as "totalHours", "Gesamtarbeitszeit" as "totalHours",
"Arbeitstage" as "workingDays", "Arbeitstage" as "workingDays",
"Home-Office-Tage" as "homeOfficeDays",
"Überstunden" as "overtime" "Überstunden" as "overtime"
from "Arbeitszeiten - Monat" from "Arbeitszeiten - Monat"
SQL; SQL;
@@ -23,7 +23,8 @@ class WorkingHoursRepository implements RepositoryReaderInterface, RepositoryWri
protected const string SQL_SELECT = <<<'SQL' protected const string SQL_SELECT = <<<'SQL'
select select
"Datum" as "workingDay", "Datum" as "workingDay",
"Arbeitszeit" as "workingTime" "Arbeitszeit" as "workingTime",
"HomeOffice" as "isHomeOfficeDay"
from public."Arbeitszeiten" from public."Arbeitszeiten"
SQL; SQL;
@@ -73,7 +74,8 @@ SQL;
$model = new WorkingHours(); $model = new WorkingHours();
$model $model
->setWorkingDay(new DateTime($row['workingDay'])) ->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; $models[] = $model;
} }
return $models; return $models;
@@ -130,7 +132,8 @@ SQL;
$model = new WorkingHours(); $model = new WorkingHours();
$model $model
->setWorkingDay(new DateTime($row['workingDay'])) ->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; return $model;
} }
@@ -145,6 +148,7 @@ SQL;
if ($model instanceof WorkingHours) { if ($model instanceof WorkingHours) {
$workingDay = $model->getWorkingDay()->format('Y-m-d'); $workingDay = $model->getWorkingDay()->format('Y-m-d');
$workingTime = $model->getWorkingTime()->format('%H:%I:%S'); $workingTime = $model->getWorkingTime()->format('%H:%I:%S');
$isHomeOffice = $model->isHomeOffice();
} else { } else {
throw new InvalidArgumentException('Es wird ein Modell der Klasse "' . WorkingHours::class . '" verlangt.'); throw new InvalidArgumentException('Es wird ein Modell der Klasse "' . WorkingHours::class . '" verlangt.');
} }
@@ -154,11 +158,15 @@ SQL;
throw new RepositoryRecordAlreadyExistException(); throw new RepositoryRecordAlreadyExistException();
} /** @noinspection PhpUnusedLocalVariableInspection */ } /** @noinspection PhpUnusedLocalVariableInspection */
catch (RepositoryRecordNotFoundException $exception) { catch (RepositoryRecordNotFoundException $exception) {
$stmt = $this->database->prepare( $stmt = $this->database->prepare(<<<'SQL'
'insert into public."Arbeitszeiten" ("Datum", "Arbeitszeit") values (?, ?) on conflict do nothing' insert into public."Arbeitszeiten" ("Datum", "Arbeitszeit", "HomeOffice")
); values (?, ?, ?)
on conflict do nothing
SQL
);
$stmt->bindParam(1, $workingDay); $stmt->bindParam(1, $workingDay);
$stmt->bindParam(2, $workingTime); $stmt->bindParam(2, $workingTime);
$stmt->bindParam(3, $isHomeOffice, PDO::PARAM_BOOL);
$stmt->execute(); $stmt->execute();
} }
} }
@@ -173,12 +181,19 @@ SQL;
if ($model instanceof WorkingHours) { if ($model instanceof WorkingHours) {
$workingDay = $model->getWorkingDay()->format('Y-m-d'); $workingDay = $model->getWorkingDay()->format('Y-m-d');
$workingTime = $model->getWorkingTime()->format('%H:%I:%S'); $workingTime = $model->getWorkingTime()->format('%H:%I:%S');
$isHomeOffice = $model->isHomeOffice();
} else { } else {
throw new InvalidArgumentException('Es wird ein Modell der Klasse "' . WorkingHours::class . '" verlangt.'); 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(1, $workingTime);
$stmt->bindParam(2, $workingDay); $stmt->bindParam(2, $isHomeOffice, PDO::PARAM_BOOL);
$stmt->bindParam(3, $workingDay);
$stmt->execute(); $stmt->execute();
if ($stmt->rowCount() < 1) { if ($stmt->rowCount() < 1) {
throw new RepositoryRecordNotFoundException(); throw new RepositoryRecordNotFoundException();
@@ -16,6 +16,7 @@ class WorkingHoursWeeklyViewRepository extends AbstractWorkingHoursViewRepositor
"Woche" as "period", "Woche" as "period",
"Gesamtarbeitszeit" as "totalHours", "Gesamtarbeitszeit" as "totalHours",
"Arbeitstage" as "workingDays", "Arbeitstage" as "workingDays",
"Home-Office-Tage" as "homeOfficeDays",
"Überstunden" as "overtime" "Überstunden" as "overtime"
from "Arbeitszeiten - Woche" from "Arbeitszeiten - Woche"
SQL; SQL;
@@ -16,6 +16,7 @@ class WorkingHoursYearlyViewRepository extends AbstractWorkingHoursViewRepositor
"Jahr" as "period", "Jahr" as "period",
"Gesamtarbeitszeit" as "totalHours", "Gesamtarbeitszeit" as "totalHours",
"Arbeitstage" as "workingDays", "Arbeitstage" as "workingDays",
"Home-Office-Tage" as "homeOfficeDays",
"Überstunden" as "overtime" "Überstunden" as "overtime"
from "Arbeitszeiten - Jahr" from "Arbeitszeiten - Jahr"
SQL; SQL;
+105 -88
View File
@@ -22,127 +22,144 @@ DROP TABLE IF EXISTS public."Arbeitszeiten";
-- Name: Arbeitszeiten; Type: TABLE; Schema: public; Owner: torsten -- Name: Arbeitszeiten; Type: TABLE; Schema: public; Owner: torsten
-- --
CREATE TABLE public."Arbeitszeiten" ( CREATE TABLE public."Arbeitszeiten"
(
"Datum" date not null, "Datum" date not null,
"Arbeitszeit" interval(6) null, "Arbeitszeit" interval(6) null,
"HomeOffice" boolean default FALSE not null,
constraint "Arbeitszeiten_pkey" primary key ("Datum") 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 -- Name: Arbeitszeiten - Jahr; Type: VIEW; Schema: public; Owner: torsten
-- --
CREATE OR REPLACE VIEW public."Arbeitszeiten - Jahr" AS CREATE OR REPLACE VIEW public."Arbeitszeiten - Jahr" AS
SELECT (date_part('year'::text, "Arbeitszeiten"."Datum"))::text AS "Jahr", SELECT (date_part('year'::text, "Arbeitszeiten"."Datum"))::text AS "Jahr",
sum("Arbeitszeiten"."Arbeitszeit") AS "Gesamtarbeitszeit", sum("Arbeitszeiten"."Arbeitszeit")::interval AS "Gesamtarbeitszeit",
count("Arbeitszeiten"."Datum") AS "Arbeitstage", count("Arbeitszeiten"."Datum")::bigint AS "Arbeitstage",
(sum("Arbeitszeiten"."Arbeitszeit") - ((count("Arbeitszeiten"."Datum"))::double precision * '08:00:00'::interval)) AS "Überstunden" (sum("Arbeitszeiten"."Arbeitszeit") -
FROM public."Arbeitszeiten" ((count("Arbeitszeiten"."Datum"))::double precision * '08:00:00'::interval)) AS "Überstunden",
GROUP BY (date_part('year'::text, "Arbeitszeiten"."Datum")) (count(*) filter (where "Arbeitszeiten"."HomeOffice"))::int as "Home-Office-Tage"
ORDER BY (date_part('year'::text, "Arbeitszeiten"."Datum")); 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 -- Name: Arbeitszeiten - Monat; Type: VIEW; Schema: public; Owner: torsten
-- --
CREATE OR REPLACE VIEW public."Arbeitszeiten - Monat" AS 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", SELECT to_char(date_trunc('month'::text, ("Arbeitszeiten"."Datum")::timestamp with time zone),
sum("Arbeitszeiten"."Arbeitszeit") AS "Gesamtarbeitszeit", 'TMYYYY TMMonth'::text) AS "Monat",
count("Arbeitszeiten"."Datum") AS "Arbeitstage", sum("Arbeitszeiten"."Arbeitszeit")::interval AS "Gesamtarbeitszeit",
(sum("Arbeitszeiten"."Arbeitszeit") - ((count("Arbeitszeiten"."Datum"))::double precision * '08:00:00'::interval)) AS "Überstunden" count("Arbeitszeiten"."Datum")::bigint AS "Arbeitstage",
FROM public."Arbeitszeiten" (sum("Arbeitszeiten"."Arbeitszeit") -
GROUP BY (date_trunc('month'::text, ("Arbeitszeiten"."Datum")::timestamp with time zone)) ((count("Arbeitszeiten"."Datum"))::double precision * '08:00:00'::interval)) AS "Überstunden",
ORDER BY (date_trunc('month'::text, ("Arbeitszeiten"."Datum")::timestamp with time zone)); (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 -- Name: Arbeitszeiten - Woche; Type: VIEW; Schema: public; Owner: torsten
-- --
CREATE OR REPLACE VIEW public."Arbeitszeiten - Woche" AS 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", SELECT to_char(date_trunc('week'::text, ("Arbeitszeiten"."Datum")::timestamp with time zone),
sum("Arbeitszeiten"."Arbeitszeit") AS "Gesamtarbeitszeit", 'TMYYYY#WW'::text) AS "Woche",
count("Arbeitszeiten"."Datum") AS "Arbeitstage", sum("Arbeitszeiten"."Arbeitszeit")::interval AS "Gesamtarbeitszeit",
(sum("Arbeitszeiten"."Arbeitszeit") - ((count("Arbeitszeiten"."Datum"))::double precision * '08:00:00'::interval)) AS "Überstunden" count("Arbeitszeiten"."Datum")::bigint AS "Arbeitstage",
FROM public."Arbeitszeiten" (sum("Arbeitszeiten"."Arbeitszeit") -
GROUP BY (date_trunc('week'::text, ("Arbeitszeiten"."Datum")::timestamp with time zone)) ((count("Arbeitszeiten"."Datum"))::double precision * '08:00:00'::interval)) AS "Überstunden",
ORDER BY (date_trunc('week'::text, ("Arbeitszeiten"."Datum")::timestamp with time zone)); (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 -- Data for Name: Arbeitszeiten; Type: TABLE DATA; Schema: public; Owner: torsten
-- --
INSERT INTO public."Arbeitszeiten" ("Datum", "Arbeitszeit") VALUES INSERT INTO public."Arbeitszeiten" ("Datum", "Arbeitszeit")
('2020-01-07','08:06:00'), VALUES ('2020-01-07', '08:06:00'),
('2020-01-08','08:16:00'), ('2020-01-08', '08:16:00'),
('2020-01-09','07:49:00'), ('2020-01-09', '07:49:00'),
('2020-01-10','07:30:00'), ('2020-01-10', '07:30:00'),
('2020-01-13','08:06:00'), ('2020-01-13', '08:06:00'),
('2020-01-14','08:04:00'), ('2020-01-14', '08:04:00'),
('2020-01-15','08:20:00'), ('2020-01-15', '08:20:00'),
('2020-01-16','07:13:00'), ('2020-01-16', '07:13:00'),
('2020-01-17','07:57:00'), ('2020-01-17', '07:57:00'),
('2020-01-20','07:53:00'), ('2020-01-20', '07:53:00'),
('2020-01-21','07:49:00'), ('2020-01-21', '07:49:00'),
('2020-01-22','07:56:00'), ('2020-01-22', '07:56:00'),
('2020-01-23','07:55:00'), ('2020-01-23', '07:55:00'),
('2020-01-24','07:08:00'), ('2020-01-24', '07:08:00'),
('2020-01-27','08:23:00'), ('2020-01-27', '08:23:00'),
('2020-01-28','07:20:00'), ('2020-01-28', '07:20:00'),
('2020-01-29','08:13:00'), ('2020-01-29', '08:13:00'),
('2020-01-30','08:43:00'), ('2020-01-30', '08:43:00'),
('2020-01-31','07:21:00'), ('2020-01-31', '07:21:00'),
('2020-02-03','07:56:00'), ('2020-02-03', '07:56:00'),
('2020-02-04','08:05:00'), ('2020-02-04', '08:05:00'),
('2020-02-05','07:58:00'), ('2020-02-05', '07:58:00'),
('2020-02-06','08:01: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'), INSERT INTO public."Arbeitszeiten" ("Datum", "Arbeitszeit", "HomeOffice")
('2020-02-12','08:14:00'), VALUES ('2020-02-07', '07:59:00', FALSE),
('2020-02-13','08:12:00'), ('2020-02-10', '07:50:00', FALSE),
('2020-02-14','07:51:00'), ('2020-02-11', '08:01:00', FALSE),
('2020-02-17','07:59:00'), ('2020-02-12', '08:14:00', FALSE),
('2020-02-18','08:05:00'), ('2020-02-13', '08:12:00', FALSE),
('2020-02-19','07:34:00'), ('2020-02-14', '07:51:00', FALSE),
('2020-02-20','07:33:00'), ('2020-02-17', '07:59:00', FALSE),
('2020-02-21','07:56:00'), ('2020-02-18', '08:05:00', FALSE),
('2020-02-24','08:02:00'), ('2020-02-19', '07:34:00', FALSE),
('2020-02-25','08:13:00'), ('2020-02-20', '07:33:00', FALSE),
('2020-02-26','08:42:00'), ('2020-02-21', '07:56:00', FALSE),
('2020-02-27','07:49:00'), ('2020-02-24', '08:02:00', FALSE),
('2020-02-28','08:16:00'), ('2020-02-25', '08:13:00', FALSE),
('2020-03-02','08:22:00'), ('2020-02-26', '08:42:00', FALSE),
('2020-03-03','08:20:00'), ('2020-02-27', '07:49:00', TRUE),
('2020-03-04','08:12:00'), ('2020-02-28', '08:16:00', FALSE),
('2020-03-05','08:32:00'), ('2020-03-02', '08:22:00', FALSE),
('2020-03-06','04:41:00'), ('2020-03-03', '08:20:00', FALSE),
('2020-03-09','07:05:00'), ('2020-03-04', '08:12:00', FALSE),
('2020-03-10','07:34:00'), ('2020-03-05', '08:32:00', FALSE),
('2020-03-11','07:39:00'), ('2020-03-06', '04:41:00', FALSE),
('2020-03-12','07:50:00'), ('2020-03-09', '07:05:00', FALSE),
('2020-03-13','08:26:00'), ('2020-03-10', '07:34:00', FALSE),
('2020-03-16','07:51:00'), ('2020-03-11', '07:39:00', TRUE),
('2020-03-17','07:50:00'), ('2020-03-12', '07:50:00', FALSE),
('2020-03-18','07:19:00'), ('2020-03-13', '08:26:00', FALSE),
('2020-03-19','07:55:00'), ('2020-03-16', '07:51:00', FALSE),
('2020-03-20','05:43:00'), ('2020-03-17', '07:50:00', FALSE),
('2020-03-23','08:05:00'), ('2020-03-18', '07:19:00', FALSE),
('2020-03-24','08:21:00'), ('2020-03-19', '07:55:00', FALSE),
('2020-03-25','08:10:00'), ('2020-03-20', '05:43:00', TRUE),
('2020-03-26','10:31:00'), ('2020-03-23', '08:05:00', FALSE),
('2020-03-27','04:55:00'), ('2020-03-24', '08:21:00', FALSE),
('2020-03-30','08:21:00'), ('2020-03-25', '08:10:00', FALSE),
('2020-03-31','08:01:00') ('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)
; ;
+3
View File
@@ -1,4 +1,5 @@
<?php <?php
namespace Helper; namespace Helper;
// here you can define custom actions // here you can define custom actions
@@ -15,6 +16,7 @@ class Api extends Module
const WORKING_HOURS_JSON_FORMAT = [ const WORKING_HOURS_JSON_FORMAT = [
'workingDay' => self::FORMAT_DATE, 'workingDay' => self::FORMAT_DATE,
'workingTime' => self::FORMAT_TIME, 'workingTime' => self::FORMAT_TIME,
'isHomeOffice' => 'boolean',
]; ];
const WORKING_HOURS_VIEW_JSON_FORMAT = [ const WORKING_HOURS_VIEW_JSON_FORMAT = [
@@ -22,6 +24,7 @@ class Api extends Module
"periodDesignation" => "string", "periodDesignation" => "string",
"totalHours" => self::FORMAT_TIME, "totalHours" => self::FORMAT_TIME,
"workingDays" => 'integer:>0', "workingDays" => 'integer:>0',
'homeOfficeDays' => 'integer:>=0',
"overtime" => self::FORMAT_TIME, "overtime" => self::FORMAT_TIME,
]; ];
@@ -21,6 +21,22 @@ class CreateWorkingHoursCest
} }
public function createWorkingHoursWithNewValidRecord(ApiTester $I): void public function createWorkingHoursWithNewValidRecord(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',
'isHomeOffice' => false,
]);
$I->seeResponseCodeIs(HttpCode::CREATED);
$I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN);
$I->seeResponseIsJson();
$I->seeResponseMatchesJsonType(Api::WORKING_HOURS_JSON_FORMAT);
}
public function createWorkingHoursWithNewValidRecordWithoutHomeOffice(ApiTester $I): void
{ {
$I->haveHttpHeader('accept', 'application/json'); $I->haveHttpHeader('accept', 'application/json');
$I->haveHttpHeader('content-type', 'application/json'); $I->haveHttpHeader('content-type', 'application/json');
@@ -44,6 +60,7 @@ class CreateWorkingHoursCest
$I->sendPost('/working-hours', [ $I->sendPost('/working-hours', [
'workingDay' => '2020-04-01', 'workingDay' => '2020-04-01',
'workingTime' => 8.0, 'workingTime' => 8.0,
'isHomeOffice' => false,
]); ]);
$I->seeResponseCodeIs(HttpCode::BAD_REQUEST); $I->seeResponseCodeIs(HttpCode::BAD_REQUEST);
$I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN); $I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN);
@@ -60,6 +77,7 @@ class CreateWorkingHoursCest
$I->sendPost('/working-hours', [ $I->sendPost('/working-hours', [
'workingDay' => '2020-01-15', 'workingDay' => '2020-01-15',
'workingTime' => '08:00:00', 'workingTime' => '08:00:00',
'isHomeOffice' => false,
]); ]);
$I->seeResponseCodeIs(HttpCode::CONFLICT); $I->seeResponseCodeIs(HttpCode::CONFLICT);
$I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN); $I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN);
@@ -1,4 +1,5 @@
<?php /** @noinspection PhpUnused */ <?php
/** @noinspection PhpUnused */
namespace TorstenHettstedt\TimekeepingApi\Tests\Api\WorkingHours; namespace TorstenHettstedt\TimekeepingApi\Tests\Api\WorkingHours;
@@ -28,6 +29,7 @@ class UpdateWorkingHoursCest
$I->sendPut('/working-hours/2020-01-15', [ $I->sendPut('/working-hours/2020-01-15', [
'workingDay' => '2020-01-15', 'workingDay' => '2020-01-15',
'workingTime' => '10:00:00', 'workingTime' => '10:00:00',
'isHomeOffice' => false,
]); ]);
$I->seeResponseCodeIs(HttpCode::OK); $I->seeResponseCodeIs(HttpCode::OK);
$I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN); $I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN);
@@ -43,6 +45,7 @@ class UpdateWorkingHoursCest
$I->sendPut('/working-hours/2020-04-15', [ $I->sendPut('/working-hours/2020-04-15', [
'workingDay' => '2020-04-15', 'workingDay' => '2020-04-15',
'workingTime' => '10:00:00', 'workingTime' => '10:00:00',
'isHomeOffice' => false,
]); ]);
$I->seeResponseCodeIs(HttpCode::NOT_FOUND); $I->seeResponseCodeIs(HttpCode::NOT_FOUND);
$I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN); $I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN);
@@ -58,6 +61,7 @@ class UpdateWorkingHoursCest
$I->sendPut('/working-hours/2020-01-15', [ $I->sendPut('/working-hours/2020-01-15', [
'workingDay' => '2020-01-15', 'workingDay' => '2020-01-15',
'workingTime' => 8.0, 'workingTime' => 8.0,
'isHomeOffice' => false,
]); ]);
$I->seeResponseCodeIs(HttpCode::BAD_REQUEST); $I->seeResponseCodeIs(HttpCode::BAD_REQUEST);
$I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN); $I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN);
@@ -4,6 +4,7 @@ namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Controller;
use Codeception\Attribute\DataProvider; use Codeception\Attribute\DataProvider;
use Exception; use Exception;
use PHPUnit\Framework\MockObject\Exception as MockException;
use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface; use Psr\Container\NotFoundExceptionInterface;
@@ -25,7 +26,7 @@ class WorkingHoursControllerTest extends AbstractControllerTest
/** /**
* @throws Exception * @throws Exception
* @throws \PHPUnit\Framework\MockObject\Exception * @throws MockException
*/ */
protected function _before(): void protected function _before(): void
{ {
@@ -34,6 +35,7 @@ class WorkingHoursControllerTest extends AbstractControllerTest
'getParsedBody' => [ 'getParsedBody' => [
'workingDay' => self::EXISTING_DATE, 'workingDay' => self::EXISTING_DATE,
'workingTime' => self::EXISTING_INTERVAL, 'workingTime' => self::EXISTING_INTERVAL,
'isHomeOffice' => true,
], ],
]); ]);
} }
@@ -260,6 +262,28 @@ class WorkingHoursControllerTest extends AbstractControllerTest
$this->assertNotEmpty($response->getBody()->getContents()); $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 ContainerExceptionInterface
* @throws NotDatabasesException * @throws NotDatabasesException
@@ -293,21 +317,19 @@ class WorkingHoursControllerTest extends AbstractControllerTest
} }
/** /**
* @param string $date
* @param string $time
*
* @throws ContainerExceptionInterface * @throws ContainerExceptionInterface
* @throws NotDatabasesException * @throws NotDatabasesException
* @throws NotFoundExceptionInterface * @throws NotFoundExceptionInterface
* @throws Exception * @throws Exception
*/ */
#[DataProvider('invalidCreatDataProvider')] #[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, [ $this->request = $this->makeEmpty(Request::class, [
'getParsedBody' => [ 'getParsedBody' => [
'workingDay' => $date, 'workingDay' => $date,
'workingTime' => $time, 'workingTime' => $time,
'isHomeOffice' => $isHomeOffice,
], ],
]); ]);
$controller = new WorkingHoursController($this->container); $controller = new WorkingHoursController($this->container);
@@ -322,6 +344,27 @@ class WorkingHoursControllerTest extends AbstractControllerTest
* @throws Exception * @throws Exception
*/ */
public function testCreatNewRecord(): void public function testCreatNewRecord(): void
{
$this->request = $this->makeEmpty(Request::class, [
'getParsedBody' => [
'workingDay' => self::NEW_DATE,
'workingTime' => self::NEW_INTERVAL,
'isHomeOffice' => 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, [ $this->request = $this->makeEmpty(Request::class, [
'getParsedBody' => [ 'getParsedBody' => [
@@ -347,6 +390,7 @@ class WorkingHoursControllerTest extends AbstractControllerTest
'getParsedBody' => [ 'getParsedBody' => [
'workingDay' => self::EXISTING_DATE, 'workingDay' => self::EXISTING_DATE,
'workingTime' => self::EXISTING_INTERVAL, 'workingTime' => self::EXISTING_INTERVAL,
'isHomeOffice' => true,
], ],
]); ]);
$controller = new WorkingHoursController($this->container); $controller = new WorkingHoursController($this->container);
+37 -1
View File
@@ -17,6 +17,7 @@ class WorkingHoursTest extends Unit
$obj = new WorkingHours(); $obj = new WorkingHours();
$this->assertNull($obj->getWorkingDay()); $this->assertNull($obj->getWorkingDay());
$this->assertNull($obj->getWorkingTime()); $this->assertNull($obj->getWorkingTime());
$this->assertFalse($obj->isHomeOffice());
$this->expectException(Error::class); $this->expectException(Error::class);
$obj->jsonSerialize(); $obj->jsonSerialize();
@@ -34,7 +35,9 @@ class WorkingHoursTest extends Unit
[ [
'workingDay' => '2020-11-30', 'workingDay' => '2020-11-30',
'workingTime' => '08:00:00', 'workingTime' => '08:00:00',
'isHomeOffice' => true,
], ],
true,
], ],
[ [
new DateTime('2021-05-03'), new DateTime('2021-05-03'),
@@ -42,7 +45,9 @@ class WorkingHoursTest extends Unit
[ [
'workingDay' => '2021-05-03', 'workingDay' => '2021-05-03',
'workingTime' => '07:45:00', 'workingTime' => '07:45:00',
'isHomeOffice' => false,
], ],
false,
], ],
]; ];
} }
@@ -51,10 +56,38 @@ class WorkingHoursTest extends Unit
* @param DateTime $date * @param DateTime $date
* @param DateInterval $interval * @param DateInterval $interval
* @param array<string, string> $shouldJson * @param array<string, string> $shouldJson
* @param bool $isHomeOffice
*/ */
#[DataProvider('workingHoursProvider')] #[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<string, string> $shouldJson
*/
#[DataProvider('workingHoursProvider')]
public function testWorkingHoursWithContentWithoutHomeOfficeInformation(DateTime $date, DateInterval $interval, array $shouldJson): void
{
$shouldJson['isHomeOffice'] = false;
$obj = new WorkingHours(); $obj = new WorkingHours();
$obj->setWorkingDay($date)->setWorkingTime($interval); $obj->setWorkingDay($date)->setWorkingTime($interval);
@@ -66,6 +99,9 @@ class WorkingHoursTest extends Unit
$this->assertInstanceOf(DateInterval::class, $obj->getWorkingTime()); $this->assertInstanceOf(DateInterval::class, $obj->getWorkingTime());
$this->assertEquals($interval, $obj->getWorkingTime()); $this->assertEquals($interval, $obj->getWorkingTime());
$this->assertIsBool($obj->isHomeOffice());
$this->assertFalse($obj->isHomeOffice());
$json = $obj->jsonSerialize(); $json = $obj->jsonSerialize();
$this->assertEquals($shouldJson, $json); $this->assertEquals($shouldJson, $json);
} }
+19 -1
View File
@@ -20,6 +20,7 @@ class WorkingHoursViewTest extends Unit
{ {
$date = new DateTime('2020-11-30'); $date = new DateTime('2020-11-30');
$work_days = 15; $work_days = 15;
$home_work_days = 12;
$total_hours = new DateInterval("PT32H22M"); $total_hours = new DateInterval("PT32H22M");
$overtime = new DateInterval("PT22M"); $overtime = new DateInterval("PT22M");
$absence_time = clone $overtime; $absence_time = clone $overtime;
@@ -30,12 +31,14 @@ class WorkingHoursViewTest extends Unit
PeriodDesignationEnum::WEEKLY, PeriodDesignationEnum::WEEKLY,
$work_days, $work_days,
$total_hours, $total_hours,
$home_work_days,
$overtime, $overtime,
[ [
'period' => '2020#49', 'period' => '2020#49',
'periodDesignation' => 'weekly', 'periodDesignation' => 'weekly',
'workingDays' => 15, 'workingDays' => 15,
'totalHours' => '32:22:00', 'totalHours' => '32:22:00',
'homeOfficeDays' => $home_work_days,
'overtime' => '00:22:00' 'overtime' => '00:22:00'
], ],
], ],
@@ -44,12 +47,14 @@ class WorkingHoursViewTest extends Unit
PeriodDesignationEnum::WEEKLY, PeriodDesignationEnum::WEEKLY,
$work_days, $work_days,
$total_hours, $total_hours,
$home_work_days,
$absence_time, $absence_time,
[ [
'period' => '2020#49', 'period' => '2020#49',
'periodDesignation' => 'weekly', 'periodDesignation' => 'weekly',
'workingDays' => 15, 'workingDays' => 15,
'totalHours' => '32:22:00', 'totalHours' => '32:22:00',
'homeOfficeDays' => $home_work_days,
'overtime' => '-00:22:00' 'overtime' => '-00:22:00'
], ],
], ],
@@ -58,12 +63,14 @@ class WorkingHoursViewTest extends Unit
PeriodDesignationEnum::MONTHLY, PeriodDesignationEnum::MONTHLY,
$work_days, $work_days,
$total_hours, $total_hours,
$home_work_days,
$overtime, $overtime,
[ [
'period' => '2020-11', 'period' => '2020-11',
'periodDesignation' => 'monthly', 'periodDesignation' => 'monthly',
'workingDays' => 15, 'workingDays' => 15,
'totalHours' => '32:22:00', 'totalHours' => '32:22:00',
'homeOfficeDays' => $home_work_days,
'overtime' => '00:22:00' 'overtime' => '00:22:00'
], ],
], ],
@@ -72,12 +79,14 @@ class WorkingHoursViewTest extends Unit
PeriodDesignationEnum::MONTHLY, PeriodDesignationEnum::MONTHLY,
$work_days, $work_days,
$total_hours, $total_hours,
$home_work_days,
$absence_time, $absence_time,
[ [
'period' => '2020-11', 'period' => '2020-11',
'periodDesignation' => 'monthly', 'periodDesignation' => 'monthly',
'workingDays' => 15, 'workingDays' => 15,
'totalHours' => '32:22:00', 'totalHours' => '32:22:00',
'homeOfficeDays' => $home_work_days,
'overtime' => '-00:22:00' 'overtime' => '-00:22:00'
], ],
], ],
@@ -86,12 +95,14 @@ class WorkingHoursViewTest extends Unit
PeriodDesignationEnum::YEARLY, PeriodDesignationEnum::YEARLY,
$work_days, $work_days,
$total_hours, $total_hours,
$home_work_days,
$overtime, $overtime,
[ [
'period' => '2020', 'period' => '2020',
'periodDesignation' => 'yearly', 'periodDesignation' => 'yearly',
'workingDays' => 15, 'workingDays' => 15,
'totalHours' => '32:22:00', 'totalHours' => '32:22:00',
'homeOfficeDays' => $home_work_days,
'overtime' => '00:22:00' 'overtime' => '00:22:00'
], ],
], ],
@@ -100,12 +111,14 @@ class WorkingHoursViewTest extends Unit
PeriodDesignationEnum::YEARLY, PeriodDesignationEnum::YEARLY,
$work_days, $work_days,
$total_hours, $total_hours,
$home_work_days,
$absence_time, $absence_time,
[ [
'period' => '2020', 'period' => '2020',
'periodDesignation' => 'yearly', 'periodDesignation' => 'yearly',
'workingDays' => 15, 'workingDays' => 15,
'totalHours' => '32:22:00', 'totalHours' => '32:22:00',
'homeOfficeDays' => $home_work_days,
'overtime' => '-00:22:00' 'overtime' => '-00:22:00'
], ],
], ],
@@ -117,6 +130,7 @@ class WorkingHoursViewTest extends Unit
* @param PeriodDesignationEnum $periodDesignation * @param PeriodDesignationEnum $periodDesignation
* @param int $workingDays * @param int $workingDays
* @param DateInterval $totalHours * @param DateInterval $totalHours
* @param int $homeWorkDays ,
* @param DateInterval $overtime * @param DateInterval $overtime
* @param array<string, string> $shouldJson * @param array<string, string> $shouldJson
*/ */
@@ -126,10 +140,11 @@ class WorkingHoursViewTest extends Unit
PeriodDesignationEnum $periodDesignation, PeriodDesignationEnum $periodDesignation,
int $workingDays, int $workingDays,
DateInterval $totalHours, DateInterval $totalHours,
int $homeWorkDays,
DateInterval $overtime, DateInterval $overtime,
array $shouldJson array $shouldJson
): void { ): 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->assertInstanceOf(DateTimeInterface::class, $obj->getPeriod());
$this->assertEquals($period, $obj->getPeriod()); $this->assertEquals($period, $obj->getPeriod());
@@ -142,6 +157,9 @@ class WorkingHoursViewTest extends Unit
$this->assertInstanceOf(DateInterval::class, $obj->getTotalHours()); $this->assertInstanceOf(DateInterval::class, $obj->getTotalHours());
$this->assertEquals($totalHours, $obj->getTotalHours()); $this->assertEquals($totalHours, $obj->getTotalHours());
$this->assertIsInt($obj->getHomeOfficeDays());
$this->assertEquals($homeWorkDays, $obj->getHomeOfficeDays());
$this->assertInstanceOf(DateInterval::class, $obj->getOvertime()); $this->assertInstanceOf(DateInterval::class, $obj->getOvertime());
$this->assertEquals($overtime, $obj->getOvertime()); $this->assertEquals($overtime, $obj->getOvertime());
@@ -159,6 +159,7 @@ class WorkingHoursRepositoryTest extends Unit
$model = $this->make(WorkingHours::class, [ $model = $this->make(WorkingHours::class, [
'workingDay' => new DateTime(self::NEW_DATE), 'workingDay' => new DateTime(self::NEW_DATE),
'workingTime' => new DateInterval(self::NEW_INTERVAL), 'workingTime' => new DateInterval(self::NEW_INTERVAL),
'isHomeOffice' => false,
]); ]);
$repository->insert($model); $repository->insert($model);
$model = $repository->findByKey(self::NEW_DATE); $model = $repository->findByKey(self::NEW_DATE);
@@ -176,6 +177,7 @@ class WorkingHoursRepositoryTest extends Unit
$model = $this->make(WorkingHours::class, [ $model = $this->make(WorkingHours::class, [
'workingDay' => new DateTime(self::EXISTING_DATE), 'workingDay' => new DateTime(self::EXISTING_DATE),
'workingTime' => new DateInterval(self::NEW_INTERVAL), 'workingTime' => new DateInterval(self::NEW_INTERVAL),
'isHomeOffice' => false,
]); ]);
$this->expectException(RepositoryRecordAlreadyExistException::class); $this->expectException(RepositoryRecordAlreadyExistException::class);
$repository->insert($model); $repository->insert($model);
@@ -203,6 +205,7 @@ class WorkingHoursRepositoryTest extends Unit
$model = $this->make(WorkingHours::class, [ $model = $this->make(WorkingHours::class, [
'workingDay' => new DateTime(self::EXISTING_DATE), 'workingDay' => new DateTime(self::EXISTING_DATE),
'workingTime' => new DateInterval(self::EXISTING_INTERVAL), 'workingTime' => new DateInterval(self::EXISTING_INTERVAL),
'isHomeOffice' => false,
]); ]);
$repository->delete($model); $repository->delete($model);
$this->expectException(RepositoryRecordNotFoundException::class); $this->expectException(RepositoryRecordNotFoundException::class);
@@ -219,6 +222,7 @@ class WorkingHoursRepositoryTest extends Unit
$model = $this->make(WorkingHours::class, [ $model = $this->make(WorkingHours::class, [
'workingDay' => new DateTime(self::NEW_DATE), 'workingDay' => new DateTime(self::NEW_DATE),
'workingTime' => new DateInterval(self::NEW_INTERVAL), 'workingTime' => new DateInterval(self::NEW_INTERVAL),
'isHomeOffice' => false,
]); ]);
$this->expectException(RepositoryRecordNotFoundException::class); $this->expectException(RepositoryRecordNotFoundException::class);
$repository->delete($model); $repository->delete($model);
@@ -246,6 +250,7 @@ class WorkingHoursRepositoryTest extends Unit
$model = $this->make(WorkingHours::class, [ $model = $this->make(WorkingHours::class, [
'workingDay' => new DateTime(self::EXISTING_DATE), 'workingDay' => new DateTime(self::EXISTING_DATE),
'workingTime' => new DateInterval(self::NEW_INTERVAL), 'workingTime' => new DateInterval(self::NEW_INTERVAL),
'isHomeOffice' => false,
]); ]);
$repository->update($model); $repository->update($model);
$model_db = $repository->findByKey(self::EXISTING_DATE); $model_db = $repository->findByKey(self::EXISTING_DATE);
@@ -261,6 +266,7 @@ class WorkingHoursRepositoryTest extends Unit
$model = $this->make(WorkingHours::class, [ $model = $this->make(WorkingHours::class, [
'workingDay' => new DateTime(self::NEW_DATE), 'workingDay' => new DateTime(self::NEW_DATE),
'workingTime' => new DateInterval(self::NEW_INTERVAL), 'workingTime' => new DateInterval(self::NEW_INTERVAL),
'isHomeOffice' => false,
]); ]);
$this->expectException(RepositoryRecordNotFoundException::class); $this->expectException(RepositoryRecordNotFoundException::class);
$repository->update($model); $repository->update($model);
@@ -6,7 +6,8 @@ use Codeception\Attribute\DataProvider;
use Codeception\Test\Unit; use Codeception\Test\Unit;
use DateTime; use DateTime;
use DateTimeZone; 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\RepositoryRecordNotFoundException;
use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursWeeklyViewRepository; use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursWeeklyViewRepository;
use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursMonthlyViewRepository; use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursMonthlyViewRepository;
@@ -19,7 +20,9 @@ class WorkingHoursViewRepositoryTest extends Unit
protected function _before(): void protected function _before(): void
{ {
/** @noinspection SpellCheckingInspection */ /** @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(); parent::_before();
} }
@@ -41,9 +44,27 @@ class WorkingHoursViewRepositoryTest extends Unit
public function existingObjectProvider(): array public function existingObjectProvider(): array
{ {
return [ 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'))], WorkingHoursYearlyViewRepository::class,
[WorkingHoursWeeklyViewRepository::class, '2020#03', 5, date_create('2020-01-13', new DateTimeZone('UCT'))], '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,
],
]; ];
} }
@@ -70,13 +91,19 @@ class WorkingHoursViewRepositoryTest extends Unit
} }
#[DataProvider(('existingObjectProvider'))] #[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); $repository = new $periodClass($this->pdoObject);
$model = $repository->findByKey($search); $model = $repository->findByKey($search);
$this->assertInstanceOf(WorkingHoursView::class, $model); $this->assertInstanceOf(WorkingHoursView::class, $model);
$this->assertEquals($workingDays, $model->getWorkingDays()); $this->assertEquals($workingDays, $model->getWorkingDays());
$this->assertEquals($period->format('Ymd'), $model->getPeriod()->format('Ymd')); $this->assertEquals($period->format('Ymd'), $model->getPeriod()->format('Ymd'));
$this->assertEquals($homeOfficeDays, $model->getHomeOfficeDays());
} }
#[DataProvider(('notExistingObjectProvider'))] #[DataProvider(('notExistingObjectProvider'))]
+31 -27
View File
@@ -4,42 +4,46 @@ create table public."Arbeitszeiten"
( (
"Datum" date not null, "Datum" date not null,
"Arbeitszeit" interval(6) null, "Arbeitszeit" interval(6) null,
"HomeOffice" boolean default FALSE not null,
constraint "Arbeitszeiten_pkey" primary key ("Datum") constraint "Arbeitszeiten_pkey" primary key ("Datum")
); );
-- # Definition der Ansichten -- # Definition der Ansichten
-- ## Wöchentlich -- ## Wöchentlich
create view "Arbeitszeiten - Woche" ("Woche", "Gesamtarbeitszeit", "Arbeitstage", "Überstunden") as create view "Arbeitszeiten - Woche" ("Woche", "Gesamtarbeitszeit", "Arbeitstage", "Überstunden", "Home-Office-Tage") as
select select to_char(date_trunc('week'::text, "Arbeitszeiten"."Datum"::timestamp with time zone),
to_char(date_trunc('week'::text, "Arbeitszeiten"."Datum"::timestamp with time zone),
'TMYYYY#WW'::text) as "Woche", 'TMYYYY#WW'::text) as "Woche",
sum("Arbeitszeiten"."Arbeitszeit") as "Gesamtarbeitszeit", sum("Arbeitszeiten"."Arbeitszeit")::interval AS "Gesamtarbeitszeit",
count("Arbeitszeiten"."Datum") as "Arbeitstage", count("Arbeitszeiten"."Datum")::bigint AS "Arbeitstage",
sum("Arbeitszeiten"."Arbeitszeit") - count("Arbeitszeiten"."Datum") ::double precision * '08:00:00'::interval as "Überstunden" (sum("Arbeitszeiten"."Arbeitszeit") -
from "Arbeitszeiten" ((count("Arbeitszeiten"."Datum"))::double precision * '08:00:00'::interval)) AS "Überstunden",
group by (date_trunc('week'::text, "Arbeitszeiten"."Datum"::timestamp with time zone)) (count(*) filter (where "Arbeitszeiten"."HomeOffice"))::int as "Home-Office-Tage"
order by (date_trunc('week'::text, "Arbeitszeiten"."Datum"::timestamp with time zone)); 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 -- ## Monatlich
create view "Arbeitszeiten - Monat" ("Monat", "Gesamtarbeitszeit", "Arbeitstage", "Überstunden") as create view "Arbeitszeiten - Monat" ("Monat", "Gesamtarbeitszeit", "Arbeitstage", "Überstunden", "Home-Office-Tage") as
select select to_char(date_trunc('month'::text, "Arbeitszeiten"."Datum"::timestamp with time zone),
to_char(date_trunc('month'::text, "Arbeitszeiten"."Datum"::timestamp with time zone),
'TMYYYY TMMonth'::text) as "Monat", 'TMYYYY TMMonth'::text) as "Monat",
sum("Arbeitszeiten"."Arbeitszeit") as "Gesamtarbeitszeit", sum("Arbeitszeiten"."Arbeitszeit")::interval AS "Gesamtarbeitszeit",
count("Arbeitszeiten"."Datum") as "Arbeitstage", count("Arbeitszeiten"."Datum")::bigint AS "Arbeitstage",
sum("Arbeitszeiten"."Arbeitszeit") - count("Arbeitszeiten"."Datum") ::double precision * '08:00:00'::interval as "Überstunden" (sum("Arbeitszeiten"."Arbeitszeit") -
from "Arbeitszeiten" ((count("Arbeitszeiten"."Datum"))::double precision * '08:00:00'::interval)) AS "Überstunden",
group by (date_trunc('month'::text, "Arbeitszeiten"."Datum"::timestamp with time zone)) (count(*) filter (where "Arbeitszeiten"."HomeOffice"))::int as "Home-Office-Tage"
order by (date_trunc('month'::text, "Arbeitszeiten"."Datum"::timestamp with time zone)); 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 -- ## Jährlich
create view "Arbeitszeiten - Jahr"("Jahr", "Gesamtarbeitszeit", "Arbeitstage", "Überstunden") as create view "Arbeitszeiten - Jahr"("Jahr", "Gesamtarbeitszeit", "Arbeitstage", "Überstunden", "Home-Office-Tage") as
select select date_part('year'::text, "Arbeitszeiten"."Datum")::text as "Jahr",
date_part('year'::text, "Arbeitszeiten"."Datum")::text as "Jahr", sum("Arbeitszeiten"."Arbeitszeit")::interval AS "Gesamtarbeitszeit",
sum("Arbeitszeiten"."Arbeitszeit") as "Gesamtarbeitszeit", count("Arbeitszeiten"."Datum")::bigint AS "Arbeitstage",
count("Arbeitszeiten"."Datum") as "Arbeitstage", (sum("Arbeitszeiten"."Arbeitszeit") -
sum("Arbeitszeiten"."Arbeitszeit") - count("Arbeitszeiten"."Datum") ::double precision * '08:00:00'::interval as "Überstunden" ((count("Arbeitszeiten"."Datum"))::double precision * '08:00:00'::interval)) AS "Überstunden",
from "Arbeitszeiten" (count(*) filter (where "Arbeitszeiten"."HomeOffice"))::int as "Home-Office-Tage"
group by (date_part('year'::text, "Arbeitszeiten"."Datum")) from "Arbeitszeiten"
order by (date_part('year'::text, "Arbeitszeiten"."Datum")); group by (date_part('year'::text, "Arbeitszeiten"."Datum"))
order by (date_part('year'::text, "Arbeitszeiten"."Datum"));
+1 -1
View File
@@ -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; 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; 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;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; 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; 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; Arbeit;04:55;2021-07-20 07:37;2021-07-20 12:32;
1 Aktivität Dauer Von Bis Kommentar
23 Arbeit 03:11 2021-07-21 13:37 2021-07-21 16:48
24 Mittagspause 01:07 2021-07-21 12:29 2021-07-21 13:37
25 Arbeit 04:58 2021-07-21 07:31 2021-07-21 12:29
26 Arbeit Verkehr 01:17 2021-07-20 18:44 2021-07-20 20:02
27 Arbeit 03:50 2021-07-20 13:14 2021-07-20 17:04
28 Mittagspause 00:41 2021-07-20 12:32 2021-07-20 13:14
29 Arbeit 04:55 2021-07-20 07:37 2021-07-20 12:32
+2 -1
View File
@@ -1,9 +1,10 @@
export class PeriodRecord { 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) { constructor(record) {
this.workingDays = Number(record.workingDays) this.workingDays = Number(record.workingDays)
this.homeOfficeDays = Number(record.homeOfficeDays)
this.totalHours = String(record.totalHours) this.totalHours = String(record.totalHours)
this.period = String(record.period) this.period = String(record.period)
this.overtime = String('##:##:##') this.overtime = String('##:##:##')
+72
View File
@@ -0,0 +1,72 @@
<script lang="ts">
let {promise_data, actual_records= $bindable(), page_count = 10} = $props();
let records: Array<any[]> = $state([])
let page_number = $state(0)
let max_page_number = $state(0)
const setPage = (new_page: number) => {
page_number = new_page
actual_records = records[page_number - 1]
}
const init = async () => {
let page_records: any[] = []
console.log(promise_data, actual_records)
for (let index in promise_data) {
page_records.push(promise_data[index])
if (page_records.length >= page_count) {
records.push(page_records.values().toArray())
page_records = []
}
}
if (page_records.length > 0) {
records.push(page_records.values().toArray())
}
max_page_number = records.length
setPage(1)
}
init()
</script>
<style>
div.table-pagination {
text-align: center;
}
div.table-pagination > button {
display: inline-block;
width: 2rem;
}
div.table-pagination > button[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
div.table-pagination > span {
display: inline-block;
width: 5rem;
text-align: center;
}
</style>
<div class="table-pagination">
<button
type="button"
disabled={page_number === 1}
onclick={() => setPage(1)}>&ll;
</button>
<button
type="button"
disabled={page_number === 1}
onclick={() => setPage(page_number - 1)}>&lt;
</button>
<span>{page_number} von {max_page_number}</span>
<button
type="button"
disabled={page_number === max_page_number}
onclick={() => setPage(page_number + 1)}>&gt;
</button>
<button
type="button"
disabled={page_number === max_page_number}
onclick={() => setPage(max_page_number)}>&gg;
</button>
</div>
+18 -9
View File
@@ -1,16 +1,16 @@
<script> <script>
import {PeriodRecord} from './Models.svelte.js' import {PeriodRecord} from './Models.svelte.js'
import TablePagination from "$lib/TablePagination.svelte";
/** let { promise, title } = $props();
* @type {{ promise: Promise<object>; title: string;}}
*/
let {params} = $props();
/** @type PeriodRecord[] **/ /** @type PeriodRecord[] **/
let records = $state([]) let records = $state([])
/** @type PeriodRecord[] **/
let actual_records = $state([])
let isLoading = $state(true) let isLoading = $state(true)
const init = async () => await params.promise.then( const init = async () => await promise.then(
/** /**
* @param {Array<object>} data * @param {Array<{ workingDays: Number; totalHours: any; overtime: string; period: string; homeOfficeDays: Number; }>} data
*/ */
data => { data => {
records = data.map(row => new PeriodRecord(row)) records = data.map(row => new PeriodRecord(row))
@@ -21,12 +21,12 @@
</script> </script>
<svelte:head> <svelte:head>
<title>{params.title}</title> <title>{title}</title>
</svelte:head> </svelte:head>
<section id="list-records"> <section id="list-records">
<header> <header>
<h1>{params.title}</h1> <h1>{title}</h1>
</header> </header>
<article> <article>
{#if isLoading === true} {#if isLoading === true}
@@ -43,20 +43,29 @@
<tr> <tr>
<th>Zeitraum</th> <th>Zeitraum</th>
<th>Arbeitstage</th> <th>Arbeitstage</th>
<th>davon HomeOffice</th>
<th>Arbeitsstunden</th> <th>Arbeitsstunden</th>
<th>Über- / Unterstunden</th> <th>Über- / Unterstunden</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each records as record} {#each actual_records as record}
<tr> <tr>
<td>{record.period}</td> <td>{record.period}</td>
<td>{record.workingDays}</td> <td>{record.workingDays}</td>
<td>{record.homeOfficeDays}</td>
<td>{record.totalHours}</td> <td>{record.totalHours}</td>
<td class="{record.isMinus ? 'absence-time' : ''}">{record.overtime}</td> <td class="{record.isMinus ? 'absence-time' : ''}">{record.overtime}</td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>
<tfoot>
<tr>
<td colspan="4">
<TablePagination promise_data={records} bind:actual_records={actual_records}/>
</td>
</tr>
</tfoot>
</table> </table>
{/if} {/if}
</article> </article>
+1 -1
View File
@@ -3,5 +3,5 @@
let { data } = $props(); let { data } = $props();
</script> </script>
<Views params={data}/> <Views {...data}/>
+1 -1
View File
@@ -3,5 +3,5 @@
let { data } = $props(); let { data } = $props();
</script> </script>
<Views params={data}/> <Views {...data}/>
+1 -1
View File
@@ -3,5 +3,5 @@
let { data } = $props(); let { data } = $props();
</script> </script>
<Views params={data}/> <Views {...data}/>
+16 -3
View File
@@ -34,6 +34,7 @@ export const actions = {
const body_data = { const body_data = {
workingDay: data.get('workingDay'), workingDay: data.get('workingDay'),
workingTime: data.get('workingTime'), workingTime: data.get('workingTime'),
isHomeOffice: data.get('isHomeOffice'),
} }
await WorkingHoursRepository.update(env.API_URL, data.get('workingDay'), body_data); await WorkingHoursRepository.update(env.API_URL, data.get('workingDay'), body_data);
}, },
@@ -42,6 +43,7 @@ export const actions = {
const body_data = { const body_data = {
workingDay: form_data.get('workingDay'), workingDay: form_data.get('workingDay'),
workingTime: form_data.get('workingTime'), workingTime: form_data.get('workingTime'),
isHomeOffice: form_data.get('isHomeOffice'),
} }
await WorkingHoursRepository.add(env.API_URL, body_data); await WorkingHoursRepository.add(env.API_URL, body_data);
}, },
@@ -56,13 +58,19 @@ export const actions = {
actualDayObject.setDate(actualDayObject.getDate() + 1) actualDayObject.setDate(actualDayObject.getDate() + 1)
}) })
for (const pair of form_data.entries()) { for (const pair of form_data.entries()) {
const working_day = day_list[pair[0]] ?? null let key = /(?<day>\w+)\[time]/.exec(pair[0])
if (working_day === null) { 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 continue
} }
const body_data = { const body_data = {
workingDay: working_day, workingDay: working_day,
workingTime: pair[1], workingTime: pair[1],
isHomeOffice: form_data.has(day + '[isHomeOffice]')
} }
await WorkingHoursRepository.addOrUpdate(env.API_URL, working_day, body_data); await WorkingHoursRepository.addOrUpdate(env.API_URL, working_day, body_data);
} }
@@ -70,10 +78,15 @@ export const actions = {
'import-csv': async ({ request }) => { 'import-csv': async ({ request }) => {
const form_data = await request.formData(); const form_data = await request.formData();
for (const pair of form_data.entries()) { for (const pair of form_data.entries()) {
const working_day = pair[0] let key = /^(?<day>\d{4}-\d{2}-\d{2})\[time]$/.exec(pair[0])
if (key === null) {
continue
}
let working_day = key?.groups?.day ?? ''
const body_data = { const body_data = {
workingDay: working_day, workingDay: working_day,
workingTime: pair[1], workingTime: pair[1],
isHomeOffice: form_data.has(working_day + '[isHomeOffice]')
} }
await WorkingHoursRepository.addOrUpdate(env.API_URL, working_day, body_data); await WorkingHoursRepository.addOrUpdate(env.API_URL, working_day, body_data);
} }
+17 -5
View File
@@ -2,13 +2,16 @@
import documentIcon from "@iconify-icons/oi/document.js"; import documentIcon from "@iconify-icons/oi/document.js";
import wrenchIcon from "@iconify-icons/oi/wrench.js"; import wrenchIcon from "@iconify-icons/oi/wrench.js";
import IconifyIcon from "@iconify/svelte"; import IconifyIcon from "@iconify/svelte";
import TablePagination from "$lib/TablePagination.svelte";
let { data } = $props(); let { data } = $props();
const TITLE = "Bearbeitung Einträge" const TITLE = "Bearbeitung Einträge"
/** @type {{workingDay: String, workingTime: String, }[]|null} **/ /** @type {{workingDay: String, workingTime: String, isHomeOffice: Boolean, }[]|null} **/
let records = $state([]) let records = $state(null)
/** @type {{workingDay: String, workingTime: String, isHomeOffice: Boolean, }[]} **/
let actual_records = $state([])
const init = async () => await data.promise.then( const init = async () => await data.promise.then(
/** /**
* @param {{workingDay: String, workingTime: String, }[]} data * @param {{workingDay: String, workingTime: String, isHomeOffice: Boolean, }[]} data
*/ */
data => { data => {
records = data records = data
@@ -53,14 +56,16 @@
<tr> <tr>
<th>Datum</th> <th>Datum</th>
<th>Arbeitsstunden</th> <th>Arbeitsstunden</th>
<th>Zu Hause gearbeitet?</th>
<th>Aktionen</th> <th>Aktionen</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each records as record} {#each actual_records as record}
<tr> <tr style="{record.isHomeOffice ? 'background: aqua' : ''}">
<td>{record.workingDay}</td> <td>{record.workingDay}</td>
<td>{record.workingTime}</td> <td>{record.workingTime}</td>
<td>{record.isHomeOffice ? 'ja' : ''}</td>
<td> <td>
<button formaction={'/working-hours/' + record.workingDay} type="submit">Bearbeiten <button formaction={'/working-hours/' + record.workingDay} type="submit">Bearbeiten
<IconifyIcon icon={wrenchIcon}/> <IconifyIcon icon={wrenchIcon}/>
@@ -69,6 +74,13 @@
</tr> </tr>
{/each} {/each}
</tbody> </tbody>
<tfoot>
<tr>
<td colspan="4">
<TablePagination promise_data={records} bind:actual_records={actual_records} page_count={15}/>
</td>
</tr>
</tfoot>
</table> </table>
{/if} {/if}
</form> </form>
@@ -4,11 +4,11 @@
import xIcon from "@iconify-icons/oi/x"; import xIcon from "@iconify-icons/oi/x";
let { data } = $props(); let { data } = $props();
const TITLE = "Bearbeitung eines Eintrags" const TITLE = "Bearbeitung eines Eintrags"
/** @type {{workingDay: String, workingTime: String, }|null} **/ /** @type {{workingDay: String, workingTime: String, isHomeOffice: Boolean, }|null} **/
let activeDate = $state(null) let activeDate = $state(null)
const init = async () => await data.promise.then( const init = async () => await data.promise.then(
/** /**
* @param {{workingDay: String, workingTime: String, }} data * @param {{workingDay: String, workingTime: String, isHomeOffice: Boolean, }} data
*/ */
data => { data => {
activeDate = data activeDate = data
@@ -42,6 +42,10 @@
value="{activeDate.workingTime}" value="{activeDate.workingTime}"
id="workingTime" required pattern="[0-5]\d:[0-5]\d:[0-5]\d"> id="workingTime" required pattern="[0-5]\d:[0-5]\d:[0-5]\d">
</div> </div>
<div>
<label for="isHomeOffice">Arbeitszeit</label>
<input name="isHomeOffice" type="checkbox" checked={activeDate.isHomeOffice} id="isHomeOffice">
</div>
</fieldset> </fieldset>
{/if} {/if}
<fieldset name="buttons"> <fieldset name="buttons">
@@ -29,6 +29,10 @@
<input name="workingTime" placeholder="Arbeitszeit" type="time" step="1" value="" <input name="workingTime" placeholder="Arbeitszeit" type="time" step="1" value=""
id="workingTime" required pattern="[0-5]\d:[0-5]\d:[0-5]\d"> id="workingTime" required pattern="[0-5]\d:[0-5]\d:[0-5]\d">
</div> </div>
<div>
<label for="isHomeOffice">Arbeitszeit</label>
<input name="isHomeOffice" type="checkbox" id="isHomeOffice">
</div>
</fieldset> </fieldset>
<fieldset name="buttons"> <fieldset name="buttons">
<button type="submit">Übernehmen und Weiter <button type="submit">Übernehmen und Weiter
@@ -14,6 +14,22 @@
tbody.weekend td input, tbody.weekend td { tbody.weekend td input, tbody.weekend td {
color: red; color: red;
} }
tbody.weekend label {
color: initial;
}
section form fieldset div input {
display: inline-block;
padding: 0.25rem;
border-style: solid;
border-width: thin;
}
section form fieldset div input[type=checkbox] {
display: inline-block;
width: auto;
margin: 0 1rem 1.0rem;
}
</style> </style>
@@ -39,10 +55,19 @@
{day} {day}
</td> </td>
<td> <td>
<input name="{day}" value="08:00:00" type="time" step="1" <input name="{day}[time]" value="08:00:00" type="time" step="1"
required pattern="[0-5]\d:[0-5]\d:[0-5]\d"> required pattern="[0-5]\d:[0-5]\d:[0-5]\d">
</td> </td>
</tr> </tr>
<tr>
<td>
</td>
<td>
<label>wurde zu Hause gearbeitet?
<input name="{day}[isHomeOffice]" type="checkbox">
</label>
</td>
</tr>
{/each} {/each}
</tbody> </tbody>
<tbody class="weekend"> <tbody class="weekend">
@@ -52,10 +77,19 @@
{day} {day}
</td> </td>
<td> <td>
<input name="{day}" value="00:00:00" type="time" step="1" <input name="{day}[time]" value="00:00:00" type="time" step="1"
required pattern="[0-5]\d:[0-5]\d:[0-5]\d"> required pattern="[0-5]\d:[0-5]\d:[0-5]\d">
</td> </td>
</tr> </tr>
<tr>
<td>
</td>
<td>
<label>wurde zu Hause gearbeitet?
<input name="{day}[isHomeOffice]" type="checkbox">
</label>
</td>
</tr>
{/each} {/each}
</tbody> </tbody>
</table> </table>
@@ -8,7 +8,7 @@
const TITLE = "Erstellung neuer Einträge für eine Woche" const TITLE = "Erstellung neuer Einträge für eine Woche"
/** @type {{ workingTime: String, workingDay: String, }[]} **/ /** @type {{ workingTime: String, workingDay: String, isHomeOffice: Boolean, }[]} **/
let imported_data = $state([]) let imported_data = $state([])
@@ -30,6 +30,8 @@
complete: results => { complete: results => {
/** @type {Object<String, Number>} **/ /** @type {Object<String, Number>} **/
let cash_work_days = {} let cash_work_days = {}
/** @type {Object<String, Boolean>} **/
let home_office_days = {}
results.data.forEach( results.data.forEach(
/** @param {Object<String, any>} item **/ /** @param {Object<String, any>} item **/
item => { item => {
@@ -42,6 +44,9 @@
let work_to = new Date(item['Bis']) let work_to = new Date(item['Bis'])
let actual_day = work_from.toISOString().substring(0, 10) let actual_day = work_from.toISOString().substring(0, 10)
let actual_work_hours = (work_to.getTime() - work_from.getTime()) / (1000 * 60 * 60) let actual_work_hours = (work_to.getTime() - work_from.getTime()) / (1000 * 60 * 60)
if (activity.trim() === 'Verkehr') {
home_office_days[actual_day] = true
}
cash_work_days[actual_day] ??= 0 cash_work_days[actual_day] ??= 0
cash_work_days[actual_day] += actual_work_hours cash_work_days[actual_day] += actual_work_hours
} }
@@ -52,7 +57,11 @@
let minutes_digits = Math.round((hours - hours_digits) * 60) let minutes_digits = Math.round((hours - hours_digits) * 60)
let working_time = String(hours_digits).padStart(2, '0') + ':' let working_time = String(hours_digits).padStart(2, '0') + ':'
+ String(minutes_digits).padStart(2, '0') + ':00' + String(minutes_digits).padStart(2, '0') + ':00'
imported_data.push({workingTime: working_time, workingDay: day}) imported_data.push({
workingTime: working_time,
workingDay: day,
isHomeOffice: home_office_days[day] ?? false,
})
} }
}, },
}) })
@@ -90,7 +99,10 @@
{item.workingDay} {item.workingDay}
</td> </td>
<td> <td>
<input type="time" readonly name="{item.workingDay}" value="{item.workingTime}"> <input type="time" readonly name="{item.workingDay}[time]" value="{item.workingTime}">
</td>
<td>
<input type="checkbox" readonly name="{item.workingDay}[isHomeOffice]" checked={item.isHomeOffice}>
</td> </td>
</tr> </tr>
{/each} {/each}