mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-13 02:32:15 +01:00
Compare commits
1 Commits
feature/fi
...
feature/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aedb9a016d |
@@ -86,8 +86,7 @@ class TimeEntryController extends Controller
|
|||||||
$this->checkPermission($organization, 'time-entries:view:all');
|
$this->checkPermission($organization, 'time-entries:view:all');
|
||||||
}
|
}
|
||||||
|
|
||||||
$canAccessPremiumFeatures = $this->canAccessPremiumFeatures($organization);
|
$timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member);
|
||||||
$timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member, $canAccessPremiumFeatures);
|
|
||||||
|
|
||||||
$totalCount = $timeEntriesQuery->count();
|
$totalCount = $timeEntriesQuery->count();
|
||||||
|
|
||||||
@@ -141,15 +140,13 @@ class TimeEntryController extends Controller
|
|||||||
/**
|
/**
|
||||||
* @return Builder<TimeEntry>
|
* @return Builder<TimeEntry>
|
||||||
*/
|
*/
|
||||||
private function getTimeEntriesQuery(Organization $organization, TimeEntryIndexRequest|TimeEntryIndexExportRequest $request, ?Member $member, bool $canAccessPremiumFeatures): Builder
|
private function getTimeEntriesQuery(Organization $organization, TimeEntryIndexRequest|TimeEntryIndexExportRequest $request, ?Member $member): Builder
|
||||||
{
|
{
|
||||||
$select = TimeEntry::SELECT_COLUMNS;
|
$select = TimeEntry::SELECT_COLUMNS;
|
||||||
$roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null;
|
if ($request->getRoundingType() !== null && $request->getRoundingMinutes() !== null) {
|
||||||
$roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null;
|
|
||||||
if ($roundingType !== null && $roundingMinutes !== null) {
|
|
||||||
$select = array_diff($select, ['start', 'end']);
|
$select = array_diff($select, ['start', 'end']);
|
||||||
$select[] = DB::raw(app(TimeEntryService::class)->getStartSelectRawForRounding($roundingType, $roundingMinutes).' as start');
|
$select[] = DB::raw(app(TimeEntryService::class)->getStartSelectRawForRounding($request->getRoundingType(), $request->getRoundingMinutes()).' as start');
|
||||||
$select[] = DB::raw(app(TimeEntryService::class)->getEndSelectRawForRounding($roundingType, $roundingMinutes).' as end');
|
$select[] = DB::raw(app(TimeEntryService::class)->getEndSelectRawForRounding($request->getRoundingType(), $request->getRoundingMinutes()).' as end');
|
||||||
}
|
}
|
||||||
$timeEntriesQuery = TimeEntry::query()
|
$timeEntriesQuery = TimeEntry::query()
|
||||||
->whereBelongsTo($organization, 'organization')
|
->whereBelongsTo($organization, 'organization')
|
||||||
@@ -187,19 +184,18 @@ class TimeEntryController extends Controller
|
|||||||
} else {
|
} else {
|
||||||
$this->checkPermission($organization, 'time-entries:view:all');
|
$this->checkPermission($organization, 'time-entries:view:all');
|
||||||
}
|
}
|
||||||
$canAccessPremiumFeatures = $this->canAccessPremiumFeatures($organization);
|
|
||||||
$debug = $request->getDebug();
|
$debug = $request->getDebug();
|
||||||
$format = $request->getFormatValue();
|
$format = $request->getFormatValue();
|
||||||
if ($format === ExportFormat::PDF && ! $canAccessPremiumFeatures) {
|
if ($format === ExportFormat::PDF && ! $this->canAccessPremiumFeatures($organization)) {
|
||||||
throw new FeatureIsNotAvailableInFreePlanApiException;
|
throw new FeatureIsNotAvailableInFreePlanApiException;
|
||||||
}
|
}
|
||||||
$user = $this->user();
|
$user = $this->user();
|
||||||
$timezone = $user->timezone;
|
$timezone = $user->timezone;
|
||||||
$showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates;
|
$showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates;
|
||||||
$roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null;
|
$roundingType = $request->getRoundingType();
|
||||||
$roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null;
|
$roundingMinutes = $request->getRoundingMinutes();
|
||||||
|
|
||||||
$timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member, $canAccessPremiumFeatures);
|
$timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member);
|
||||||
$timeEntriesQuery->with([
|
$timeEntriesQuery->with([
|
||||||
'task',
|
'task',
|
||||||
'client',
|
'client',
|
||||||
@@ -336,15 +332,14 @@ class TimeEntryController extends Controller
|
|||||||
} else {
|
} else {
|
||||||
$this->checkPermission($organization, 'time-entries:view:all');
|
$this->checkPermission($organization, 'time-entries:view:all');
|
||||||
}
|
}
|
||||||
$canAccessPremiumFeatures = $this->canAccessPremiumFeatures($organization);
|
|
||||||
$user = $this->user();
|
$user = $this->user();
|
||||||
$showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates;
|
$showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates;
|
||||||
|
|
||||||
$group1Type = $request->getGroup();
|
$group1Type = $request->getGroup();
|
||||||
$group2Type = $request->getSubGroup();
|
$group2Type = $request->getSubGroup();
|
||||||
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $member);
|
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $member);
|
||||||
$roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null;
|
$roundingType = $request->getRoundingType();
|
||||||
$roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null;
|
$roundingMinutes = $request->getRoundingMinutes();
|
||||||
|
|
||||||
$aggregatedData = $timeEntryAggregationService->getAggregatedTimeEntries(
|
$aggregatedData = $timeEntryAggregationService->getAggregatedTimeEntries(
|
||||||
$timeEntriesAggregateQuery,
|
$timeEntriesAggregateQuery,
|
||||||
@@ -385,7 +380,6 @@ class TimeEntryController extends Controller
|
|||||||
} else {
|
} else {
|
||||||
$this->checkPermission($organization, 'time-entries:view:all');
|
$this->checkPermission($organization, 'time-entries:view:all');
|
||||||
}
|
}
|
||||||
$canAccessPremiumFeatures = $this->canAccessPremiumFeatures($organization);
|
|
||||||
$format = $request->getFormatValue();
|
$format = $request->getFormatValue();
|
||||||
if ($format === ExportFormat::PDF && ! $this->canAccessPremiumFeatures($organization)) {
|
if ($format === ExportFormat::PDF && ! $this->canAccessPremiumFeatures($organization)) {
|
||||||
throw new FeatureIsNotAvailableInFreePlanApiException;
|
throw new FeatureIsNotAvailableInFreePlanApiException;
|
||||||
@@ -397,8 +391,8 @@ class TimeEntryController extends Controller
|
|||||||
$group = $request->getGroup();
|
$group = $request->getGroup();
|
||||||
$subGroup = $request->getSubGroup();
|
$subGroup = $request->getSubGroup();
|
||||||
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $member);
|
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $member);
|
||||||
$roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null;
|
$roundingType = $request->getRoundingType();
|
||||||
$roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null;
|
$roundingMinutes = $request->getRoundingMinutes();
|
||||||
|
|
||||||
$aggregatedData = $timeEntryAggregationService->getAggregatedTimeEntriesWithDescriptions(
|
$aggregatedData = $timeEntryAggregationService->getAggregatedTimeEntriesWithDescriptions(
|
||||||
$timeEntriesAggregateQuery->clone(),
|
$timeEntriesAggregateQuery->clone(),
|
||||||
|
|||||||
2052
public/fonts/Inter-Variable.ttf
Normal file
2052
public/fonts/Inter-Variable.ttf
Normal file
File diff suppressed because one or more lines are too long
2052
public/fonts/Inter-Variable.woff2
Normal file
2052
public/fonts/Inter-Variable.woff2
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
@@ -163,8 +163,10 @@ body {
|
|||||||
/* Inter Variable Font with browser compatibility considerations */
|
/* Inter Variable Font with browser compatibility considerations */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Inter';
|
font-family: 'Inter';
|
||||||
src: url('/fonts/Inter-Variable.woff2') format('woff2'),
|
src: url('/fonts/Inter-Variable.woff2') format('woff2 supports variations'),
|
||||||
url('/fonts/Inter-Variable.ttf') format('truetype');
|
url('/fonts/Inter-Variable.woff2') format('woff2-variations'),
|
||||||
|
url('/fonts/Inter-Variable.ttf') format('truetype supports variations'),
|
||||||
|
url('/fonts/Inter-Variable.ttf') format('truetype-variations');
|
||||||
font-weight: 100 900;
|
font-weight: 100 900;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
|
|||||||
@@ -20,9 +20,6 @@ import {
|
|||||||
import { ArrowsUpDownIcon } from '@heroicons/vue/20/solid';
|
import { ArrowsUpDownIcon } from '@heroicons/vue/20/solid';
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
|
||||||
import { Link } from '@inertiajs/vue3';
|
|
||||||
import { CreditCardIcon } from '@heroicons/vue/20/solid';
|
|
||||||
// TimeEntryRoundingType definition
|
// TimeEntryRoundingType definition
|
||||||
const TimeEntryRoundingType = {
|
const TimeEntryRoundingType = {
|
||||||
Up: 'up' as const,
|
Up: 'up' as const,
|
||||||
@@ -153,17 +150,7 @@ const iconClass = computed(() => {
|
|||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent class="w-72 p-4">
|
<PopoverContent class="w-72 p-4">
|
||||||
<div v-if="!isAllowedToPerformPremiumAction()" class="flex flex-col space-y-2">
|
<div class="space-y-4">
|
||||||
<span class="font-semibold text-xs">Premium</span>
|
|
||||||
<span class="text-xs text-text-secondary flex-1">Rounding is a premium feature. Upgrade to unlock this feature.</span>
|
|
||||||
<Link href="/billing">
|
|
||||||
<Button size="sm" variant="input" class="items-center space-x-1">
|
|
||||||
<CreditCardIcon class="w-3.5 h-3.5 text-text-tertiary mr-1" />
|
|
||||||
Go to Billing
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div v-else class="space-y-4">
|
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<InputLabel for="enable-rounding" value="Enable Rounding" />
|
<InputLabel for="enable-rounding" value="Enable Rounding" />
|
||||||
|
|||||||
@@ -409,7 +409,6 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
|||||||
'start' => Carbon::createFromFormat('Y-m-d H:i:s', '2020-01-01 00:00:07'),
|
'start' => Carbon::createFromFormat('Y-m-d H:i:s', '2020-01-01 00:00:07'),
|
||||||
'end' => null,
|
'end' => null,
|
||||||
]);
|
]);
|
||||||
$this->actAsOrganizationWithSubscription();
|
|
||||||
Passport::actingAs($data->user);
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -436,52 +435,6 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_index_endpoint_ignores_rounding_if_organization_has_no_premium_features(): void
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
$this->travelTo(Carbon::createFromFormat('Y-m-d H:i:s', '2020-01-01 00:15:04'));
|
|
||||||
$data = $this->createUserWithPermission([
|
|
||||||
'time-entries:view:own',
|
|
||||||
]);
|
|
||||||
$timeEntry1 = TimeEntry::factory()->forOrganization($data->organization)
|
|
||||||
->forMember($data->member)
|
|
||||||
->create([
|
|
||||||
'start' => Carbon::createFromFormat('Y-m-d H:i:s', '2020-01-01 00:00:08'),
|
|
||||||
'end' => Carbon::createFromFormat('Y-m-d H:i:s', '2020-01-01 00:00:01'),
|
|
||||||
]);
|
|
||||||
$timeEntry2 = TimeEntry::factory()->forOrganization($data->organization)
|
|
||||||
->forMember($data->member)
|
|
||||||
->create([
|
|
||||||
'start' => Carbon::createFromFormat('Y-m-d H:i:s', '2020-01-01 00:00:07'),
|
|
||||||
'end' => null,
|
|
||||||
]);
|
|
||||||
$this->actAsOrganizationWithoutSubscriptionAndWithoutTrial();
|
|
||||||
Passport::actingAs($data->user);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
$response = $this->getJson(route('api.v1.time-entries.index', [
|
|
||||||
$data->organization->getKey(),
|
|
||||||
'member_id' => $data->member->getKey(),
|
|
||||||
'rounding_type' => TimeEntryRoundingType::Up,
|
|
||||||
'rounding_minutes' => 6,
|
|
||||||
]));
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
$this->assertResponseCode($response, 200);
|
|
||||||
$response->assertJson(fn (AssertableJson $json) => $json
|
|
||||||
->has('data')
|
|
||||||
->has('meta')
|
|
||||||
->where('meta.total', 2)
|
|
||||||
->count('data', 2)
|
|
||||||
->where('data.0.id', $timeEntry1->getKey())
|
|
||||||
->where('data.0.start', '2020-01-01T00:00:08Z')
|
|
||||||
->where('data.0.end', '2020-01-01T00:00:01Z')
|
|
||||||
->where('data.1.id', $timeEntry2->getKey())
|
|
||||||
->where('data.1.start', '2020-01-01T00:00:07Z')
|
|
||||||
->where('data.1.end', null)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_index_endpoint_can_round_down(): void
|
public function test_index_endpoint_can_round_down(): void
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -501,7 +454,6 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
|||||||
'start' => Carbon::createFromFormat('Y-m-d H:i:s', '2020-01-01 00:00:07'),
|
'start' => Carbon::createFromFormat('Y-m-d H:i:s', '2020-01-01 00:00:07'),
|
||||||
'end' => null,
|
'end' => null,
|
||||||
]);
|
]);
|
||||||
$this->actAsOrganizationWithSubscription();
|
|
||||||
Passport::actingAs($data->user);
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -547,7 +499,6 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
|||||||
'start' => Carbon::createFromFormat('Y-m-d H:i:s', '2020-01-01 00:00:07'),
|
'start' => Carbon::createFromFormat('Y-m-d H:i:s', '2020-01-01 00:00:07'),
|
||||||
'end' => null,
|
'end' => null,
|
||||||
]);
|
]);
|
||||||
$this->actAsOrganizationWithSubscription();
|
|
||||||
Passport::actingAs($data->user);
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|||||||
Reference in New Issue
Block a user