mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-11 09:42:15 +01:00
Compare commits
4 Commits
feature/fi
...
feature/ro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb41d60a21 | ||
|
|
6a740015b7 | ||
|
|
be873c72fe | ||
|
|
bbfa411f32 |
@@ -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(),
|
||||||
|
|||||||
@@ -118,8 +118,7 @@
|
|||||||
"extra": {
|
"extra": {
|
||||||
"laravel": {
|
"laravel": {
|
||||||
"dont-discover": [
|
"dont-discover": [
|
||||||
"laravel/telescope",
|
"laravel/telescope"
|
||||||
"nwidart/laravel-modules"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ use App\Enums\NumberFormat;
|
|||||||
use App\Enums\TimeFormat;
|
use App\Enums\TimeFormat;
|
||||||
use Illuminate\Support\Facades\Facade;
|
use Illuminate\Support\Facades\Facade;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
use Nwidart\Modules\LaravelModulesServiceProvider;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
@@ -198,7 +197,6 @@ return [
|
|||||||
App\Providers\FortifyServiceProvider::class,
|
App\Providers\FortifyServiceProvider::class,
|
||||||
App\Providers\JetstreamServiceProvider::class,
|
App\Providers\JetstreamServiceProvider::class,
|
||||||
// Warning: Do not add TelescopeServiceProvider here since it is already conditionally registered in AppServiceProvider
|
// Warning: Do not add TelescopeServiceProvider here since it is already conditionally registered in AppServiceProvider
|
||||||
LaravelModulesServiceProvider::class,
|
|
||||||
])->toArray(),
|
])->toArray(),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -114,8 +114,8 @@ test('test that project filtering works in reporting', async ({ page }) => {
|
|||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
// Verify only project1 time entries are shown
|
// Verify only project1 time entries are shown
|
||||||
await expect(page.getByTestId('reporting_view').getByText(project1)).toBeVisible();
|
await expect(page.getByText(project1)).toBeVisible();
|
||||||
await expect(page.getByTestId('reporting_view').getByText(project2)).not.toBeVisible();
|
await expect(page.getByText(project2)).not.toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('test that tag filtering works in reporting', async ({ page }) => {
|
test('test that tag filtering works in reporting', async ({ page }) => {
|
||||||
@@ -142,7 +142,7 @@ test('test that tag filtering works in reporting', async ({ page }) => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// Verify only time entries with tag1 are shown
|
// Verify only time entries with tag1 are shown
|
||||||
await expect(page.getByTestId('reporting_view').getByText('1h 00min').first()).toBeVisible();
|
await expect(page.getByText('1h 00min').first()).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('test that billable status filtering works in reporting', async ({ page }) => {
|
test('test that billable status filtering works in reporting', async ({ page }) => {
|
||||||
@@ -164,7 +164,7 @@ test('test that billable status filtering works in reporting', async ({ page })
|
|||||||
]);
|
]);
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
await expect(page.getByTestId('reporting_view').getByText('1h 00min').first()).toBeVisible();
|
await expect(page.getByText('1h 00min').first()).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
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" />
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ function onSelectChange(checked: boolean) {
|
|||||||
"></BillableToggleButton>
|
"></BillableToggleButton>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<button
|
<button
|
||||||
:class="twMerge('text-text-secondary px-1 py-1.5 bg-transparent text-center hover:bg-card-background rounded-lg border border-transparent hover:border-card-border text-sm font-medium focus-visible:outline-none focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:bg-tertiary', organization?.time_format === '12-hours' ? 'w-[170px]' : 'w-[120px]')"
|
:class="twMerge('text-text-secondary w-[110px] px-1 py-1.5 bg-transparent text-center hover:bg-card-background rounded-lg border border-transparent hover:border-card-border text-sm font-medium focus-visible:outline-none focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:bg-tertiary', organization?.time_format === '12-hours' ? 'w-[160px]' : 'w-[110px]')"
|
||||||
@click="expanded = !expanded">
|
@click="expanded = !expanded">
|
||||||
{{ formatStartEnd(timeEntry.start, timeEntry.end, organization?.time_format) }}
|
{{ formatStartEnd(timeEntry.start, timeEntry.end, organization?.time_format) }}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -21,17 +21,13 @@ abstract class TestCase extends BaseTestCase
|
|||||||
{
|
{
|
||||||
use CreatesApplication;
|
use CreatesApplication;
|
||||||
|
|
||||||
protected bool $mockBillingContract = true;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
Mail::fake();
|
Mail::fake();
|
||||||
LogFake::bind();
|
LogFake::bind();
|
||||||
Http::preventStrayRequests();
|
Http::preventStrayRequests();
|
||||||
if ($this->mockBillingContract) {
|
$this->actAsOrganizationWithoutSubscriptionAndWithoutTrial();
|
||||||
$this->actAsOrganizationWithoutSubscriptionAndWithoutTrial();
|
|
||||||
}
|
|
||||||
// Note: The following line can be used to test timezone edge cases.
|
// Note: The following line can be used to test timezone edge cases.
|
||||||
// $this->travelTo(Carbon::now()->timezone('Europe/Vienna')->setHour(0)->setMinute(59)->setSecond(0));
|
// $this->travelTo(Carbon::now()->timezone('Europe/Vienna')->setHour(0)->setMinute(59)->setSecond(0));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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