#39 - generate timesheet (#56)

* #39 - wip

* #39 - fix

* #39 - wip

* #39 - wip

* #39 - wip

* Update app/Domain/Enums/Month.php

Co-authored-by: Marcin Tracz <marcin.tracz@blumilk.pl>

* #39 - cr fixes

Co-authored-by: EwelinaLasowy <ewelina.lasowy@blumilk.pl>
Co-authored-by: Marcin Tracz <marcin.tracz@blumilk.pl>
This commit is contained in:
Adrian Hopek
2022-02-21 16:09:45 +01:00
committed by GitHub
parent 77a4d5711c
commit 39b464388c
12 changed files with 1085 additions and 143 deletions

View File

@@ -4,9 +4,8 @@ declare(strict_types=1);
namespace Toby\Domain;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Carbon\CarbonPeriod;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Toby\Domain\Enums\VacationRequestState;
use Toby\Eloquent\Helpers\YearPeriodRetriever;
@@ -20,33 +19,16 @@ class CalendarGenerator
) {
}
public function generate(YearPeriod $yearPeriod, string $month): array
public function generate(Carbon $month): array
{
$date = CarbonImmutable::create($yearPeriod->year, $this->monthNameToNumber($month));
$period = CarbonPeriod::create($date->startOfMonth(), $date->endOfMonth());
$period = CarbonPeriod::create($month->copy()->startOfMonth(), $month->copy()->endOfMonth());
$yearPeriod = YearPeriod::findByYear($month->year);
$holidays = $yearPeriod->holidays()->pluck("date");
return $this->generateCalendar($period, $holidays);
}
protected function monthNameToNumber($name): int
{
return match ($name) {
default => CarbonInterface::JANUARY,
"february" => CarbonInterface::FEBRUARY,
"march" => CarbonInterface::MARCH,
"april" => CarbonInterface::APRIL,
"may" => CarbonInterface::MAY,
"june" => CarbonInterface::JUNE,
"july" => CarbonInterface::JULY,
"august" => CarbonInterface::AUGUST,
"september" => CarbonInterface::SEPTEMBER,
"october" => CarbonInterface::OCTOBER,
"november" => CarbonInterface::NOVEMBER,
"december" => CarbonInterface::DECEMBER,
};
}
protected function generateCalendar(CarbonPeriod $period, Collection $holidays): array
{
$calendar = [];

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Enums;
use Carbon\CarbonInterface;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
enum Month: string
{
case January = "january";
case February = "february";
case March = "march";
case April = "april";
case May = "may";
case June = "june";
case July = "july";
case August = "august";
case September = "september";
case October = "october";
case November = "november";
case December = "december";
public function toCarbonNumber(): int
{
return match ($this) {
self::January => CarbonInterface::JANUARY,
self::February => CarbonInterface::FEBRUARY,
self::March => CarbonInterface::MARCH,
self::April => CarbonInterface::APRIL,
self::May => CarbonInterface::MAY,
self::June => CarbonInterface::JUNE,
self::July => CarbonInterface::JULY,
self::August => CarbonInterface::AUGUST,
self::September => CarbonInterface::SEPTEMBER,
self::October => CarbonInterface::OCTOBER,
self::November => CarbonInterface::NOVEMBER,
self::December => CarbonInterface::DECEMBER,
};
}
public static function current(): Month
{
return Month::from(Str::lower(Carbon::now()->englishMonth));
}
public static function fromNameOrCurrent(string $name): Month
{
return Month::tryFrom($name) ?? Month::current();
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Toby\Domain;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Carbon;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
use Toby\Eloquent\Models\User;
class TimesheetExport implements WithMultipleSheets
{
protected Collection $users;
protected Carbon $month;
public function sheets(): array
{
return $this->users
->map(fn(User $user) => new TimesheetPerUserSheet($user, $this->month))
->toArray();
}
public function forUsers(Collection $users): static
{
$this->users = $users;
return $this;
}
public function forMonth(Carbon $month): static
{
$this->month = $month;
return $this;
}
}

View File

@@ -0,0 +1,222 @@
<?php
declare(strict_types=1);
namespace Toby\Domain;
use Carbon\CarbonInterface;
use Carbon\CarbonPeriod;
use Generator;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\FromGenerator;
use Maatwebsite\Excel\Concerns\RegistersEventListeners;
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithStrictNullComparison;
use Maatwebsite\Excel\Concerns\WithStyles;
use Maatwebsite\Excel\Concerns\WithTitle;
use Maatwebsite\Excel\Events\AfterSheet;
use PhpOffice\PhpSpreadsheet\Shared\Date;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Toby\Domain\Enums\VacationRequestState;
use Toby\Domain\Enums\VacationType;
use Toby\Eloquent\Models\Holiday;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\Vacation;
class TimesheetPerUserSheet implements WithTitle, WithHeadings, WithEvents, WithStyles, WithStrictNullComparison, ShouldAutoSize, FromGenerator
{
use RegistersEventListeners;
protected const HOURS_PER_DAY = 8;
protected const START_HOUR = 8;
protected const END_HOUR = 16;
public function __construct(
protected User $user,
protected Carbon $month,
) {
}
public function title(): string
{
return $this->user->fullName;
}
public function headings(): array
{
$types = VacationType::cases();
$headings = [
__("Date"),
__("Day of week"),
__("Start date"),
__("End date"),
__("Worked hours"),
];
foreach ($types as $type) {
$headings[] = $type->label();
}
return $headings;
}
public function generator(): Generator
{
$period = CarbonPeriod::create($this->month->copy()->startOfMonth(), $this->month->copy()->endOfMonth());
$vacations = $this->getVacationsForPeriod($this->user, $period);
$holidays = $this->getHolidaysForPeriod($period);
foreach ($period as $day) {
$vacationsForDay = $vacations->get($day->toDateString(), new Collection());
$workedThisDay = $this->checkIfWorkedThisDay($day, $holidays, $vacationsForDay);
$row = [
Date::dateTimeToExcel($day),
$day->translatedFormat("l"),
$workedThisDay ? $this->toExcelTime(Carbon::createFromTime(static::START_HOUR)) : null,
$workedThisDay ? $this->toExcelTime(Carbon::createFromTime(static::END_HOUR)) : null,
$workedThisDay ? static::HOURS_PER_DAY : null,
];
foreach (VacationType::cases() as $type) {
$row[] = $vacationsForDay->has($type->value) ? static::HOURS_PER_DAY : null;
}
yield $row;
}
}
public function styles(Worksheet $sheet): void
{
$lastRow = $sheet->getHighestRow();
$lastColumn = $sheet->getHighestColumn();
$sheet->getStyle("A1:{$lastColumn}1")
->getFont()->setBold(true);
$sheet->getStyle("A1:{$lastColumn}1")
->getAlignment()
->setVertical(Alignment::VERTICAL_CENTER);
$sheet->getStyle("A1:{$lastColumn}1")
->getFill()
->setFillType(Fill::FILL_SOLID)
->getStartColor()
->setRGB("D9D9D9");
$sheet->getStyle("C1:{$lastColumn}{$lastRow}")
->getAlignment()
->setHorizontal(Alignment::HORIZONTAL_CENTER);
$sheet->getStyle("A2:A{$lastRow}")
->getNumberFormat()
->setFormatCode(NumberFormat::FORMAT_DATE_DDMMYYYY);
$sheet->getStyle("C1:D{$lastRow}")
->getNumberFormat()
->setFormatCode(NumberFormat::FORMAT_DATE_TIME3);
$sheet->getStyle("A2:A{$lastRow}")
->getFont()
->setBold(true);
for ($i = 2; $i < $lastRow; $i++) {
$date = Date::excelToDateTimeObject($sheet->getCell("A{$i}")->getValue());
if (Carbon::createFromInterface($date)->isWeekend()) {
$sheet->getStyle("A{$i}:{$lastColumn}{$i}")
->getFill()
->setFillType(Fill::FILL_SOLID)
->getStartColor()
->setRGB("FEE2E2");
}
}
$sheet->getStyle("A1:{$lastColumn}{$lastRow}")
->getBorders()
->getAllBorders()
->setBorderStyle(Border::BORDER_THIN)
->getColor()
->setRGB("B7B7B7");
}
public static function afterSheet(AfterSheet $event): void
{
$sheet = $event->getSheet();
$lastRow = $sheet->getDelegate()->getHighestRow();
$sheet->append([
__("Sum:"),
null,
null,
null,
"=SUM(E2:E{$lastRow})",
]);
$lastRow++;
$sheet->getDelegate()->getStyle("A{$lastRow}")
->getAlignment()
->setHorizontal(Alignment::HORIZONTAL_RIGHT);
$sheet->getDelegate()->getStyle("A{$lastRow}")
->getFont()
->setBold(true);
$sheet->getDelegate()->getStyle("E{$lastRow}")
->getAlignment()
->setHorizontal(Alignment::HORIZONTAL_CENTER);
$sheet->getDelegate()->mergeCells("A{$lastRow}:D{$lastRow}");
$sheet->getDelegate()->getStyle("A{$lastRow}:E{$lastRow}")
->getBorders()
->getAllBorders()
->setBorderStyle(Border::BORDER_THIN)
->getColor()
->setRGB("B7B7B7");
}
protected function getVacationsForPeriod(User $user, CarbonPeriod $period): Collection
{
return $user->vacations()
->with("vacationRequest")
->whereBetween("date", [$period->start, $period->end])
->whereRelation("vacationRequest", "state", VacationRequestState::Approved->value)
->get()
->groupBy(
[
fn(Vacation $vacation) => $vacation->date->toDateString(),
fn(Vacation $vacation) => $vacation->vacationRequest->type->value,
],
);
}
protected function getHolidaysForPeriod(CarbonPeriod $period): Collection
{
return Holiday::query()
->whereBetween("date", [$period->start, $period->end])
->pluck("date");
}
protected function toExcelTime(Carbon $time): float
{
$excelTimestamp = Date::dateTimeToExcel($time);
$excelDate = floor($excelTimestamp);
return $excelTimestamp - $excelDate;
}
protected function checkIfWorkedThisDay(CarbonInterface $day, Collection $holidays, Collection $vacations): bool
{
return $day->isWeekday() && $holidays->doesntContain($day) && $vacations->isEmpty();
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Http\Controllers;
use Illuminate\Support\Carbon;
use Maatwebsite\Excel\Facades\Excel;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Toby\Domain\Enums\Month;
use Toby\Domain\TimesheetExport;
use Toby\Eloquent\Helpers\YearPeriodRetriever;
use Toby\Eloquent\Models\User;
class TimesheetController extends Controller
{
public function __invoke(Month $month, YearPeriodRetriever $yearPeriodRetriever): BinaryFileResponse
{
$yearPeriod = $yearPeriodRetriever->selected();
$carbonMonth = Carbon::create($yearPeriod->year, $month->toCarbonNumber());
$users = User::query()
->orderBy("last_name")
->orderBy("first_name")
->get();
$filename = "{$carbonMonth->translatedFormat("F Y")}.xlsx";
$timesheet = (new TimesheetExport())
->forMonth($carbonMonth)
->forUsers($users);
return Excel::download($timesheet, $filename);
}
}

View File

@@ -4,11 +4,10 @@ declare(strict_types=1);
namespace Toby\Infrastructure\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Inertia\Response;
use Toby\Domain\CalendarGenerator;
use Toby\Domain\Enums\Month;
use Toby\Eloquent\Helpers\YearPeriodRetriever;
use Toby\Eloquent\Models\User;
use Toby\Infrastructure\Http\Resources\UserResource;
@@ -16,22 +15,25 @@ use Toby\Infrastructure\Http\Resources\UserResource;
class VacationCalendarController extends Controller
{
public function index(
Request $request,
YearPeriodRetriever $yearPeriodRetriever,
CalendarGenerator $calendarGenerator,
?string $month = null,
): Response {
$month = Str::lower($request->query("month", Carbon::now()->englishMonth));
$month = Month::fromNameOrCurrent((string)$month);
$yearPeriod = $yearPeriodRetriever->selected();
$carbonMonth = Carbon::create($yearPeriod->year, $month->toCarbonNumber());
$users = User::query()
->orderBy("last_name")
->orderBy("first_name")
->get();
$calendar = $calendarGenerator->generate($yearPeriod, $month);
$calendar = $calendarGenerator->generate($carbonMonth);
return inertia("Calendar", [
"calendar" => $calendar,
"currentMonth" => $month,
"currentMonth" => $month->value,
"users" => UserResource::collection($users),
]);
}