mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-08 08:12:17 +01:00
Compare commits
12 Commits
v0.7.0
...
feature/nu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b82bf660b | ||
|
|
a0a8a7f772 | ||
|
|
5c63a94857 | ||
|
|
e377e58c98 | ||
|
|
08e0118181 | ||
|
|
730604987f | ||
|
|
80523cba3a | ||
|
|
af374c9c4d | ||
|
|
48be348c4c | ||
|
|
7e2d1ccc3d | ||
|
|
132b6cbe8f | ||
|
|
4605aa75ff |
2
.github/workflows/build-private.yml
vendored
2
.github/workflows/build-private.yml
vendored
@@ -107,7 +107,7 @@ jobs:
|
||||
- name: "Install npm dependencies in services extension"
|
||||
run: cd extensions/Services && npm ci
|
||||
|
||||
- name: "Checkout invoicing extension"
|
||||
- name: "Checkout services extension"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: solidtime-io/extension-invoicing
|
||||
|
||||
2
.github/workflows/phpunit.yml
vendored
2
.github/workflows/phpunit.yml
vendored
@@ -63,7 +63,7 @@ jobs:
|
||||
run: php artisan test --stop-on-failure --coverage-text --coverage-clover=coverage.xml
|
||||
|
||||
- name: "Upload coverage reports to Codecov"
|
||||
uses: codecov/codecov-action@v5.4.2
|
||||
uses: codecov/codecov-action@v5.4.0
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
slug: solidtime-io/solidtime
|
||||
|
||||
@@ -13,7 +13,7 @@ use Filament\Tables;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\Str;
|
||||
use Novadaemon\FilamentPrettyJson\Form\PrettyJsonField;
|
||||
use Novadaemon\FilamentPrettyJson\PrettyJson;
|
||||
|
||||
class AuditResource extends Resource
|
||||
{
|
||||
@@ -38,8 +38,8 @@ class AuditResource extends Resource
|
||||
->maxLength(255),
|
||||
Forms\Components\TextInput::make('auditable_id')
|
||||
->required(),
|
||||
PrettyJsonField::make('old_values'),
|
||||
PrettyJsonField::make('new_values'),
|
||||
PrettyJson::make('old_values'),
|
||||
PrettyJson::make('new_values'),
|
||||
Forms\Components\Textarea::make('url'),
|
||||
Forms\Components\TextInput::make('ip_address'),
|
||||
Forms\Components\TextInput::make('user_agent')
|
||||
|
||||
@@ -20,7 +20,7 @@ use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Novadaemon\FilamentPrettyJson\Form\PrettyJsonField;
|
||||
use Novadaemon\FilamentPrettyJson\PrettyJson;
|
||||
|
||||
/**
|
||||
* @source https://gitlab.com/amvisor/filament-failed-jobs
|
||||
@@ -50,7 +50,7 @@ class FailedJobResource extends Resource
|
||||
|
||||
// make text a little bit smaller because often a complete Stack Trace is shown:
|
||||
TextArea::make('exception')->disabled()->columnSpan(4)->extraInputAttributes(['style' => 'font-size: 80%;']),
|
||||
PrettyJsonField::make('payload')->disabled()->columnSpan(4),
|
||||
PrettyJson::make('payload')->disabled()->columnSpan(4),
|
||||
])->columns(4);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\ToggleColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Novadaemon\FilamentPrettyJson\Form\PrettyJsonField;
|
||||
use Novadaemon\FilamentPrettyJson\PrettyJson;
|
||||
|
||||
class ReportResource extends Resource
|
||||
{
|
||||
@@ -58,7 +58,7 @@ class ReportResource extends Resource
|
||||
Forms\Components\TextInput::make('share_secret')
|
||||
->label('Share Secret')
|
||||
->nullable(),
|
||||
PrettyJsonField::make('properties')
|
||||
PrettyJson::make('properties')
|
||||
->formatStateUsing(function (ReportPropertiesDto $state, Report $record): string {
|
||||
return $record->getRawOriginal('properties');
|
||||
})
|
||||
|
||||
@@ -40,7 +40,6 @@ class HandleInertiaRequests extends Middleware
|
||||
public function share(Request $request): array
|
||||
{
|
||||
$hasBilling = Module::has('Billing') && Module::isEnabled('Billing');
|
||||
$hasInvoicing = Module::has('Invoicing') && Module::isEnabled('Invoicing');
|
||||
|
||||
/** @var BillingContract $billing */
|
||||
$billing = app(BillingContract::class);
|
||||
@@ -49,7 +48,6 @@ class HandleInertiaRequests extends Middleware
|
||||
|
||||
return array_merge(parent::share($request), [
|
||||
'has_billing_extension' => $hasBilling,
|
||||
'has_invoicing_extension' => $hasInvoicing,
|
||||
'billing' => $billing !== null && $currentOrganization !== null ? [
|
||||
'has_subscription' => $billing->hasSubscription($currentOrganization),
|
||||
'has_trial' => $billing->hasTrial($currentOrganization),
|
||||
|
||||
@@ -11,7 +11,6 @@ use App\Rules\ColorRule;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Str;
|
||||
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
||||
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
||||
|
||||
@@ -28,7 +27,6 @@ class ProjectStoreRequest extends FormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
// Name of the project, the name needs to be unique per client and organization
|
||||
'name' => [
|
||||
'required',
|
||||
'string',
|
||||
@@ -36,13 +34,7 @@ class ProjectStoreRequest extends FormRequest
|
||||
'max:255',
|
||||
UniqueEloquent::make(Project::class, 'name', function (Builder $builder): Builder {
|
||||
/** @var Builder<Project> $builder */
|
||||
$clientId = $this->input('client_id');
|
||||
if (! is_string($clientId) || ! Str::isUuid($clientId)) {
|
||||
$clientId = null;
|
||||
}
|
||||
|
||||
return $builder->whereBelongsTo($this->organization, 'organization')
|
||||
->where('client_id', $clientId);
|
||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
||||
})->withCustomTranslation('validation.project_name_already_exists'),
|
||||
],
|
||||
'color' => [
|
||||
@@ -63,7 +55,6 @@ class ProjectStoreRequest extends FormRequest
|
||||
],
|
||||
// ID of the client
|
||||
'client_id' => [
|
||||
'present',
|
||||
'nullable',
|
||||
ExistsEloquent::make(Client::class, null, function (Builder $builder): Builder {
|
||||
/** @var Builder<Client> $builder */
|
||||
|
||||
@@ -11,7 +11,6 @@ use App\Rules\ColorRule;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Str;
|
||||
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
||||
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
|
||||
|
||||
@@ -35,13 +34,7 @@ class ProjectUpdateRequest extends FormRequest
|
||||
'max:255',
|
||||
UniqueEloquent::make(Project::class, 'name', function (Builder $builder): Builder {
|
||||
/** @var Builder<Project> $builder */
|
||||
$clientId = $this->input('client_id');
|
||||
if (! is_string($clientId) || ! Str::isUuid($clientId)) {
|
||||
$clientId = null;
|
||||
}
|
||||
|
||||
return $builder->whereBelongsTo($this->organization, 'organization')
|
||||
->where('client_id', $clientId);
|
||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
||||
})->ignore($this->project?->getKey())->withCustomTranslation('validation.project_name_already_exists'),
|
||||
],
|
||||
'color' => [
|
||||
@@ -61,7 +54,6 @@ class ProjectUpdateRequest extends FormRequest
|
||||
'boolean',
|
||||
],
|
||||
'client_id' => [
|
||||
'present',
|
||||
'nullable',
|
||||
ExistsEloquent::make(Client::class, null, function (Builder $builder): Builder {
|
||||
/** @var Builder<Client> $builder */
|
||||
|
||||
@@ -40,7 +40,7 @@ class ReportStoreRequest extends FormRequest
|
||||
'required',
|
||||
'boolean',
|
||||
],
|
||||
// After this date the report will be automatically set to private (is_public=false) (Format: "Y-m-d\TH:i:s\Z", UTC timezone, Example: "2000-02-22T14:58:59Z")
|
||||
// After this date the report will be automatically set to private (is_public=false) (ISO 8601 format, UTC timezone)
|
||||
'public_until' => [
|
||||
'nullable',
|
||||
'date_format:Y-m-d\TH:i:s\Z',
|
||||
|
||||
@@ -59,12 +59,12 @@ class TimeEntryStoreRequest extends FormRequest
|
||||
->where('project_id', $this->input('project_id'));
|
||||
})->uuid()->withMessage(__('validation.task_belongs_to_project')),
|
||||
],
|
||||
// Start of time entry (Format: "Y-m-d\TH:i:s\Z", UTC timezone, Example: "2000-02-22T14:58:59Z")
|
||||
// Start of time entry (ISO 8601 format, UTC timezone)
|
||||
'start' => [
|
||||
'required',
|
||||
'date_format:Y-m-d\TH:i:s\Z',
|
||||
],
|
||||
// End of time entry (Format: "Y-m-d\TH:i:s\Z", UTC timezone, Example: "2000-02-22T14:58:59Z")
|
||||
// End of time entry (ISO 8601 format, UTC timezone)
|
||||
'end' => [
|
||||
'nullable',
|
||||
'date_format:Y-m-d\TH:i:s\Z',
|
||||
|
||||
@@ -59,11 +59,11 @@ class TimeEntryUpdateRequest extends FormRequest
|
||||
->where('project_id', $this->input('project_id'));
|
||||
})->uuid()->withMessage(__('validation.task_belongs_to_project')),
|
||||
],
|
||||
// Start of time entry (Format: "Y-m-d\TH:i:s\Z", UTC timezone, Example: "2000-02-22T14:58:59Z")
|
||||
// Start of time entry (ISO 8601 format, UTC timezone)
|
||||
'start' => [
|
||||
'date_format:Y-m-d\TH:i:s\Z',
|
||||
],
|
||||
// End of time entry (Format: "Y-m-d\TH:i:s\Z", UTC timezone, Example: "2000-02-22T14:58:59Z")
|
||||
// End of time entry (ISO 8601 format, UTC timezone)
|
||||
'end' => [
|
||||
'nullable',
|
||||
'date_format:Y-m-d\TH:i:s\Z',
|
||||
|
||||
@@ -12,10 +12,6 @@ abstract class BaseResource extends JsonResource
|
||||
protected function formatDateTime(?Carbon $carbon): ?string
|
||||
{
|
||||
return $carbon?->toIso8601ZuluString();
|
||||
}
|
||||
|
||||
protected function formatDate(?Carbon $carbon): ?string
|
||||
{
|
||||
return $carbon?->format('Y-m-d');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,9 +37,9 @@ class ClockifyProjectsImporter extends DefaultImporter
|
||||
if ($record['Project'] !== '') {
|
||||
$projectId = $this->projectImportHelper->getKey([
|
||||
'name' => $record['Project'],
|
||||
'client_id' => $clientId,
|
||||
'organization_id' => $this->organization->id,
|
||||
], [
|
||||
'client_id' => $clientId,
|
||||
'color' => $this->colorService->getRandomColor(),
|
||||
'is_billable' => $record['Billability'] === 'Yes',
|
||||
'billable_rate' => $billableRateKey !== null && $record[$billableRateKey] !== '' ? (int) (((float) $record[$billableRateKey]) * 100) : null,
|
||||
|
||||
@@ -83,9 +83,9 @@ class ClockifyTimeEntriesImporter extends DefaultImporter
|
||||
if ($record['Project'] !== '') {
|
||||
$projectId = $this->projectImportHelper->getKey([
|
||||
'name' => $record['Project'],
|
||||
'client_id' => $clientId,
|
||||
'organization_id' => $this->organization->id,
|
||||
], [
|
||||
'client_id' => $clientId,
|
||||
'color' => $this->colorService->getRandomColor(),
|
||||
'is_billable' => false,
|
||||
]);
|
||||
@@ -124,59 +124,34 @@ class ClockifyTimeEntriesImporter extends DefaultImporter
|
||||
$timeEntry->is_imported = true;
|
||||
|
||||
// Start
|
||||
$start = null;
|
||||
try {
|
||||
$startDateStr = $record['Start Date'];
|
||||
$startTimeStr = $record['Start Time'];
|
||||
$startStr = $startDateStr.' '.$startTimeStr;
|
||||
$matches = [];
|
||||
$checkResult = preg_match('/^([0-9]{1,2})\/([0-9]{1,2})\/([0-9]{4}) ([0-9]{1,2}):([0-9]{1,2})(:[0-9]{1,2})? (AM|PM)$/', $startStr, $matches);
|
||||
|
||||
if ($checkResult === 1) {
|
||||
if ((int) $matches[1] > 12) {
|
||||
throw new ImportException('Start date ("'.$startDateStr.'") is invalid, please select the correct date format before exporting from Clockify');
|
||||
}
|
||||
if ($matches[6] === '') {
|
||||
$start = Carbon::createFromFormat('m/d/Y h:i A', $startStr, $timezone);
|
||||
} else {
|
||||
$start = Carbon::createFromFormat('m/d/Y H:i:s A', $startStr, $timezone);
|
||||
}
|
||||
if (preg_match('/^[0-9]{1,2}:[0-9]{1,2} (AM|PM)$/', $record['Start Time']) === 1) {
|
||||
$start = Carbon::createFromFormat('m/d/Y h:i A', $record['Start Date'].' '.$record['Start Time'], $timezone);
|
||||
} else {
|
||||
$start = Carbon::createFromFormat('m/d/Y H:i:s A', $record['Start Date'].' '.$record['Start Time'], $timezone);
|
||||
}
|
||||
} catch (InvalidFormatException) {
|
||||
throw new ImportException('Start date ("'.$startDateStr.'") or time ("'.$startTimeStr.'") are invalid');
|
||||
throw new ImportException('Start date ("'.$record['Start Date'].'") or time ("'.$record['Start Time'].'") are invalid');
|
||||
}
|
||||
if ($start === null) {
|
||||
throw new ImportException('Start date ("'.$startDateStr.'") or time ("'.$startTimeStr.'") are invalid');
|
||||
throw new ImportException('Start date ("'.$record['Start Date'].'") or time ("'.$record['Start Time'].'") are invalid');
|
||||
}
|
||||
$timeEntry->start = $start->utc();
|
||||
|
||||
// End
|
||||
$end = null;
|
||||
try {
|
||||
$endDateStr = $record['End Date'];
|
||||
$endTimeStr = $record['End Time'];
|
||||
$endStr = $endDateStr.' '.$endTimeStr;
|
||||
$matches = [];
|
||||
$checkResult = preg_match('/^([0-9]{1,2})\/([0-9]{1,2})\/([0-9]{4}) ([0-9]{1,2}):([0-9]{1,2})(:[0-9]{1,2})? (AM|PM)$/', $endStr, $matches);
|
||||
|
||||
if ($checkResult === 1) {
|
||||
if ((int) $matches[1] > 12) {
|
||||
throw new ImportException('Start date ("'.$endDateStr.'") is invalid, please select the correct date format before exporting from Clockify');
|
||||
}
|
||||
if ($matches[6] === '') {
|
||||
$end = Carbon::createFromFormat('m/d/Y h:i A', $endStr, $timezone);
|
||||
} else {
|
||||
$end = Carbon::createFromFormat('m/d/Y H:i:s A', $endStr, $timezone);
|
||||
}
|
||||
if (preg_match('/^[0-9]{1,2}:[0-9]{1,2} (AM|PM)$/', $record['End Time']) === 1) {
|
||||
$end = Carbon::createFromFormat('m/d/Y h:i A', $record['End Date'].' '.$record['End Time'], $timezone);
|
||||
} else {
|
||||
$end = Carbon::createFromFormat('m/d/Y H:i:s A', $record['End Date'].' '.$record['End Time'], $timezone);
|
||||
}
|
||||
} catch (InvalidFormatException) {
|
||||
throw new ImportException('End date ("'.$endDateStr.'") or time ("'.$endTimeStr.'") are invalid');
|
||||
throw new ImportException('End date ("'.$record['End Date'].'") or time ("'.$record['End Time'].'") are invalid');
|
||||
}
|
||||
if ($end === null) {
|
||||
throw new ImportException('End date ("'.$endDateStr.'") or time ("'.$endTimeStr.'") are invalid');
|
||||
throw new ImportException('End date ("'.$record['End Date'].'") or time ("'.$record['End Time'].'") are invalid');
|
||||
}
|
||||
$timeEntry->end = $end->utc();
|
||||
|
||||
$timeEntry->billable_rate = $this->billableRateService->getBillableRateForTimeEntryWithGivenRelations(
|
||||
$timeEntry,
|
||||
$projectMember,
|
||||
|
||||
@@ -97,7 +97,7 @@ abstract class DefaultImporter implements ImporterContract
|
||||
'in:placeholder',
|
||||
],
|
||||
]);
|
||||
$this->projectImportHelper = new ImportDatabaseHelper(Project::class, ['name', 'client_id', 'organization_id'], true, function (Builder $builder) {
|
||||
$this->projectImportHelper = new ImportDatabaseHelper(Project::class, ['name', 'organization_id'], true, function (Builder $builder) {
|
||||
/** @var Builder<Project> $builder */
|
||||
return $builder->where('organization_id', $this->organization->id);
|
||||
}, validate: [
|
||||
@@ -114,11 +114,6 @@ abstract class DefaultImporter implements ImporterContract
|
||||
'integer',
|
||||
'max:2147483647',
|
||||
],
|
||||
'client_id' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'uuid',
|
||||
],
|
||||
], beforeSave: function (Project $project): void {
|
||||
if ($project->billable_rate === 0) {
|
||||
$project->billable_rate = null;
|
||||
|
||||
@@ -55,12 +55,12 @@ class GenericProjectsImporter extends DefaultImporter
|
||||
}
|
||||
$this->projectImportHelper->getKey([
|
||||
'name' => $record['name'],
|
||||
'client_id' => $clientId,
|
||||
'organization_id' => $this->organization->id,
|
||||
], [
|
||||
'color' => isset($record['color']) && $record['color'] !== '' ? $record['color'] : app(ColorService::class)->getRandomColor(),
|
||||
'billable_rate' => isset($record['billable_rate']) && $record['billable_rate'] !== '' ? (int) $record['billable_rate'] : null,
|
||||
'is_public' => isset($record['is_public']) && $record['is_public'] === 'true',
|
||||
'client_id' => $clientId,
|
||||
'is_billable' => isset($record['billable_default']) && $record['billable_default'] === 'true',
|
||||
'estimated_time' => isset($record['estimated_time']) && $record['estimated_time'] !== '' && is_numeric($record['estimated_time']) && ((int) $record['estimated_time'] !== 0) ? (int) $record['estimated_time'] : null,
|
||||
'archived_at' => $archivedAt,
|
||||
|
||||
@@ -99,9 +99,9 @@ class GenericTimeEntriesImporter extends DefaultImporter
|
||||
if ($record['project'] !== '') {
|
||||
$projectId = $this->projectImportHelper->getKey([
|
||||
'name' => $record['project'],
|
||||
'client_id' => $clientId,
|
||||
'organization_id' => $this->organization->id,
|
||||
], [
|
||||
'client_id' => $clientId,
|
||||
'is_billable' => false,
|
||||
'color' => $this->colorService->getRandomColor(),
|
||||
]);
|
||||
|
||||
@@ -60,10 +60,10 @@ class HarvestProjectsImporter extends DefaultImporter
|
||||
$billableHours = $billableHoursField !== '' && is_numeric($billableHoursField) ? (int) ((float) $billableHoursField) : null;
|
||||
$this->projectImportHelper->getKey([
|
||||
'name' => $record['Project'],
|
||||
'client_id' => $clientId,
|
||||
'organization_id' => $this->organization->id,
|
||||
], [
|
||||
'color' => $this->colorService->getRandomColor(),
|
||||
'client_id' => $clientId,
|
||||
'estimated_time' => $estimatedTime,
|
||||
'is_billable' => $billableHours > 0,
|
||||
]);
|
||||
|
||||
@@ -78,9 +78,9 @@ class HarvestTimeEntriesImporter extends DefaultImporter
|
||||
if ($record['Project'] !== '') {
|
||||
$projectId = $this->projectImportHelper->getKey([
|
||||
'name' => $record['Project'],
|
||||
'client_id' => $clientId,
|
||||
'organization_id' => $this->organization->id,
|
||||
], [
|
||||
'client_id' => $clientId,
|
||||
'color' => $this->colorService->getRandomColor(),
|
||||
'is_billable' => true,
|
||||
]);
|
||||
|
||||
@@ -176,12 +176,12 @@ class SolidtimeImporter extends DefaultImporter
|
||||
|
||||
$this->projectImportHelper->getKey([
|
||||
'name' => $project['name'],
|
||||
'client_id' => $clientId,
|
||||
'organization_id' => $this->organization->getKey(),
|
||||
], [
|
||||
'color' => $project['color'],
|
||||
'billable_rate' => $project['billable_rate'] === '' ? null : (int) $project['billable_rate'],
|
||||
'is_public' => $project['is_public'] === 'true',
|
||||
'client_id' => $clientId,
|
||||
'is_billable' => $project['is_billable'] === 'true',
|
||||
'archived_at' => $project['archived_at'] !== '' ? Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $project['archived_at'], 'UTC') : null,
|
||||
], $project['id']);
|
||||
|
||||
@@ -137,9 +137,9 @@ class TogglDataImporter extends DefaultImporter
|
||||
|
||||
$projectId = $this->projectImportHelper->getKey([
|
||||
'name' => $project->name,
|
||||
'client_id' => $clientId,
|
||||
'organization_id' => $this->organization->getKey(),
|
||||
], [
|
||||
'client_id' => $clientId,
|
||||
'color' => $project->color,
|
||||
'is_billable' => $project->billable,
|
||||
'is_public' => ! $project->is_private,
|
||||
|
||||
@@ -83,9 +83,9 @@ class TogglTimeEntriesImporter extends DefaultImporter
|
||||
if ($record['Project'] !== '') {
|
||||
$projectId = $this->projectImportHelper->getKey([
|
||||
'name' => $record['Project'],
|
||||
'client_id' => $clientId,
|
||||
'organization_id' => $this->organization->id,
|
||||
], [
|
||||
'client_id' => $clientId,
|
||||
'is_billable' => false,
|
||||
'color' => $this->colorService->getRandomColor(),
|
||||
]);
|
||||
|
||||
@@ -280,20 +280,6 @@ class TimeEntryAggregationService
|
||||
'color' => null,
|
||||
];
|
||||
}
|
||||
} elseif ($type === TimeEntryAggregationType::Description) {
|
||||
foreach ($keys as $key) {
|
||||
$descriptorMap[$key] = [
|
||||
'description' => $key,
|
||||
'color' => null,
|
||||
];
|
||||
}
|
||||
} elseif ($type === TimeEntryAggregationType::Billable) {
|
||||
foreach ($keys as $key) {
|
||||
$descriptorMap[$key] = [
|
||||
'description' => $key === '0' ? 'Non-billable' : 'Billable',
|
||||
'color' => null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $descriptorMap;
|
||||
|
||||
1671
composer.lock
generated
1671
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,36 +0,0 @@
|
||||
<?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('projects', function (Blueprint $table): void {
|
||||
$table->bigInteger('spent_time')->unsigned()->default(0)->change();
|
||||
});
|
||||
Schema::table('tasks', function (Blueprint $table): void {
|
||||
$table->bigInteger('spent_time')->unsigned()->default(0)->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('projects', function (Blueprint $table): void {
|
||||
$table->integer('spent_time')->unsigned()->default(0)->change();
|
||||
});
|
||||
Schema::table('tasks', function (Blueprint $table): void {
|
||||
$table->integer('spent_time')->unsigned()->default(0)->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -202,7 +202,7 @@ return [
|
||||
'currency' => 'The :attribute field must be a valid currency code (ISO 4217).',
|
||||
'organization' => 'The :attribute does not exist.',
|
||||
'task_belongs_to_project' => 'The :attribute is not part of the given project.',
|
||||
'project_name_already_exists' => 'A project with the same name and client already exists in the organization.',
|
||||
'project_name_already_exists' => 'A project with the same name already exists in the organization.',
|
||||
'tag_name_already_exists' => 'A tag with the same name already exists in the organization.',
|
||||
'client_name_already_exists' => 'A client with the same name already exists in the organization.',
|
||||
'task_name_already_exists' => 'A task with the same name already exists in the project.',
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from "vue";
|
||||
import { useTheme } from "@/utils/theme.js";
|
||||
import { onMounted, watch } from "vue";
|
||||
import { theme } from "@/utils/theme.js";
|
||||
|
||||
onMounted(async () => {
|
||||
useTheme()
|
||||
document.documentElement.classList.add(theme.value);
|
||||
watch(theme, (newTheme, oldTheme) => {
|
||||
document.documentElement.classList.remove(oldTheme);
|
||||
document.documentElement.classList.add(newTheme);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -20,24 +20,24 @@ import {
|
||||
import NavigationSidebarItem from '@/Components/NavigationSidebarItem.vue';
|
||||
import UserSettingsIcon from '@/Components/UserSettingsIcon.vue';
|
||||
import MainContainer from '@/packages/ui/src/MainContainer.vue';
|
||||
import { onMounted, ref } from "vue";
|
||||
import { onMounted, ref, watch } from "vue";
|
||||
import NotificationContainer from '@/Components/NotificationContainer.vue';
|
||||
import { initializeStores, refreshStores } from '@/utils/init';
|
||||
import {
|
||||
canManageBilling,
|
||||
canUpdateOrganization,
|
||||
canViewClients, canViewInvoices,
|
||||
canViewClients,
|
||||
canViewMembers,
|
||||
canViewProjects, canViewReport,
|
||||
canViewTags,
|
||||
} from '@/utils/permissions';
|
||||
import { isBillingActivated, isInvoicingActivated } from '@/utils/billing';
|
||||
import { isBillingActivated } from '@/utils/billing';
|
||||
import type { User } from '@/types/models';
|
||||
import { ArrowsRightLeftIcon } from '@heroicons/vue/16/solid';
|
||||
import { fetchToken, isTokenValid } from '@/utils/session';
|
||||
import UpdateSidebarNotification from '@/Components/UpdateSidebarNotification.vue';
|
||||
import BillingBanner from '@/Components/Billing/BillingBanner.vue';
|
||||
import { useTheme } from "@/utils/theme";
|
||||
import { theme } from "@/utils/theme";
|
||||
|
||||
defineProps({
|
||||
title: String,
|
||||
@@ -47,7 +47,12 @@ const showSidebarMenu = ref(false);
|
||||
const isUnloading = ref(false);
|
||||
onMounted(async () => {
|
||||
|
||||
useTheme()
|
||||
document.documentElement.classList.add(theme.value);
|
||||
watch(theme, (newTheme, oldTheme) => {
|
||||
document.documentElement.classList.remove(oldTheme);
|
||||
document.documentElement.classList.add(newTheme);
|
||||
});
|
||||
|
||||
// make sure that the initial requests are only loaded once, this can be removed once we move away from inertia
|
||||
if (window.initialDataLoaded !== true) {
|
||||
window.initialDataLoaded = true;
|
||||
@@ -183,7 +188,6 @@ const page = usePage<{
|
||||
:current="route().current('tags')"
|
||||
:href="route('tags')"></NavigationSidebarItem>
|
||||
<NavigationSidebarItem
|
||||
v-if="isInvoicingActivated() && canViewInvoices()"
|
||||
title="Invoices"
|
||||
:icon="DocumentTextIcon"
|
||||
:current="route().current('invoices')"
|
||||
@@ -268,6 +272,8 @@ const page = usePage<{
|
||||
v-if="$slots.header"
|
||||
class="bg-default-background border-b border-default-background-separator shadow">
|
||||
<div class="pt-8 pb-3">
|
||||
|
||||
|
||||
<MainContainer>
|
||||
<slot name="header" />
|
||||
</MainContainer>
|
||||
|
||||
@@ -339,8 +339,6 @@ const tableData = computed(() => {
|
||||
@submit="updateReporting">
|
||||
<template #trigger>
|
||||
<ReportingFilterBadge
|
||||
:count="selectedClients.length"
|
||||
:active="selectedClients.length > 0"
|
||||
title="Clients"
|
||||
:icon="FolderIcon"></ReportingFilterBadge>
|
||||
</template>
|
||||
|
||||
@@ -308,8 +308,6 @@ async function downloadExport(format: ExportFormat) {
|
||||
@submit="updateFilteredTimeEntries">
|
||||
<template #trigger>
|
||||
<ReportingFilterBadge
|
||||
:count="selectedClients.length"
|
||||
:active="selectedClients.length > 0"
|
||||
title="Clients"
|
||||
:icon="FolderIcon"></ReportingFilterBadge>
|
||||
</template>
|
||||
|
||||
@@ -14,7 +14,6 @@ import { api } from '@/packages/api/src';
|
||||
import { getRandomColorWithSeed } from '@/packages/ui/src/utils/color';
|
||||
import { useReportingStore } from '@/utils/useReporting';
|
||||
import { Head } from '@inertiajs/vue3';
|
||||
import { useTheme } from "@/utils/theme";
|
||||
|
||||
const sharedSecret = ref<string | null>(null);
|
||||
|
||||
@@ -137,10 +136,6 @@ function getGroupLabel(key: string) {
|
||||
return option.value === key;
|
||||
})?.label;
|
||||
}
|
||||
onMounted(async () => {
|
||||
useTheme();
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -66,7 +66,6 @@ const InvoiceResource = z
|
||||
buyer_name: z.string(),
|
||||
status: z.string(),
|
||||
date: z.string(),
|
||||
due_at: z.string(),
|
||||
created_at: z.union([z.string(), z.null()]),
|
||||
updated_at: z.union([z.string(), z.null()]),
|
||||
})
|
||||
@@ -107,8 +106,6 @@ const InvoiceStoreRequest = z
|
||||
discount_type: InvoiceDiscountType.optional(),
|
||||
footer: z.union([z.string(), z.null()]).optional(),
|
||||
notes: z.union([z.string(), z.null()]).optional(),
|
||||
payment_terms: z.union([z.string(), z.null()]).optional(),
|
||||
is_eu_reverse_charge: z.boolean().optional(),
|
||||
entries: z
|
||||
.array(
|
||||
z
|
||||
@@ -130,7 +127,7 @@ const InvoiceEntryResource = z
|
||||
name: z.string(),
|
||||
description: z.union([z.string(), z.null()]),
|
||||
unit_price: z.number().int(),
|
||||
quantity: z.string(),
|
||||
quantity: z.number().int(),
|
||||
order_index: z.number().int(),
|
||||
created_at: z.union([z.string(), z.null()]),
|
||||
updated_at: z.union([z.string(), z.null()]),
|
||||
@@ -161,7 +158,7 @@ const DetailedInvoiceResource = z
|
||||
buyer_address_country: z.string(),
|
||||
buyer_phone: z.string(),
|
||||
buyer_email: z.string(),
|
||||
paid_at: z.union([z.string(), z.null()]),
|
||||
paid_at: z.string(),
|
||||
due_at: z.string(),
|
||||
discount_type: z.string(),
|
||||
discount_amount: z.string(),
|
||||
@@ -171,8 +168,6 @@ const DetailedInvoiceResource = z
|
||||
date: z.string(),
|
||||
footer: z.string(),
|
||||
notes: z.string(),
|
||||
payment_terms: z.string(),
|
||||
is_eu_reverse_charge: z.string(),
|
||||
billing_period_start: z.string(),
|
||||
billing_period_end: z.string(),
|
||||
created_at: z.union([z.string(), z.null()]),
|
||||
@@ -216,8 +211,6 @@ const InvoiceUpdateRequest = z
|
||||
discount_type: InvoiceDiscountType,
|
||||
footer: z.union([z.string(), z.null()]),
|
||||
notes: z.union([z.string(), z.null()]),
|
||||
payment_terms: z.union([z.string(), z.null()]),
|
||||
is_eu_reverse_charge: z.boolean(),
|
||||
entries: z.array(
|
||||
z
|
||||
.object({
|
||||
@@ -232,9 +225,6 @@ const InvoiceUpdateRequest = z
|
||||
})
|
||||
.partial()
|
||||
.passthrough();
|
||||
const InvoiceDownloadRequest = z
|
||||
.object({ with_e_invoice: z.boolean() })
|
||||
.passthrough();
|
||||
const InvoiceSettingResource = z
|
||||
.object({
|
||||
seller_name: z.union([z.string(), z.null()]),
|
||||
@@ -472,9 +462,9 @@ const ReportStoreRequest = z
|
||||
task_ids: z
|
||||
.union([z.array(z.string().uuid()), z.null()])
|
||||
.optional(),
|
||||
group: TimeEntryAggregationType,
|
||||
sub_group: TimeEntryAggregationType,
|
||||
history_group: TimeEntryAggregationTypeInterval,
|
||||
group: TimeEntryAggregationType.optional(),
|
||||
sub_group: TimeEntryAggregationType.optional(),
|
||||
history_group: TimeEntryAggregationTypeInterval.optional(),
|
||||
week_start: Weekday.optional(),
|
||||
timezone: z.union([z.string(), z.null()]).optional(),
|
||||
})
|
||||
@@ -762,7 +752,6 @@ export const schemas = {
|
||||
DetailedInvoiceResource,
|
||||
InvoiceStatus,
|
||||
InvoiceUpdateRequest,
|
||||
InvoiceDownloadRequest,
|
||||
InvoiceSettingResource,
|
||||
InvoiceSettingUpdateRequest,
|
||||
MemberResource,
|
||||
@@ -1391,7 +1380,7 @@ const endpoints = makeApi([
|
||||
schema: z.string(),
|
||||
},
|
||||
],
|
||||
response: z.void(),
|
||||
response: z.null(),
|
||||
errors: [
|
||||
{
|
||||
status: 400,
|
||||
@@ -1676,7 +1665,7 @@ const endpoints = makeApi([
|
||||
schema: z.string(),
|
||||
},
|
||||
],
|
||||
response: z.void(),
|
||||
response: z.null(),
|
||||
errors: [
|
||||
{
|
||||
status: 400,
|
||||
@@ -1733,7 +1722,7 @@ const endpoints = makeApi([
|
||||
schema: z.string(),
|
||||
},
|
||||
],
|
||||
response: z.void(),
|
||||
response: z.null(),
|
||||
errors: [
|
||||
{
|
||||
status: 401,
|
||||
@@ -1769,7 +1758,7 @@ const endpoints = makeApi([
|
||||
schema: z.string(),
|
||||
},
|
||||
],
|
||||
response: z.void(),
|
||||
response: z.null(),
|
||||
errors: [
|
||||
{
|
||||
status: 401,
|
||||
@@ -2061,7 +2050,7 @@ const endpoints = makeApi([
|
||||
schema: z.string(),
|
||||
},
|
||||
],
|
||||
response: z.void(),
|
||||
response: z.null(),
|
||||
errors: [
|
||||
{
|
||||
status: 401,
|
||||
@@ -2086,11 +2075,6 @@ const endpoints = makeApi([
|
||||
alias: 'downloadInvoice',
|
||||
requestFormat: 'json',
|
||||
parameters: [
|
||||
{
|
||||
name: 'body',
|
||||
type: 'Body',
|
||||
schema: z.object({ with_e_invoice: z.boolean() }).passthrough(),
|
||||
},
|
||||
{
|
||||
name: 'organization',
|
||||
type: 'Path',
|
||||
@@ -2119,16 +2103,6 @@ const endpoints = makeApi([
|
||||
description: `Not found`,
|
||||
schema: z.object({ message: z.string() }).passthrough(),
|
||||
},
|
||||
{
|
||||
status: 422,
|
||||
description: `Validation error`,
|
||||
schema: z
|
||||
.object({
|
||||
message: z.string(),
|
||||
errors: z.record(z.array(z.string())),
|
||||
})
|
||||
.passthrough(),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -2192,7 +2166,7 @@ const endpoints = makeApi([
|
||||
schema: z.string(),
|
||||
},
|
||||
],
|
||||
response: z.void(),
|
||||
response: z.null(),
|
||||
errors: [
|
||||
{
|
||||
status: 400,
|
||||
@@ -2384,7 +2358,7 @@ const endpoints = makeApi([
|
||||
schema: z.string(),
|
||||
},
|
||||
],
|
||||
response: z.void(),
|
||||
response: z.null(),
|
||||
errors: [
|
||||
{
|
||||
status: 400,
|
||||
@@ -2431,7 +2405,7 @@ const endpoints = makeApi([
|
||||
schema: z.string(),
|
||||
},
|
||||
],
|
||||
response: z.void(),
|
||||
response: z.null(),
|
||||
errors: [
|
||||
{
|
||||
status: 400,
|
||||
@@ -2478,7 +2452,7 @@ const endpoints = makeApi([
|
||||
schema: z.string(),
|
||||
},
|
||||
],
|
||||
response: z.void(),
|
||||
response: z.null(),
|
||||
errors: [
|
||||
{
|
||||
status: 400,
|
||||
@@ -2576,7 +2550,7 @@ const endpoints = makeApi([
|
||||
schema: z.string(),
|
||||
},
|
||||
],
|
||||
response: z.void(),
|
||||
response: z.null(),
|
||||
errors: [
|
||||
{
|
||||
status: 401,
|
||||
@@ -2828,7 +2802,7 @@ const endpoints = makeApi([
|
||||
schema: z.string(),
|
||||
},
|
||||
],
|
||||
response: z.void(),
|
||||
response: z.null(),
|
||||
errors: [
|
||||
{
|
||||
status: 400,
|
||||
@@ -3201,7 +3175,7 @@ const endpoints = makeApi([
|
||||
schema: z.string(),
|
||||
},
|
||||
],
|
||||
response: z.void(),
|
||||
response: z.null(),
|
||||
errors: [
|
||||
{
|
||||
status: 401,
|
||||
@@ -3369,7 +3343,7 @@ const endpoints = makeApi([
|
||||
schema: z.string(),
|
||||
},
|
||||
],
|
||||
response: z.void(),
|
||||
response: z.null(),
|
||||
errors: [
|
||||
{
|
||||
status: 400,
|
||||
@@ -3596,7 +3570,7 @@ const endpoints = makeApi([
|
||||
schema: z.string(),
|
||||
},
|
||||
],
|
||||
response: z.void(),
|
||||
response: z.null(),
|
||||
errors: [
|
||||
{
|
||||
status: 400,
|
||||
@@ -3976,7 +3950,7 @@ Users with the permission `time-entries:view:own` can only use this en
|
||||
schema: z.string(),
|
||||
},
|
||||
],
|
||||
response: z.void(),
|
||||
response: z.null(),
|
||||
errors: [
|
||||
{
|
||||
status: 401,
|
||||
@@ -4591,7 +4565,7 @@ Please note that the access token is only shown in this response and cannot be r
|
||||
schema: z.string(),
|
||||
},
|
||||
],
|
||||
response: z.void(),
|
||||
response: z.null(),
|
||||
errors: [
|
||||
{
|
||||
status: 400,
|
||||
@@ -4633,7 +4607,7 @@ Please note that the access token is only shown in this response and cannot be r
|
||||
schema: z.string(),
|
||||
},
|
||||
],
|
||||
response: z.void(),
|
||||
response: z.null(),
|
||||
errors: [
|
||||
{
|
||||
status: 400,
|
||||
@@ -4698,11 +4672,6 @@ Please note that the access token is only shown in this response and cannot be r
|
||||
description: `Unauthenticated`,
|
||||
schema: z.object({ message: z.string() }).passthrough(),
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
description: `Authorization error`,
|
||||
schema: z.object({ message: z.string() }).passthrough(),
|
||||
},
|
||||
{
|
||||
status: 404,
|
||||
description: `Not found`,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/Components/ui/popover';
|
||||
import { watch } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -37,12 +36,6 @@ function onOpenChange(value: boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
watch(open, (value) => {
|
||||
if (value === false) {
|
||||
emit('submit');
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -9,14 +9,6 @@ export function isBillingActivated() {
|
||||
return page.props.has_billing_extension;
|
||||
}
|
||||
|
||||
export function isInvoicingActivated() {
|
||||
const page = usePage<{
|
||||
has_invoicing_extension: boolean;
|
||||
}>();
|
||||
|
||||
return page.props.has_invoicing_extension;
|
||||
}
|
||||
|
||||
export function isInTrial() {
|
||||
const page = usePage<{
|
||||
billing: {
|
||||
|
||||
@@ -122,7 +122,3 @@ export function canDeleteReport() {
|
||||
export function canViewAllTimeEntries() {
|
||||
return currentUserHasPermission('time-entries:view:all');
|
||||
}
|
||||
export function canViewInvoices() {
|
||||
return currentUserHasPermission('invoices:view');
|
||||
}
|
||||
|
||||
|
||||
@@ -22,12 +22,4 @@ const theme = computed(() => {
|
||||
return themeSetting.value
|
||||
});
|
||||
|
||||
function useTheme() {
|
||||
document.documentElement.classList.add(theme.value);
|
||||
watch(theme, (newTheme, oldTheme) => {
|
||||
document.documentElement.classList.remove(oldTheme);
|
||||
document.documentElement.classList.add(newTheme);
|
||||
});
|
||||
}
|
||||
|
||||
export { type themeOption, themeSetting, theme, useTheme };
|
||||
export { type themeOption, themeSetting, theme };
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
"Project","Client","Description","Task","User","Group","Email","Tags","Type","Billable","Invoiced","Invoice ID","Start Date","Start Time","End Date","End Time","Duration (h)","Duration (decimal)","Billable Rate (EUR)","Billable Amount (EUR)","Date of creation"
|
||||
"Real World Project","Real World Client","\\ 🔥 Special characters ''''''`!@#$%^&*()_+\-=\[\]{};':''\\|,.''<>\/?~ \\\","A giant task","Peter Tester","Group1, Group2","peter.test@email.test","","Regular","Yes","Yes","Invoice100","13/15/2024","11:00:00 AM","10/15/2024","11:30:00 AM","00:30:00","0.50","1000.00","500.00","10/15/2024"
|
||||
|
@@ -277,7 +277,6 @@ class ProjectEndpointTest extends ApiEndpointTestAbstract
|
||||
$response = $this->postJson(route('api.v1.projects.store', [$data->organization->getKey()]), [
|
||||
'name' => $projectFake->name,
|
||||
'color' => $projectFake->color,
|
||||
'client_id' => null,
|
||||
'is_billable' => $projectFake->is_billable,
|
||||
]);
|
||||
|
||||
@@ -300,7 +299,6 @@ class ProjectEndpointTest extends ApiEndpointTestAbstract
|
||||
'name' => $projectFake->name,
|
||||
'color' => $projectFake->color,
|
||||
'is_billable' => $projectFake->is_billable,
|
||||
'client_id' => null,
|
||||
'billable_rate' => $billableRate,
|
||||
]);
|
||||
|
||||
@@ -311,7 +309,6 @@ class ProjectEndpointTest extends ApiEndpointTestAbstract
|
||||
'color' => $projectFake->color,
|
||||
'organization_id' => $projectFake->organization_id,
|
||||
'is_billable' => $projectFake->is_billable,
|
||||
'client_id' => null,
|
||||
'billable_rate' => $billableRate,
|
||||
]);
|
||||
}
|
||||
@@ -331,7 +328,6 @@ class ProjectEndpointTest extends ApiEndpointTestAbstract
|
||||
'name' => $projectFake->name,
|
||||
'color' => $projectFake->color,
|
||||
'is_billable' => $projectFake->is_billable,
|
||||
'client_id' => null,
|
||||
'billable_rate' => $billableRate,
|
||||
]);
|
||||
|
||||
@@ -355,7 +351,6 @@ class ProjectEndpointTest extends ApiEndpointTestAbstract
|
||||
$response = $this->postJson(route('api.v1.projects.store', [$data->organization->getKey()]), [
|
||||
'name' => $projectFake->name,
|
||||
'color' => $projectFake->color,
|
||||
'client_id' => null,
|
||||
'is_billable' => $projectFake->is_billable,
|
||||
]);
|
||||
|
||||
@@ -365,7 +360,6 @@ class ProjectEndpointTest extends ApiEndpointTestAbstract
|
||||
'name' => $projectFake->name,
|
||||
'color' => $projectFake->color,
|
||||
'organization_id' => $projectFake->organization_id,
|
||||
'client_id' => null,
|
||||
'is_billable' => $projectFake->is_billable,
|
||||
]);
|
||||
}
|
||||
@@ -384,7 +378,6 @@ class ProjectEndpointTest extends ApiEndpointTestAbstract
|
||||
'name' => $projectFake->name,
|
||||
'color' => $projectFake->color,
|
||||
'is_billable' => $projectFake->is_billable,
|
||||
'client_id' => null,
|
||||
'estimated_time' => 10000,
|
||||
]);
|
||||
|
||||
@@ -401,7 +394,6 @@ class ProjectEndpointTest extends ApiEndpointTestAbstract
|
||||
'color' => $projectFake->color,
|
||||
'organization_id' => $projectFake->organization_id,
|
||||
'is_billable' => $projectFake->is_billable,
|
||||
'client_id' => null,
|
||||
'estimated_time' => null,
|
||||
]);
|
||||
}
|
||||
@@ -421,7 +413,6 @@ class ProjectEndpointTest extends ApiEndpointTestAbstract
|
||||
'name' => $projectFake->name,
|
||||
'color' => $projectFake->color,
|
||||
'is_billable' => $projectFake->is_billable,
|
||||
'client_id' => null,
|
||||
'estimated_time' => 10000,
|
||||
]);
|
||||
|
||||
@@ -438,47 +429,11 @@ class ProjectEndpointTest extends ApiEndpointTestAbstract
|
||||
'color' => $projectFake->color,
|
||||
'organization_id' => $projectFake->organization_id,
|
||||
'is_billable' => $projectFake->is_billable,
|
||||
'client_id' => null,
|
||||
'estimated_time' => 10000,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_store_endpoint_can_create_project_if_project_name_already_exists_in_organization_but_with_different_client(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'projects:create',
|
||||
]);
|
||||
$name = 'Project Name';
|
||||
$clientA = Client::factory()->forOrganization($data->organization)->create();
|
||||
$clientB = Client::factory()->forOrganization($data->organization)->create();
|
||||
$projectA = Project::factory()->forOrganization($data->organization)->forClient($clientA)->create([
|
||||
'name' => $name,
|
||||
]);
|
||||
$projectFake = Project::factory()->forOrganization($data->organization)->make();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->postJson(route('api.v1.projects.store', [$data->organization->getKey()]), [
|
||||
'name' => $name,
|
||||
'color' => $projectFake->color,
|
||||
'client_id' => $clientB->getKey(),
|
||||
'is_billable' => $projectFake->is_billable,
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(201);
|
||||
$this->assertDatabaseHas(Project::class, [
|
||||
'name' => $name,
|
||||
'client_id' => $clientB->getKey(),
|
||||
]);
|
||||
$this->assertDatabaseHas(Project::class, [
|
||||
'name' => $name,
|
||||
'client_id' => $clientA->getKey(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_store_endpoint_fails_without_client_if_name_is_already_used_for_project_without_client_in_organization(): void
|
||||
public function test_store_endpoint_fails_if_name_is_already_used_in_organization(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
@@ -495,43 +450,13 @@ class ProjectEndpointTest extends ApiEndpointTestAbstract
|
||||
$response = $this->postJson(route('api.v1.projects.store', [$data->organization->getKey()]), [
|
||||
'name' => $name,
|
||||
'color' => $projectFake->color,
|
||||
'client_id' => null,
|
||||
'is_billable' => $projectFake->is_billable,
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(422);
|
||||
$response->assertJsonValidationErrors([
|
||||
'name' => 'A project with the same name and client already exists in the organization.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_store_endpoint_fails_with_client_if_name_is_already_used_for_the_same_client(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'projects:create',
|
||||
]);
|
||||
$name = 'Project Name';
|
||||
$client = Client::factory()->forOrganization($data->organization)->create();
|
||||
$project = Project::factory()->forOrganization($data->organization)->forClient($client)->create([
|
||||
'name' => $name,
|
||||
]);
|
||||
$projectFake = Project::factory()->forOrganization($data->organization)->make();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->postJson(route('api.v1.projects.store', [$data->organization->getKey()]), [
|
||||
'name' => $name,
|
||||
'color' => $projectFake->color,
|
||||
'client_id' => $client->getKey(),
|
||||
'is_billable' => $projectFake->is_billable,
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(422);
|
||||
$response->assertJsonValidationErrors([
|
||||
'name' => 'A project with the same name and client already exists in the organization.',
|
||||
'name' => 'A project with the same name already exists in the organization.',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -553,7 +478,6 @@ class ProjectEndpointTest extends ApiEndpointTestAbstract
|
||||
$response = $this->postJson(route('api.v1.projects.store', [$data->organization->getKey()]), [
|
||||
'name' => $name,
|
||||
'color' => $projectFake->color,
|
||||
'client_id' => null,
|
||||
'is_billable' => $projectFake->is_billable,
|
||||
]);
|
||||
|
||||
@@ -610,7 +534,6 @@ class ProjectEndpointTest extends ApiEndpointTestAbstract
|
||||
$response = $this->postJson(route('api.v1.projects.store', [$data->organization->getKey()]), [
|
||||
'name' => $projectFake->name,
|
||||
'color' => $projectFake->color,
|
||||
'client_id' => null,
|
||||
'is_billable' => true,
|
||||
'billable_rate' => 10001,
|
||||
]);
|
||||
@@ -642,7 +565,6 @@ class ProjectEndpointTest extends ApiEndpointTestAbstract
|
||||
$response = $this->putJson(route('api.v1.projects.update', [$data->organization->getKey(), $project->getKey()]), [
|
||||
'name' => $projectFake->name,
|
||||
'color' => $projectFake->color,
|
||||
'client_id' => null,
|
||||
'is_billable' => $projectFake->is_billable,
|
||||
]);
|
||||
|
||||
@@ -663,7 +585,6 @@ class ProjectEndpointTest extends ApiEndpointTestAbstract
|
||||
$response = $this->putJson(route('api.v1.projects.update', [$data->organization->getKey(), $project->getKey()]), [
|
||||
'name' => $projectFake->name,
|
||||
'color' => $projectFake->color,
|
||||
'client_id' => null,
|
||||
'is_billable' => $projectFake->is_billable,
|
||||
]);
|
||||
|
||||
@@ -671,43 +592,7 @@ class ProjectEndpointTest extends ApiEndpointTestAbstract
|
||||
$response->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_update_endpoint_can_update_project_if_project_name_already_exists_in_organization_but_with_different_client(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'projects:update',
|
||||
]);
|
||||
$name = 'Project Name';
|
||||
$clientA = Client::factory()->forOrganization($data->organization)->create();
|
||||
$clientB = Client::factory()->forOrganization($data->organization)->create();
|
||||
$projectWithTheName = Project::factory()->forOrganization($data->organization)->forClient($clientA)->create([
|
||||
'name' => $name,
|
||||
]);
|
||||
$project = Project::factory()->forOrganization($data->organization)->create();
|
||||
$projectFake = Project::factory()->forOrganization($data->organization)->create();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->putJson(route('api.v1.projects.update', [$data->organization->getKey(), $project->getKey()]), [
|
||||
'name' => $name,
|
||||
'color' => $projectFake->color,
|
||||
'client_id' => $clientB->getKey(),
|
||||
'is_billable' => $projectFake->is_billable,
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(200);
|
||||
$this->assertDatabaseHas(Project::class, [
|
||||
'name' => $name,
|
||||
'client_id' => $clientA->getKey(),
|
||||
]);
|
||||
$this->assertDatabaseHas(Project::class, [
|
||||
'name' => $name,
|
||||
'client_id' => $clientB->getKey(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_update_endpoint_fails_without_client_if_name_is_already_used_for_project_without_client_in_organization(): void
|
||||
public function test_update_endpoint_fails_if_name_is_already_used_in_organization(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
@@ -725,44 +610,13 @@ class ProjectEndpointTest extends ApiEndpointTestAbstract
|
||||
$response = $this->putJson(route('api.v1.projects.update', [$data->organization->getKey(), $project->getKey()]), [
|
||||
'name' => $name,
|
||||
'color' => $projectFake->color,
|
||||
'client_id' => null,
|
||||
'is_billable' => $projectFake->is_billable,
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(422);
|
||||
$response->assertJsonValidationErrors([
|
||||
'name' => 'A project with the same name and client already exists in the organization.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_update_endpoint_fails_with_client_if_name_is_already_used_for_the_same_client(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission([
|
||||
'projects:update',
|
||||
]);
|
||||
$name = 'Project Name';
|
||||
$client = Client::factory()->forOrganization($data->organization)->create();
|
||||
$projectWithTheName = Project::factory()->forOrganization($data->organization)->forClient($client)->create([
|
||||
'name' => $name,
|
||||
]);
|
||||
$project = Project::factory()->forOrganization($data->organization)->create();
|
||||
$projectFake = Project::factory()->forOrganization($data->organization)->create();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->putJson(route('api.v1.projects.update', [$data->organization->getKey(), $project->getKey()]), [
|
||||
'name' => $name,
|
||||
'color' => $projectFake->color,
|
||||
'client_id' => $client->getKey(),
|
||||
'is_billable' => $projectFake->is_billable,
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertStatus(422);
|
||||
$response->assertJsonValidationErrors([
|
||||
'name' => 'A project with the same name and client already exists in the organization.',
|
||||
'name' => 'A project with the same name already exists in the organization.',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -866,7 +720,6 @@ class ProjectEndpointTest extends ApiEndpointTestAbstract
|
||||
$response = $this->putJson(route('api.v1.projects.update', [$data->organization->getKey(), $project->getKey()]), [
|
||||
'name' => $name,
|
||||
'color' => $projectFake->color,
|
||||
'client_id' => null,
|
||||
'is_billable' => $projectFake->is_billable,
|
||||
]);
|
||||
|
||||
@@ -929,7 +782,6 @@ class ProjectEndpointTest extends ApiEndpointTestAbstract
|
||||
$response = $this->putJson(route('api.v1.projects.update', [$data->organization->getKey(), $project->getKey()]), [
|
||||
'name' => $projectFake->name,
|
||||
'color' => $projectFake->color,
|
||||
'client_id' => null,
|
||||
'is_billable' => $projectFake->is_billable,
|
||||
'estimated_time' => 10000,
|
||||
]);
|
||||
@@ -963,7 +815,6 @@ class ProjectEndpointTest extends ApiEndpointTestAbstract
|
||||
$response = $this->putJson(route('api.v1.projects.update', [$data->organization->getKey(), $project->getKey()]), [
|
||||
'name' => $projectFake->name,
|
||||
'color' => $projectFake->color,
|
||||
'client_id' => null,
|
||||
'is_billable' => $projectFake->is_billable,
|
||||
'estimated_time' => 10000,
|
||||
]);
|
||||
@@ -997,7 +848,6 @@ class ProjectEndpointTest extends ApiEndpointTestAbstract
|
||||
$response = $this->putJson(route('api.v1.projects.update', [$data->organization->getKey(), $project->getKey()]), [
|
||||
'name' => $projectFake->name,
|
||||
'color' => $projectFake->color,
|
||||
'client_id' => null,
|
||||
'is_billable' => $projectFake->is_billable,
|
||||
'billable_rate' => $project->billable_rate,
|
||||
]);
|
||||
@@ -1030,7 +880,6 @@ class ProjectEndpointTest extends ApiEndpointTestAbstract
|
||||
$response = $this->putJson(route('api.v1.projects.update', [$data->organization->getKey(), $project->getKey()]), [
|
||||
'name' => $projectFake->name,
|
||||
'color' => $projectFake->color,
|
||||
'client_id' => null,
|
||||
'is_billable' => $projectFake->is_billable,
|
||||
'billable_rate' => 10003,
|
||||
]);
|
||||
@@ -1058,7 +907,6 @@ class ProjectEndpointTest extends ApiEndpointTestAbstract
|
||||
$response = $this->putJson(route('api.v1.projects.update', [$data->organization->getKey(), $project->getKey()]), [
|
||||
'name' => $projectFake->name,
|
||||
'color' => $projectFake->color,
|
||||
'client_id' => null,
|
||||
'is_billable' => $projectFake->is_billable,
|
||||
'is_archived' => true,
|
||||
]);
|
||||
@@ -1087,7 +935,6 @@ class ProjectEndpointTest extends ApiEndpointTestAbstract
|
||||
$response = $this->putJson(route('api.v1.projects.update', [$data->organization->getKey(), $project->getKey()]), [
|
||||
'name' => $projectFake->name,
|
||||
'color' => $projectFake->color,
|
||||
'client_id' => null,
|
||||
'is_billable' => $projectFake->is_billable,
|
||||
'is_archived' => false,
|
||||
]);
|
||||
|
||||
@@ -16,13 +16,11 @@ class ForceHttpsMiddlewareTest extends MiddlewareTestAbstract
|
||||
{
|
||||
private function createTestRoute(): string
|
||||
{
|
||||
$uri = Route::get('/test-route', function () {
|
||||
return Route::get('/test-route', function () {
|
||||
return [
|
||||
'is_secure' => request()->secure(),
|
||||
];
|
||||
})->middleware(ForceHttps::class)->uri;
|
||||
|
||||
return url($uri, [], false);
|
||||
}
|
||||
|
||||
public function test_if_config_app_force_https_is_true_then_the_request_will_be_modified_to_make_the_app_think_it_was_a_https_request(): void
|
||||
|
||||
@@ -185,19 +185,4 @@ class ProjectModelTest extends ModelTestAbstract
|
||||
// Assert
|
||||
$this->assertFalse($isArchived);
|
||||
}
|
||||
|
||||
public function test_project_can_store_big_amounts_of_spent_time(): void
|
||||
{
|
||||
// Arrange
|
||||
$project = Project::factory()->create();
|
||||
$spentTime = 100 * 365 * 24 * 60 * 60; // 100 years in seconds
|
||||
|
||||
// Act
|
||||
$project->spent_time = $spentTime;
|
||||
$project->save();
|
||||
$project->refresh();
|
||||
|
||||
// Assert
|
||||
$this->assertSame($spentTime, $project->spent_time);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,19 +114,4 @@ class TaskModelTest extends ModelTestAbstract
|
||||
// Assert
|
||||
$this->assertFalse($task->is_done);
|
||||
}
|
||||
|
||||
public function test_task_can_store_big_amounts_of_spent_time(): void
|
||||
{
|
||||
// Arrange
|
||||
$task = Task::factory()->create();
|
||||
$spentTime = 100 * 365 * 24 * 60 * 60; // 100 years in seconds
|
||||
|
||||
// Act
|
||||
$task->spent_time = $spentTime;
|
||||
$task->save();
|
||||
$task->refresh();
|
||||
|
||||
// Assert
|
||||
$this->assertSame($spentTime, $task->spent_time);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,25 +94,4 @@ class ClockifyTimeEntriesImporterTest extends ImporterTestAbstract
|
||||
$this->assertSame(0, $report->projectsCreated);
|
||||
$this->assertSame(0, $report->clientsCreated);
|
||||
}
|
||||
|
||||
public function test_import_fails_if_month_in_date_is_bigger_than_12(): void
|
||||
{
|
||||
// Arrange
|
||||
$organization = Organization::factory()->create();
|
||||
$timezone = 'Europe/Vienna';
|
||||
$importer = new ClockifyTimeEntriesImporter;
|
||||
$importer->init($organization);
|
||||
$data = Storage::disk('testfiles')->get('clockify_time_entries_import_test_3.csv');
|
||||
|
||||
// Act
|
||||
try {
|
||||
$importer->importData($data, $timezone);
|
||||
} catch (ImportException $e) {
|
||||
// Assert
|
||||
$this->assertSame('Start date ("13/15/2024") is invalid, please select the correct date format before exporting from Clockify', $e->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
$this->fail();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,198 +498,4 @@ class TimeEntryAggregationServiceTest extends TestCaseWithDatabase
|
||||
],
|
||||
], $result);
|
||||
}
|
||||
|
||||
public function test_aggregated_time_entries_with_descriptions_by_description_and_billable(): void
|
||||
{
|
||||
// Arrange
|
||||
TimeEntry::factory()->startWithDuration(now(), 10)->create([
|
||||
'description' => 'TEST 1',
|
||||
'billable' => true,
|
||||
]);
|
||||
TimeEntry::factory()->startWithDuration(now(), 10)->create([
|
||||
'description' => '',
|
||||
'billable' => false,
|
||||
]);
|
||||
TimeEntry::factory()->startWithDuration(now(), 10)->create([
|
||||
'description' => 'TEST 1',
|
||||
'billable' => false,
|
||||
]);
|
||||
TimeEntry::factory()->startWithDuration(now(), 10)->create([
|
||||
'description' => '',
|
||||
'billable' => false,
|
||||
]);
|
||||
$query = TimeEntry::query();
|
||||
|
||||
// Act
|
||||
$result = $this->service->getAggregatedTimeEntriesWithDescriptions(
|
||||
$query,
|
||||
TimeEntryAggregationType::Description,
|
||||
TimeEntryAggregationType::Billable,
|
||||
'Europe/Vienna',
|
||||
Weekday::Monday,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
true
|
||||
);
|
||||
|
||||
// Assert
|
||||
$this->assertSame([
|
||||
'seconds' => 40,
|
||||
'cost' => 0,
|
||||
'grouped_type' => 'description',
|
||||
'grouped_data' => [
|
||||
[
|
||||
'key' => null,
|
||||
'seconds' => 20,
|
||||
'cost' => 0,
|
||||
'grouped_type' => 'billable',
|
||||
'grouped_data' => [
|
||||
[
|
||||
'key' => '0',
|
||||
'seconds' => 20,
|
||||
'cost' => 0,
|
||||
'grouped_type' => null,
|
||||
'grouped_data' => null,
|
||||
'description' => 'Non-billable',
|
||||
'color' => null,
|
||||
],
|
||||
],
|
||||
'description' => null,
|
||||
'color' => null,
|
||||
],
|
||||
[
|
||||
'key' => 'TEST 1',
|
||||
'seconds' => 20,
|
||||
'cost' => 0,
|
||||
'grouped_type' => 'billable',
|
||||
'grouped_data' => [
|
||||
[
|
||||
'key' => '0',
|
||||
'seconds' => 10,
|
||||
'cost' => 0,
|
||||
'grouped_type' => null,
|
||||
'grouped_data' => null,
|
||||
'description' => 'Non-billable',
|
||||
'color' => null,
|
||||
],
|
||||
[
|
||||
'key' => '1',
|
||||
'seconds' => 10,
|
||||
'cost' => 0,
|
||||
'grouped_type' => null,
|
||||
'grouped_data' => null,
|
||||
'description' => 'Billable',
|
||||
'color' => null,
|
||||
],
|
||||
],
|
||||
'description' => 'TEST 1',
|
||||
'color' => null,
|
||||
],
|
||||
],
|
||||
], $result);
|
||||
}
|
||||
|
||||
public function test_aggregated_time_entries_with_descriptions_by_client_and_project(): void
|
||||
{
|
||||
// Arrange
|
||||
$client1 = Client::factory()->create();
|
||||
$client2 = Client::factory()->create();
|
||||
$project1 = Project::factory()->forClient($client1)->create();
|
||||
$project2 = Project::factory()->forClient($client2)->create();
|
||||
$project3 = Project::factory()->create();
|
||||
TimeEntry::factory()->startWithDuration(now(), 10)->forProject($project1)->create();
|
||||
TimeEntry::factory()->startWithDuration(now(), 10)->forProject($project2)->create();
|
||||
TimeEntry::factory()->startWithDuration(now(), 10)->forProject($project3)->create();
|
||||
TimeEntry::factory()->startWithDuration(now(), 10)->create();
|
||||
$query = TimeEntry::query();
|
||||
|
||||
// Act
|
||||
$result = $this->service->getAggregatedTimeEntriesWithDescriptions(
|
||||
$query,
|
||||
TimeEntryAggregationType::Client,
|
||||
TimeEntryAggregationType::Project,
|
||||
'Europe/Vienna',
|
||||
Weekday::Monday,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
true
|
||||
);
|
||||
|
||||
// Assert
|
||||
$this->assertEqualsCanonicalizing([
|
||||
'seconds' => 40,
|
||||
'cost' => 0,
|
||||
'grouped_type' => 'client',
|
||||
'grouped_data' => [
|
||||
[
|
||||
'key' => null,
|
||||
'seconds' => 20,
|
||||
'cost' => 0,
|
||||
'grouped_type' => 'project',
|
||||
'grouped_data' => [
|
||||
[
|
||||
'key' => null,
|
||||
'seconds' => 10,
|
||||
'cost' => 0,
|
||||
'grouped_type' => null,
|
||||
'grouped_data' => null,
|
||||
'description' => null,
|
||||
'color' => null,
|
||||
],
|
||||
[
|
||||
'key' => $project3->getKey(),
|
||||
'seconds' => 10,
|
||||
'cost' => 0,
|
||||
'grouped_type' => null,
|
||||
'grouped_data' => null,
|
||||
'description' => $project3->name,
|
||||
'color' => $project3->color,
|
||||
],
|
||||
],
|
||||
'description' => null,
|
||||
'color' => null,
|
||||
],
|
||||
[
|
||||
'key' => $client1->getKey(),
|
||||
'seconds' => 10,
|
||||
'cost' => 0,
|
||||
'grouped_type' => 'project',
|
||||
'grouped_data' => [
|
||||
[
|
||||
'key' => $project1->getKey(),
|
||||
'seconds' => 10,
|
||||
'cost' => 0,
|
||||
'grouped_type' => null,
|
||||
'grouped_data' => null,
|
||||
'description' => $project1->name,
|
||||
'color' => $project1->color,
|
||||
],
|
||||
],
|
||||
'description' => $client1->name,
|
||||
'color' => null,
|
||||
],
|
||||
[
|
||||
'key' => $client2->getKey(),
|
||||
'seconds' => 10,
|
||||
'cost' => 0,
|
||||
'grouped_type' => 'project',
|
||||
'grouped_data' => [
|
||||
[
|
||||
'key' => $project2->getKey(),
|
||||
'seconds' => 10,
|
||||
'cost' => 0,
|
||||
'grouped_type' => null,
|
||||
'grouped_data' => null,
|
||||
'description' => $project2->name,
|
||||
'color' => $project2->color,
|
||||
],
|
||||
],
|
||||
'description' => $client2->name,
|
||||
'color' => null,
|
||||
],
|
||||
],
|
||||
], $result);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user