mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-06-15 13:32:43 +01:00
Add localization settings
This commit is contained in:
committed by
Gregor Vostrak
parent
3c9160a08a
commit
ae00fdb0e9
@@ -76,6 +76,11 @@ class CreateNewUser implements CreatesNewUsers
|
||||
$ipLookupResponse = app(IpLookupServiceContract::class)->lookup(request()->ip());
|
||||
|
||||
$startOfWeek = Weekday::Monday;
|
||||
$numberFormat = null;
|
||||
$currencyFormat = null;
|
||||
$dateFormat = null;
|
||||
$intervalFormat = null;
|
||||
$timeFormat = null;
|
||||
$currency = null;
|
||||
if ($ipLookupResponse !== null) {
|
||||
$startOfWeek = $ipLookupResponse->startOfWeek ?? Weekday::Monday;
|
||||
@@ -85,7 +90,7 @@ class CreateNewUser implements CreatesNewUsers
|
||||
$currency = $ipLookupResponse->currency;
|
||||
}
|
||||
$user = null;
|
||||
DB::transaction(function () use (&$user, $input, $timezone, $startOfWeek, $currency): void {
|
||||
DB::transaction(function () use (&$user, $input, $timezone, $startOfWeek, $currency, $numberFormat, $currencyFormat, $dateFormat, $intervalFormat, $timeFormat): void {
|
||||
$userService = app(UserService::class);
|
||||
$user = $userService->createUser(
|
||||
$input['name'],
|
||||
@@ -93,7 +98,12 @@ class CreateNewUser implements CreatesNewUsers
|
||||
$input['password'],
|
||||
$timezone ?? 'UTC',
|
||||
$startOfWeek,
|
||||
$currency ?? 'EUR',
|
||||
$currency,
|
||||
$numberFormat,
|
||||
$currencyFormat,
|
||||
$dateFormat,
|
||||
$intervalFormat,
|
||||
$timeFormat
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -4,10 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Jetstream;
|
||||
|
||||
use App\Enums\Role;
|
||||
use App\Events\AfterCreateOrganization;
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use App\Service\IpLookup\IpLookupServiceContract;
|
||||
use App\Service\OrganizationService;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
@@ -33,16 +34,18 @@ class CreateOrganization implements CreatesTeams
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
])->validateWithBag('createTeam');
|
||||
|
||||
$organization = new Organization;
|
||||
$organization->name = $input['name'];
|
||||
$organization->personal_team = false;
|
||||
$organization->owner()->associate($user);
|
||||
$organization->save();
|
||||
$ipLookupResponse = app(IpLookupServiceContract::class)->lookup(request()->ip());
|
||||
|
||||
$organization->users()->attach(
|
||||
$user, [
|
||||
'role' => Role::Owner->value,
|
||||
]
|
||||
$currency = null;
|
||||
if ($ipLookupResponse !== null) {
|
||||
$currency = $ipLookupResponse->currency;
|
||||
}
|
||||
|
||||
$organization = app(OrganizationService::class)->createOrganization(
|
||||
$input['name'],
|
||||
$user,
|
||||
false,
|
||||
$currency
|
||||
);
|
||||
|
||||
$user->switchTeam($organization);
|
||||
|
||||
@@ -64,8 +64,8 @@ class UserCreateCommand extends Command
|
||||
$password,
|
||||
'UTC',
|
||||
Weekday::Monday,
|
||||
'EUR',
|
||||
$verifyEmail
|
||||
null,
|
||||
verifyEmail: $verifyEmail
|
||||
);
|
||||
});
|
||||
/** @var Organization|null $organization */
|
||||
|
||||
36
app/Enums/CurrencyFormat.php
Normal file
36
app/Enums/CurrencyFormat.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
|
||||
|
||||
enum CurrencyFormat: string
|
||||
{
|
||||
use LaravelEnumHelper;
|
||||
|
||||
case ISOCodeBeforeWithSpace = 'iso-code-before-with-space';
|
||||
case ISOCodeAfterWithSpace = 'iso-code-after-with-space';
|
||||
|
||||
case SymbolBefore = 'symbol-before';
|
||||
|
||||
case SymbolAfter = 'symbol-after';
|
||||
|
||||
case SymbolBeforeWithSpace = 'symbol-before-with-space';
|
||||
|
||||
case SymbolAfterWithSpace = 'symbol-after-with-space';
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function toSelectArray(): array
|
||||
{
|
||||
$selectArray = [];
|
||||
foreach (self::values() as $value) {
|
||||
$selectArray[(string) $value] = (string) __('enum.currency_format.'.$value);
|
||||
}
|
||||
|
||||
return $selectArray;
|
||||
}
|
||||
}
|
||||
48
app/Enums/DateFormat.php
Normal file
48
app/Enums/DateFormat.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
|
||||
|
||||
enum DateFormat: string
|
||||
{
|
||||
use LaravelEnumHelper;
|
||||
|
||||
case PointSeperatedDMYYYY = 'point-seperated-d-m-yyyy';
|
||||
case SlashSeperatedMMDDYYYY = 'slash-seperated-mm-dd-yyyy';
|
||||
|
||||
case SlashSeperatedDDMMYYYY = 'slash-seperated-dd-mm-yyyy';
|
||||
|
||||
case HyphenSeperatedDDMMYYY = 'hyphen-seperated-dd-mm-yyyy';
|
||||
|
||||
case HyphenSeperatedMMDDDYYYY = 'hyphen-seperated-mm-dd-yyyy';
|
||||
|
||||
case HyphenSeperatedYYYYMMDD = 'hyphen-seperated-yyyy-mm-dd';
|
||||
|
||||
public function toCarbonFormat(): string
|
||||
{
|
||||
return match ($this->value) {
|
||||
self::PointSeperatedDMYYYY->value => 'j.n.Y',
|
||||
self::SlashSeperatedMMDDYYYY->value => 'm/d/Y',
|
||||
self::SlashSeperatedDDMMYYYY->value => 'd/m/Y',
|
||||
self::HyphenSeperatedDDMMYYY->value => 'd-m-Y',
|
||||
self::HyphenSeperatedMMDDDYYYY->value => 'm-d-Y',
|
||||
self::HyphenSeperatedYYYYMMDD->value => 'Y-m-d',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function toSelectArray(): array
|
||||
{
|
||||
$selectArray = [];
|
||||
foreach (self::values() as $value) {
|
||||
$selectArray[(string) $value] = (string) __('enum.date_format.'.$value);
|
||||
}
|
||||
|
||||
return $selectArray;
|
||||
}
|
||||
}
|
||||
32
app/Enums/IntervalFormat.php
Normal file
32
app/Enums/IntervalFormat.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
|
||||
|
||||
enum IntervalFormat: string
|
||||
{
|
||||
use LaravelEnumHelper;
|
||||
|
||||
case Decimal = 'decimal';
|
||||
case HoursMinutes = 'hours-minutes';
|
||||
|
||||
case HoursMinutesColonSeperated = 'hours-minutes-colon-seperated';
|
||||
|
||||
case HoursMinutesSecondsColonSeperated = 'hours-minutes-seconds-colon-seperated';
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function toSelectArray(): array
|
||||
{
|
||||
$selectArray = [];
|
||||
foreach (self::values() as $value) {
|
||||
$selectArray[(string) $value] = (string) __('enum.interval_format.'.$value);
|
||||
}
|
||||
|
||||
return $selectArray;
|
||||
}
|
||||
}
|
||||
37
app/Enums/NumberFormat.php
Normal file
37
app/Enums/NumberFormat.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
|
||||
|
||||
/**
|
||||
* @info https://en.wikipedia.org/wiki/Decimal_separator
|
||||
*/
|
||||
enum NumberFormat: string
|
||||
{
|
||||
use LaravelEnumHelper;
|
||||
|
||||
case ThousandsPointDecimalComma = 'point-comma';
|
||||
|
||||
case ThousandsCommaDecimalPoint = 'comma-point';
|
||||
case ThousandsSpaceDecimalComma = 'space-comma';
|
||||
|
||||
case ThousandsSpaceDecimalPoint = 'space-point';
|
||||
|
||||
case ThousandsApostropheDecimalPoint = 'apostrophe-point';
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function toSelectArray(): array
|
||||
{
|
||||
$selectArray = [];
|
||||
foreach (self::values() as $value) {
|
||||
$selectArray[(string) $value] = (string) __('enum.number_format.'.$value);
|
||||
}
|
||||
|
||||
return $selectArray;
|
||||
}
|
||||
}
|
||||
28
app/Enums/TimeFormat.php
Normal file
28
app/Enums/TimeFormat.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
|
||||
|
||||
enum TimeFormat: string
|
||||
{
|
||||
use LaravelEnumHelper;
|
||||
|
||||
case TwelveHours = '12-hours';
|
||||
case TwentyFourHours = '24-hours';
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function toSelectArray(): array
|
||||
{
|
||||
$selectArray = [];
|
||||
foreach (self::values() as $value) {
|
||||
$selectArray[(string) $value] = (string) __('enum.time_format.'.$value);
|
||||
}
|
||||
|
||||
return $selectArray;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Enums\CurrencyFormat;
|
||||
use App\Enums\DateFormat;
|
||||
use App\Enums\IntervalFormat;
|
||||
use App\Enums\NumberFormat;
|
||||
use App\Enums\TimeFormat;
|
||||
use App\Filament\Resources\OrganizationResource\Pages;
|
||||
use App\Filament\Resources\OrganizationResource\RelationManagers\InvitationsRelationManager;
|
||||
use App\Filament\Resources\OrganizationResource\RelationManagers\UsersRelationManager;
|
||||
@@ -56,6 +61,21 @@ class OrganizationResource extends Resource
|
||||
->searchable(['name', 'email'])
|
||||
->disabledOn(['edit'])
|
||||
->required(),
|
||||
Select::make('date_format')
|
||||
->options(DateFormat::toSelectArray())
|
||||
->required(),
|
||||
Select::make('currency_format')
|
||||
->options(CurrencyFormat::toSelectArray())
|
||||
->required(),
|
||||
Select::make('interval_format')
|
||||
->options(IntervalFormat::toSelectArray())
|
||||
->required(),
|
||||
Select::make('number_format')
|
||||
->options(NumberFormat::toSelectArray())
|
||||
->required(),
|
||||
Select::make('time_format')
|
||||
->options(TimeFormat::toSelectArray())
|
||||
->required(),
|
||||
Forms\Components\Select::make('currency')
|
||||
->label('Currency')
|
||||
->options(function (): array {
|
||||
|
||||
@@ -24,7 +24,7 @@ class CreateUser extends CreateRecord
|
||||
$data['timezone'],
|
||||
Weekday::from($data['week_start']),
|
||||
$data['currency'],
|
||||
(bool) $data['is_email_verified']
|
||||
verifyEmail: (bool) $data['is_email_verified']
|
||||
);
|
||||
|
||||
return $user;
|
||||
|
||||
@@ -40,15 +40,35 @@ class OrganizationController extends Controller
|
||||
{
|
||||
$this->checkPermission($organization, 'organizations:update');
|
||||
|
||||
$organization->name = $request->input('name');
|
||||
$oldBillableRate = $organization->billable_rate;
|
||||
if ($request->has('employees_can_see_billable_rates')) {
|
||||
$organization->employees_can_see_billable_rates = $request->validated('employees_can_see_billable_rates');
|
||||
if ($request->getName() !== null) {
|
||||
$organization->name = $request->getName();
|
||||
}
|
||||
if ($request->getEmployeesCanSeeBillableRates() !== null) {
|
||||
$organization->employees_can_see_billable_rates = $request->getEmployeesCanSeeBillableRates();
|
||||
}
|
||||
if ($request->getNumberFormat() !== null) {
|
||||
$organization->number_format = $request->getNumberFormat();
|
||||
}
|
||||
if ($request->getCurrencyFormat() !== null) {
|
||||
$organization->currency_format = $request->getCurrencyFormat();
|
||||
}
|
||||
if ($request->getDateFormat() !== null) {
|
||||
$organization->date_format = $request->getDateFormat();
|
||||
}
|
||||
if ($request->getIntervalFormat() !== null) {
|
||||
$organization->interval_format = $request->getIntervalFormat();
|
||||
}
|
||||
if ($request->getTimeFormat() !== null) {
|
||||
$organization->time_format = $request->getTimeFormat();
|
||||
}
|
||||
$hasBillableRate = $request->has('billable_rate');
|
||||
if ($hasBillableRate) {
|
||||
$oldBillableRate = $organization->billable_rate;
|
||||
$organization->billable_rate = $request->getBillableRate();
|
||||
}
|
||||
$organization->save();
|
||||
|
||||
if ($oldBillableRate !== $request->getBillableRate()) {
|
||||
if ($hasBillableRate && $oldBillableRate !== $request->getBillableRate()) {
|
||||
$billableRateService->updateTimeEntriesBillableRateForOrganization($organization);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ use App\Models\Organization;
|
||||
use App\Models\Project;
|
||||
use App\Models\Task;
|
||||
use App\Models\TimeEntry;
|
||||
use App\Service\LocalizationService;
|
||||
use App\Service\ReportExport\TimeEntriesDetailedCsvExport;
|
||||
use App\Service\ReportExport\TimeEntriesDetailedExport;
|
||||
use App\Service\ReportExport\TimeEntriesReportExport;
|
||||
@@ -194,6 +195,7 @@ class TimeEntryController extends Controller
|
||||
$filename = 'time-entries-export-'.now()->format('Y-m-d_H-i-s').'.'.$format->getFileExtension();
|
||||
$folderPath = 'exports';
|
||||
$path = $folderPath.'/'.$filename;
|
||||
$localizationService = LocalizationService::forOrganization($organization);
|
||||
if ($format === ExportFormat::CSV) {
|
||||
$export = new TimeEntriesDetailedCsvExport(config('filesystems.private'), $folderPath, $filename, $timeEntriesQuery, 1000, $timezone);
|
||||
$export->export();
|
||||
@@ -223,6 +225,7 @@ class TimeEntryController extends Controller
|
||||
'currency' => $organization->currency,
|
||||
'start' => $request->getStart()->timezone($timezone),
|
||||
'end' => $request->getEnd()->timezone($timezone),
|
||||
'localization' => $localizationService,
|
||||
]);
|
||||
$footerViewFile = file_get_contents(resource_path('views/reports/time-entry-index/pdf-footer.blade.php'));
|
||||
if ($footerViewFile === false) {
|
||||
@@ -257,7 +260,7 @@ class TimeEntryController extends Controller
|
||||
->putFileAs($folderPath, new File($tempFolder->path($filenameTemp)), $filename);
|
||||
} else {
|
||||
Excel::store(
|
||||
new TimeEntriesDetailedExport($timeEntriesQuery, $format, $timezone),
|
||||
new TimeEntriesDetailedExport($timeEntriesQuery, $format, $timezone, $localizationService),
|
||||
$path,
|
||||
config('filesystems.private'),
|
||||
$format->getExportPackageType(),
|
||||
@@ -394,6 +397,7 @@ class TimeEntryController extends Controller
|
||||
);
|
||||
$currency = $organization->currency;
|
||||
$timezone = app(TimezoneService::class)->getTimezoneFromUser($this->user());
|
||||
$localizationService = LocalizationService::forOrganization($organization);
|
||||
|
||||
$filename = 'time-entries-report-'.now()->format('Y-m-d_H-i-s').'.'.$format->getFileExtension();
|
||||
$folderPath = 'exports';
|
||||
@@ -423,6 +427,7 @@ class TimeEntryController extends Controller
|
||||
'start' => $request->getStart()->timezone($timezone),
|
||||
'end' => $request->getEnd()->timezone($timezone),
|
||||
'debug' => $debug,
|
||||
'localization' => $localizationService,
|
||||
]);
|
||||
$footerViewFile = file_get_contents(resource_path('views/reports/time-entry-aggregate/pdf-footer.blade.php'));
|
||||
if ($footerViewFile === false) {
|
||||
|
||||
@@ -6,7 +6,6 @@ namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Resources\V1\User\UserResource;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
@@ -19,7 +18,7 @@ class UserController extends Controller
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function me(): JsonResource
|
||||
public function me(): UserResource
|
||||
{
|
||||
$user = $this->user();
|
||||
|
||||
|
||||
@@ -4,9 +4,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\V1\Organization;
|
||||
|
||||
use App\Enums\CurrencyFormat;
|
||||
use App\Enums\DateFormat;
|
||||
use App\Enums\IntervalFormat;
|
||||
use App\Enums\NumberFormat;
|
||||
use App\Enums\TimeFormat;
|
||||
use App\Models\Organization;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
/**
|
||||
* @property Organization $organization Organization from model binding
|
||||
@@ -16,13 +21,12 @@ class OrganizationUpdateRequest extends FormRequest
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, array<string|ValidationRule>>
|
||||
* @return array<string, array<string|\Illuminate\Contracts\Validation\Rule>>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
@@ -35,13 +39,63 @@ class OrganizationUpdateRequest extends FormRequest
|
||||
'employees_can_see_billable_rates' => [
|
||||
'boolean',
|
||||
],
|
||||
'number_format' => [
|
||||
Rule::enum(NumberFormat::class),
|
||||
],
|
||||
'currency_format' => [
|
||||
Rule::enum(CurrencyFormat::class),
|
||||
],
|
||||
'date_format' => [
|
||||
Rule::enum(DateFormat::class),
|
||||
],
|
||||
'interval_format' => [
|
||||
Rule::enum(IntervalFormat::class),
|
||||
],
|
||||
'time_format' => [
|
||||
Rule::enum(TimeFormat::class),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->has('name') ? (string) $this->input('name') : null;
|
||||
}
|
||||
|
||||
public function getNumberFormat(): ?NumberFormat
|
||||
{
|
||||
return $this->has('number_format') ? NumberFormat::from($this->input('number_format')) : null;
|
||||
}
|
||||
|
||||
public function getCurrencyFormat(): ?CurrencyFormat
|
||||
{
|
||||
return $this->has('currency_format') ? CurrencyFormat::from($this->input('currency_format')) : null;
|
||||
}
|
||||
|
||||
public function getDateFormat(): ?DateFormat
|
||||
{
|
||||
return $this->has('date_format') ? DateFormat::from($this->input('date_format')) : null;
|
||||
}
|
||||
|
||||
public function getIntervalFormat(): ?IntervalFormat
|
||||
{
|
||||
return $this->has('interval_format') ? IntervalFormat::from($this->input('interval_format')) : null;
|
||||
}
|
||||
|
||||
public function getTimeFormat(): ?TimeFormat
|
||||
{
|
||||
return $this->has('time_format') ? TimeFormat::from($this->input('time_format')) : null;
|
||||
}
|
||||
|
||||
public function getBillableRate(): ?int
|
||||
{
|
||||
$input = $this->input('billable_rate');
|
||||
|
||||
return $input !== null && $input !== 0 ? (int) $this->input('billable_rate') : null;
|
||||
}
|
||||
|
||||
public function getEmployeesCanSeeBillableRates(): ?bool
|
||||
{
|
||||
return $this->has('employees_can_see_billable_rates') ? $this->boolean('employees_can_see_billable_rates') : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,16 @@ class OrganizationResource extends BaseResource
|
||||
'employees_can_see_billable_rates' => $this->resource->employees_can_see_billable_rates,
|
||||
/** @var string $currency Currency code (ISO 4217) */
|
||||
'currency' => $this->resource->currency,
|
||||
/** @var string $number_format Number format */
|
||||
'number_format' => $this->resource->number_format->value,
|
||||
/** @var string $currency_format Currency format */
|
||||
'currency_format' => $this->resource->currency_format->value,
|
||||
/** @var string $date_format Date format */
|
||||
'date_format' => $this->resource->date_format->value,
|
||||
/** @var string $interval_format Interval format */
|
||||
'interval_format' => $this->resource->interval_format->value,
|
||||
/** @var string $time_format Time format */
|
||||
'time_format' => $this->resource->time_format->value,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ class DetailedReportResource extends BaseResource
|
||||
/** @var bool $is_public Whether the report can be accessed via an external link */
|
||||
'is_public' => $this->resource->is_public,
|
||||
/** @var string|null $public_until Date until the report is public */
|
||||
'public_until' => $this->resource->public_until?->toIso8601ZuluString(),
|
||||
'public_until' => $this->formatDateTime($this->resource->public_until),
|
||||
/** @var string|null $shareable_link Get link to access the report externally, not set if the report is private */
|
||||
'shareable_link' => $this->resource->getShareableLink(),
|
||||
'properties' => [
|
||||
@@ -41,9 +41,9 @@ class DetailedReportResource extends BaseResource
|
||||
/** @var string $history_group Type of grouping of the historic aggregation (time chart) */
|
||||
'history_group' => $this->resource->properties->historyGroup->value,
|
||||
/** @var string $start Start date of the report */
|
||||
'start' => $this->resource->properties->start->toIso8601ZuluString(),
|
||||
'start' => $this->formatDateTime($this->resource->properties->start),
|
||||
/** @var string $end End date of the report */
|
||||
'end' => $this->resource->properties->end->toIso8601ZuluString(),
|
||||
'end' => $this->formatDateTime($this->resource->properties->end),
|
||||
/** @var bool|null $active Whether the report is active */
|
||||
'active' => $this->resource->properties->active,
|
||||
/** @var array<string>|null $member_ids Filter by multiple member IDs, member IDs are OR combined */
|
||||
@@ -60,9 +60,9 @@ class DetailedReportResource extends BaseResource
|
||||
'task_ids' => $this->resource->properties->taskIds?->toArray(),
|
||||
],
|
||||
/** @var string $created_at Date when the report was created */
|
||||
'created_at' => $this->resource->created_at?->toIso8601ZuluString(),
|
||||
'created_at' => $this->formatDateTime($this->resource->created_at),
|
||||
/** @var string $updated_at Date when the report was last updated */
|
||||
'updated_at' => $this->resource->updated_at?->toIso8601ZuluString(),
|
||||
'updated_at' => $this->formatDateTime($this->resource->updated_at),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ class DetailedWithDataReportResource extends BaseResource
|
||||
/** @var string|null $email Description */
|
||||
'description' => $this->resource->description,
|
||||
/** @var string|null $public_until Date until the report is public */
|
||||
'public_until' => $this->resource->public_until?->toIso8601ZuluString(),
|
||||
'public_until' => $this->formatDateTime($this->resource->public_until),
|
||||
/** @var string $currency Currency code (ISO 4217) */
|
||||
'currency' => $this->resource->organization->currency,
|
||||
'properties' => [
|
||||
@@ -81,9 +81,9 @@ class DetailedWithDataReportResource extends BaseResource
|
||||
/** @var string $history_group Type of grouping of the historic aggregation (time chart) */
|
||||
'history_group' => $this->resource->properties->historyGroup->value,
|
||||
/** @var string $start Start date of the report */
|
||||
'start' => $this->resource->properties->start->toIso8601ZuluString(),
|
||||
'start' => $this->formatDateTime($this->resource->properties->start),
|
||||
/** @var string $end End date of the report */
|
||||
'end' => $this->resource->properties->end->toIso8601ZuluString(),
|
||||
'end' => $this->formatDateTime($this->resource->properties->end),
|
||||
],
|
||||
/** @var array{
|
||||
* grouped_type: string|null,
|
||||
|
||||
@@ -30,13 +30,13 @@ class ReportResource extends BaseResource
|
||||
/** @var bool $is_public Whether the report can be accessed via an external link */
|
||||
'is_public' => $this->resource->is_public,
|
||||
/** @var string|null $public_until Date until the report is public */
|
||||
'public_until' => $this->resource->public_until?->toIso8601ZuluString(),
|
||||
'public_until' => $this->formatDateTime($this->resource->public_until),
|
||||
/** @var string|null $shareable_link Get link to access the report externally, not set if the report is private */
|
||||
'shareable_link' => $this->resource->getShareableLink(),
|
||||
/** @var string $created_at Date when the report was created */
|
||||
'created_at' => $this->resource->created_at?->toIso8601ZuluString(),
|
||||
'created_at' => $this->formatDateTime($this->resource->created_at),
|
||||
/** @var string $updated_at Date when the report was last updated */
|
||||
'updated_at' => $this->resource->updated_at?->toIso8601ZuluString(),
|
||||
'updated_at' => $this->formatDateTime($this->resource->updated_at),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\CurrencyFormat;
|
||||
use App\Enums\DateFormat;
|
||||
use App\Enums\IntervalFormat;
|
||||
use App\Enums\NumberFormat;
|
||||
use App\Enums\TimeFormat;
|
||||
use App\Models\Concerns\CustomAuditable;
|
||||
use App\Models\Concerns\HasUuids;
|
||||
use Database\Factories\OrganizationFactory;
|
||||
@@ -18,7 +23,6 @@ use Illuminate\Support\Str;
|
||||
use Laravel\Jetstream\Events\TeamCreated;
|
||||
use Laravel\Jetstream\Events\TeamDeleted;
|
||||
use Laravel\Jetstream\Events\TeamUpdated;
|
||||
use Laravel\Jetstream\Jetstream;
|
||||
use Laravel\Jetstream\Team as JetstreamTeam;
|
||||
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
|
||||
|
||||
@@ -37,6 +41,11 @@ use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
|
||||
* @property Collection<int, User> $realUsers
|
||||
* @property-read Collection<int, OrganizationInvitation> $teamInvitations
|
||||
* @property Member $membership
|
||||
* @property NumberFormat $number_format
|
||||
* @property CurrencyFormat $currency_format
|
||||
* @property DateFormat $date_format
|
||||
* @property IntervalFormat $interval_format
|
||||
* @property TimeFormat $time_format
|
||||
*
|
||||
* @method HasMany<OrganizationInvitation> teamInvitations()
|
||||
* @method static OrganizationFactory factory()
|
||||
@@ -60,6 +69,11 @@ class Organization extends JetstreamTeam implements AuditableContract
|
||||
'personal_team' => 'boolean',
|
||||
'currency' => 'string',
|
||||
'employees_can_see_billable_rates' => 'boolean',
|
||||
'number_format' => NumberFormat::class,
|
||||
'currency_format' => CurrencyFormat::class,
|
||||
'date_format' => DateFormat::class,
|
||||
'interval_format' => IntervalFormat::class,
|
||||
'time_format' => TimeFormat::class,
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -89,7 +103,6 @@ class Organization extends JetstreamTeam implements AuditableContract
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected $attributes = [
|
||||
'currency' => 'EUR',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
377
app/Service/CurrencyService.php
Normal file
377
app/Service/CurrencyService.php
Normal file
@@ -0,0 +1,377 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use Brick\Money\Money;
|
||||
|
||||
class CurrencyService
|
||||
{
|
||||
/**
|
||||
* @source https://gist.github.com/stephenfrank/a8245c2486f3e546107c5363706ac93e
|
||||
*
|
||||
* @const array<string, array<{ symbol: string }>>
|
||||
*/
|
||||
private const array CURRENCIES = [
|
||||
'ALL' => [
|
||||
'symbol' => 'L',
|
||||
],
|
||||
'AFN' => [
|
||||
'symbol' => '؋',
|
||||
],
|
||||
'ARS' => [
|
||||
'symbol' => '$',
|
||||
],
|
||||
'AWG' => [
|
||||
'symbol' => 'ƒ',
|
||||
],
|
||||
'AUD' => [
|
||||
'symbol' => '$',
|
||||
],
|
||||
'AZN' => [
|
||||
'symbol' => '₼',
|
||||
],
|
||||
'BSD' => [
|
||||
'symbol' => '$',
|
||||
],
|
||||
'BBD' => [
|
||||
'symbol' => '$',
|
||||
],
|
||||
'BDT' => [
|
||||
'symbol' => '৳',
|
||||
],
|
||||
'BYR' => [
|
||||
'symbol' => 'Br',
|
||||
],
|
||||
'BZD' => [
|
||||
'symbol' => 'BZ$',
|
||||
],
|
||||
'BMD' => [
|
||||
'symbol' => '$',
|
||||
],
|
||||
'BOB' => [
|
||||
'symbol' => '$b',
|
||||
],
|
||||
'BAM' => [
|
||||
'symbol' => 'KM',
|
||||
],
|
||||
'BWP' => [
|
||||
'symbol' => 'P',
|
||||
],
|
||||
'BGN' => [
|
||||
'symbol' => 'лв',
|
||||
],
|
||||
'BRL' => [
|
||||
'symbol' => 'R$',
|
||||
],
|
||||
'BND' => [
|
||||
'symbol' => '$',
|
||||
],
|
||||
'KHR' => [
|
||||
'symbol' => '៛',
|
||||
],
|
||||
'CAD' => [
|
||||
'symbol' => '$',
|
||||
],
|
||||
'KYD' => [
|
||||
'symbol' => '$',
|
||||
],
|
||||
'CLP' => [
|
||||
'symbol' => '$',
|
||||
],
|
||||
'CNY' => [
|
||||
'symbol' => '¥',
|
||||
],
|
||||
'COP' => [
|
||||
'symbol' => '$',
|
||||
],
|
||||
'CRC' => [
|
||||
'symbol' => '₡',
|
||||
],
|
||||
'HRK' => [
|
||||
'symbol' => 'kn',
|
||||
],
|
||||
'CUP' => [
|
||||
'symbol' => '₱',
|
||||
],
|
||||
'CZK' => [
|
||||
'symbol' => 'Kč',
|
||||
],
|
||||
'DKK' => [
|
||||
'symbol' => 'kr',
|
||||
],
|
||||
'DOP' => [
|
||||
'symbol' => 'RD$',
|
||||
],
|
||||
'XCD' => [
|
||||
'symbol' => '$',
|
||||
],
|
||||
'EGP' => [
|
||||
'symbol' => '£',
|
||||
],
|
||||
'SVC' => [
|
||||
'symbol' => '$',
|
||||
],
|
||||
'EEK' => [
|
||||
'symbol' => 'kr',
|
||||
],
|
||||
'EUR' => [
|
||||
'symbol' => '€',
|
||||
],
|
||||
'FKP' => [
|
||||
'symbol' => '£',
|
||||
],
|
||||
'FJD' => [
|
||||
'symbol' => '$',
|
||||
],
|
||||
'GHC' => [
|
||||
'symbol' => '₵',
|
||||
],
|
||||
'GIP' => [
|
||||
'symbol' => '£',
|
||||
],
|
||||
'GTQ' => [
|
||||
'symbol' => 'Q',
|
||||
],
|
||||
'GGP' => [
|
||||
'symbol' => '£',
|
||||
],
|
||||
'GYD' => [
|
||||
'symbol' => '$',
|
||||
],
|
||||
'HNL' => [
|
||||
'symbol' => 'L',
|
||||
],
|
||||
'HKD' => [
|
||||
'symbol' => '$',
|
||||
],
|
||||
'HUF' => [
|
||||
'symbol' => 'Ft',
|
||||
],
|
||||
'ISK' => [
|
||||
'symbol' => 'kr',
|
||||
],
|
||||
'INR' => [
|
||||
'symbol' => '₹',
|
||||
],
|
||||
'IDR' => [
|
||||
'symbol' => 'Rp',
|
||||
],
|
||||
'IRR' => [
|
||||
'symbol' => '﷼',
|
||||
],
|
||||
'IMP' => [
|
||||
'symbol' => '£',
|
||||
],
|
||||
'ILS' => [
|
||||
'symbol' => '₪',
|
||||
],
|
||||
'JMD' => [
|
||||
'symbol' => 'J$',
|
||||
],
|
||||
'JPY' => [
|
||||
'symbol' => '¥',
|
||||
],
|
||||
'JEP' => [
|
||||
'symbol' => '£',
|
||||
],
|
||||
'KZT' => [
|
||||
'symbol' => 'лв',
|
||||
],
|
||||
'KPW' => [
|
||||
'symbol' => '₩',
|
||||
],
|
||||
'KRW' => [
|
||||
'symbol' => '₩',
|
||||
],
|
||||
'KGS' => [
|
||||
'symbol' => 'лв',
|
||||
],
|
||||
'LAK' => [
|
||||
'symbol' => '₭',
|
||||
],
|
||||
'LVL' => [
|
||||
'symbol' => 'Ls',
|
||||
],
|
||||
'LBP' => [
|
||||
'symbol' => '£',
|
||||
],
|
||||
'LRD' => [
|
||||
'symbol' => '$',
|
||||
],
|
||||
'LTL' => [
|
||||
'symbol' => 'Lt',
|
||||
],
|
||||
'MKD' => [
|
||||
'symbol' => 'ден',
|
||||
],
|
||||
'MYR' => [
|
||||
'symbol' => 'RM',
|
||||
],
|
||||
'MUR' => [
|
||||
'symbol' => '₨',
|
||||
],
|
||||
'MXN' => [
|
||||
'symbol' => '$',
|
||||
],
|
||||
'MNT' => [
|
||||
'symbol' => '₮',
|
||||
],
|
||||
'MZN' => [
|
||||
'symbol' => 'MT',
|
||||
],
|
||||
'NAD' => [
|
||||
'symbol' => '$',
|
||||
],
|
||||
'NPR' => [
|
||||
'symbol' => '₨',
|
||||
],
|
||||
'ANG' => [
|
||||
'symbol' => 'ƒ',
|
||||
],
|
||||
'NZD' => [
|
||||
'symbol' => '$',
|
||||
],
|
||||
'NIO' => [
|
||||
'symbol' => 'C$',
|
||||
],
|
||||
'NGN' => [
|
||||
'symbol' => '₦',
|
||||
],
|
||||
'NOK' => [
|
||||
'symbol' => 'kr',
|
||||
],
|
||||
'OMR' => [
|
||||
'symbol' => '﷼',
|
||||
],
|
||||
'PKR' => [
|
||||
'symbol' => '₨',
|
||||
],
|
||||
'PAB' => [
|
||||
'symbol' => 'B/.',
|
||||
],
|
||||
'PYG' => [
|
||||
'symbol' => 'Gs',
|
||||
],
|
||||
'PEN' => [
|
||||
'symbol' => 'S/.',
|
||||
],
|
||||
'PHP' => [
|
||||
'symbol' => '₱',
|
||||
],
|
||||
'PLN' => [
|
||||
'symbol' => 'zł',
|
||||
],
|
||||
'QAR' => [
|
||||
'symbol' => '﷼',
|
||||
],
|
||||
'RON' => [
|
||||
'symbol' => 'lei',
|
||||
],
|
||||
'RUB' => [
|
||||
'symbol' => '₽',
|
||||
],
|
||||
'SHP' => [
|
||||
'symbol' => '£',
|
||||
],
|
||||
'SAR' => [
|
||||
'symbol' => '﷼',
|
||||
],
|
||||
'RSD' => [
|
||||
'symbol' => 'Дин.',
|
||||
],
|
||||
'SCR' => [
|
||||
'symbol' => '₨',
|
||||
],
|
||||
'SGD' => [
|
||||
'symbol' => '$',
|
||||
],
|
||||
'SBD' => [
|
||||
'symbol' => '$',
|
||||
],
|
||||
'SOS' => [
|
||||
'symbol' => 'S',
|
||||
],
|
||||
'ZAR' => [
|
||||
'symbol' => 'R',
|
||||
],
|
||||
'LKR' => [
|
||||
'symbol' => '₨',
|
||||
],
|
||||
'SEK' => [
|
||||
'symbol' => 'kr',
|
||||
],
|
||||
'CHF' => [
|
||||
'symbol' => 'CHF',
|
||||
],
|
||||
'SRD' => [
|
||||
'symbol' => '$',
|
||||
],
|
||||
'SYP' => [
|
||||
'symbol' => '£',
|
||||
],
|
||||
'TWD' => [
|
||||
'symbol' => 'NT$',
|
||||
],
|
||||
'THB' => [
|
||||
'symbol' => '฿',
|
||||
],
|
||||
'TTD' => [
|
||||
'symbol' => 'TT$',
|
||||
],
|
||||
'TRY' => [
|
||||
'symbol' => '₺',
|
||||
],
|
||||
'TRL' => [
|
||||
'symbol' => '₤',
|
||||
],
|
||||
'TVD' => [
|
||||
'symbol' => '$',
|
||||
],
|
||||
'UAH' => [
|
||||
'symbol' => '₴',
|
||||
],
|
||||
'GBP' => [
|
||||
'symbol' => '£',
|
||||
],
|
||||
'UGX' => [
|
||||
'symbol' => 'USh',
|
||||
],
|
||||
'USD' => [
|
||||
'symbol' => '$',
|
||||
],
|
||||
'UYU' => [
|
||||
'symbol' => '$U',
|
||||
],
|
||||
'UZS' => [
|
||||
'symbol' => 'лв',
|
||||
],
|
||||
'VEF' => [
|
||||
'symbol' => 'Bs',
|
||||
],
|
||||
'VND' => [
|
||||
'symbol' => '₫',
|
||||
],
|
||||
'YER' => [
|
||||
'symbol' => '﷼',
|
||||
],
|
||||
'ZWD' => [
|
||||
'symbol' => 'Z$',
|
||||
],
|
||||
];
|
||||
|
||||
public function getCurrencySymbolForMoney(Money $money): string
|
||||
{
|
||||
return $this->getCurrencySymbol($money->getCurrency()->getCurrencyCode());
|
||||
}
|
||||
|
||||
public function getCurrencySymbol(string $currencyCode): string
|
||||
{
|
||||
if (isset(self::CURRENCIES[$currencyCode]['symbol'])) {
|
||||
return self::CURRENCIES[$currencyCode]['symbol'];
|
||||
}
|
||||
|
||||
return $currencyCode;
|
||||
}
|
||||
}
|
||||
144
app/Service/LocalizationService.php
Normal file
144
app/Service/LocalizationService.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Enums\CurrencyFormat;
|
||||
use App\Enums\DateFormat;
|
||||
use App\Enums\IntervalFormat;
|
||||
use App\Enums\NumberFormat;
|
||||
use App\Enums\TimeFormat;
|
||||
use App\Models\Organization;
|
||||
use Brick\Math\BigDecimal;
|
||||
use Brick\Money\Money;
|
||||
use Carbon\CarbonInterface;
|
||||
use Carbon\CarbonInterval;
|
||||
|
||||
class LocalizationService
|
||||
{
|
||||
private CurrencyFormat $currencyFormat;
|
||||
|
||||
private IntervalFormat $intervalFormat;
|
||||
|
||||
private DateFormat $dateFormat;
|
||||
|
||||
private TimeFormat $timeFormat;
|
||||
|
||||
private NumberFormat $numberFormat;
|
||||
|
||||
public function __construct(CurrencyFormat $currencyFormat, DateFormat $dateFormat, TimeFormat $timeFormat, NumberFormat $numberFormat, IntervalFormat $intervalFormat)
|
||||
{
|
||||
$this->currencyFormat = $currencyFormat;
|
||||
$this->dateFormat = $dateFormat;
|
||||
$this->timeFormat = $timeFormat;
|
||||
$this->numberFormat = $numberFormat;
|
||||
$this->intervalFormat = $intervalFormat;
|
||||
}
|
||||
|
||||
public static function forOrganization(Organization $organization): self
|
||||
{
|
||||
return new LocalizationService(
|
||||
$organization->currency_format,
|
||||
$organization->date_format,
|
||||
$organization->time_format,
|
||||
$organization->number_format,
|
||||
$organization->interval_format
|
||||
);
|
||||
}
|
||||
|
||||
public function formatNumber(BigDecimal|float $number): string
|
||||
{
|
||||
$numberFloat = $number instanceof BigDecimal ? $number->toFloat() : $number;
|
||||
|
||||
if ($this->numberFormat === NumberFormat::ThousandsPointDecimalComma) {
|
||||
return number_format($numberFloat, 2, ',', '.');
|
||||
} elseif ($this->numberFormat === NumberFormat::ThousandsSpaceDecimalPoint) {
|
||||
return number_format($numberFloat, 2, '.', ' ');
|
||||
} elseif ($this->numberFormat === NumberFormat::ThousandsCommaDecimalPoint) {
|
||||
return number_format($numberFloat, 2, '.', ',');
|
||||
} elseif ($this->numberFormat === NumberFormat::ThousandsSpaceDecimalComma) {
|
||||
return number_format($numberFloat, 2, ',', ' ');
|
||||
} elseif ($this->numberFormat === NumberFormat::ThousandsApostropheDecimalPoint) {
|
||||
return number_format($numberFloat, 2, '.', '\'');
|
||||
}
|
||||
}
|
||||
|
||||
public function formatInterval(CarbonInterval $interval): string
|
||||
{
|
||||
if ($this->intervalFormat === IntervalFormat::Decimal) {
|
||||
$interval->cascade();
|
||||
|
||||
return $this->formatNumber($interval->totalHours);
|
||||
} elseif ($this->intervalFormat === IntervalFormat::HoursMinutes) {
|
||||
$interval->cascade();
|
||||
|
||||
return ((int) floor($interval->totalHours)).'h '.$interval->format('%I').'m';
|
||||
} elseif ($this->intervalFormat === IntervalFormat::HoursMinutesColonSeperated) {
|
||||
$interval->cascade();
|
||||
|
||||
return ((int) floor($interval->totalHours)).':'.$interval->format('%I');
|
||||
} elseif ($this->intervalFormat === IntervalFormat::HoursMinutesSecondsColonSeperated) {
|
||||
$interval->cascade();
|
||||
|
||||
return ((int) floor($interval->totalHours)).':'.$interval->format('%I:%S');
|
||||
}
|
||||
}
|
||||
|
||||
public function formatCurrency(Money $money): string
|
||||
{
|
||||
$currencyService = app(CurrencyService::class);
|
||||
if ($this->currencyFormat === CurrencyFormat::ISOCodeAfterWithSpace) {
|
||||
return $this->formatNumber($money->getAmount()).' '.$money->getCurrency()->getCurrencyCode();
|
||||
} elseif ($this->currencyFormat === CurrencyFormat::ISOCodeBeforeWithSpace) {
|
||||
return $money->getCurrency()->getCurrencyCode().' '.$this->formatNumber($money->getAmount());
|
||||
} elseif ($this->currencyFormat === CurrencyFormat::SymbolAfter) {
|
||||
return $this->formatNumber($money->getAmount()).$currencyService->getCurrencySymbolForMoney($money);
|
||||
} elseif ($this->currencyFormat === CurrencyFormat::SymbolBefore) {
|
||||
return $currencyService->getCurrencySymbolForMoney($money).$this->formatNumber($money->getAmount());
|
||||
} elseif ($this->currencyFormat === CurrencyFormat::SymbolBeforeWithSpace) {
|
||||
return $currencyService->getCurrencySymbolForMoney($money).' '.$this->formatNumber($money->getAmount());
|
||||
} elseif ($this->currencyFormat === CurrencyFormat::SymbolAfterWithSpace) {
|
||||
return $this->formatNumber($money->getAmount()).' '.$currencyService->getCurrencySymbolForMoney($money);
|
||||
}
|
||||
}
|
||||
|
||||
public function formatTime(CarbonInterface $time): string
|
||||
{
|
||||
if ($this->timeFormat === TimeFormat::TwelveHours) {
|
||||
return $time->format('h:i a'); // Examples: "11:01 am", "1:02 am"
|
||||
} elseif ($this->timeFormat === TimeFormat::TwentyFourHours) {
|
||||
return $time->format('H:i'); // Examples: "23:01", "01:02"
|
||||
}
|
||||
}
|
||||
|
||||
public function formatDate(CarbonInterface $date): string
|
||||
{
|
||||
return $date->format($this->dateFormat->toCarbonFormat());
|
||||
}
|
||||
|
||||
public function setDateFormat(DateFormat $dateFormat): void
|
||||
{
|
||||
$this->dateFormat = $dateFormat;
|
||||
}
|
||||
|
||||
public function setCurrencyFormat(CurrencyFormat $currencyFormat): void
|
||||
{
|
||||
$this->currencyFormat = $currencyFormat;
|
||||
}
|
||||
|
||||
public function setIntervalFormat(IntervalFormat $intervalFormat): void
|
||||
{
|
||||
$this->intervalFormat = $intervalFormat;
|
||||
}
|
||||
|
||||
public function setTimeFormat(TimeFormat $timeFormat): void
|
||||
{
|
||||
$this->timeFormat = $timeFormat;
|
||||
}
|
||||
|
||||
public function setNumberFormat(NumberFormat $numberFormat): void
|
||||
{
|
||||
$this->numberFormat = $numberFormat;
|
||||
}
|
||||
}
|
||||
68
app/Service/OrganizationService.php
Normal file
68
app/Service/OrganizationService.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Enums\CurrencyFormat;
|
||||
use App\Enums\DateFormat;
|
||||
use App\Enums\IntervalFormat;
|
||||
use App\Enums\NumberFormat;
|
||||
use App\Enums\Role;
|
||||
use App\Enums\TimeFormat;
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
|
||||
class OrganizationService
|
||||
{
|
||||
public function createOrganization(
|
||||
string $name,
|
||||
User $owner,
|
||||
bool $personalOrganization,
|
||||
?string $currency = null,
|
||||
?NumberFormat $numberFormat = null,
|
||||
?CurrencyFormat $currencyFormat = null,
|
||||
?DateFormat $dateFormat = null,
|
||||
?IntervalFormat $intervalFormat = null,
|
||||
?TimeFormat $timeFormat = null,
|
||||
): Organization {
|
||||
|
||||
$organization = new Organization;
|
||||
$organization->name = $name;
|
||||
$organization->personal_team = $personalOrganization;
|
||||
if ($currency === null) {
|
||||
$currency = config('app.localization.default_currency');
|
||||
}
|
||||
$organization->currency = $currency;
|
||||
if ($numberFormat === null) {
|
||||
$numberFormat = NumberFormat::from(config('app.localization.default_number_format'));
|
||||
}
|
||||
$organization->number_format = $numberFormat;
|
||||
if ($currencyFormat === null) {
|
||||
$currencyFormat = CurrencyFormat::from(config('app.localization.default_currency_format'));
|
||||
}
|
||||
$organization->currency_format = $currencyFormat;
|
||||
if ($dateFormat === null) {
|
||||
$dateFormat = DateFormat::from(config('app.localization.default_date_format'));
|
||||
}
|
||||
$organization->date_format = $dateFormat;
|
||||
if ($intervalFormat === null) {
|
||||
$intervalFormat = IntervalFormat::from(config('app.localization.default_interval_format'));
|
||||
}
|
||||
$organization->interval_format = $intervalFormat;
|
||||
if ($timeFormat === null) {
|
||||
$timeFormat = TimeFormat::from(config('app.localization.default_time_format'));
|
||||
}
|
||||
$organization->time_format = $timeFormat;
|
||||
$organization->owner()->associate($owner);
|
||||
$organization->save();
|
||||
|
||||
$organization->users()->attach(
|
||||
$owner, [
|
||||
'role' => Role::Owner->value,
|
||||
]
|
||||
);
|
||||
|
||||
return $organization;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ namespace App\Service\ReportExport;
|
||||
|
||||
use App\Enums\ExportFormat;
|
||||
use App\Models\TimeEntry;
|
||||
use App\Service\IntervalService;
|
||||
use App\Service\LocalizationService;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use LogicException;
|
||||
use Maatwebsite\Excel\Concerns\Exportable;
|
||||
@@ -37,14 +37,17 @@ class TimeEntriesDetailedExport implements FromQuery, ShouldAutoSize, WithColumn
|
||||
|
||||
private string $timezone;
|
||||
|
||||
private LocalizationService $localizationService;
|
||||
|
||||
/**
|
||||
* @param Builder<TimeEntry> $builder
|
||||
*/
|
||||
public function __construct(Builder $builder, ExportFormat $exportFormat, string $timezone)
|
||||
public function __construct(Builder $builder, ExportFormat $exportFormat, string $timezone, LocalizationService $localizationService)
|
||||
{
|
||||
$this->builder = $builder;
|
||||
$this->exportFormat = $exportFormat;
|
||||
$this->timezone = $timezone;
|
||||
$this->localizationService = $localizationService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,7 +116,6 @@ class TimeEntriesDetailedExport implements FromQuery, ShouldAutoSize, WithColumn
|
||||
*/
|
||||
public function map($model): array
|
||||
{
|
||||
$interval = app(IntervalService::class);
|
||||
$duration = $model->getDuration();
|
||||
|
||||
if ($this->exportFormat === ExportFormat::XLSX) {
|
||||
@@ -125,7 +127,7 @@ class TimeEntriesDetailedExport implements FromQuery, ShouldAutoSize, WithColumn
|
||||
$model->user->name,
|
||||
Date::dateTimeToExcel($model->start->timezone($this->timezone)),
|
||||
$model->end !== null ? Date::dateTimeToExcel($model->end->timezone($this->timezone)) : null,
|
||||
$duration !== null ? $interval->format($duration) : null,
|
||||
$duration !== null ? $this->localizationService->formatInterval($duration) : null,
|
||||
$duration?->totalHours,
|
||||
$model->billable ? 'Yes' : 'No',
|
||||
$model->tagsRelation->pluck('name')->implode(', '),
|
||||
@@ -139,7 +141,7 @@ class TimeEntriesDetailedExport implements FromQuery, ShouldAutoSize, WithColumn
|
||||
$model->user->name,
|
||||
$model->start->timezone($this->timezone)->format('Y-m-d H:i:s'),
|
||||
$model->end?->timezone($this->timezone)?->format('Y-m-d H:i:s'),
|
||||
$duration !== null ? (int) floor($duration->totalHours).':'.$duration->format('%I:%S') : null,
|
||||
$duration !== null ? $this->localizationService->formatInterval($duration) : null,
|
||||
$duration?->totalHours,
|
||||
$model->billable ? 'Yes' : 'No',
|
||||
$model->tagsRelation->pluck('name')->implode(', '),
|
||||
|
||||
@@ -4,7 +4,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Enums\CurrencyFormat;
|
||||
use App\Enums\DateFormat;
|
||||
use App\Enums\IntervalFormat;
|
||||
use App\Enums\NumberFormat;
|
||||
use App\Enums\Role;
|
||||
use App\Enums\TimeFormat;
|
||||
use App\Enums\Weekday;
|
||||
use App\Events\AfterCreateOrganization;
|
||||
use App\Models\Member;
|
||||
@@ -17,8 +22,20 @@ use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class UserService
|
||||
{
|
||||
public function createUser(string $name, string $email, string $password, string $timezone, Weekday $weekStart, string $currency, bool $verifyEmail = false): User
|
||||
{
|
||||
public function createUser(
|
||||
string $name,
|
||||
string $email,
|
||||
string $password,
|
||||
string $timezone,
|
||||
Weekday $weekStart,
|
||||
?string $currency,
|
||||
?NumberFormat $numberFormat = null,
|
||||
?CurrencyFormat $currencyFormat = null,
|
||||
?DateFormat $dateFormat = null,
|
||||
?IntervalFormat $intervalFormat = null,
|
||||
?TimeFormat $timeFormat = null,
|
||||
bool $verifyEmail = false
|
||||
): User {
|
||||
$user = new User;
|
||||
$user->name = $name;
|
||||
$user->email = $email;
|
||||
@@ -30,17 +47,16 @@ class UserService
|
||||
}
|
||||
$user->save();
|
||||
|
||||
$organization = new Organization;
|
||||
$organization->name = explode(' ', $user->name, 2)[0]."'s Organization";
|
||||
$organization->personal_team = true;
|
||||
$organization->currency = $currency;
|
||||
$organization->owner()->associate($user);
|
||||
$organization->save();
|
||||
|
||||
$organization->users()->attach(
|
||||
$user, [
|
||||
'role' => Role::Owner->value,
|
||||
]
|
||||
$organization = app(OrganizationService::class)->createOrganization(
|
||||
$this->getOrganizationNameForUserName($user->name),
|
||||
$user,
|
||||
true,
|
||||
$currency,
|
||||
$numberFormat,
|
||||
$currencyFormat,
|
||||
$dateFormat,
|
||||
$intervalFormat,
|
||||
$timeFormat,
|
||||
);
|
||||
|
||||
$user->ownedTeams()->save($organization);
|
||||
@@ -78,14 +94,11 @@ class UserService
|
||||
}
|
||||
|
||||
// Create a new organization
|
||||
$organization = new Organization;
|
||||
$organization->name = $user->name."'s Organization";
|
||||
$organization->personal_team = true;
|
||||
$organization->user_id = $user->id;
|
||||
$organization->save();
|
||||
|
||||
// Attach the user to the organization
|
||||
$organization->users()->attach($user, ['role' => Role::Owner->value]);
|
||||
$organization = app(OrganizationService::class)->createOrganization(
|
||||
$this->getOrganizationNameForUserName($user->name),
|
||||
$user,
|
||||
true
|
||||
);
|
||||
|
||||
// Set the organization as the user's current organization
|
||||
$user->currentOrganization()->associate($organization);
|
||||
@@ -94,6 +107,11 @@ class UserService
|
||||
AfterCreateOrganization::dispatch($organization);
|
||||
}
|
||||
|
||||
public function getOrganizationNameForUserName(string $username): string
|
||||
{
|
||||
return explode(' ', $username, 2)[0]."'s Organization";
|
||||
}
|
||||
|
||||
public function makeSureUserHasCurrentOrganization(User $user): void
|
||||
{
|
||||
if ($user->currentOrganization !== null) {
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Enums\CurrencyFormat;
|
||||
use App\Enums\DateFormat;
|
||||
use App\Enums\IntervalFormat;
|
||||
use App\Enums\NumberFormat;
|
||||
use App\Enums\TimeFormat;
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
@@ -138,6 +143,15 @@ return [
|
||||
|
||||
'cipher' => 'AES-256-CBC',
|
||||
|
||||
'localization' => [
|
||||
'default_currency' => env('LOCALIZATION_DEFAULT_CURRENCY', 'EUR'),
|
||||
'default_number_format' => env('LOCALIZATION_DEFAULT_NUMBER_FORMAT', NumberFormat::ThousandsPointDecimalComma->value),
|
||||
'default_currency_format' => env('LOCALIZATION_DEFAULT_CURRENCY_FORMAT', CurrencyFormat::ISOCodeAfterWithSpace->value),
|
||||
'default_date_format' => env('LOCALIZATION_DEFAULT_DATE_FORMAT', DateFormat::HyphenSeperatedYYYYMMDD->value),
|
||||
'default_time_format' => env('LOCALIZATION_DEFAULT_TIME_FORMAT', TimeFormat::TwentyFourHours->value),
|
||||
'default_interval_format' => env('LOCALIZATION_DEFAULT_INTERVAL_FORMAT', IntervalFormat::HoursMinutes->value),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Maintenance Mode Driver
|
||||
|
||||
@@ -4,6 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Enums\CurrencyFormat;
|
||||
use App\Enums\DateFormat;
|
||||
use App\Enums\IntervalFormat;
|
||||
use App\Enums\NumberFormat;
|
||||
use App\Enums\TimeFormat;
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
@@ -27,6 +32,11 @@ class OrganizationFactory extends Factory
|
||||
'user_id' => User::factory(),
|
||||
'personal_team' => true,
|
||||
'employees_can_see_billable_rates' => false,
|
||||
'number_format' => $this->faker->randomElement(NumberFormat::values()),
|
||||
'currency_format' => $this->faker->randomElement(CurrencyFormat::values()),
|
||||
'date_format' => $this->faker->randomElement(DateFormat::values()),
|
||||
'interval_format' => $this->faker->randomElement(IntervalFormat::values()),
|
||||
'time_format' => $this->faker->randomElement(TimeFormat::values()),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('organizations', function (Blueprint $table): void {
|
||||
$table->string('number_format')->default(config('app.localization.default_number_format'))->nullable(false);
|
||||
$table->string('currency_format')->default(config('app.localization.default_currency_format'))->nullable(false);
|
||||
$table->string('date_format')->default(config('app.localization.default_date_format'))->nullable(false);
|
||||
$table->string('interval_format')->default(config('app.localization.default_interval_format'))->nullable(false);
|
||||
$table->string('time_format')->default(config('app.localization.default_time_format'))->nullable(false);
|
||||
});
|
||||
|
||||
Schema::table('organizations', function (Blueprint $table): void {
|
||||
$table->string('number_format')->default(null)->nullable(false)->change();
|
||||
$table->string('currency_format')->default(null)->nullable(false)->change();
|
||||
$table->string('date_format')->default(null)->nullable(false)->change();
|
||||
$table->string('interval_format')->default(null)->nullable(false)->change();
|
||||
$table->string('time_format')->default(null)->nullable(false)->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('organizations', function (Blueprint $table): void {
|
||||
$table->dropColumn('number_format');
|
||||
$table->dropColumn('currency_format');
|
||||
$table->dropColumn('date_format');
|
||||
$table->dropColumn('interval_format');
|
||||
$table->dropColumn('time_format');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Enums\CurrencyFormat;
|
||||
use App\Enums\DateFormat;
|
||||
use App\Enums\IntervalFormat;
|
||||
use App\Enums\NumberFormat;
|
||||
use App\Enums\TimeFormat;
|
||||
use App\Enums\Weekday;
|
||||
|
||||
return [
|
||||
@@ -16,4 +21,42 @@ return [
|
||||
Weekday::Sunday->value => 'Sunday',
|
||||
],
|
||||
|
||||
'number_format' => [
|
||||
NumberFormat::ThousandsPointDecimalComma->value => '1.111,11',
|
||||
NumberFormat::ThousandsCommaDecimalPoint->value => '1,111.11',
|
||||
NumberFormat::ThousandsSpaceDecimalComma->value => '1 111,11',
|
||||
NumberFormat::ThousandsSpaceDecimalPoint->value => '1 111.11',
|
||||
NumberFormat::ThousandsApostropheDecimalPoint->value => '1\'111.11',
|
||||
],
|
||||
|
||||
'date_format' => [
|
||||
DateFormat::PointSeperatedDMYYYY->value => 'D.M.YYYY',
|
||||
DateFormat::SlashSeperatedMMDDYYYY->value => 'MM/DD/YYYY',
|
||||
DateFormat::SlashSeperatedDDMMYYYY->value => 'DD/MM/YYYY',
|
||||
DateFormat::HyphenSeperatedDDMMYYY->value => 'DD-MM-YYYY',
|
||||
DateFormat::HyphenSeperatedMMDDDYYYY->value => 'MM-DD-YYYY',
|
||||
DateFormat::HyphenSeperatedYYYYMMDD->value => 'YYYY-MM-DD',
|
||||
],
|
||||
|
||||
'time_format' => [
|
||||
TimeFormat::TwelveHours->value => '12-hour clock',
|
||||
TimeFormat::TwentyFourHours->value => '24-hour clock',
|
||||
],
|
||||
|
||||
'interval_format' => [
|
||||
IntervalFormat::Decimal->value => 'Decimal',
|
||||
IntervalFormat::HoursMinutes->value => '12h 3m',
|
||||
IntervalFormat::HoursMinutesColonSeperated->value => '12:03',
|
||||
IntervalFormat::HoursMinutesSecondsColonSeperated->value => '12:03:45',
|
||||
],
|
||||
|
||||
'currency_format' => [
|
||||
CurrencyFormat::ISOCodeBeforeWithSpace->value => 'EUR 111',
|
||||
CurrencyFormat::ISOCodeAfterWithSpace->value => '111 EUR',
|
||||
CurrencyFormat::SymbolBefore->value => '€111',
|
||||
CurrencyFormat::SymbolAfter->value => '111€',
|
||||
CurrencyFormat::SymbolBeforeWithSpace->value => '€ 111',
|
||||
CurrencyFormat::SymbolAfterWithSpace->value => '111 €',
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
@use('Brick\Money\Money')
|
||||
@use('PhpOffice\PhpSpreadsheet\Cell\DataType')
|
||||
@use('Carbon\CarbonInterval')
|
||||
@inject('interval', 'App\Service\IntervalService')
|
||||
@inject('colorService', 'App\Service\ColorService')
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@@ -139,7 +138,7 @@
|
||||
<div>
|
||||
<p style="font-size: 32px; font-weight: 600; margin-bottom: 5px;">Report</p>
|
||||
<div style="font-size: 16px; font-weight: 600; color: #71717a;">
|
||||
<span>{{ $start->timezone($timezone)->format('d.m.Y') }} - {{ $end->timezone($timezone)->format('d.m.Y') }}</span><br><br>
|
||||
<span>{{ $localization->formatDate($start->timezone($timezone)) }} - {{ $localization->formatDate($end->timezone($timezone)) }}</span><br><br>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -151,12 +150,12 @@
|
||||
<div style="padding: 8px 12px; border-radius: 8px;">
|
||||
<div style="color: #71717a; font-weight: 600;">Duration</div>
|
||||
<div
|
||||
style="font-size: 24px; font-weight: 500; margin-top: 2px;">{{ $interval->format(CarbonInterval::seconds($aggregatedData['seconds'])) }} </div>
|
||||
style="font-size: 24px; font-weight: 500; margin-top: 2px;">{{ $localization->formatInterval(CarbonInterval::seconds($aggregatedData['seconds'])) }} </div>
|
||||
</div>
|
||||
<div style="padding: 8px 12px; border-radius: 8px;">
|
||||
<div style="color: #71717a; font-weight: 600;">Total cost</div>
|
||||
<div
|
||||
style="font-size: 24px; font-weight: 500; margin-top: 2px;">{{ Money::of(BigDecimal::ofUnscaledValue($aggregatedData['cost'], 2)->__toString(), $currency)->formatTo('en_US') }} </div>
|
||||
style="font-size: 24px; font-weight: 500; margin-top: 2px;">{{ $localization->formatCurrency(Money::of(BigDecimal::ofUnscaledValue($aggregatedData['cost'], 2)->__toString(), $currency)) }} </div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -200,10 +199,10 @@
|
||||
</span>
|
||||
</td>
|
||||
<td style="text-align: left;">
|
||||
{{ $interval->format(CarbonInterval::seconds($group1Entry['seconds'])) }}
|
||||
{{ $localization->formatInterval(CarbonInterval::seconds($group1Entry['seconds'])) }}
|
||||
</td>
|
||||
<td style="text-align: right;">
|
||||
{{ Money::of(BigDecimal::ofUnscaledValue($group1Entry['cost'], 2)->__toString(), $currency)->formatTo('en_US') }}
|
||||
{{ $localization->formatCurrency(Money::of(BigDecimal::ofUnscaledValue($group1Entry['cost'], 2)->__toString(), $currency)) }}
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
@@ -214,10 +213,10 @@
|
||||
Total
|
||||
</td>
|
||||
<td style="font-weight: 500;color: #18181b;">
|
||||
{{ $interval->format(CarbonInterval::seconds($aggregatedData['seconds'])) }}
|
||||
{{ $localization->formatInterval(CarbonInterval::seconds($aggregatedData['seconds'])) }}
|
||||
</td>
|
||||
<td style="text-align: right; font-weight: 500;color: #18181b;">
|
||||
{{ Money::of(BigDecimal::ofUnscaledValue($aggregatedData['cost'], 2)->__toString(), $currency)->formatTo('en_US') }}
|
||||
{{ $localization->formatCurrency(Money::of(BigDecimal::ofUnscaledValue($aggregatedData['cost'], 2)->__toString(), $currency)) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
@@ -278,13 +277,13 @@
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
{{ $interval->format($duration) }}
|
||||
{{ $localization->formatInterval($duration) }}
|
||||
</td>
|
||||
<td>
|
||||
{{ round($duration->totalHours, 2) }}
|
||||
{{ $localization->formatNumber($duration->totalHours) }}
|
||||
</td>
|
||||
<td>
|
||||
{{ Money::of(BigDecimal::ofUnscaledValue($group2Entry['cost'], 2)->__toString(), $currency)->formatTo('en_US') }}
|
||||
{{ $localization->formatCurrency(Money::of(BigDecimal::ofUnscaledValue($group2Entry['cost'], 2)->__toString(), $currency)) }}
|
||||
</td>
|
||||
</tr>
|
||||
@php
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
<div>
|
||||
<p style="font-size: 32px; font-weight: 600; margin-bottom: 5px;">Detailed Report</p>
|
||||
<div style="font-size: 16px; font-weight: 600; color: #71717a;">
|
||||
<span>{{ $start->timezone($timezone)->format('d.m.Y') }} - {{ $end->timezone($timezone)->format('d.m.Y') }}</span><br><br>
|
||||
<span>{{ $localization->formatDate($start->timezone($timezone)) }} - {{ $localization->formatDate($end->timezone($timezone)) }}</span><br><br>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
@@ -139,12 +139,12 @@
|
||||
<div style="padding: 8px 12px; border-radius: 8px;">
|
||||
<div style="color: #71717a; font-weight: 600;">Duration</div>
|
||||
<div
|
||||
style="font-size: 24px; font-weight: 500; margin-top: 2px;">{{ $interval->format(CarbonInterval::seconds($aggregatedData['seconds'])) }} </div>
|
||||
style="font-size: 24px; font-weight: 500; margin-top: 2px;">{{ $localization->formatInterval(CarbonInterval::seconds($aggregatedData['seconds'])) }} </div>
|
||||
</div>
|
||||
<div style="padding: 8px 12px; border-radius: 8px;">
|
||||
<div style="color: #71717a; font-weight: 600;">Total cost</div>
|
||||
<div
|
||||
style="font-size: 24px; font-weight: 500; margin-top: 2px;">{{ Money::of(BigDecimal::ofUnscaledValue($aggregatedData['cost'], 2)->__toString(), $currency)->formatTo('en_US') }} </div>
|
||||
style="font-size: 24px; font-weight: 500; margin-top: 2px;">{{ $localization->formatCurrency(Money::of(BigDecimal::ofUnscaledValue($aggregatedData['cost'], 2)->__toString(), $currency)) }} </div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -180,15 +180,15 @@
|
||||
<td style="overflow-wrap: break-word; min-width: 75px;">{{ $timeEntry->user->name }}</td>
|
||||
<td style="overflow-wrap: break-word; min-width: 150px; text-align: center;">
|
||||
@if($timeEntry->start->timezone($timezone)->format('Y-m-d') === $timeEntry->end->timezone($timezone)->format('Y-m-d'))
|
||||
{{ $timeEntry->start->timezone($timezone)->format('Y-m-d') }}
|
||||
{{ $localization->formatDate($timeEntry->start->timezone($timezone)) }}
|
||||
@else
|
||||
{{ $timeEntry->start->timezone($timezone)->format('Y-m-d') }} - <br> {{ $timeEntry->end->timezone($timezone)->format('Y-m-d') }}
|
||||
{{ $localization->formatDate($timeEntry->start->timezone($timezone)) }} - <br> {{ $localization->formatDate($timeEntry->end->timezone($timezone)) }}
|
||||
@endif
|
||||
<br>
|
||||
{{ $timeEntry->start->timezone($timezone)->format('H:i:s') }} - {{ $timeEntry->end->timezone($timezone)->format('H:i:s') }}
|
||||
{{ $localization->formatDate($timeEntry->start->timezone($timezone)) }} - {{ $localization->formatDate($timeEntry->end->timezone($timezone)) }}
|
||||
</td>
|
||||
<td style="overflow-wrap: break-word; min-width: 75px;">
|
||||
{{ $interval->format($timeEntry->getDuration()) }}
|
||||
{{ $localization->formatInterval($timeEntry->getDuration()) }}
|
||||
</td>
|
||||
<td style="overflow-wrap: break-word;">{{ $timeEntry->billable ? 'Yes' : 'No' }}</td>
|
||||
<td style="overflow-wrap: break-word; min-width: 75px;">{{ count($timeEntry->tagsRelation) === 0 ? '-' : $timeEntry->tagsRelation->implode('name', ', ') }}</td>
|
||||
|
||||
@@ -110,7 +110,7 @@ class OrganizationEndpointTest extends ApiEndpointTestAbstract
|
||||
$response->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_update_endpoint_updates_project(): void
|
||||
public function test_update_endpoint_can_update_the_organization_name(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
@@ -123,14 +123,55 @@ class OrganizationEndpointTest extends ApiEndpointTestAbstract
|
||||
// Act
|
||||
$response = $this->putJson(route('api.v1.organizations.update', [$data->organization->getKey()]), [
|
||||
'name' => $organizationFake->name,
|
||||
'billable_rate' => $organizationFake->billable_rate,
|
||||
'billable_rate' => null,
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(200);
|
||||
$this->assertDatabaseHas(Organization::class, [
|
||||
'name' => $organizationFake->name,
|
||||
'billable_rate' => $organizationFake->billable_rate,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_update_endpoint_can_update_formats(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'organizations:update',
|
||||
]);
|
||||
$this->assertBillableRateServiceIsUnused();
|
||||
$organizationFake = Organization::factory()->make();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->putJson(route('api.v1.organizations.update', [$data->organization->getKey()]), [
|
||||
'name' => $organizationFake->name,
|
||||
'number_format' => $organizationFake->number_format->value,
|
||||
'currency_format' => $organizationFake->currency_format->value,
|
||||
'date_format' => $organizationFake->date_format->value,
|
||||
'interval_format' => $organizationFake->interval_format->value,
|
||||
'time_format' => $organizationFake->time_format->value,
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(200);
|
||||
$response->assertJson([
|
||||
'data' => [
|
||||
'id' => $data->organization->getKey(),
|
||||
'number_format' => $organizationFake->number_format->value,
|
||||
'currency_format' => $organizationFake->currency_format->value,
|
||||
'date_format' => $organizationFake->date_format->value,
|
||||
'interval_format' => $organizationFake->interval_format->value,
|
||||
'time_format' => $organizationFake->time_format->value,
|
||||
],
|
||||
]);
|
||||
$this->assertDatabaseHas(Organization::class, [
|
||||
'name' => $organizationFake->name,
|
||||
'number_format' => $organizationFake->number_format,
|
||||
'currency_format' => $organizationFake->currency_format,
|
||||
'date_format' => $organizationFake->date_format,
|
||||
'interval_format' => $organizationFake->interval_format,
|
||||
'time_format' => $organizationFake->time_format,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -146,14 +187,21 @@ class OrganizationEndpointTest extends ApiEndpointTestAbstract
|
||||
|
||||
// Act
|
||||
$response = $this->putJson(route('api.v1.organizations.update', [$data->organization->getKey()]), [
|
||||
'name' => $organizationFake->name,
|
||||
'billable_rate' => $organizationFake->billable_rate,
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(200);
|
||||
$response->assertJson([
|
||||
'data' => [
|
||||
'id' => $data->organization->getKey(),
|
||||
'name' => $data->organization->name,
|
||||
'billable_rate' => $organizationFake->billable_rate,
|
||||
],
|
||||
]);
|
||||
$this->assertDatabaseHas(Organization::class, [
|
||||
'name' => $organizationFake->name,
|
||||
'id' => $data->organization->getKey(),
|
||||
'name' => $data->organization->name,
|
||||
'billable_rate' => $organizationFake->billable_rate,
|
||||
]);
|
||||
}
|
||||
|
||||
97
tests/Unit/Service/CurrencyServiceTest.php
Normal file
97
tests/Unit/Service/CurrencyServiceTest.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Service;
|
||||
|
||||
use App\Service\CurrencyService;
|
||||
use Brick\Money\Currency;
|
||||
use Brick\Money\Money;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use Tests\TestCaseWithDatabase;
|
||||
|
||||
#[CoversClass(CurrencyService::class)]
|
||||
#[UsesClass(CurrencyService::class)]
|
||||
class CurrencyServiceTest extends TestCaseWithDatabase
|
||||
{
|
||||
private CurrencyService $currencyService;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->currencyService = new CurrencyService;
|
||||
}
|
||||
|
||||
public function test_get_currency_symbol_for_currency_eur(): void
|
||||
{
|
||||
// Arrange
|
||||
$money = Money::of(1, Currency::of('EUR'));
|
||||
|
||||
// Act
|
||||
$symbol = $this->currencyService->getCurrencySymbolForMoney($money);
|
||||
|
||||
// Assert
|
||||
$this->assertSame('€', $symbol);
|
||||
}
|
||||
|
||||
public function test_get_currency_symbol_for_currency_usd(): void
|
||||
{
|
||||
// Arrange
|
||||
$money = Money::of(1, Currency::of('USD'));
|
||||
|
||||
// Act
|
||||
$symbol = $this->currencyService->getCurrencySymbolForMoney($money);
|
||||
|
||||
// Assert
|
||||
$this->assertSame('$', $symbol);
|
||||
}
|
||||
|
||||
public function test_get_currency_symbol_for_currency_gbp(): void
|
||||
{
|
||||
// Arrange
|
||||
$money = Money::of(1, Currency::of('GBP'));
|
||||
|
||||
// Act
|
||||
$symbol = $this->currencyService->getCurrencySymbolForMoney($money);
|
||||
|
||||
// Assert
|
||||
$this->assertSame('£', $symbol);
|
||||
}
|
||||
|
||||
public function test_get_currency_symbol_for_currency_cad(): void
|
||||
{
|
||||
// Arrange
|
||||
$money = Money::of(1, Currency::of('CAD'));
|
||||
|
||||
// Act
|
||||
$symbol = $this->currencyService->getCurrencySymbolForMoney($money);
|
||||
|
||||
// Assert
|
||||
$this->assertSame('$', $symbol);
|
||||
}
|
||||
|
||||
public function test_get_currency_symbol_for_currency_cop(): void
|
||||
{
|
||||
// Arrange
|
||||
$money = Money::of(1, Currency::of('COP'));
|
||||
|
||||
// Act
|
||||
$symbol = $this->currencyService->getCurrencySymbolForMoney($money);
|
||||
|
||||
// Assert
|
||||
$this->assertSame('$', $symbol);
|
||||
}
|
||||
|
||||
public function test_get_currency_symbol_for_currency_without_known_symbol(): void
|
||||
{
|
||||
// Arrange
|
||||
$currency = 'XXX';
|
||||
|
||||
// Act
|
||||
$symbol = $this->currencyService->getCurrencySymbol($currency);
|
||||
|
||||
// Assert
|
||||
$this->assertSame('XXX', $symbol);
|
||||
}
|
||||
}
|
||||
256
tests/Unit/Service/LocalizationServiceTest.php
Normal file
256
tests/Unit/Service/LocalizationServiceTest.php
Normal file
@@ -0,0 +1,256 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Service;
|
||||
|
||||
use App\Enums\CurrencyFormat;
|
||||
use App\Enums\DateFormat;
|
||||
use App\Enums\IntervalFormat;
|
||||
use App\Enums\NumberFormat;
|
||||
use App\Enums\TimeFormat;
|
||||
use App\Service\LocalizationService;
|
||||
use Brick\Money\Currency;
|
||||
use Brick\Money\Money;
|
||||
use Carbon\CarbonInterval;
|
||||
use Illuminate\Support\Carbon;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use Tests\TestCaseWithDatabase;
|
||||
|
||||
#[CoversClass(LocalizationService::class)]
|
||||
#[UsesClass(LocalizationService::class)]
|
||||
class LocalizationServiceTest extends TestCaseWithDatabase
|
||||
{
|
||||
private LocalizationService $localizationService;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->localizationService = new LocalizationService(
|
||||
CurrencyFormat::SymbolAfterWithSpace,
|
||||
DateFormat::PointSeperatedDMYYYY,
|
||||
TimeFormat::TwelveHours,
|
||||
NumberFormat::ThousandsPointDecimalComma,
|
||||
IntervalFormat::Decimal,
|
||||
);
|
||||
}
|
||||
|
||||
public function test_format_interval_with_type_decimal_and_number_format_thousands_comma_decimal_point(): void
|
||||
{
|
||||
// Arrange
|
||||
$interval = CarbonInterval::seconds(4 + (60 * 3) + (60 * 60 * 30001));
|
||||
$this->localizationService->setIntervalFormat(IntervalFormat::Decimal);
|
||||
$this->localizationService->setNumberFormat(NumberFormat::ThousandsCommaDecimalPoint);
|
||||
|
||||
// Act
|
||||
$formatted = $this->localizationService->formatInterval($interval);
|
||||
|
||||
// Assert
|
||||
$this->assertSame('30,001.05', $formatted);
|
||||
}
|
||||
|
||||
public function test_format_interval_with_type_decimal_and_number_format_thousands_space_decimal_point(): void
|
||||
{
|
||||
// Arrange
|
||||
$interval = CarbonInterval::seconds(4 + (60 * 3) + (60 * 60 * 30001));
|
||||
$this->localizationService->setIntervalFormat(IntervalFormat::Decimal);
|
||||
$this->localizationService->setNumberFormat(NumberFormat::ThousandsSpaceDecimalPoint);
|
||||
|
||||
// Act
|
||||
$formatted = $this->localizationService->formatInterval($interval);
|
||||
|
||||
// Assert
|
||||
$this->assertSame('30 001.05', $formatted);
|
||||
}
|
||||
|
||||
public function test_format_interval_with_type_decimal_and_number_format_thousands_point_decimal_comma(): void
|
||||
{
|
||||
// Arrange
|
||||
$interval = CarbonInterval::seconds(4 + (60 * 3) + (60 * 60 * 30001));
|
||||
$this->localizationService->setIntervalFormat(IntervalFormat::Decimal);
|
||||
$this->localizationService->setNumberFormat(NumberFormat::ThousandsPointDecimalComma);
|
||||
|
||||
// Act
|
||||
$formatted = $this->localizationService->formatInterval($interval);
|
||||
|
||||
// Assert
|
||||
$this->assertSame('30.001,05', $formatted);
|
||||
}
|
||||
|
||||
public function test_format_interval_with_type_decimal_and_number_format_thousands_apostrophe_decimal_point(): void
|
||||
{
|
||||
// Arrange
|
||||
$interval = CarbonInterval::seconds(4 + (60 * 3) + (60 * 60 * 30001));
|
||||
$this->localizationService->setIntervalFormat(IntervalFormat::Decimal);
|
||||
$this->localizationService->setNumberFormat(NumberFormat::ThousandsApostropheDecimalPoint);
|
||||
|
||||
// Act
|
||||
$formatted = $this->localizationService->formatInterval($interval);
|
||||
|
||||
// Assert
|
||||
$this->assertSame('30\'001.05', $formatted);
|
||||
}
|
||||
|
||||
public function test_format_interval_with_type_hours_minutes(): void
|
||||
{
|
||||
// Arrange
|
||||
$interval = CarbonInterval::seconds(4 + (60 * 3) + (60 * 60 * 30001));
|
||||
$this->localizationService->setIntervalFormat(IntervalFormat::HoursMinutes);
|
||||
|
||||
// Act
|
||||
$formatted = $this->localizationService->formatInterval($interval);
|
||||
|
||||
// Assert
|
||||
$this->assertSame('30001h 03m', $formatted);
|
||||
}
|
||||
|
||||
public function test_format_interval_with_type_hours_minutes_colon_seperated(): void
|
||||
{
|
||||
// Arrange
|
||||
$interval = CarbonInterval::seconds(4 + (60 * 3) + (60 * 60 * 30001));
|
||||
$this->localizationService->setIntervalFormat(IntervalFormat::HoursMinutesColonSeperated);
|
||||
|
||||
// Act
|
||||
$formatted = $this->localizationService->formatInterval($interval);
|
||||
|
||||
// Assert
|
||||
$this->assertSame('30001:03', $formatted);
|
||||
}
|
||||
|
||||
public function test_format_interval_with_type_hours_minutes_seconds_colon_seperated(): void
|
||||
{
|
||||
// Arrange
|
||||
$interval = CarbonInterval::seconds(4 + (60 * 3) + (60 * 60 * 30001));
|
||||
$this->localizationService->setIntervalFormat(IntervalFormat::HoursMinutesSecondsColonSeperated);
|
||||
|
||||
// Act
|
||||
$formatted = $this->localizationService->formatInterval($interval);
|
||||
|
||||
// Assert
|
||||
$this->assertSame('30001:03:04', $formatted);
|
||||
}
|
||||
|
||||
public function test_format_currency_with_type_symbol_after_with_space_and_number_format_thousands_space_decimal_comma(): void
|
||||
{
|
||||
// Arrange
|
||||
$this->localizationService->setCurrencyFormat(CurrencyFormat::SymbolAfterWithSpace);
|
||||
$this->localizationService->setNumberFormat(NumberFormat::ThousandsSpaceDecimalComma);
|
||||
$money = Money::of(1234567.89, Currency::of('EUR'));
|
||||
|
||||
// Act
|
||||
$formatted = $this->localizationService->formatCurrency($money);
|
||||
|
||||
// Assert
|
||||
$this->assertSame('1 234 567,89 €', $formatted);
|
||||
}
|
||||
|
||||
public function test_format_currency_with_type_symbol_before_with_space_and_number_format_thousands_space_decimal_comma(): void
|
||||
{
|
||||
// Arrange
|
||||
$this->localizationService->setCurrencyFormat(CurrencyFormat::SymbolBeforeWithSpace);
|
||||
$this->localizationService->setNumberFormat(NumberFormat::ThousandsSpaceDecimalComma);
|
||||
$money = Money::of(1234567.89, Currency::of('EUR'));
|
||||
|
||||
// Act
|
||||
$formatted = $this->localizationService->formatCurrency($money);
|
||||
|
||||
// Assert
|
||||
$this->assertSame('€ 1 234 567,89', $formatted);
|
||||
}
|
||||
|
||||
public function test_format_currency_with_type_symbol_before_and_number_format_thousands_space_decimal_comma(): void
|
||||
{
|
||||
// Arrange
|
||||
$this->localizationService->setCurrencyFormat(CurrencyFormat::SymbolBefore);
|
||||
$this->localizationService->setNumberFormat(NumberFormat::ThousandsSpaceDecimalComma);
|
||||
$money = Money::of(1234567.89, Currency::of('EUR'));
|
||||
|
||||
// Act
|
||||
$formatted = $this->localizationService->formatCurrency($money);
|
||||
|
||||
// Assert
|
||||
$this->assertSame('€1 234 567,89', $formatted);
|
||||
}
|
||||
|
||||
public function test_format_currency_with_type_symbol_after_and_number_format_thousands_space_decimal_comma(): void
|
||||
{
|
||||
// Arrange
|
||||
$this->localizationService->setCurrencyFormat(CurrencyFormat::SymbolAfter);
|
||||
$this->localizationService->setNumberFormat(NumberFormat::ThousandsSpaceDecimalComma);
|
||||
$money = Money::of(1234567.89, Currency::of('EUR'));
|
||||
|
||||
// Act
|
||||
$formatted = $this->localizationService->formatCurrency($money);
|
||||
|
||||
// Assert
|
||||
$this->assertSame('1 234 567,89€', $formatted);
|
||||
}
|
||||
|
||||
public function test_format_currency_with_type_iso_code_after_with_space_and_number_format_thousands_space_decimal_comma(): void
|
||||
{
|
||||
// Arrange
|
||||
$this->localizationService->setCurrencyFormat(CurrencyFormat::ISOCodeAfterWithSpace);
|
||||
$this->localizationService->setNumberFormat(NumberFormat::ThousandsSpaceDecimalComma);
|
||||
$money = Money::of(1234567.89, Currency::of('EUR'));
|
||||
|
||||
// Act
|
||||
$formatted = $this->localizationService->formatCurrency($money);
|
||||
|
||||
// Assert
|
||||
$this->assertSame('1 234 567,89 EUR', $formatted);
|
||||
}
|
||||
|
||||
public function test_format_currency_with_type_iso_code_before_with_space_and_number_format_thousands_space_decimal_comma(): void
|
||||
{
|
||||
// Arrange
|
||||
$this->localizationService->setCurrencyFormat(CurrencyFormat::ISOCodeBeforeWithSpace);
|
||||
$this->localizationService->setNumberFormat(NumberFormat::ThousandsSpaceDecimalComma);
|
||||
$money = Money::of(1234567.89, Currency::of('EUR'));
|
||||
|
||||
// Act
|
||||
$formatted = $this->localizationService->formatCurrency($money);
|
||||
|
||||
// Assert
|
||||
$this->assertSame('EUR 1 234 567,89', $formatted);
|
||||
}
|
||||
|
||||
public function test_format_date_with_type_slash_seperated_ddmmy(): void
|
||||
{
|
||||
// Arrange
|
||||
$this->localizationService->setDateFormat(DateFormat::SlashSeperatedDDMMYYYY);
|
||||
$date = Carbon::createFromDate(2001, 2, 3);
|
||||
|
||||
// Act
|
||||
$formatted = $this->localizationService->formatDate($date);
|
||||
|
||||
// Assert
|
||||
$this->assertSame('03/02/2001', $formatted);
|
||||
}
|
||||
|
||||
public function test_format_time_with_type_twelve_hours(): void
|
||||
{
|
||||
// Arrange
|
||||
$this->localizationService->setTimeFormat(TimeFormat::TwelveHours);
|
||||
$time = Carbon::createFromTime(19, 9, 8);
|
||||
|
||||
// Act
|
||||
$formatted = $this->localizationService->formatTime($time);
|
||||
|
||||
// Assert
|
||||
$this->assertSame('07:09 pm', $formatted);
|
||||
}
|
||||
|
||||
public function test_format_time_with_type_twenty_four_hours(): void
|
||||
{
|
||||
// Arrange
|
||||
$this->localizationService->setTimeFormat(TimeFormat::TwentyFourHours);
|
||||
$time = Carbon::createFromTime(14, 9, 8);
|
||||
|
||||
// Act
|
||||
$formatted = $this->localizationService->formatTime($time);
|
||||
|
||||
// Assert
|
||||
$this->assertSame('14:09', $formatted);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user