diff --git a/app/Http/Controllers/Api/V1/ChartController.php b/app/Http/Controllers/Api/V1/ChartController.php index 167b9ed1..e50bf011 100644 --- a/app/Http/Controllers/Api/V1/ChartController.php +++ b/app/Http/Controllers/Api/V1/ChartController.php @@ -4,8 +4,8 @@ declare(strict_types=1); namespace App\Http\Controllers\Api\V1; -use App\Enums\Role; use App\Models\Organization; +use App\Service\BillableRateService; use App\Service\DashboardService; use App\Service\PermissionStore; use Illuminate\Auth\Access\AuthorizationException; @@ -154,12 +154,12 @@ class ChartController extends Controller * * @response array{value: int, currency: string} */ - public function totalWeeklyBillableAmount(Organization $organization, DashboardService $dashboardService): JsonResponse + public function totalWeeklyBillableAmount(Organization $organization, DashboardService $dashboardService, BillableRateService $billableRateService): JsonResponse { $this->checkPermission($organization, 'charts:view:own'); $user = $this->user(); - $showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates; + $showBillableRate = $billableRateService->canSeeBillableRates($organization, $this->member($organization)); if (! $showBillableRate) { throw new AuthorizationException('You do not have permission to view billable rates.'); } diff --git a/app/Http/Controllers/Api/V1/MemberController.php b/app/Http/Controllers/Api/V1/MemberController.php index 5f297a13..56de76f7 100644 --- a/app/Http/Controllers/Api/V1/MemberController.php +++ b/app/Http/Controllers/Api/V1/MemberController.php @@ -81,10 +81,9 @@ class MemberController extends Controller { $this->checkPermission($organization, 'members:update', $member); - if ($request->has('billable_rate') && $member->billable_rate !== $request->getBillableRate()) { + $oldBillableRate = $member->billable_rate; + if ($request->has('billable_rate')) { $member->billable_rate = $request->getBillableRate(); - - $billableRateService->updateTimeEntriesBillableRateForMember($member); } if ($request->has('role') && $member->role !== $request->getRole()->value) { $newRole = $request->getRole(); @@ -93,6 +92,10 @@ class MemberController extends Controller } $member->save(); + if ($oldBillableRate !== $member->billable_rate) { + $billableRateService->updateTimeEntriesBillableRateForMember($member); + } + return new MemberResource($member); } diff --git a/app/Http/Controllers/Api/V1/OrganizationController.php b/app/Http/Controllers/Api/V1/OrganizationController.php index 8722eb6c..85e9069f 100644 --- a/app/Http/Controllers/Api/V1/OrganizationController.php +++ b/app/Http/Controllers/Api/V1/OrganizationController.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace App\Http\Controllers\Api\V1; -use App\Enums\Role; use App\Events\AfterCreateOrganization; use App\Http\Requests\V1\Organization\OrganizationDestroyRequest; use App\Http\Requests\V1\Organization\OrganizationStoreRequest; @@ -28,11 +27,11 @@ class OrganizationController extends Controller * * @throws AuthorizationException */ - public function show(Organization $organization): OrganizationResource + public function show(Organization $organization, BillableRateService $billableRateService): OrganizationResource { $this->checkPermission($organization, 'organizations:view'); - $showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates; + $showBillableRate = $billableRateService->canSeeBillableRates($organization, $this->member($organization)); return new OrganizationResource($organization, $showBillableRate); } @@ -81,14 +80,13 @@ class OrganizationController extends Controller if ($request->getBreaksEnabled() !== null) { $organization->breaks_enabled = $request->getBreaksEnabled(); } - $hasBillableRate = $request->has('billable_rate'); - if ($hasBillableRate) { - $oldBillableRate = $organization->billable_rate; + $oldBillableRate = $organization->billable_rate; + if ($request->has('billable_rate')) { $organization->billable_rate = $request->getBillableRate(); } $organization->save(); - if ($hasBillableRate && $oldBillableRate !== $request->getBillableRate()) { + if ($oldBillableRate !== $organization->billable_rate) { $billableRateService->updateTimeEntriesBillableRateForOrganization($organization); } diff --git a/app/Http/Controllers/Api/V1/ProjectController.php b/app/Http/Controllers/Api/V1/ProjectController.php index d836b4ac..d4c0268a 100644 --- a/app/Http/Controllers/Api/V1/ProjectController.php +++ b/app/Http/Controllers/Api/V1/ProjectController.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace App\Http\Controllers\Api\V1; -use App\Enums\Role; use App\Exceptions\Api\EntityStillInUseApiException; use App\Http\Requests\V1\Project\ProjectIndexRequest; use App\Http\Requests\V1\Project\ProjectStoreRequest; @@ -39,7 +38,7 @@ class ProjectController extends Controller * * @operationId getProjects */ - public function index(Organization $organization, ProjectIndexRequest $request): ProjectCollection + public function index(Organization $organization, ProjectIndexRequest $request, BillableRateService $billableRateService): ProjectCollection { $this->checkPermission($organization, 'projects:view'); $canViewAllProjects = $this->hasPermission($organization, 'projects:view:all'); @@ -63,7 +62,7 @@ class ProjectController extends Controller ->orderBy('id') ->paginate(config('app.pagination_per_page_default')); - $showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates; + $showBillableRate = $billableRateService->canSeeBillableRates($organization, $this->member($organization)); return new ProjectCollection($projects, $showBillableRate); } @@ -137,14 +136,16 @@ class ProjectController extends Controller } $oldBillableRate = $project->billable_rate; $clientIdChanged = false; - $project->billable_rate = $request->getBillableRate(); + if ($request->has('billable_rate')) { + $project->billable_rate = $request->getBillableRate(); + } if ($project->client_id !== $request->input('client_id')) { $project->client_id = $request->input('client_id'); $clientIdChanged = true; } $project->save(); - if ($oldBillableRate !== $request->getBillableRate()) { + if ($oldBillableRate !== $project->billable_rate) { $billableRateService->updateTimeEntriesBillableRateForProject($project); } if ($clientIdChanged) { diff --git a/app/Http/Controllers/Api/V1/ProjectMemberController.php b/app/Http/Controllers/Api/V1/ProjectMemberController.php index 0cc6d4c4..03d5c247 100644 --- a/app/Http/Controllers/Api/V1/ProjectMemberController.php +++ b/app/Http/Controllers/Api/V1/ProjectMemberController.php @@ -79,7 +79,7 @@ class ProjectMemberController extends Controller $projectMember->project()->associate($project); $projectMember->save(); - if ($request->getBillableRate() !== null) { + if ($projectMember->billable_rate !== null) { $billableRateService->updateTimeEntriesBillableRateForProjectMember($projectMember); } @@ -100,7 +100,7 @@ class ProjectMemberController extends Controller $projectMember->billable_rate = $request->getBillableRate(); $projectMember->save(); - if ($oldBillableRate !== $request->getBillableRate()) { + if ($oldBillableRate !== $projectMember->billable_rate) { $billableRateService->updateTimeEntriesBillableRateForProjectMember($projectMember); } diff --git a/app/Http/Controllers/Api/V1/TimeEntryController.php b/app/Http/Controllers/Api/V1/TimeEntryController.php index 9c77b2c4..58f62ee1 100644 --- a/app/Http/Controllers/Api/V1/TimeEntryController.php +++ b/app/Http/Controllers/Api/V1/TimeEntryController.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace App\Http\Controllers\Api\V1; use App\Enums\ExportFormat; -use App\Enums\Role; use App\Enums\TimeEntryType; use App\Exceptions\Api\FeatureIsNotAvailableInFreePlanApiException; use App\Exceptions\Api\OverlappingTimeEntryApiException; @@ -29,6 +28,7 @@ use App\Models\Organization; use App\Models\Project; use App\Models\Task; use App\Models\TimeEntry; +use App\Service\BillableRateService; use App\Service\LocalizationService; use App\Service\ReportExport\TimeEntriesDetailedCsvExport; use App\Service\ReportExport\TimeEntriesDetailedExport; @@ -227,7 +227,7 @@ class TimeEntryController extends Controller * * @operationId exportTimeEntries */ - public function indexExport(Organization $organization, TimeEntryIndexExportRequest $request, TimeEntryAggregationService $timeEntryAggregationService): JsonResponse + public function indexExport(Organization $organization, TimeEntryIndexExportRequest $request, TimeEntryAggregationService $timeEntryAggregationService, BillableRateService $billableRateService): JsonResponse { $member = $this->member($organization); /** @var Member|null $memberFilter */ @@ -245,7 +245,7 @@ class TimeEntryController extends Controller } $user = $this->user(); $timezone = $user->timezone; - $showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates; + $showBillableRate = $billableRateService->canSeeBillableRates($organization, $this->member($organization)); $roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null; $roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null; @@ -384,7 +384,7 @@ class TimeEntryController extends Controller * * @throws AuthorizationException */ - public function aggregate(Organization $organization, TimeEntryAggregateRequest $request, TimeEntryAggregationService $timeEntryAggregationService): array + public function aggregate(Organization $organization, TimeEntryAggregateRequest $request, TimeEntryAggregationService $timeEntryAggregationService, BillableRateService $billableRateService): array { $member = $this->member($organization); /** @var Member|null $memberFilter */ @@ -396,7 +396,7 @@ class TimeEntryController extends Controller } $canAccessPremiumFeatures = $this->canAccessPremiumFeatures($organization); $user = $this->user(); - $showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates; + $showBillableRate = $billableRateService->canSeeBillableRates($organization, $this->member($organization)); $group1Type = $request->getGroup(); $group2Type = $request->getSubGroup(); @@ -434,7 +434,7 @@ class TimeEntryController extends Controller * @throws NoOutputFileInResponse * @throws FeatureIsNotAvailableInFreePlanApiException */ - public function aggregateExport(Organization $organization, TimeEntryAggregateExportRequest $request, TimeEntryAggregationService $timeEntryAggregationService): JsonResponse + public function aggregateExport(Organization $organization, TimeEntryAggregateExportRequest $request, TimeEntryAggregationService $timeEntryAggregationService, BillableRateService $billableRateService): JsonResponse { $member = $this->member($organization); /** @var Member|null $memberFilter */ @@ -451,7 +451,7 @@ class TimeEntryController extends Controller } $debug = $request->getDebug(); $user = $this->user(); - $showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates; + $showBillableRate = $billableRateService->canSeeBillableRates($organization, $this->member($organization)); $group = $request->getGroup(); $subGroup = $request->getSubGroup(); diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index d9c26387..d27b7583 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -60,6 +60,7 @@ class HandleInertiaRequests extends Middleware 'trial_until' => $billing->getTrialUntil($currentOrganization)?->toIso8601ZuluString(), 'is_blocked' => $billing->isBlocked($currentOrganization), 'goal_limit' => $billing->getGoalLimit($currentOrganization), + 'can_use_billable_rates' => $billing->canUseBillableRates($currentOrganization), ] : null, 'flash' => [ 'message' => fn () => $request->session()->get('message'), diff --git a/app/Http/Requests/V1/Project/ProjectUpdateRequest.php b/app/Http/Requests/V1/Project/ProjectUpdateRequest.php index 6670430f..c28bd68f 100644 --- a/app/Http/Requests/V1/Project/ProjectUpdateRequest.php +++ b/app/Http/Requests/V1/Project/ProjectUpdateRequest.php @@ -68,7 +68,7 @@ class ProjectUpdateRequest extends BaseFormRequest return $builder->whereBelongsTo($this->organization, 'organization'); })->uuid(), ], - // Billable rate in cents per hour (example: 8000 means 80.00 in the organization's currency) + // Billable rate in cents per hour (example: 8000 means 80.00 in the organization's currency), the current rate is kept if the field is omitted 'billable_rate' => array_merge([ 'nullable', ], diff --git a/app/Http/Resources/V1/Organization/OrganizationResource.php b/app/Http/Resources/V1/Organization/OrganizationResource.php index 860f9f49..f096eeb3 100644 --- a/app/Http/Resources/V1/Organization/OrganizationResource.php +++ b/app/Http/Resources/V1/Organization/OrganizationResource.php @@ -11,6 +11,7 @@ use App\Enums\NumberFormat; use App\Enums\TimeFormat; use App\Http\Resources\V1\BaseResource; use App\Models\Organization; +use App\Service\BillingContract; use App\Service\CurrencyService; use Illuminate\Http\Request; @@ -51,6 +52,8 @@ class OrganizationResource extends BaseResource 'is_personal' => $this->resource->personal_team, /** Billable rate in cents per hour */ 'billable_rate' => $this->showBillableRate ? $this->resource->billable_rate : null, + /** Whether the organization's plan allows billable flags and rates */ + 'can_use_billable_rates' => app(BillingContract::class)->canUseBillableRates($this->resource), /** Can members of the organization with role "employee" see the billable rates */ 'employees_can_see_billable_rates' => $this->resource->employees_can_see_billable_rates, /** Can members of the organization with role "employee" manage tasks in public projects and projects they are assigned to */ diff --git a/app/Models/Member.php b/app/Models/Member.php index 7cc3fefc..0fbbf29f 100644 --- a/app/Models/Member.php +++ b/app/Models/Member.php @@ -6,6 +6,7 @@ namespace App\Models; use App\Models\Concerns\CustomAuditable; use App\Models\Concerns\HasUuids; +use App\Service\BillingContract; use Database\Factories\MemberFactory; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -48,6 +49,16 @@ class Member extends Pivot implements AuditableContract */ protected $table = 'members'; + protected static function booted(): void + { + static::saving(function (Member $member): void { + // Organizations that can not use billable rates can not change the billable rate + if ($member->billable_rate !== $member->getOriginal('billable_rate') && ! app(BillingContract::class)->canUseBillableRates($member->loadMissing('organization')->organization)) { + $member->billable_rate = $member->getOriginal('billable_rate'); + } + }); + } + /** * @return BelongsTo */ diff --git a/app/Models/Organization.php b/app/Models/Organization.php index b5f8fc92..21b21a1b 100644 --- a/app/Models/Organization.php +++ b/app/Models/Organization.php @@ -11,6 +11,7 @@ use App\Enums\NumberFormat; use App\Enums\TimeFormat; use App\Models\Concerns\CustomAuditable; use App\Models\Concerns\HasUuids; +use App\Service\BillingContract; use Database\Factories\OrganizationFactory; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -96,6 +97,25 @@ class Organization extends Model implements AuditableContract protected $attributes = [ ]; + protected static function booted(): void + { + static::saving(function (Organization $organization): void { + // Organizations that can not use billable rates can keep or remove the visibility for employees, but not + // enable it, and can not change the billable rate + $enablesVisibility = $organization->employees_can_see_billable_rates && ! $organization->getOriginal('employees_can_see_billable_rates'); + if (! $enablesVisibility && $organization->billable_rate === $organization->getOriginal('billable_rate')) { + return; + } + if (app(BillingContract::class)->canUseBillableRates($organization)) { + return; + } + if ($enablesVisibility) { + $organization->employees_can_see_billable_rates = false; + } + $organization->billable_rate = $organization->getOriginal('billable_rate'); + }); + } + public function getAuditOwnerOrganizationId(): ?string { return $this->getKey(); diff --git a/app/Models/Project.php b/app/Models/Project.php index b33f05a6..b8f6b187 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -6,6 +6,7 @@ namespace App\Models; use App\Models\Concerns\CustomAuditable; use App\Models\Concerns\HasUuids; +use App\Service\BillingContract; use Database\Factories\ProjectFactory; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; @@ -136,6 +137,25 @@ class Project extends Model implements AuditableContract return $this->scopeComputedAttributesGenerate($builder, $attributes); } + protected static function booted(): void + { + static::saving(function (Project $project): void { + // Organizations that can not use billable rates can keep or remove the billable flag, but not set it, + // and can not change the billable rate + $enablesBillable = $project->is_billable && ! $project->getOriginal('is_billable'); + if (! $enablesBillable && $project->billable_rate === $project->getOriginal('billable_rate')) { + return; + } + if (app(BillingContract::class)->canUseBillableRates($project->loadMissing('organization')->organization)) { + return; + } + if ($enablesBillable) { + $project->is_billable = false; + } + $project->billable_rate = $project->getOriginal('billable_rate'); + }); + } + /** * @return BelongsTo */ diff --git a/app/Models/ProjectMember.php b/app/Models/ProjectMember.php index 1bff0042..daf16366 100644 --- a/app/Models/ProjectMember.php +++ b/app/Models/ProjectMember.php @@ -7,6 +7,7 @@ namespace App\Models; use App\Models\Concerns\AuditableThroughParent; use App\Models\Concerns\CustomAuditable; use App\Models\Concerns\HasUuids; +use App\Service\BillingContract; use Database\Factories\ProjectMemberFactory; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -48,6 +49,16 @@ class ProjectMember extends Model implements AuditableContract, AuditableThrough 'billable_rate' => 'int', ]; + protected static function booted(): void + { + static::saving(function (ProjectMember $projectMember): void { + // Organizations that can not use billable rates can not change the billable rate + if ($projectMember->billable_rate !== $projectMember->getOriginal('billable_rate') && ! app(BillingContract::class)->canUseBillableRates($projectMember->loadMissing('project.organization')->project->organization)) { + $projectMember->billable_rate = $projectMember->getOriginal('billable_rate'); + } + }); + } + /** * @return BelongsTo */ diff --git a/app/Models/TimeEntry.php b/app/Models/TimeEntry.php index 2928cca5..9cd79203 100644 --- a/app/Models/TimeEntry.php +++ b/app/Models/TimeEntry.php @@ -8,6 +8,7 @@ use App\Enums\TimeEntryType; use App\Models\Concerns\CustomAuditable; use App\Models\Concerns\HasUuids; use App\Service\BillableRateService; +use App\Service\BillingContract; use Carbon\CarbonInterval; use Database\Factories\TimeEntryFactory; use Illuminate\Database\Eloquent\Builder; @@ -140,6 +141,12 @@ class TimeEntry extends Model implements AuditableContract $timeEntry->client_id = null; $timeEntry->tags = []; } + // Organizations that can not use billable rates can keep or remove the billable flag, but not set it + if ($timeEntry->billable && ! $timeEntry->getOriginal('billable') && ! app(BillingContract::class)->canUseBillableRates($timeEntry->loadMissing('organization')->organization)) { + $timeEntry->billable = false; + // Note: The billable rate is computed before saving, non-billable time entries have no billable rate + $timeEntry->billable_rate = null; + } }); } diff --git a/app/Service/BillableRateService.php b/app/Service/BillableRateService.php index a665e458..adcf7a1c 100644 --- a/app/Service/BillableRateService.php +++ b/app/Service/BillableRateService.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Service; +use App\Enums\Role; use App\Models\Member; use App\Models\Organization; use App\Models\Project; @@ -13,6 +14,24 @@ use Illuminate\Database\Eloquent\Builder; class BillableRateService { + public function __construct( + private BillingContract $billing, + ) {} + + public function canUseBillableRates(Organization $organization): bool + { + return $this->billing->canUseBillableRates($organization); + } + + public function canSeeBillableRates(Organization $organization, Member $member): bool + { + if (! $this->canUseBillableRates($organization)) { + return false; + } + + return $member->role !== Role::Employee->value || $organization->employees_can_see_billable_rates; + } + public function updateTimeEntriesBillableRateForProjectMember(ProjectMember $projectMember): void { TimeEntry::query() diff --git a/app/Service/BillingContract.php b/app/Service/BillingContract.php index 40d0a002..61ff6abd 100644 --- a/app/Service/BillingContract.php +++ b/app/Service/BillingContract.php @@ -63,4 +63,13 @@ class BillingContract { return false; } + + /** + * Check if the organization can set and see billable rates + * Without a billing system every organization can use billable rates + */ + public function canUseBillableRates(Organization $organization): bool + { + return true; + } } diff --git a/app/Service/Import/Importers/ClockifyTimeEntriesImporter.php b/app/Service/Import/Importers/ClockifyTimeEntriesImporter.php index f74919b2..60791be8 100644 --- a/app/Service/Import/Importers/ClockifyTimeEntriesImporter.php +++ b/app/Service/Import/Importers/ClockifyTimeEntriesImporter.php @@ -119,7 +119,7 @@ class ClockifyTimeEntriesImporter extends DefaultImporter $timeEntry->task_id = $taskId; $timeEntry->project_id = $projectId; $timeEntry->client_id = $clientId; - $timeEntry->organization_id = $this->organization->id; + $timeEntry->organization()->associate($this->organization); if (strlen($record['Description']) > 5000) { throw new ImportException('Time entry description is too long'); } diff --git a/app/Service/Import/Importers/DefaultImporter.php b/app/Service/Import/Importers/DefaultImporter.php index 78d836fb..484259ee 100644 --- a/app/Service/Import/Importers/DefaultImporter.php +++ b/app/Service/Import/Importers/DefaultImporter.php @@ -126,6 +126,8 @@ abstract class DefaultImporter implements ImporterContract if ($project->billable_rate === 0) { $project->billable_rate = null; } + // Note: Prevents a query per project for the billable rate restriction (see Project::booted) + $project->setRelation('organization', $this->organization); }); $this->projectMemberImportHelper = new ImportDatabaseHelper(ProjectMember::class, ['project_id', 'member_id'], true, function (Builder $builder): Builder { /** @var Builder $builder */ @@ -140,6 +142,11 @@ abstract class DefaultImporter implements ImporterContract if ($projectMember->billable_rate === 0) { $projectMember->billable_rate = null; } + // Note: Prevents queries per project member for the billable rate restriction (see ProjectMember::booted) + $project = $this->projectImportHelper->getModelById($projectMember->project_id); + if ($project !== null) { + $projectMember->setRelation('project', $project->setRelation('organization', $this->organization)); + } }); $this->tagImportHelper = new ImportDatabaseHelper(Tag::class, ['name', 'organization_id'], true, function (Builder $builder): Builder { /** @var Builder $builder */ diff --git a/app/Service/Import/Importers/GenericTimeEntriesImporter.php b/app/Service/Import/Importers/GenericTimeEntriesImporter.php index bb19ac0e..31344ffd 100644 --- a/app/Service/Import/Importers/GenericTimeEntriesImporter.php +++ b/app/Service/Import/Importers/GenericTimeEntriesImporter.php @@ -129,7 +129,7 @@ class GenericTimeEntriesImporter extends DefaultImporter $timeEntry->task_id = $taskId; $timeEntry->project_id = $projectId; $timeEntry->client_id = $clientId; - $timeEntry->organization_id = $this->organization->id; + $timeEntry->organization()->associate($this->organization); $timeEntry->description = $record['description']; if (! in_array($record['billable'], ['true', 'false'], true)) { throw new ImportException('Invalid billable value'); diff --git a/app/Service/Import/Importers/HarvestTimeEntriesImporter.php b/app/Service/Import/Importers/HarvestTimeEntriesImporter.php index 57a66f49..1e40a511 100644 --- a/app/Service/Import/Importers/HarvestTimeEntriesImporter.php +++ b/app/Service/Import/Importers/HarvestTimeEntriesImporter.php @@ -107,7 +107,7 @@ class HarvestTimeEntriesImporter extends DefaultImporter $timeEntry->task_id = $taskId; $timeEntry->project_id = $projectId; $timeEntry->client_id = $clientId; - $timeEntry->organization_id = $this->organization->id; + $timeEntry->organization()->associate($this->organization); if (strlen($record['Notes']) > 5000) { throw new ImportException('Time entry note is too long'); } diff --git a/app/Service/Import/Importers/SolidtimeImporter.php b/app/Service/Import/Importers/SolidtimeImporter.php index f6eade0e..21377975 100644 --- a/app/Service/Import/Importers/SolidtimeImporter.php +++ b/app/Service/Import/Importers/SolidtimeImporter.php @@ -241,7 +241,7 @@ class SolidtimeImporter extends DefaultImporter $timeEntry->task_id = $taskId; $timeEntry->project_id = $projectId; $timeEntry->client_id = $clientId; - $timeEntry->organization_id = $this->organization->id; + $timeEntry->organization()->associate($this->organization); if (strlen($timeEntryRow['description']) > 5000) { throw new ImportException('Time entry description is too long'); } diff --git a/app/Service/Import/Importers/TogglTimeEntriesImporter.php b/app/Service/Import/Importers/TogglTimeEntriesImporter.php index e18cf606..5fbddf3c 100644 --- a/app/Service/Import/Importers/TogglTimeEntriesImporter.php +++ b/app/Service/Import/Importers/TogglTimeEntriesImporter.php @@ -113,7 +113,7 @@ class TogglTimeEntriesImporter extends DefaultImporter $timeEntry->task_id = $taskId; $timeEntry->project_id = $projectId; $timeEntry->client_id = $clientId; - $timeEntry->organization_id = $this->organization->id; + $timeEntry->organization()->associate($this->organization); $timeEntry->description = $record['Description']; if (! in_array($record['Billable'], ['Yes', 'No'], true)) { throw new ImportException('Invalid billable value'); diff --git a/e2e/members.spec.ts b/e2e/members.spec.ts index 7fa13766..6a594103 100644 --- a/e2e/members.spec.ts +++ b/e2e/members.spec.ts @@ -884,7 +884,7 @@ test('test that sorting members by billable rate works', async ({ page, ctx }) = await expect(table).toBeVisible(); // First click = desc (highest first), null rates last - const billableHeader = table.getByText('Billable Rate').first(); + const billableHeader = table.getByRole('button', { name: /Billable Rate/ }); await billableHeader.click(); await expect(billableHeader.locator('svg')).toBeVisible(); let names = await getTableRowNames(table); diff --git a/resources/js/Components/Common/Member/MemberEditModal.vue b/resources/js/Components/Common/Member/MemberEditModal.vue index c0e04605..690a88a0 100644 --- a/resources/js/Components/Common/Member/MemberEditModal.vue +++ b/resources/js/Components/Common/Member/MemberEditModal.vue @@ -21,6 +21,7 @@ import { getOrganizationCurrencyString } from '@/utils/money'; import BillableIcon from '@/packages/ui/src/Icons/BillableIcon.vue'; import { useOrganizationQuery } from '@/utils/useOrganizationQuery'; import { getCurrentOrganizationId } from '@/utils/useUser'; +import { useBillableRatesLock } from '@/packages/ui/src/utils/useBillableRatesLock'; const { updateMember } = useMembersStore(); const { organization } = useOrganizationQuery(getCurrentOrganizationId()!); @@ -71,6 +72,8 @@ function saveWithChecks() { } } +const { locked: billableRatesLocked } = useBillableRatesLock(); + const billableRateSelect = ref('default-rate'); onMounted(() => { @@ -159,7 +162,12 @@ const roleDescription = computed(() => { Billable Rate -
+ +